Skip to content

docs(spec): state what fieldGroups[].collapsed maps to on its own - #19699

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-19311-collapsed-implies-collapsible
Sep 22, 2026
Merged

os-support-ai merged 1 commit into
mainfrom
claude/issue-19311-collapsed-implies-collapsible

Conversation

@os-support-ai

@os-support-ai os-support-ai commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

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 stays patch exactly 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/main with a lit control. Both halves were re-measured here rather than inherited.

Half 1 — the spec side

Instrument: git grep -nE over packages/spec/src/**/*.zod.ts, plus ObjectSchema.safeParse run against the package built from this branch. Radius: this repository at eea7ccc3ec (the branch point) for the greps; the built packages/spec/dist for 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-0085 collapse enum — so the card's literal claim ("the describe does not mention collapsible") is stale. What is not stale is the gap underneath it: nothing in the published text says what collapsed maps to on its own. Measured, nine inputs on one field group:

authored on the group collapse after parse
collapsed: true 'collapsed'
collapsed: false 'none'
collapsible: true 'expanded'
collapsible: false 'none'
collapsed: true + collapsible: false 'collapsed' — collapsed outranks
collapsed: true + collapsible: true 'collapsed'
defaultExpanded: false 'collapsed'
neither key (lit control) 'none'
an explicit collapse (lit control) wins; the aliases are not read

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 collapse is 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 surface collapsed: true alone 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 alias defaultExpanded spelled 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" over packages/*/src/**, excluding packages/spec/src, tests and generated files → 0 hits. Lit control on the same instrument and the same glob: \.visibleWhen\b returns 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, which git merge-base --is-ancestor confirms (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: collapsed implies collapsible, read from the declaration) is applied, and the pin carries it.
  • packages/plugin-form/src/DrawerForm.tsx:609 and :677 — both arms pass collapsible: section.collapsible alone. 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:

before:  [DEPRECATED → collapse] Boolean pair with `collapsible`; use the `collapse` enum.
after:   [DEPRECATED → collapse] true → 'collapsed' (collapsible, starts closed) on its own — it
         needs no `collapsible` and outranks `collapsible: false`; false → 'none', or 'expanded'
         beside `collapsible: true`. Use the `collapse` enum.

content/docs/references/data/object.mdx is 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:

  1. after the edit and a spec rebuild, pnpm --filter @objectstack/spec check:docs → exit 1, naming content/docs/references/data/object.mdx as out of date. That is the lit control: the gate reads this describe.
  2. 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).
  3. 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.

run exit
the 101 commands from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands 0 — each captured before any pipe; six first returned 3 (PREREQUISITE NOT MET, never read as a pass), were cleared by building @objectstack/lint..., @objectstack/client-react, @objectstack/objectql and then the whole workspace, and re-ran at 0
dispatch-gates.mjs --ran reconciliation 0 — 101 derived families accounted for, 101 run, 0 not-measured
the six roster families whose roster sits under a directory this diff touches 0 each — run explicitly rather than read as silent
pnpm --filter @objectstack/spec test 0 — 514 files, 14991 passed, 1 todo
pnpm --filter @objectstack/spec typecheck 0 — includes check:test-typecheck; tsc --listFiles shows the main program carries 0 *.test.ts, which is why that second gate is the one covering them
pnpm --filter @objectstack/spec check:generated 0
pnpm lint (eslint . --no-inline-config, whole repo, not narrowed) 0 — 1m46s

Changeset — measured, not pattern-matched

patch on @objectstack/spec. Decided by grepping the new describe text through each entry of this package's own files[] after a real build:

  • positive — the new text is present in 20 files under dist, 11 under json-schema, and in src/data/object.zod.ts, which files[] ships verbatim as src/**/*.zod.ts. It publishes three ways over.
  • negative, with a lit leg — the same grep finds are GENERATED — do not hand-edit them in packages/spec/scripts/lib/generated-output.ts and in 0 files across every files[] path. So the instrument separates shipped from unshipped, and the positive above is not an artefact of grepping everything.

