docs(spec,ui): colSpan and span state their measured behaviour — the two claims #17328 falsified are gone - #18492
Conversation
… the falsified claims
Browser measurement at all three surface widths falsified two sentences that
were on the tree in four hand-written carriers and one generated one:
- an absolute `colSpan` "only lines up at the width the author imagined" — it
does not. The renderer clamps the span to the section's column count and
emits one container-query-scoped class, so the cell starts at a real column
boundary at every width; rendered overflow was 0px in every configuration,
including `colSpan: 4` in a 3-column section.
- `span: 'full'` gives the "whole row at any column count" — it does not. It
compiles to a span of the section's declared column count gated at the top
breakpoint only, and at the 2-column modal width it renders pixel-identical
to authoring nothing at all.
Each carrier now states the measured behaviour of its key. The preference
clauses ("[legacy — prefer `span`]", "Prefer `span`.", "Prefer this over the
absolute `colSpan`", "(preferred)", "Legacy absolute column span") are removed
rather than reversed: they rest on the falsified claim, and which spelling an
author should reach for is a renderer question this repository does not answer
today. Both keys stay, each described by what it actually does.
Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6
Co-authored-by: Claude <noreply@anthropic.com>
`content/docs/references/ui/view.mdx` is generated from the `.describe()` on `FormField.colSpan` / `FormField.span`; regenerated with `pnpm --filter @objectstack/spec check:generated --fix`, which proved exactly one artifact stale and touched only that one. Both FormField tables in the file (two occurrences each) now carry the measured text. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 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 5d06129fef3c1cfff33a983b9a858f4722ecdcdf && git checkout 5d06129fef3c1cfff33a983b9a858f4722ecdcdf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fb6b2c369e070547aac1f1c071319c1059021de7 38c1c6ccfb7af6929d0fa2171880f4e178fc62f6 && git checkout -B drift-repro fb6b2c369e070547aac1f1c071319c1059021de7 && git merge --no-ff 38c1c6ccfb7af6929d0fa2171880f4e178fc62f6
node scripts/docs-audit/affected-docs.mjs --json fb6b2c369e070547aac1f1c071319c1059021de7 |
Contract reviewServed-tier: ① Derived judgmentsNo accept-set moves:
② Semver level
③ Boundary flags
Required changes for PASSR1. Implemented-by: VERDICT: FAIL Generated by Claude Code |
…jectui commit The replacement text this branch landed was measured at the `.objectui-sha` pin only, and asserted timelessly. It is false on objectui's `main`: in a 3-column grid both `span: 'full'` and a clamped `colSpan: 4` emit `@md:col-span-2 @2XL:col-span-3` there, not the single `@2xl:col-span-3` the pin emits. Worse, the sentence the branch DELETED - "whole row at any column count" - is quoted by the objectui fix's own docblock, its two pins and its changeset as the published declaration they implement, so the text as written would have codified the behaviour that fix had just corrected against it. Each carrier now states what the key RESOLVES to - the form grid's full column count, true on both renderer states - and anchors the tier behaviour to a commit instead of asserting it timelessly. The span is the form grid's column count (explicit form `columns`, else the widest section's `columns`, else inferred), not "the section's declared column count"; those coincide only in the single-section form #17328 measured. The renderer-side fix is objectui#9253, commit `bd09957380` (2026-09-12), part of objectui#9244 - re-derived here by executing both extracted implementations. `.objectui-sha` is deliberately NOT moved: the ladder is unreleased (`@object-ui/components` 17.6.0 at both the pin and objectui HEAD, 0 tags contain the commit), and a pin bump is not a rider. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…span describe `check:doc-authoring` reds an internal issue id in customer-facing `.describe()` prose: it projects into content/docs/references/** and the generated skill artifacts, whose readers have no tracker to resolve `#NNNN` against. The parenthetical now names the measurement (Chromium at 390, 720 and 1700) without the citation-shaped token; the anchor itself stays one line above, in the TSDoc, where the gate's own guidance puts an internal reference. Regenerated content/docs/references/ui/view.mdx to match. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsNo accept-set moves:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Required changes for PASSR1. Seat provenance (PM note, not part of the record)
Generated by Claude Code |
…omitting the key The clause "at intermediate widths the field rendered identically to omitting the key" is false at the `.objectui-sha` pin `53ded82bf7` in a reachable configuration. With a form grid of 3 and a section `columns: 2` (ObjectForm passes the per-section count), a plain field with `span` omitted resolves to an effective 2 and emits `@md:col-span-2` — two of two cells at the 720px modal width — while `span: 'full'` at the pin emits `@2xl:col-span-3` only, which is one of two cells at that width. Not identical, and 'full' is the narrower of the two. The identity holds only where 'auto' resolves to a single cell, which is the one 3-column section the original measurement covered; the parenthetical scoped the measurement widths, not the form configuration. Both carriers now state what is true at every reachable configuration — the field takes a single cell, not the row — and the changeset keeps the pixel-identity reading where it was actually measured. The prose ships inside the published package and generates the public reference table, so a rendering claim that is false anywhere reachable is the defect class this card removes. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
…lause `gen:docs` projection of the `span` describe, both FormField tables. No other generated artifact moved: `check:generated --fix` reported 1 of 15 stale and regenerated only content/docs/references/**. Claude-Session: https://claude.ai/code/session_01KB5PFtxuy1x3dcR5gxudx6 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsAccept set — 0 moves, PR characterisation RIGHT. Public surface — every change is published prose that states behaviour. Judged against the renderer EXECUTED at both states (ported helpers, 3-column container
The pin column reproduces #17328's browser table (emitted classes and the 285px one-of-two modal reading) exactly.
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Clause-② carriers cleared — provenanceBoth carriers stripped in one stroke by the dispatching seat,
How this verdict was produced. This seat's measured served model is Independence pair: a Boundary item carried to #18496, filed before this PR lands so a closing keyword cannot lose it. The review named it as a required correction to shipped prose; ⛔ the seat did not let it ride unnamed, and ⛔ did not fix it in-branch, because that would move the head and void an otherwise complete review record. Pre-landing checks: ① review PASS on record ✅ · ② Generated by Claude Code |
Fixes #17670
Clause-②: yes
#17328 established, by browser measurement in Chromium at three surface widths (390 / 720 / 1700), that two shipped sentences are false. This PR removes them from the six carriers in range and states each key's measured behaviour instead. It invents no replacement guidance and takes no side between the two keys.
Round 2 — remediation. The isolated contract review of head
419e3173cfreturned FAIL: the replacement text was measured at the.objectui-shapin only and asserted timelessly, and it is false on objectui'smain. Every carrier now says what the key resolves to — true on both renderer states — and anchors the tier behaviour to a named commit.The renderer has two states, and the first round only saw one
Re-derived here, not taken on trust: both implementations were extracted from the real git blobs at each commit and executed.
resolveColSpan(packages/plugin-form/src/autoLayout.ts) is byte-identical at both, so the only difference is which classes the form renderer emits.grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3)53ded82bf7mainspan: 'full'@2xl:col-span-3@md:col-span-2 @2xl:col-span-3colSpan: 4@2xl:col-span-3@md:col-span-2 @2xl:col-span-3colSpan: 2@md:col-span-2@md:col-span-2colSpan: 1The pin column reproduces #17328's measured table exactly — the same emitted classes it recorded in Chromium at 390 / 720 / 1700. So the claims "emits it as one container-query-scoped span class" and "gated at the top breakpoint only … identical to omitting the key at every narrower width" are true at the pin and false ahead of it.
⭐ And this is why it is more than staleness. The objectui fix's own docblock, its two pin tests and its changeset quote the sentence round 1 deleted — "whole row at any column count" — as the published declaration they implement. Its commit message says so outright: "That is the spelling
@objectstack/spec(17.4.0) declares as 'whole row at any column count' … so the contract is published and the renderer was not keeping it. Fixed in the renderer rather than by relaxing the declaration." Landing round 1's text would have rewritten the contract to codify the behaviour that fix had just corrected against it.The review named the renderer-side fix
objectui#9244, commit5e9b502fa4, 2026-09-13, and said one commit since the pin touchesform.tsx. Measured on a full (un-shallowed) objectui clone:5e9b502fa4isfix(plugin-dashboard): compose a chart series label in DashboardGridLayout (#9402)— plugin-dashboard only, nothing to do with form spans.bd09957380,fix(form): emit one col-span class per breakpoint tier, not one for the widest (#9253), 2026-09-12, whose trailer readsPart of objectui#9244. It is the commit that introducesspanLadderForand ships.changeset/span-full-tier-ladder-9244.mdand the two-9244pin tests.packages/plugin-form/src/form.tsxdoes not exist; the renderer ispackages/components/src/renderers/form/form.tsx, and 7 commits since the pin touch it, not 1.objectui#9244itself answers 404 over the API, whileobjectui#9253resolves (merged 2026-09-12T06:24:38Z).So the published prose cites the verifiable pair —
objectui#9253/bd09957380— and the changeset recordspart of objectui#9244, which is the spelling objectui's own tree uses. Substance of the review's probe table: confirmed independently, above.git merge-base --is-ancestor PIN origin/mainanswered exit 1 on the shallow clone this container ships and exit 0 aftergit fetch --unshallow(control leg, a commit five back from the pin: exit 0 both times). The shallow negative was a false negative.Carriers, and what each now says
view.zod.ts—.describe()onFormField.colSpancolSpan: 4in a 3-column grid renders as 3;colSpan: 1emits no span class. No tier claim — the clamp holds on both states.view.zod.ts—.describe()onFormField.span'full'resolves to the form grid's full column count; how far down the container-query tiers that span is emitted is stated as the renderer's, anchored at the pin.view.zod.ts— TSDoc abovespan(carrier the card's table missed; it ships indist, ×30)objectui#9253/bd09957380and a "re-read at the pin bump" instruction.content/docs/references/ui/view.mdxcheck:generated --fix, never hand-edited.content/docs/ui/views.mdx:434comment and thespanrow. ThecolSpanrow at:450was already right on both states and is unchanged.content/docs/protocol/objectui/layout-dsl.mdx:223callout was already right and is unchanged.The span is the form grid's column count — the explicit form
columns, else the widest section'scolumns, else inferred — not "the section's declared column count". Those coincide only in a single-section form, which is what #17328 measured.⛔ The pin is deliberately not moved
.objectui-shastays at53ded82bf7. The ladder is unreleased:@object-ui/componentsis17.6.0at both the pin and objectui HEAD, andgit tag --contains bd09957380returns 0 tags. A pin bump is not a rider on a prose PR. This text anchors the pin; it does not move it.Counts, both sides, with controls
a3dd4bbd4a; the clause it counts was replaced in round 3 — see the round-3 section at the end for the counts at the current head. Whitespace-normalised occurrence counts, so a phrase wrapped across comment lines cannot read as a false zero (the failure mode that made the card's own carrier table miss carrier 3). Measured ata3dd4bbd4aafter the final build and regeneration.distjson-schemaview.mdxviews.mdxlayout-dsl.mdxgated at the top breakpoint onlyemits it as one container-query-scoped span classsection's declared column countit is not a whole row at every column countresolves to the form grid's full column countform grid's current column count.objectui-sha pin 53ded82bf7(the gate's historical spelling)objectui#9253colSpanzzz-no-such-tokenobjectui#9253reads 0 injson-schemaon purpose: that surface carries.describe()strings only, and the forward pointer lives in the TSDoc. The TSDoc reachingdist×30 is the measured reason carrier 3 is not "source-only".check:generatedfound exactly one of 15 artifacts stale (content/docs/references/**) and--fixmoved only that one;check:generatedre-runs green, 15/15 current.Verification
Exit codes captured by redirect-then-
$?, never through a pipe. Heavy runs throughscripts/pm/os-verify-lock.sh; the verdict quoted is the wrapper's ownVERDICT command-exitline.pnpm --filter @objectstack/spec build→ exit 0 (twice; the second after the describe edit below).pnpm --filter @objectstack/spec test→ exit 0 —Test Files 483 passed (483),Tests 13773 passed (13773).pnpm --filter @objectstack/spec typecheck→ exit 0 (tsc --noEmit,check:scripts-typecheck,check:test-typecheck).pnpm --filter @objectstack/spec check:generated→ exit 0, 15/15 current.pnpm --filter @objectstack/spec check:objectui-pin-citations→ exit 0 (R9): 26 asserting citations match.objectui-sha, 38 historical recorded — the two new historical citations parse in the recognised spelling.check:docs,check:authorable-surface,check:api-surface,check:doc-anchors,check:doc-authoring,check:docs-spec-enumerations,check:docs-single-h1,check:quick-reference-counts,check:objectui-changeset,check:docs-audit-scope,check:doc-frontmatter,check:spec-docblock-symbol-anchors,check:adr-0087-registration,check:changeset-no-major,check:empty-changeset,check:closing-keyword-parity,check:nul-bytes→ all exit 0.pnpm lint(repo-wideeslint . --no-inline-config) → exit 0 ata3dd4bbd4a, 16:57:58Z–16:59:42Z. Not narrowed; the union ran at the final commit.node scripts/pm/check-clause2-carriers.mjs --pair 18492→ exit 0, both carriers agree.spandescribe with(objectstack#17328, Chromium at 390/720/1700).check:doc-authoringreds that: an internal issue id in customer-facing.describe()prose, which projects intocontent/docs/references/**and the generated skill artifacts, whose readers have no tracker to resolve#NNNNagainst. Per the gate's own guidance the parenthetical keeps the measurement and drops the citation-shaped token; the anchor stays one line above in the TSDoc, where the gate puts internal references. Control: no other.describe()inview.zod.tscarries an issue id (0). The gate is green after the change.NOT MEASURED, stated rather than assumed green: the remaining families in
dispatch-gates --commands(102 commands at this change set) beyond those listed, the 50 artifact-roster families whose silence is a fact about a list, the 11 declared-wide families, the 5 families taking a value from the workflow, and the 6 path-scheduled CI jobs. Those are CI's run.dispatch-gatesalso warns this branch is ≥10 commits behindorigin/mainand that 6 gate scripts it derives from changed in that range — the derivation read the branch's older copies.Changeset
@objectstack/spec: minor, unchanged. Its two bullets now match the carriers, and the sentence claiming the renderer question "is not answered today" is replaced by one namingobjectui#9253/bd09957380and its unreleased status. Nothing an author writes moves: both keys unchanged, both still parse, no validation, default or emitted class changes.Acceptance notes
Out of this PR's scope; nothing here is filed by this PR.
skills/objectstack-ui/rules/navigation.md:139still carries the falsified prescription. Governed surface, already filed as skills/objectstack-ui/rules/navigation.md still prescribes span over colSpan on the basis #17328 measured false, and calls colSpan deprecated when nothing in packages/spec deprecates it #18496 by the seat — carried, not re-filed.examples/app-showcase/src/ui/views/task.view.ts:363-366carries the same claim plus the withdrawn lint rule; it escapes a line-oriented grep because the phrase breaks across a comment line. Noted, not filed — successor to the same routing round as skills/objectstack-ui/rules/navigation.md still prescribes span over colSpan on the basis #17328 measured false, and calls colSpan deprecated when nothing in packages/spec deprecates it #18496, which already records it.check:cross-package-test-inputsfails locally wheneverpackages/spec/distexists and is structurally dormant in the CI job that runs it. Already carried by [regression] check:cross-package-test-inputs 的裁决取决于被 gitignore 的空目录 packages/spec/dist 存不存在 —— 构建过的工作树一律红,CI 绿只因那一步跑在构建之前(#18340 引入) #18348 since 01:51Z; [finding]check:cross-package-test-inputsgoes red the momentpackages/specis built locally —packages/cli/test/init-created-files-summary.e2e.test.tswalks intopackages/spec/dist/and no declared glob reaches it #18495 is a probable duplicate. ⛔ Not re-filed here — the seat holds the dedupe.content/docs/protocol/objectui/layout-dsl.mdx:846lists aFormFieldinterface withcolSpanand nospan. Incompleteness, not an error — copying it does not fail. Noted, not filed; successor: whoever next edits that interface block.bd09957380— carrier 3's TSDoc says so in-place, which is the only durable channel prose has. Dedupe words:spanLadderFor,objectui#9253,span-full-tier-ladder,top breakpoint only.Maintainer note on the label
needs:contract-reviewis the seat's to hang on both carriers. This branch does not hang, strip or wait on it. Its presence is a moving state, not a fact this body can assert: the seat hung it, cleared it from both carriers on the at-tier FAIL, and re-hung it on both at 2026-09-16T19:24:04Z when the remediation push landed. ⛔ Do not read this section as a claim about the label's current state — read the labels.Round 3 — remediation of the at-tier FAIL
The at-tier contract review of head
a3dd4bbd4areturned FAIL on one clause, and only that clause. Record: #18492 (comment) · handover with R1-R5: #17670 (comment)The defect. "so at intermediate widths the field rendered identically to omitting the key" is FALSE at the
.objectui-shapin53ded82bf7in a reachable configuration: form grid 3 with a sectioncolumns: 2— a plain field withspanomitted resolves to eff 2 (round(3/2)) →@md:col-span-2→ 2 of 2 cells at 720px, whilespan: 'full'at the pin →@2xl:col-span-3→ 1 of 2 cells. Not identical, and'full'is the narrower one. The identity holds only in the single 3-column section #17328 measured.What changed — three exact string replacements, nothing else. The
spandescribe (view.zod.ts:2622), the TSDoc above it (:2616-2617, the carrier that wraps across two comment lines), and the changeset bullet. Each OLD string was verified uniquely present in the HEAD blob before replacement, and the edited tree was proved byte-identical to HEAD-plus-exactly-those-three-replacements — no near-miss, no drive-by edit. Nothing the record called RIGHT was touched.Counts at the head this PR now carries, both sides. Occurrences (not lines), wrap-tolerant, over
packages/spec/src·packages/spec/dist·packages/spec/json-schema·content/docs/references:colSpanzzz-no-such-tokenRegeneration.
check:generated --fixreported exactly 1 of 15 artifacts stale and moved onlycontent/docs/references/ui/view.mdx(2 rows, both the span row). Then, all exit 0:check:generated,check:docs(223 generated files in sync),check:doc-authoring(8619 describe strings clean),check:objectui-pin-citations(26 asserting citations match.objectui-sha, 38 historical recorded),check-changeset-no-major,check-empty-changeset,check-adr-0087-registration,check:nul-bytes, plus 40 further derived gate invocations.The item the review left NOT MEASURED is now measured.
pnpm --filter @objectstack/spec test→ exit 0, 483 test files / 13773 tests passed.Two gates refused a verdict rather than false-greening —
check:docs-transcript-drift(exit 3, PREREQUISITE NOT MET) andcheck:skill-examples(exit 1) both need a built dependency. Their prerequisites were satisfied and both then re-ran exit 0. ⛔ Neither was recorded as a red.One intermediate red, accounted for. The WIP push
d341967995failedType Check · source gates. Despite the job's name the failing step wascheck:docs— the generated projection had not been regenerated yet — and the job's own "Type check (@objectstack/spec)" step PASSED on that same head. Reproduced locally by restoring the stale projection (mutation proved on disk, then restored,git diff HEADempty). It is gone on the current head:Type Check · source gatesandTypeScript Type Checkboth success.CI on this head, latest run per check NAME: 35 names — 33 success, 2 skipped (
Console Pin Gate,Packed-tarball smoke (opt-in)— NOT MEASURED, not verdicts), 0 failure, 0 cancelled.A fresh at-tier review is owed on this head: the FAIL record named an older one, and a record that does not name the current head cannot land it.
Generated by Claude Code