Repository navigation
docs(spec): the span auto clause names the field types the form renderer really widens - #19253
Conversation
…ally widens `FormField.span`'s `'auto'` describe told authors that "wide widgets like textarea/richtext/json/file/subform take the whole row". Re-measured against objectui at the pin this repo builds against: `WIDE_FIELD_TYPES` is ten entries (`textarea`/`markdown`/`html`/`grid`/`richtext`, bare and `field:`-prefixed), so `json` and `file` are spec field types that are NOT wide, `subform` is not a spec field type at all, and `markdown`/`html` — two that ARE wide — went unnamed. Three of the five names were wrong. The clause now names the long-form family the renderer actually widens and says it resolves to the full COLUMN COUNT, leaving the tier sentence beside it to state how far down the container-query tiers that span is emitted. The pin citation moves from the historical spelling to the asserting one, so `check:objectui-pin-citations` reds at the next pin bump and the sentence cannot rot silently. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
`pnpm --filter @objectstack/spec check:generated` proved exactly one artifact stale (`content/docs/references/**`); `gen:docs` moved the two `span` rows in `content/docs/references/ui/view.mdx`. The string's full population re-derived on this tree: src 1 · dist 30 · json-schema 12 · docs 2, and zero occurrences of the retired wording remain. `dist` and `json-schema` are gitignored but both sit in the package's `files[]`, so published bytes move and a `patch` changeset is owed. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 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 ed16912ba16318c966e5ce69b6f436aa536c63ce && git checkout ed16912ba16318c966e5ce69b6f436aa536c63ce
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7056ca50d1d94c596fc7d56a58e758b5339062e5 6a34e6f67a22478036bbe674a7089a5b981192bf && git checkout -B drift-repro 7056ca50d1d94c596fc7d56a58e758b5339062e5 && git merge --no-ff 6a34e6f67a22478036bbe674a7089a5b981192bf
node scripts/docs-audit/affected-docs.mjs --json 7056ca50d1d94c596fc7d56a58e758b5339062e5 |
Contract reviewReviewed at 2026-09-20T05:43Z by the isolated at-tier review subagent for the Served-tier: Trees the readings were taken against: objectstack head ① Derived judgmentsJ1 — BLOCKING. The new The sentence under review asserts that at the pin "only the long-form types (textarea, markdown, html, richtext) resolve to the full column count". I re-derived the wide set by executing the pin's own code over every member of the spec's
Controls for the zeros in this section: What this does and does not say about the fix: the sentence needs to name J2 — PASS. The tier sentence is true at the pin and does not over-claim. Executed: J3 — PASS. The pin guard guards; the control leg proves the conversion bought something. J4 — PASS. Scope held; J5 — PASS. The generated page matches its source. Both ② Semver level
③ Boundary flags
Noted (not blocking):
Implemented-by: VERDICT: FAIL One BLOCKING finding (J1); J2 through J5 pass and are stated plainly as such. What unblocks: the Generated by Claude Code |
…ime path, not the bare one The contract review on #19253 found the replacement clause false in the other direction: it said ONLY textarea/markdown/html/richtext resolve to the full column count, and `repeater` does too. Re-swept along the path the form actually walks — `mapFieldTypeToFormType` then `isWideFieldType` then `resolveColSpan`, every instrument read out of objectui at the pin — over the whole `FieldType` population (49 members, two instruments agreeing). The runtime-path set is five: textarea, markdown, html, richtext and repeater. `repeater` maps to `field:grid`, which is a member of `WIDE_FIELD_TYPES`, so it resolves to 3 cells at grid 3 and 4 at grid 4 and emits `@2xl:col-span-3`. The first sweep took the BARE path (`isWideFieldType(specName)`), which is also what objectui's own pin test measures; that reading is true of the bare path and is not the one an author's field takes. Controls in the same run: json maps to `field:code` and file to `field:file`, both one cell; composite/record map to `field:object`, one cell; `subform` is not a `FieldType` member at all. Delta vs the bare path is exactly `["repeater"]`. No renderer file, no objectui file and no accept set is touched, and the asserting pin citation is kept exactly as it was. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
`check:generated` proved one artifact stale and `--fix` regenerated only it; the re-check then reads "All 16 generated artifacts are up to date". Population of the corrected string, both instruments: src 1 file / 1 occurrence, dist 30 / 30, json-schema 12 files / 22 occurrences, docs 2. The round-1 wording is gone from the tree entirely; the only hit for the ORIGINAL sentence is the changeset's own quotation of it, which is a historical record by construction. `check:objectui-pin-citations` still reads 27 asserting / 45 historical — the asserting citation this rework was told to keep is untouched. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewReviewed at 2026-09-20T06:27Z — round 2, the narrow re-review of the rework — by the isolated at-tier review subagent for the Served-tier: Trees the readings were taken against: objectstack head ① Derived judgmentsJ1 — PASS. The five-member set is complete and correct along the runtime path at the pin. The round-1 defect was one missed member under a closed-set framing; the rework kept the framing, so I re-derived the set over the whole population rather than checking the one name.
J2 — PASS, one imprecision noted. The stub is answer-preserving over the population — shown by execution, not by the argument.
J3 — PASS. The population is 49, by two instruments of mine, and the round-1 55 is explained.
J4 — PASS. The delta is the clause, the changeset and the regenerated page; the guard survived;
J5 — PASS. The PR body and the changeset tell the truth, with one stale sentence in the body's main text that its own footnote withdraws.
② Semver level
③ Boundary flags
Nothing is blocking. The one BLOCKING finding of round 1 is closed by the measurement in J1, and no second missed member exists. Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #18516
Clause-②: no
The defect
packages/spec/src/ui/view.zod.ts—FormField.span, the'auto'clause — shipped this sentence to authors:Three of those five names are wrong, and two types that really are wide went unnamed. An author reads that sentence to decide how to lay out a form.
Re-derived on this tree — not inherited from the card
Both falsifications were re-measured here against objectui at the
.objectui-shapin53ded82bf7, by executing that tree's own code:git show PIN:packages/plugin-form/src/autoLayout.tsverbatim, plus lines 2472-2521 ofgit show PIN:packages/components/src/renderers/form/form.tsx(pickSpanClass, extracted verbatim by line range, 50 lines, sha256 recorded) wrapped in a driver that supplies thecontainerClassthe pin's ownCONTAINER_GRID_COLSproduces.1.
WIDE_FIELD_TYPESis ten entries, and the three named types are in none of thempackages/plugin-form/src/autoLayout.ts:57at the pin, exactly:The
plugin-detailtwin atautoLayout.ts:53carries the same ten in a different order.Executed —
span: 'auto'(key omitted), 3-column grid, at the pin:Swept along the RUNTIME path —⚠️ The BARE path (
mapFieldTypeToFormType→isWideFieldType→resolveColSpan→pickSpanClass.isWideFieldType(specName), which is what objectui's own pin test measures) answers four; every site assigningFormField.typeat the pin maps first, andrepeaterreaches the wide widget only along the mapped path. That difference is what round 1 missed.isWideFieldTyperesolveColSpantextarea@2xl:col-span-3markdown@2xl:col-span-3html@2xl:col-span-3richtext@2xl:col-span-3jsonfilesubformtext— narrow controlrepeaterfield:grid@2xl:col-span-3field:textarea— widget-id control@2xl:col-span-3Swept across the whole⚠️ objectui's own pin test asserts the bare-path four, and that assertion is true of the bare path — it is not the path an authored field takes.
FieldTypevocabulary (49 members, two instruments agreeing — the executedFieldType.optionsand the enum source tokens with comments stripped), the runtime-path wide set is exactly five:textarea,markdown,html,richtextandrepeater.Every zero reading carries a firing control. The three full-row decision sites at the pin are
plugin-form/src/autoLayout.ts,plugin-detail/src/autoLayout.tsandapp-shell/.../studio-design/formFieldType.ts. Searching them forjson/file/subformas type literals: 0 in each. Controltextareain the same three files: 2 / 2 / 0 — the third is a mapper, not a decision site, which is why it reads zero on the control too. Wider control:col-span-fullunderpackages/*/src/*at the pin is 15 hits (section dividers inform.tsx:61, dashboard wrappers, and the designer canvas) — the instrument fires, and none of the 15 is a field-type path for any of the three names.2. The tier claim, and why the fix must not flip its sign
resolveColSpanreturns a cell count, not a class. The class comes frompickSpanClass, which picks the first breakpoint whose column count reaches the target:So at the
@mdtier the grid is two columns and the wide field carries no span class at all — one cell of two, not the row. Probe row, executed:textarea,span: 'auto', 3-column grid, at the pin, class@2xl:col-span-3, 1 of 2 at@md. A replacement sentence promising a whole row at every tier would be exactly as false as the one being replaced, so the clause states the resolved column count and leaves the tier sentence beside it to say how far down the tiers that span is emitted.What the clause says now
Before:
After:
One
.describe()string, one line of diff insrc.The guard — #18508's asserting spelling, and why it applies
The card notes this sentence goes stale at the bump absorbing objectui
bd09957380, which emits one clamped class per multi-column tier.check:objectui-pin-citations(#18508) distinguishes two spellings: an asserting citation (`.objectui-sha` = `SHA`) claims this is the pin we build against and is compared against the pin file, while a historical one (`.objectui-sha` pin `SHA`) is a dated record and is never checked.The clause was carrying the historical spelling while making a claim about today — that is precisely the silent-rot state the gate exists to end — so the citation moves to the asserting form, and the two pin-sensitive halves (the wide-type list and the tier behaviour) now sit under one citation, the second referring to it as "that same pin" so there is one citation, not two.
Reverse-verified, both legs, fix committed first, mutation proven on disk by
scripts/ablation-replace.mjs:1a2b3c4d5e6packages/spec/src/ui/view.zod.ts:2925 cites 1a2b3c4d5e6The control leg is the argument for the spelling change: with a plainly wrong sha, the spelling this clause used to carry passes. Both legs restored under a
trap, each restore proven by blob hash (cd5a238e1930== HEAD) withgit diff HEADempty. The gate readspackages/spec/srcdirectly, sodistis not on its path and no dist preflight applies.Gate after the change: 27 asserting citations match the pin (was 26), 45 historical (was 46) — exactly the one citation converted, nothing else moved.
Population the one string moves — re-derived
The card measured⚠️ that last figure is a file count. Re-derived on this tree for the new string, with the unit stated:
src1 ·dist30 ·json-schema12 · docs 2 —src1 file / 1 occurrence ·dist30 / 30 ·json-schema12 files / 22 occurrences ·content/docs/references/ui/view.mdx2 occurrences. The retired wording is gone from the tree with one deliberate exception: the changeset quotes the original sentence, which is a historical record by construction. ⛔ The earlier phrasing of this line — that zero occurrences remain anywhere outside.git— is withdrawn as over-claimed.check:generatedproved exactly one artifact stale (content/docs/references/**) and--fixregenerated only that one;check:authorable-surface,check:api-surfaceandcheck:api-surface-declarationsall stayed green — the last because it excludes leading TSDoc by design, so a re-worded describe does not churn it.Changeset — owed, and why
@objectstack/specshipsdist,json-schemaandsrc/**/*.zod.tsin itsfiles[], so all three of the moved populations are published bytes. Apatchchangeset is owed and is in this PR;skip-changesetwould be wrong here.Verification
pnpm --filter @objectstack/spec build— green (under the shared verify lock).pnpm --filter @objectstack/spec typecheck— green.pnpm --filter @objectstack/spec test— 500 files / 14632 tests passed.pnpm --filter @objectstack/spec check:generated— green after regenerating the one stale artifact.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 100 derived, 94 run green, 6 NOT MEASURED, 0 unrun — reconciled with--ran(exit 0), every line carrying its exit code captured before any pipe.@objectstack/formula,@objectstack/lint,@objectstack/client-react,@objectstack/objectql, and in one case most of the monorepo).packages/spechas no workspace dependencies, so the closure build is an empty run and those builds are CI's farm:pnpm --filter @objectstack/lint run check:doc-formula-expressions(exit 3),check:doc-security-posture(exit 3),pnpm --filter @objectstack/spec run check:skill-examples(exit 1, needsclient-reactbuilt),pnpm check:docs-transcript-drift(exit 3),pnpm check:dual-build-cjs-loads(exit 3),pnpm check:lean-entry-closure(exit 3). Exit 3 is this repo's "nothing was measured" code — neither a pass nor a finding.Scope
Triage's fence, verbatim and binding:
WIDE_FIELD_TYPESand every other objectui file were read only — no renderer behaviour is touched, and nothing here widens an accept set. No key, default, enum member or exported name moves: the same authored metadata is accepted and refused before and after, which is whatClause-②: nodeclares.Acceptance notes
noted, not filed— the docblock abovespan(view.zod.ts~2914-2924) carries its own citation of the same pin in the historical spelling plus the prose reminder "re-read this block at the pin bump that absorbs it". It states nothing false today and is not an authored-metadata trap, so it is not one of the three filing classes. It is left untouched deliberately: the describe's asserting citation now reds on the adjacent line at the next bump, and whoever re-reads it has the docblock in front of them. Successor: the pin-bump PR that absorbs objectuibd09957380.noted, not filed— the twoWIDE_FIELD_TYPESsets in objectui (plugin-formandplugin-detail) are maintained as byte-equal twins by hand, with no cross-repo pin holding them equal;richtextSurfaceParity.test.tsxcompares the two exported predicates rather than the sets. Nothing is wrong today. Successor: none in this repo — it is an objectui-side observation, and this card's fence forbids touching those files.Body edited by the dispatching seat
domain:spec#4(session_01AmH9bKvGoLjiY86Q4Z3og2) after the rework landed at6a34e6f67a22478036bbe674a7089a5b981192bf. The contract review of record on the previous head (cee5969dee) returned FAIL on one finding — the clause was false in the other direction, becauserepeatermaps to the widefield:gridwidget — and the sections above are updated to the runtime-path measurement. Two counts the review corrected are fixed here: theFieldTypepopulation is 49, not 55, and the json-schema figure is 12 files / 22 occurrences, not 12 occurrences. The round-1 claim that no occurrence of the retired wording remains anywhere is withdrawn: the changeset quotes the original sentence, by construction. The implementer named all of these edits in its report and correctly declined to patch this body itself — a dev writes a PR body once, at the POST that opens it.Generated by Claude Code