skip-changeset would 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/docs corpus — 216 tracked files, generated content/docs/references/** excluded by pathspec. Instrument: git grep -nw per token.

token hit lines
collapsed 17
collapsible 16
collapse 24
fieldGroups 21
unique lines, deduplicated 72, across 31 pages
dark control collapsable (near-miss spelling) 0 — exit 1
lit control defaultExpanded 1 — exit 0

Not a double zero, so the instrument discriminates.

Reading: nothing is falsified. No hand-written page states the fieldGroups[].collapsible / collapsed mapping 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 only defaultExpanded as 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 authors collapsed: true on a form section with no collapsible beside it. At the pinned .objectui-sha, the two DrawerForm arms honour that literally.
  • content/docs/ui/views.mdx:416-417 — the section key table reads collapsible = "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-355 writes 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: true alone parses to collapse: 'expanded' and its describe says only Boolean pair with `collapsed`. Same family, same correction shape; left out because the card names collapsed and the dispatch said the describe and nothing else.
  • The @deprecated TSDoc 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

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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 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
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7e1b048a1dbd7142bd6aa2cd9cbcccf7e1c69345 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from cdf09f9d865c0ec496d93bbfd1ec8849aff9065a — the merge of head 1fae1ecc405616715050383da71dc690dd3a707f into base 7e1b048a1dbd7142bd6aa2cd9cbcccf7e1c69345, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 79/79 CONTRACT_REVIEW_TIER (seat-measured, 2026-09-22T12:53Z)
Head-sha: 1fae1ecc405616715050383da71dc690dd3a707f

Rendered by an isolated at-tier review subagent on the head above.

① Derived judgments

  1. Declaration Clause-②: no — judged CORRECT. The diff's only source change is one string literal inside .describe() at packages/spec/src/data/object.zod.ts:1220. normalizeSemanticRoleAliases (:2818–2843) and the ObjectSchema parse wrappers (:2861, :2864) are untouched; no key is added, removed or re-typed; no export changes (check:api-surface on a full DTS build: unchanged). Neither limb of the clause fires — the accept set is not widened and the public surface is not enlarged. A Contract-text: citation is NOT owed: the rule attaches it to a no that deletes a false rejection, and this diff deletes no rejection. The declaration carries no (widening)/(narrowing) arm, which AGENTS.md permits. Both carriers read no — check-clause2-carriers.mjs --pair 19699 exit 0, governing claim comment and PR body line agreeing.
  2. Every claim in the new contract text verified against the built package at head — a 12-input probe through ObjectSchema.safeParse against dist/data/index.js: collapsed:true alone → 'collapsed'; it needs no collapsible (same row); collapsed:true + collapsible:false → 'collapsed' (the ternary at :2831 tests collapsed === true first, so "outranks" is literally the code order); collapsed:false alone, +collapsible:false, +defaultExpanded:false all → 'none'; collapsed:false + collapsible:true → 'expanded'; the parenthetical "(collapsible, starts closed)" is the enum's own describe at :1209 verbatim. Lit controls in the same run: neither key → 'none' (default arm), explicit collapse:'expanded' beside collapsed:true → 'expanded' (aliases unread). No published statement is false. The pinned objectui (.objectui-sha 87af769e) derives the same precedence at ObjectGroupInspector.tsx:74, so the text contradicts no downstream reader either.
  3. Reach of the claim — a precision limit that predates the PR, recorded not failed. The mapping runs only inside ObjectSchema.parse/safeParse (the sync wrappers). Measured on the same build: standalone ObjectFieldGroupSchema.safeParse({key, label, collapsed:true}) → collapse:'none' with collapsed:true preserved; parseAsync/safeParseAsync are not wrapped. The describe names no entry point — but neither does the sibling defaultExpanded describe it mirrors (:1216), which carries the identical conditionality. No standalone or async consumer exists: in-repo the group schema is reached only via ObjectSchema.fieldGroups (:2022); at the objectui pin it is imported as a type only (object-fields-io.ts:334). Judged consistent with the established describe precision on this schema.
  4. collapsible's describe left uncorrected (:1218) — defensible, and a finding. Not a contradiction: the corrected collapsed row now says "needs no collapsible" and covers collapsible:true beside collapsed:false. What stays unstated is collapsible:true alone → 'expanded' and collapsible:false alone → 'none' — a silence that predates the PR, reported by the dev in the acceptance notes and within the dispatch's "add that clause" for collapsed. Shipping the pair half-described is a sibling-card follow-up, ⛔ not grounds to hold correct text back.
  5. Public face: the describe is published contract text — shipped verbatim in src/data/object.zod.ts (files[] entry src/**/*.zod.ts), in 20 dist files and 11 json-schema files including json-schema/data/ObjectFieldGroup.json and json-schema/data/Object.json, and in the regenerated content/docs/references/data/object.mdx. That is why this review is owed; it enlarges nothing.

② Semver level

Declared patch on @objectstack/spec (.changeset/19311-collapsed-alias-alone-mapping.md). Measured, not pattern-matched: a real build in an isolated worktree at head (gen:schema 1546 schemas, gen:openapi, tsup JS, tsup DTS — each exit 0), then pnpm pack → objectstack-spec-17.4.0.tgz, 1902 entries.

  • Positive controls: package/src/data/object.zod.ts present, package/dist/index.js present, 1538 package/json-schema/ entries; the new text found in 1 src + 20 dist + 11 json-schema files INSIDE the tarball; the old text 0.
  • Negative controls: package/scripts/build-docs.ts absent, zero *.test.ts, src/data/field-group-layout.ts (a non-.zod.ts src file) absent; the generator banner "are GENERATED — do not hand-edit them" 0 in the tarball against 1 in the unshipped scripts/lib/generated-output.ts. The instrument separates shipped from unshipped.

It publishes, so a changeset is owed and skip-changeset would be a false declaration. Description-only, check:api-surface unchanged, and a no declaration ⇒ patch is the correct level; the at-least-minor rule binds only a yes. Agreed.

Check conclusions are read from the head's check-runs, not locally: Check Changeset newest run by that name (106687408973, 09:20:30Z) is success; the earlier failure (106686491479, 09:17:30Z) ran against the pre-edit body and is superseded by name.

