Repository navigation
docs(spec): state what fieldGroups[].collapsed maps to on its own - #19699
Conversation
The ADR-0085 `collapse` enum replaced the `collapsible` / `collapsed` boolean pair and `ObjectSchema.parse` still folds the pair onto it, but the `collapsed` alias's `describe()` said only "Boolean pair with `collapsible`; use the `collapse` enum" — which is the sentence `content/docs/references/data/object.mdx` publishes, and it leaves an author to guess whether `collapsed: true` needs `collapsible` beside it. It does not: measured against the built package, a field group authored `collapsed: true` and nothing else parses to `collapse: 'collapsed'`, which the enum's own describe spells out as "collapsible, starts closed", and `collapsed: true` outranks an explicit `collapsible: false`. The sibling `defaultExpanded` alias already spelled its mapping out. This gives `collapsed` the same treatment, in the describe and nothing else: no key is added, removed or re-typed, no accept set moves and the normalizer is untouched. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cdf09f9d865c0ec496d93bbfd1ec8849aff9065a && git checkout cdf09f9d865c0ec496d93bbfd1ec8849aff9065a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7e1b048a1dbd7142bd6aa2cd9cbcccf7e1c69345 1fae1ecc405616715050383da71dc690dd3a707f && git checkout -B drift-repro 7e1b048a1dbd7142bd6aa2cd9cbcccf7e1c69345 && git merge --no-ff 1fae1ecc405616715050383da71dc690dd3a707f
node scripts/docs-audit/affected-docs.mjs --json 7e1b048a1dbd7142bd6aa2cd9cbcccf7e1c69345 |
Contract reviewServed-tier: 79/79 Rendered by an isolated at-tier review subagent on the head above. ① Derived judgments
② Semver levelDeclared
It publishes, so a changeset is owed and Check conclusions are read from the head's check-runs, not locally: ③ Boundary flags
Implemented-by: VERDICT: PASS Tier control — measured by the seat, not reported by the reviewerThe reviewer self-reported
The reviewer was served at tier for every request it made. Two carried readings the seat is recording rather than burying
Seat follow-ups this record does not discharge
Generated by Claude Code |
…ectstack-ai#19736) Fixes objectstack-ai#19311 Clause-②: no Why that reading: two `.describe()` strings and one TSDoc block. No key is added, removed or re-typed, no closed set gains a member, no export moves and no refinement changes. `check:authorable-surface` and `check:api-surface` are both green with no delta, and the wizard-step and `group` co-declaration refusals parse identically before and after. Grade: `patch`, measured below rather than pattern-matched. ## The defect, re-derived on `origin/main` rather than taken on trust `FormSectionSchema` declares two authorable booleans with **no `.describe()` at all**, between neighbours that have one: ``` packages/spec/src/ui/view.zod.ts:3315 collapsible: z.boolean().default(false), packages/spec/src/ui/view.zod.ts:3316 collapsed: z.boolean().default(false), ``` **Lit control, same file, same instrument:** `view.zod.ts:1054` reads `collapsed: z.boolean().default(false).describe('Collapse groups by default (presentation only)')` — a different schema (group-by presentation). So a describe on a key of this name IS visible to the grep, and the zero at `:3315–3316` is a reading, not a blind spot. **Dark control:** `git grep -n "collapsedd"` over the same file → 0 hits, exit 1. Downstream of that silence, the generated page printed two empty Description cells (`content/docs/references/ui/view.mdx:334-335`, and again at `:466-467` and `:482-483`, the three places that page projects this schema). **This is NOT the surface the first increment landed on.** PR objectstack-ai#19699 (merged `fa29803417`) corrected `packages/spec/src/data/object.zod.ts:1220` — the `ObjectFieldGroup` pair, a different schema in a different file, which is why it merged as `Part of`. Both keys of **this** pair are described here, not just `collapsed`: describing one of a silent pair recreates the ambiguity one key over. ## Probe — every sentence measured against the BUILT package Built with `pnpm --filter @objectstack/spec build`, then probed through the module the `exports` map resolves (`require.resolve('@objectstack/spec/ui')` → `packages/spec/dist/ui/index.js`, printed in the same run). ### A. `FormSectionSchema.safeParse`, one section, all four combinations | authored | `collapsible` out | `collapsed` out | | :--- | :--- | :--- | | neither — **CONTROL (lit)**: default arm must fire | `false` | `false` | | `collapsible: true` | `true` | `false` | | `collapsible: false` | `false` | `false` | | `collapsed: true` | `false` | `true` | | `collapsed: false` | `false` | `false` | | both `true` | `true` | `true` | | `collapsed: true` + `collapsible: false` | `false` | `true` | | `collapsed: false` + `collapsible: true` | `true` | `false` | | both `false` | `false` | `false` | | `collapsable: true` — **CONTROL (dark)** | REFUSED `unrecognized_keys` | — | | `collapsed: 'true'` — **CONTROL (dark)** | REFUSED `invalid_type@collapsed` | — | Both lit and both dark controls answered as predicted, so the nine readings are measurements. ### B. The normalizer question the dispatch asked — answered NO There is no normalizer, no parse wrapper and no fold between these two keys. `{ collapsed: true }` parses to `{ collapsible: false, collapsed: true }`, verbatim, with both keys present in the output (`.default(false)`), and `safeParseAsync` returns the same. The section's only `.transform` is `normalizeVisibleWhen`, which touches `visibleOn` and nothing else. ⇒ That is the **precision limit**, and it is stated in the text rather than left implied: the implication `collapsed` ⇒ `collapsible` is a **renderer** rule applied from the declaration, never a parse-time rewrite. The describes say so explicitly, and say that the parsed `collapsible` must never be read as "a disclosure control renders". This is the exact opposite of the `ObjectFieldGroup` pair, where a real parse-time mapping folds the booleans onto the ADR-0085 `collapse` enum — the two surfaces share key names and share nothing else, so the describes name that difference. ### C. The two refusals the describes claim | input | wizard form | beside `group` | | :--- | :--- | :--- | | `collapsed: true` | REFUSED at `sections.0.collapsed` | REFUSED at `collapsed` | | `collapsible: true` | REFUSED at `sections.0.collapsible` | REFUSED at `collapsible` | | `collapsed: false` | ACCEPTED | ACCEPTED | | `collapsible: false` | ACCEPTED | ACCEPTED | | neither — **CONTROL (lit)** | ACCEPTED | ACCEPTED | | `simple` form + `collapsed: true` — **CONTROL (lit)** | ACCEPTED (the refusal is wizard-only) | — | | `group` + `fields` — **CONTROL (dark)** | — | REFUSED at `group` |⚠️ The first run of this block had its two lit controls come out REFUSED. That was a malformed fixture on my side (`FormViewSchema` is the form CONFIG and carries no `name` / `label` / `objectName`), not a reading — so those rows were discarded and the block re-run after the fixture was repaired. The table above is the repaired run. Recorded because the controls are the only reason the first, wrong version did not ship as a finding. ### D. Reverse verification — the gate can see this change After the describe edit and a spec rebuild, `pnpm --filter @objectstack/spec check:generated` exits **1** and names exactly one stale artifact — `content/docs/references/**` — while the other 14 stay green. **Direction observed = turns red, as predicted.** After `gen:schema && gen:docs` the same aggregate exits 0. That is what makes the final green a measurement rather than a silence. ## Downstream consumer, read at the PINNED sha Read at `.objectui-sha` = **`87af769e9a3ee28ace099fdd653d3ebd79fe82e2`** (not objectui's head, which is `0cf2d66`); `git merge-base --is-ancestor` on the pin exits 0, which is self-proving. - `packages/plugin-form/src/ObjectForm.tsx:1560` applies the ruling: `Boolean(section.collapsible) || Boolean(section.collapsed)`, with a block comment that states it is read from the DECLARATION and never from live collapse state, and that letters B (refuse the combination) and C (a dev-only warning) were both refused. The describes follow that, and ⛔ neither adds a refinement nor asks for a lint rule. - `packages/plugin-form/src/DrawerForm.tsx:609` and `:677` still push `collapsible: section.collapsible` **alone** on both drawer arms. That file's own comment declares the gap deliberate and fenced, and says converging it *is* the `collapsed` / `collapsible` decision. **So the contract this PR publishes is the thing that arm was waiting on** — it is a sibling-repo residual, another seat's card, and nothing here touches it. Reported, not ridden along. - In **this** repo nothing consumes the raw pair: `git grep` for a `.collapsible` / `.collapsed` member access across `packages/**` returns only the `ObjectFieldGroup` mapping and test files; `packages/lint/src` has **0** hits for `collapsible`, with a positive control (`form-section-group-unknown`, 2 files) firing on the same command and scope. - All 22 first-party form producers under `packages/spec/src/**/*.form.ts` pair `collapsible: true` with `collapsed: true`, so **zero** in-repo producers exercise the trap. It is an author-facing trap, not a live in-repo defect — which is precisely why the remedy is declaration text. ## Generated artifacts — the four-step sequence, run mechanically `content/docs/references/**` is routed `merge=os-regen`, the class that merges with exit 0 and zero conflict markers while silently dropping one side. 1. `bash scripts/pm/os-regen-merge.sh` — steps 1–3, exit 0. It merged `origin/main` (`e37ea4d060`), took main's side of the generated artifacts main moved, and committed the merge **before** any regeneration. ⛔ No rebase, ⛔ no force-push, ⛔ no `git stash`, ⛔ no hand merge of a routed path. 2. `gen:schema` was run only **after** `MERGE_HEAD` was gone and the tree was clean, so the authorable-surface anchor could not roll back to the old fork point. `packages/spec/authorable-surface.base.json` is byte-identical to `origin/main`. 3. Regenerated `pnpm --filter @objectstack/spec build && gen:schema && gen:docs`. **Exactly one** file moved, and by exactly the rows this one change derives: 6 rows in `content/docs/references/ui/view.mdx` — the pair, three times, because that page projects `FormSectionSchema` three times. `git diff --name-only origin/main -- content/docs/references/` names that file and no other. 4. Inspected the **staged** diff (`git diff --cached`), not the working-tree diff, before committing. ### Survival assertion for the in-flight sibling, PR objectstack-ai#19618 objectstack-ai#19618 (open, draft, head `7cc0ca1b3d`) touches four `content/docs/references/**` pages. It is **unmerged**, so what my regeneration could destroy is main's copy of those pages. Quoted-exact `git grep -F`, counts identical on my tree and `origin/main`: | needle | path | mine | `origin/main` | | :--- | :--- | :--- | :--- | | the pre-objectstack-ai#19618 `tenancy` type spelling carrying `organizationField?` | `api/metadata.mdx` | 1 | 1 | | same | `data/object.mdx` | 1 | 1 | | same | `system/migration.mdx` | 2 | 2 | | objectstack-ai#19618's post-change `tenancy` spelling | all four | 0 | 0 (it has not landed) | | `STAMP-ONLY: column carrying the organization a row is ABOUT` (its deletion target) | `data/object.mdx` | 2 | 2 | | **CONTROL (lit)** `Multi-tenancy configuration for SaaS applications` | all four | 1 / 1 / 2 | fires | | **CONTROL (dark)** a near-miss of the same spelling | all four | 0, exit 1 | discriminates | All four of its reference pages are **byte-identical** to `origin/main` in my tree (`git hash-object` vs `git rev-parse origin/main:PATH`, four matching pairs). Every os-regen-routed path outside `content/docs/references/ui/view.mdx` differs from `origin/main` by **zero** files. Also asserted, because the merge driver swallows implementation bodies and not only index entries: the **previous increment's** body survives — `git grep -cF "true → 'collapsed' (collapsible, starts closed) on its own"` reads 1 in `packages/spec/src/data/object.zod.ts` and 2 in `content/docs/references/data/object.mdx`, identical on my tree and `origin/main`, with a near-miss dark control at 0 / exit 1. ## Changeset — measured, not pattern-matched `npm pack --dry-run --json` in `packages/spec` (exit 0, 2031 files): | file | verdict | | :--- | :--- | | `src/ui/view.zod.ts` — **the edited file** | SHIPS | | `package.json` — **positive control** | SHIPS | | `src/data/object.zod.ts` — **positive control** | SHIPS | | `src/ui/view.test.ts` — **negative control** | does NOT ship | | `scripts/build-docs.ts` — **negative control** | does NOT ship | | `tsconfig.json` — **negative control** | does NOT ship | Scanning the tarball's own file list for the new describe text: **42 shipped files carry it** — 30 under `dist/`, 11 under `json-schema/`, 1 under `src/`. Negative control with a lit leg: the string `are GENERATED — do not hand-edit them` is present in `packages/spec/scripts/lib/generated-output.ts` and in **0** shipped files, because that file does not ship. ⇒ The bytes publish, so `skip-changeset` would be a false declaration. A `patch` changeset is in the diff. ## Acceptance notes — found, ⛔ not fixed here - `packages/spec/src/ui/view.zod.ts:3274` and `:5951` describe the pair in prose and in a doc example without the dependency; neither is falsified by this change and neither is a trap as written (the `:5951` example writes both keys). Noted, not filed. - The `sections` / `groups` rows on the generated page print a truncated inline type (`… collapsible?: boolean; …`) with no Description at all. That is the generator's type-column truncation, not this pair, and it is unchanged here. - `packages/lint` carries no rule on this pair (0 hits, positive control firing). Consistent with the 2026-09-18 ruling, which refused letter C — a dev-only warning — so this is a deliberate absence, ⛔ not a gap to fill. --- _Generated by [Claude Code](https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #19311
Clause-②: no
Why that reading: one
.describe()string on an already-deprecated alias. No key is added, removed or re-typed, no accept set moves, the normalizer is untouched, and the public export set is byte-identical. The grade stayspatchexactly as measured below.What the card asked, and what re-measuring found
#19311 is a verification request: its own body says the reading is INHERITED, was not seat-verified, and asks whether it still holds on today's
origin/mainwith a lit control. Both halves were re-measured here rather than inherited.Half 1 — the spec side
Instrument:
git grep -nEoverpackages/spec/src/**/*.zod.ts, plusObjectSchema.safeParserun against the package built from this branch. Radius: this repository ateea7ccc3ec(the branch point) for the greps; the builtpackages/spec/distfor the parse. Dark control: a nonsense token of the same shape (collapsibul) over the same glob returns 0 hits, so the instrument discriminates.The describes do already name
collapsible, and both keys are deprecated in favour of the ADR-0085collapseenum — so the card's literal claim ("the describe does not mentioncollapsible") is stale. What is not stale is the gap underneath it: nothing in the published text says whatcollapsedmaps to on its own. Measured, nine inputs on one field group:collapseafter parsecollapsed: true'collapsed'collapsed: false'none'collapsible: true'expanded'collapsible: false'none'collapsed: true+collapsible: false'collapsed'—collapsedoutrankscollapsed: true+collapsible: true'collapsed'defaultExpanded: false'collapsed''none'collapse(lit control)The last two rows are the run's own controls: the default arm fires when nothing is authored, and the alias arm is skipped when
collapseis present — so the other seven rows are the normalizer's doing and not an artefact.collapse: 'collapsed'is described one screen above as collapsible, starts closed. So on this surfacecollapsed: truealone already implies collapsible — the behaviour was never the gap; the text was. The mapping lives only in the docblock of a non-exported helper, which nothing publishes, while the sibling aliasdefaultExpandedspelled its own mapping out in its describe and these two did not.Half 2 — the consumer side
Two radii, measured separately.
This repository.
git grep -nE "\.collapsed\b|\.collapsible\b"overpackages/*/src/**, excludingpackages/spec/src, tests and generated files → 0 hits. Lit control on the same instrument and the same glob:\.visibleWhen\breturns hits in five or more files. Nothing here reads the raw pair; rendering is the sibling repo's.The pinned sibling. Read at
.objectui-sha=87af769e9a3ee28ace099fdd653d3ebd79fe82e2, whichgit merge-base --is-ancestorconfirms (exit 0, self-proving) is an ancestor of the objectui checkout on this box. Dark control on a nonsense token of the same shape over the same blob: 0 hits.packages/plugin-form/src/ObjectForm.tsx:1560—const isCollapsible = Boolean(section.collapsible) || Boolean(section.collapsed);— the maintainer ruling of 2026-09-18 (letter A:collapsedimpliescollapsible, read from the declaration) is applied, and the pin carries it.packages/plugin-form/src/DrawerForm.tsx:609and:677— both arms passcollapsible: section.collapsiblealone. The file names objectui#9780 in a comment and declares the residual deliberately fenced.So the downstream half is partly repaired and partly still open, and the open part is another repo's and another seat's. Nothing about it is touched here.
The change
One describe, on the declared file surface and nothing else:
content/docs/references/data/object.mdxis regenerated from it by the repo's own generator, never by hand.Reverse verification — the gate can see this text
Run in order on this branch, exit codes captured before any pipe:
pnpm --filter @objectstack/spec check:docs→ exit 1, namingcontent/docs/references/data/object.mdxas out of date. That is the lit control: the gate reads this describe.gen:schema && gen:docs→ the two published rows change and nothing else does (the shape is nested under two parents, hence two identical row edits).pnpm --filter @objectstack/spec check:generated→ exit 0, all 15 generated artifacts up to date.Step 1 is what makes step 3 a measurement rather than a silence.
Verification
Measured at
1fae1ecc40, the last commit on this branch.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands@objectstack/lint...,@objectstack/client-react,@objectstack/objectqland then the whole workspace, and re-ran at 0dispatch-gates.mjs --ranreconciliationpnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typecheckcheck:test-typecheck;tsc --listFilesshows the main program carries 0*.test.ts, which is why that second gate is the one covering thempnpm --filter @objectstack/spec check:generatedpnpm lint(eslint . --no-inline-config, whole repo, not narrowed)Changeset — measured, not pattern-matched
patchon@objectstack/spec. Decided by grepping the new describe text through each entry of this package's ownfiles[]after a real build:dist, 11 underjson-schema, and insrc/data/object.zod.ts, whichfiles[]ships verbatim assrc/**/*.zod.ts. It publishes three ways over.are GENERATED — do not hand-edit theminpackages/spec/scripts/lib/generated-output.tsand in 0 files across everyfiles[]path. So the instrument separates shipped from unshipped, and the positive above is not an artefact of grepping everything.skip-changesetwould therefore be wrong here.Hand-written docs corpus — key-name probe
The Docs Drift Check bot listed nothing on this head and says in its own words that an empty list is not a clean bill of health: an emitter-only diff shares no identifier with a page that states the rule by its inputs. So the probe was dropped from the sentence to the narrowest token any page restating this rule must still carry.
Radius: the whole hand-written
content/docscorpus — 216 tracked files, generatedcontent/docs/references/**excluded by pathspec. Instrument:git grep -nwper token.collapsedcollapsiblecollapsefieldGroupscollapsable(near-miss spelling)defaultExpandedNot a double zero, so the instrument discriminates.
Reading: nothing is falsified. No hand-written page states the
fieldGroups[].collapsible/collapsedmapping at all, so none of them can contradict a describe that now states it. The one page that documents the enum —content/docs/data-modeling/schema-design.mdx:316— already spells'collapsed'as collapsible and starts closed, which is exactly what the corrected describe now says. It names onlydefaultExpandedas the deprecated alias and omits this pair: an omission, not a contradiction, and not edited here. Every single-hit page (18 of them) uses "collapse" in the ordinary English sense of folding two things into one, not as a metadata key.content/docs/releases/**contributed 14 of the 72 lines across five pages. Release-owned and read-only: none of them states the mapping rule, all are feature-note prose, and none was touched.Three hits are about the OTHER surface — the form-view
sections[]pair (FormSectionSchema,packages/spec/src/ui/view.zod.ts), which this PR does not touch and which has no alias mapping behind it. They are reported, not edited, and routing them is the dispatching seat's:content/docs/protocol/objectui/concept.mdx:588-596— a YAML example captioned "Advanced section (collapsed by default)" that authorscollapsed: trueon a form section with nocollapsiblebeside it. At the pinned.objectui-sha, the twoDrawerFormarms honour that literally.content/docs/ui/views.mdx:416-417— the section key table readscollapsible= "Can section be collapsed",collapsed= "Initially collapsed", silent about the dependency the 2026-09-18 ruling settled.content/docs/protocol/objectui/layout-dsl.mdx:354-355,:422,:529,:835-836— the same pair documented four more times without the dependency. The authored example at:354-355writes both keys together, so it is safe as written.Limit of this radius, stated: the probe sees a page that NAMES one of the four tokens. It does not see a page that describes collapse behaviour in prose with no key name at all. Nothing here claims more than the token can carry.
Acceptance notes
Noted, deliberately not ridden along:
collapsible's describe carries the mirror-image silence.collapsible: truealone parses tocollapse: 'expanded'and its describe says onlyBoolean pair with `collapsed`. Same family, same correction shape; left out because the card namescollapsedand the dispatch said the describe and nothing else.@deprecatedTSDoc lines above both keys still read "pair with X", the framing that produced the card. They are IDE text, not published-page text, so they are a separate, smaller surface.A measured finding on a file outside this PR's declared surface is reported to the dispatching seat rather than filed from here.
Generated by Claude Code