③ Boundary flags

  • Manual floor: none crossed — no ADR, no docs/adr/**, no content/docs/releases/**, no CHANGELOG edit; no security or permission surface, no runtime behaviour.
  • Regeneration verified from the tree: pnpm --filter @objectstack/spec check:docs on the head worktree after a real build → exit 0, 225 generated files in sync, worktree clean afterwards. The two changed rows in object.mdx (:314 and :670 — the shape nests under two parents) are generator output, not hand edits. Regen-provenance: not owed: one commit, no regen-only head move.
  • Dev flags and open_questions: (1) card/PR surface mismatch — settled by the seat as Part of, outside this question; the delivered change is sound on its own terms. (2) REST-proxy channel deviation — process, not contract; escalated to the seat as the report records. (3) the collapsible mirror silence and the @deprecated TSDoc "pair with X" framing — answered in ① item 4: leave, file a sibling card. (4) not raised by the dev, raised by the review: the mapping is conditional on the ObjectSchema sync parse entry (① item 3) — recorded, no change requested.
  • Construction tier — recorded, ⛔ not a FAIL. The claim's container line reads model: claude-opus-5 while asserting construction "runs at CONTRACT_REVIEW_TIER"; on origin/main that constant has read claude-fable-5-1 since 372931e512 (pm(tooling): the contract-review tier is fable — revert the opus constant and the retired-word list #19684, 2026-09-22T08:04Z), which is before the claim was written (2026-09-22T08:22Z). Construction was therefore at TIER_DEFAULT, and this at-tier review is the designed compensation for exactly that — so not a FAIL. The claim's self-description was simply stale when written; the seat acknowledged it in an earlier comment on this PR.
  • CI on head, newest run per check name: all success or skipped by design (Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke); Type Check · source gates (check:generated reconcile) and Type Check · consumer gates (check:api-surface) both success.

Implemented-by: claude/issue-19311-collapsed-implies-collapsible
Reviewed-by: session_013RDBh5DqXd2xnLwvHLgLFr

VERDICT: PASS


Tier control — measured by the seat, not reported by the reviewer

The reviewer self-reported 16/16 by counting its own turns, and said plainly that a self-count is not a reading. It is right: the governing control is the transcript's per-request harness model stamp, and get_session is ⛔ not admissible as corroboration. So the seat counted the transcript itself:

  • 79 assistant requests, 79 stamped claude-fable-5-1 — every per-request "message":{"model":…}, no exceptions.
  • 93/93 counting every model field in the transcript, per-request stamps and advisorModel alike.
  • Dark control: 0. Filtering every "model"/"advisorModel" occurrence for anything other than claude-fable-5-1 returns empty — no other model name appears anywhere in the transcript.
  • The four keyword hits for fallback/overload were read in context: three are tool-schema prose, one is the reviewer's own sentence about not being able to observe a fallback from inside. ⛔ None is a fallback notice.
  • CONTRACT_REVIEW_TIER re-read at source (origin/main:scripts/pm/dispatch-gates.mjs) = claude-fable-5-1, matching the stamps.

The reviewer was served at tier for every request it made.

Two carried readings the seat is recording rather than burying

  • Carried, not re-measured (by the rule that derived gate families are read from CI, never re-run by the reviewer): the dev's pnpm --filter @objectstack/spec test (14991 passed) and whole-repo pnpm lint. Test Core 1–6 and Lint & Repo Gates are green on this head.
  • The reviewer's first local check:generated flagged api-surface/ — because that build had no .d.ts yet, which the tool itself says. After the DTS pass check:api-surface reads clean, exit 0, worktree clean. The stale flag was the missing-DTS artefact, ⛔ not the PR.
  • ⛔ Could not measure: the merge-base "before" behaviour by execution — a structural argument stands in its place (the diff touches only a string literal inside .describe(), which cannot alter parsing); and the objectui renderer beyond git grep at the pin (no build or run of it).

Seat follow-ups this record does not discharge

  1. collapsible's describe silence (① item 4) — a sibling card, ⛔ not owed on this PR.
  2. The ObjectSchema-sync-entry precision limit (① item 3) applies to all three aliases alike, not just collapsed — pre-existing, recorded.
  3. #19311 is NOT satisfied by this PR. Its card names packages/spec/src/ui/view.zod.ts:3315–3316, where collapsible and collapsed both sit with no .describe() at all — a different schema from the packages/spec/src/data/object.zod.ts row this PR corrects. The seat re-dispatches finding(spec): the sections[].collapsed describe owes an 「implies collapsible」 clause #19311 at its own file; this PR closes as Part of, not Fixes.
  4. One shared side effect to clean up: the reviewer's git worktree add registered a worktree in the shared repo's list. Removable with git worktree remove; left in place as evidence for now.

Generated by Claude Code

@os-support-ai
os-support-ai marked this pull request as ready for review September 22, 2026 12:53
@os-support-ai
os-support-ai added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit fa29803 Sep 22, 2026
43 of 44 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-19311-collapsed-implies-collapsible branch September 22, 2026 13:15
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants