Skip to content

docs(spec,ui): colSpan and span state their measured behaviour — the two claims #17328 falsified are gone - #18492

Merged
os-litant merged 6 commits into
mainfrom
claude/issue-17670-colspan-span-false-claims
Sep 17, 2026
Merged

os-litant merged 6 commits into
mainfrom
claude/issue-17670-colspan-span-false-claims

Conversation

@os-warren

@os-warren os-warren commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

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 419e3173cf returned FAIL: the replacement text was measured at the .objectui-sha pin only and asserted timelessly, and it is false on objectui's main. 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.

probe, 3-column grid (grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3) effective span at the pin 53ded82bf7 at objectui main
span: 'full' 3 @2xl:col-span-3 @md:col-span-2 @2xl:col-span-3
colSpan: 4 3 @2xl:col-span-3 @md:col-span-2 @2xl:col-span-3
colSpan: 2 2 @md:col-span-2 @md:col-span-2
colSpan: 1 1 (no class) (no class)

The 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.

⚠️ A correction to the review record's own citation

The review named the renderer-side fix objectui#9244, commit 5e9b502fa4, 2026-09-13, and said one commit since the pin touches form.tsx. Measured on a full (un-shallowed) objectui clone:

  • 5e9b502fa4 is fix(plugin-dashboard): compose a chart series label in DashboardGridLayout (#9402) — plugin-dashboard only, nothing to do with form spans.
  • The span-ladder commit is bd09957380, fix(form): emit one col-span class per breakpoint tier, not one for the widest (#9253), 2026-09-12, whose trailer reads Part of objectui#9244. It is the commit that introduces spanLadderFor and ships .changeset/span-full-tier-ladder-9244.md and the two -9244 pin tests.
  • packages/plugin-form/src/form.tsx does not exist; the renderer is packages/components/src/renderers/form/form.tsx, and 7 commits since the pin touch it, not 1.
  • objectui#9244 itself answers 404 over the API, while objectui#9253 resolves (merged 2026-09-12T06:24:38Z).

So the published prose cites the verifiable pair — objectui#9253 / bd09957380 — and the changeset records part 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/main answered exit 1 on the shallow clone this container ships and exit 0 after git 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

# carrier now
1 view.zod.ts.describe() on FormField.colSpan clamped to the form grid's current column count; colSpan: 4 in a 3-column grid renders as 3; colSpan: 1 emits no span class. No tier claim — the clamp holds on both states.
2 view.zod.ts.describe() on FormField.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.
3 view.zod.ts — TSDoc above span (carrier the card's table missed; it ships in dist, ×30) same shape, plus the forward pointer to objectui#9253 / bd09957380 and a "re-read at the pin bump" instruction.
4 content/docs/references/ui/view.mdx regeneration onlycheck:generated --fix, never hand-edited.
5 content/docs/ui/views.mdx :434 comment and the span row. The colSpan row at :450 was already right on both states and is unchanged.
6 content/docs/protocol/objectui/layout-dsl.mdx "Custom Span Widths" prose. The :223 callout was already right and is unchanged.

The span is the form grid's column count — the explicit form columns, else the widest section's columns, 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-sha stays at 53ded82bf7. The ladder is unreleased: @object-ui/components is 17.6.0 at both the pin and objectui HEAD, and git tag --contains bd09957380 returns 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

⚠️ The table below is round-2 vintage, measured at 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 at a3dd4bbd4a after the final build and regeneration.

spelling src dist json-schema generated view.mdx views.mdx layout-dsl.mdx changeset
OLD gated at the top breakpoint only 0 0 0 0 0 0 0
OLD emits it as one container-query-scoped span class 0 0 0 0 0 0 0
OLD section's declared column count 0 0 0 0 0 0 0
OLD it is not a whole row at every column count 0 0 0 0 0 0 0
NEW resolves to the form grid's full column count 2 60 22 2 1 1 1
NEW form grid's current column count 1 30 22 2 0 0 0
NEW .objectui-sha pin 53ded82bf7 (the gate's historical spelling) 2 60 22 2 0 0 1
NEW objectui#9253 1 30 0 0 1 1 1
lit control colSpan 4 852 88 8 1 2 5
dark control zzz-no-such-token 0 0 0 0 0 0 0

objectui#9253 reads 0 in json-schema on purpose: that surface carries .describe() strings only, and the forward pointer lives in the TSDoc. The TSDoc reaching dist ×30 is the measured reason carrier 3 is not "source-only".

check:generated found exactly one of 15 artifacts stale (content/docs/references/**) and --fix moved only that one; check:generated re-runs green, 15/15 current.

Verification

Exit codes captured by redirect-then-$?, never through a pipe. Heavy runs through scripts/pm/os-verify-lock.sh; the verdict quoted is the wrapper's own VERDICT command-exit line.

  • pnpm --filter @objectstack/spec buildexit 0 (twice; the second after the describe edit below).
  • pnpm --filter @objectstack/spec testexit 0Test Files 483 passed (483), Tests 13773 passed (13773).
  • pnpm --filter @objectstack/spec typecheckexit 0 (tsc --noEmit, check:scripts-typecheck, check:test-typecheck).
  • pnpm --filter @objectstack/spec check:generatedexit 0, 15/15 current.
  • pnpm --filter @objectstack/spec check:objectui-pin-citationsexit 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-wide eslint . --no-inline-config) → exit 0 at a3dd4bbd4a, 16:57:58Z–16:59:42Z. Not narrowed; the union ran at the final commit.
  • node scripts/pm/check-clause2-carriers.mjs --pair 18492exit 0, both carriers agree. ⚠️ Round-2 vintage. After the at-tier FAIL, the seat cleared the gate from both carriers (that is what a FAIL does), the head moved twice, and the gate was re-hung on both carriers at 2026-09-16T19:24:04Z. Between the clear and the re-hang the same command read exit 4, naming exactly that state. Re-read it rather than quoting this line.

⚠️ One red, and it falsifies the review's R2 string. R2 prescribes ending the span describe with (objectstack#17328, Chromium at 390/720/1700). check:doc-authoring reds that: an internal issue id in customer-facing .describe() prose, which projects into content/docs/references/** and the generated skill artifacts, whose readers have no tracker to resolve #NNNN against. 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() in view.zod.ts carries 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-gates also warns this branch is ≥10 commits behind origin/main and 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 naming objectui#9253 / bd09957380 and 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.

  1. skills/objectstack-ui/rules/navigation.md:139 still 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.
  2. examples/app-showcase/src/ui/views/task.view.ts:363-366 carries 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.
  3. check:cross-package-test-inputs fails locally whenever packages/spec/dist exists 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-inputs goes red the moment packages/spec is built locally — packages/cli/test/init-created-files-summary.e2e.test.ts walks into packages/spec/dist/ and no declared glob reaches it #18495 is a probable duplicate. ⛔ Not re-filed here — the seat holds the dedupe.
  4. content/docs/protocol/objectui/layout-dsl.mdx:846 lists a FormField interface with colSpan and no span. Incompleteness, not an error — copying it does not fail. Noted, not filed; successor: whoever next edits that interface block.
  5. The spec/objectui contract will need reconciling at the pin bump that absorbs 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-review is 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 a3dd4bbd4a returned 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-sha pin 53ded82bf7 in a reachable configuration: form grid 3 with a section columns: 2 — a plain field with span omitted resolves to eff 2 (round(3/2)) → @md:col-span-2 → 2 of 2 cells at 720px, while span: '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 span describe (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:

string src dist json-schema generated docs
OLD "identically to omitting the key" 0 0 0 0
NEW "a single cell, not the row" 2 60 22 2
lit control colSpan 131 852 88 8
dark control zzz-no-such-token 0 0 0 0

Regeneration. check:generated --fix reported exactly 1 of 15 artifacts stale and moved only content/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-greeningcheck:docs-transcript-drift (exit 3, PREREQUISITE NOT MET) and check: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 d341967995 failed Type Check · source gates. Despite the job's name the failing step was check: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 HEAD empty). It is gone on the current head: Type Check · source gates and TypeScript Type Check both 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

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

github-actions Bot commented Sep 16, 2026

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 listnot 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.

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 fb6b2c369e070547aac1f1c071319c1059021de7packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5d06129fef3c1cfff33a983b9a858f4722ecdcdf — the merge of head 38c1c6ccfb7af6929d0fa2171880f4e178fc62f6 into base fb6b2c369e070547aac1f1c071319c1059021de7, 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 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

⚠️ 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: CONTRACT_REVIEW_TIER
Head-sha: 419e3173cf283095184eb54098ce2bb5961dae35

① Derived judgments

No accept-set moves: colSpan stays z.number().int().min(1).max(4).optional(), span stays z.enum(['auto','full']).default('auto') (diff, packages/spec/src/ui/view.zod.ts:2610,2617). Every change is published prose. Judged one by one, against the renderer executed at BOTH states it exists in — the .objectui-sha pin 53ded82bf7 (2026-09-05) and objectui main ff1d5ea8 (2026-09-14), which differ by exactly one commit touching form.tsx: 5e9b502fa4 (2026-09-13, objectui#9244, spanLadderFor).

  1. FormField.colSpan.describe() (src ×1, dist ×30, json-schema ×12, generated view.mdx ×2 rows) — WRONG in two clauses. "clamps it to the current column count … cell starts at a real column boundary at every surface width … colSpan: 1 emits no class at all" — RIGHT on both renderer states (resolveColSpan clamps min(colSpan, grid); colSpan && colSpan > 1 gates the class; probes: colSpan:4 in 3-col → eff=3 at both, colSpan:1"" at both). "emits it as one container-query-scoped span class … that class is inert below the breakpoint at which that many columns exist" — FALSE on objectui ≥ 5e9b502fa4: colSpan:4 in a 3-column grid emits @md:col-span-2 @2xl:col-span-3 (two classes, the first active at the 2-column tier). True only at the pin (@2xl:col-span-3).
  2. FormField.span.describe() 'full' clauseWRONG. "compiles to a span of the section's declared column count, gated at the top breakpoint only — … identical to omitting the key at every narrower width" — FALSE on objectui ≥ 5e9b502fa4: span:'full' in a 3-column grid emits @md:col-span-2 @2xl:col-span-3, the whole row at the 2-column tier too. True at the pin (@2xl:col-span-3 only — [finding] absolute-colspan-discouraged asserts a misalignment that does not happen — measured in Chromium at all three surface widths, and its recommended alternative span: 'full' is the spelling that misrenders #17328's reading). Also imprecise on both states: the span is the form grid's column count = explicit form columns ?? widest section's columns ?? inferred (ObjectForm.tsx:1359-1361), not "the section's declared column count" (they coincide only in a single-section form, which is what [finding] absolute-colspan-discouraged asserts a misalignment that does not happen — measured in Chromium at all three surface widths, and its recommended alternative span: 'full' is the spelling that misrenders #17328 measured). ⭐ objectui#9244's own docblock, its two tests (form-span-tier-ladder-9244.test.tsx, spanFullTierLadder-9244.test.tsx) and its changeset (.changeset/span-full-tier-ladder-9244.md) quote the sentence this PR DELETES — "whole row at any column count" — as "the published declaration" they implement. The PR rewrites the contract to codify the bug the implementer fixed against that contract, three days before the PR.
  3. TSDoc above spanWRONG (same clause), and it ships: Browser measurement at three surface widths is in src ×1 AND dist ×30 (tsup preserves it). The round's "source-only carrier" holds for the generated docs only (grep -c 2578 view.mdx = 0, control colSpan = 2).
  4. Generated content/docs/references/ui/view.mdx — RIGHT as a regeneration: both describe strings appear exactly twice each (pipe-escaped), check:docs ✓ in check:generated on the built worktree; it inherits the wrong text of 1–2.
  5. content/docs/ui/views.mdx:434 comment and :449 span row WRONG (same clause); :450 colSpan row RIGHT ("clamped … grid-aligned at every surface width" holds on both states).
  6. content/docs/protocol/objectui/layout-dsl.mdx:223 callout RIGHT (only "legacy" removed); :257-261 "Custom Span Widths" WRONG (same clause).
  7. Prescription removal — option A — RIGHT and complete in scope. Evidence gathered here: packages/spec has no deprecation record for colSpan.deprecated(/retiredKey/UNKNOWN_KEY_GUIDANCE/ADR-0087 conversion hits for colSpan = 0 each (lit control retiredKey = 931 lines); .changeset/17328-colspan-rule-withdrawn.md:13-14 states "FormField.colSpan is unchanged and still valid" and its adr-0087 marker says "FormField.colSpan is untouched". C ("legacy" as history) would assert a status no record supports; B would leave a prescription whose only basis was measured false. Residue inside the five files after the PR: legacy|prefer|deprecat near colSpan|span = 0 lines (lit control: 6 colSpan lines). One soft residue: TSDoc "authored instead of an absolute column span" — intent, not prescription; removed anyway by R3 below.
  8. Changeset .changeset/17670-colspan-span-measured-behaviour.md — level consistent with the declaration (§②); body restates the false-on-main clauses as the keys' behaviour and says the renderer question "is not answered today" — objectui#9244 answered it on 2026-09-13.

② Semver level

@objectstack/spec: minor. Rule: AGENTS.md:1067-1068 — "The declaration is Clause-②: yes|noyes takes at least minor". Declaration read with the repo's own parser (readClause2Line): card Claim comment 5699630645 → {"kind":"declared","value":"yes","arm":null}; PR body → {"kind":"near-miss","reason":"inline-key"} (the key is mid-sentence, not line-initial); changeset → null. .claude/skills/pm-dispatch/SKILL.md:474 says the enqueue gate reads only the claim comment and only line-initial keys, and check-clause2-carriers.mjs --pair 18492 --pair-json → exit 0 "readable in the fixed spelling and both carriers agree"; CI Check Changeset success. Re-derivation on the packed tarball (pnpm packobjectstack-spec-17.4.0.tgz, 1887 files): OLD sentences 0 in src/dist/json-schema/other; NEW sentences src 1 / dist 30 / json-schema 12; lit control colSpan 2/30/12/1; dark control 0. Nothing but prose moves — no key, export, enum member or default. By SKILL.md:476's criterion (放宽接受集或扩大公开面) the honest declaration is no and the floor patch (AGENTS.md:1065-1066 "a bug fix in a released package takes a patch"); the seat's yes is over-conservative and minor is its consistent floor — gates accept it (check-adr-0087-registration ✓, check-changeset-no-major --event level axis ✓ "declares clause-② yes, no package … graded patch", check-empty-changeset ✓). Level stands as declared; not a blocker.

③ Boundary flags

Required changes for PASS

R1. packages/spec/src/ui/view.zod.ts:2610 — replace the colSpan describe string with exactly: Absolute column span (1-4). The renderer clamps it to the form grid's current column count, so the cell starts at a real column boundary at every surface width and never overflows (colSpan: 4in a 3-column grid renders as 3); acolSpanwithin the column count renders as authored, andcolSpan: 1 emits no span class at all.
R2. view.zod.ts:2617 — replace the 'full': clause (from 'full': to the closing quote) with exactly: 'full': resolves to the form grid's full column count. How far down the container-query tiers that span is emitted is the renderer's, not this key's: at the .objectui-shapin53ded82bf7 only the widest tier's class is emitted (@2XL:col-span-3 for a 3-column grid), so at intermediate widths the field rendered identically to omitting the key (objectstack#17328, Chromium at 390/720/1700).
R3. view.zod.ts:2611-2616 — replace the TSDoc block with exactly: /** [#2578] Relative field width. 'full' resolves to the form grid's full column count (plugin-form resolveColSpan); which container-query tiers receive the span class is the form renderer's. At the .objectui-shapin53ded82bf7it emits the widest tier's class only, so at intermediate widths the field renders identically to omitting the key (objectstack#17328). objectui#9244 (objectui5e9b502fa4, ahead of that pin) emits one clamped class per multi-column tier, making 'full' the whole row at every multi-column tier — re-read this block at the pin bump that absorbs it. */
R4. content/docs/ui/views.mdx:434 — comment becomes // Resolves to the form grid's full column count.
R5. views.mdx:449 span row description becomes: Relative width — 'full'resolves to the form grid's full column count; which container-query tiers emit the span is the renderer's (widest tier only at objectui53ded82bf7, per objectstack#17328; one clamped class per tier since objectui#9244). Keep :450 unchanged.
R6. content/docs/protocol/objectui/layout-dsl.mdx:257-261 — replace the span: full … sentences with: `span: full` resolves to the form grid's full column count (the explicit form `columns`, else the widest section's `columns`, else inferred from field count). Which container-query tiers emit that span is the renderer's: widest tier only at objectui `53ded82bf7` (objectstack#17328), one clamped class per tier since objectui#9244. Omitting `span` (`auto`) lets the renderer size the field from its widget type and the current column count.
R7. Regenerate: pnpm --filter @objectstack/spec check:generated --fix (only content/docs/references/** may move), then check:generated green again; report the four counts both sides (OLD "gated at the top breakpoint only" → 0/0, NEW "resolves to the form grid's full column count" → 1 src / 2 generated) with the colSpan control.
R8. .changeset/17670-colspan-span-measured-behaviour.md — bullet 1: drop "emits one container-query-scoped class"; bullet 2: anchor "gated at the top breakpoint only" to "at the .objectui-sha pin 53ded82bf7"; replace "a renderer question … that this repository does not answer today" with a sentence naming objectui#9244 (5e9b502fa4, 2026-09-13, unreleased at that pin) as the renderer-side fix that emits one clamped class per tier. Level stays minor under the declared yes.
R9. Re-run pnpm --filter @objectstack/spec check:objectui-pin-citations (the new historical-spelling citations must parse) and pnpm --filter @objectstack/spec test; report exit codes by redirect-then-$?.
R10 (recommended, not blocking). Add a line-initial Clause-②: yes to the PR body.

Implemented-by: claude/issue-17670-colspan-span-false-claims
Reviewed-by: session_01KB5PFtxuy1x3dcR5gxudx6

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>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a3dd4bbd4a66186d1200f08ae7de06c3ce7e8bb6

① Derived judgments

No accept-set moves: colSpan stays z.number().int().min(1).max(4).optional() and span stays z.enum(['auto','full']).default('auto') on both sides of the diff (merge-base 85c6d76ec4 vs head); packages/spec/api-surface (17 tracked files) diff merge-base..head is empty; the packed tarball's json-schema carries colSpan as integer 1..4 and span as enum ['auto','full'] default 'auto' in all 12 files that carry the describe. Every change is published prose. Judged one by one against the renderer executed at both states — the .objectui-sha pin 53ded82bf7 (form.tsx blob 1c0be43581, autoLayout.ts blob d5c39cadd4) and objectui main 542718f457 (2026-09-16T16:29:41Z; form.tsx blob a8b587354b, autoLayout.ts blob 9373fc11e7) — with the implementations extracted from the git blobs by line range (pickSpanClass pin L2472-2521 + gate L2523-2528; SPAN_BREAKPOINTS/SPAN_CLASS/BARE_SPAN_CLASS/spanLadderFor main L126,142-163,166-170,204-232 + call site L2785-2786; resolveColSpan/applyAutoColSpan/CONTAINER_GRID_COLS — byte-identical at both states, diff exit 0 on the 67 extracted lines) and run under Node 22.22 type-stripping. 3-column grid (grid gap-4 grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3): span:'full' → eff 3 → pin @2xl:col-span-3 (cells 1/1 · 1/2 · 3/3), main @md:col-span-2 @2xl:col-span-3 (1/1 · 2/2 · 3/3); colSpan:4 → identical to span:'full' at both states; colSpan:2@md:col-span-2 both; colSpan:1 → no class both. The pin column reproduces #17328's body table rows 35-46 exactly.

  1. FormField.colSpan.describe() (src ×1, dist ×30, json-schema ×12, generated view.mdx ×2) — RIGHT. "never overflows … starts at a real column boundary at every surface width": every probe row at both states has cells ≤ tier columns. "colSpan: 4 in a 3-column grid renders as 3": 3/3 at the 3-column tier, both states. "within the column count renders as authored": colSpan:2 in 3-col → @md:col-span-2 (2/2, 2/3) both. "colSpan: 1 emits no span class": (none) both. No tier claim is made, so the state difference at the @md tier (pin 1/2, main 2/2 for colSpan:4) does not falsify it. Soft note only: "clamps it to the form grid's current column count" reads as per-tier, and at the pin the intermediate tier renders one cell, not the tier's count — noted in ③ F8, not required.
  2. FormField.span.describe() 'full' clause (src ×1, dist ×30, json-schema ×12, generated ×2) — WRONG in one clause. RIGHT: "resolves to the form grid's full column count" (resolveColSpan returns grid; grid = explicit form columns ?? widest section columns ?? inferColumns(field count), ObjectForm.tsx pin :1259-1260 / main :1360-1361, one grid per form, fieldContainerClass = containerGridColsFor(formColumns) pin :1347). RIGHT and correctly scoped: "at the .objectui-sha pin 53ded82bf7 only the widest tier's class is emitted (@2xl:col-span-3 for a 3-column grid)" — probe, pin column; also 2-col → @md:col-span-2, 4-col → @4xl:col-span-4. FALSE at the pin in a reachable configuration: "so at intermediate widths the field rendered identically to omitting the key". Form grid 3 with a section columns: 2 (ObjectForm passes secCols per section, pin :1329-1330): a plain field with span omitted resolves to eff 2 (round(3/2)) → @md:col-span-22 of 2 cells at 720px at both states; span:'full' at the pin → @2xl:col-span-31 of 2 cells at 720px. Not identical, and 'full' is the narrower one. The identity holds only where auto resolves to one cell — the single 3-column section [finding] absolute-colspan-discouraged asserts a misalignment that does not happen — measured in Chromium at all three surface widths, and its recommended alternative span: 'full' is the spelling that misrenders #17328 measured. The parenthetical scopes the measurement widths, not the form configuration, so the sentence is not correctly scoped. A rendering claim in shipped .describe() prose that is false in a reachable configuration is the defect class this PR exists to remove.
  3. TSDoc above span (ships: dist ×30 files incl. the .js/.mjs bundles, objectui#9253 30/30 in the tarball) — same clause WRONG ("renders identically to omitting the key", :2616). The rest RIGHT: plugin-form resolveColSpan ✓; objectui#9253 / bd09957380 / 2026-09-12 / "ahead of that pin" ✓ (REST pulls/9253 merged 2026-09-12T06:24:38Z, merge_commit_sha bd0995738035…; compare/53ded82bf7...bd09957380 ahead_by 726 behind_by 0; unshallowed merge-base --is-ancestor exit 0); "one clamped class per multi-column tier, making 'full' the whole row at every multi-column tier" ✓ at main (4-col: 2/2 · 3/3 · 4/4); spanLadderFor enters objectui in exactly one commit (git log -Sbd09957380 only) and the span region L110-235 plus call site are byte-identical from bd09957380 to main (diff exit 0). "re-read this block at the pin bump" — an in-place instruction only; see ③ F2.
  4. Generated content/docs/references/ui/view.mdxRIGHT as a regeneration (check:generated 15/15 current at head, exit 0; check:docs "223 generated files in sync", exit 0); inherits item 2's wrong clause in both rows.
  5. content/docs/ui/views.mdx :434 comment and :449 span row — RIGHT on both states ("resolves to the form grid's full column count"; "widest tier only at objectui 53ded82bf7, per objectstack#17328; one clamped class per tier since objectui#9253" — true for 'full' at main, and #9253 is the sole introducing commit). :450 colSpan row unchanged, RIGHT on both.
  6. content/docs/protocol/objectui/layout-dsl.mdx :223 (only "legacy" removed) RIGHT; :257-262 RIGHT — the resolution order "(explicit form columns, else the widest section's columns, else inferred from field count)" verified in ObjectForm.tsx at both states and applyAutoLayoutinferColumns(fields.length) for the flat path; tier sentence correctly scoped to 53ded82bf7 / since objectui#9253.
  7. Changeset — level consistent (§②). Bullet 1 RIGHT. Bullet 2's "so at the 2-column modal width it rendered pixel-identical to authoring nothing at all" is the same over-general clause as item 2 (WRONG in the same configuration). "objectui#9253 (commit bd09957380, 2026-09-12, part of objectui#9244)": the pair resolves; objectui#9244 → HTTP 404 on both issues/9244 and pulls/9244 (lit control: 9243/9245/9246 → 200; repo public) — it is objectui's own lineage spelling (commit trailer "Part of objectui#9244", artifacts -9244), and CHANGELOG.md is in files[], so it will ship; ③ F4, not blocking. "unreleased" RIGHT: @object-ui/components and plugin-form 17.6.0 at pin, ladder and main; npm latest 17.6.0 published 2026-08-24T14:44:48Z (before the pin); .changeset/span-full-tier-ladder-9244.md still present at main (unconsumed); 0 of 2896 tags contain bd09957380.
  8. Prescription removal (option A)RIGHT, unchanged from the earlier record's reading; residue in the five carriers: prefer \span`, Prefer this over the absolute, legacy absolute= 0 outside the PR changeset's own quotations (sweep, lit controlcolSpan` 35 files / 200 occurrences, dark 0).
  9. Citation correction (seat's Q1 = A)RIGHT. 5e9b502fa4 = fix(plugin-dashboard): compose a chart series label in DashboardGridLayout (#9402) (REST commits/5e9b502fa4: 5 files, all packages/plugin-dashboard/** + a changeset); it appears 0 times on the head tree and 0 in the tarball. packages/plugin-form/src/form.tsx does not exist at pin or main (cat-file -e exit 128 both); 7 commits since the pin touch the real renderer, only bd09957380 span-related. Publishing the withdrawn sha would have been the defect this PR removes.
  10. Gate resolution (seat's Q2 = A)RIGHT. The R2 string reproduces the red: check:doc-authoring exit 1, packages/spec/src/ui/view.zod.ts:2622 #17328 [.describe()]; at head exit 0 (8619 describe strings clean). The gate's own guidance: "Where the id was the whole parenthetical, the parenthetical goes with it … move it to an adjacent // comment" — here the id was half of the parenthetical, the measurement is the customer-meaningful half, and the TSDoc already carried [#2578] (house convention for ids in doc comments), so keeping the measurement and moving the anchor is the guidance's own path. Note the TSDoc ships in dist (IDE hover), which the gate tolerates by design (comments are outside its positions). Dropping the parenthetical entirely would also pass but loses the evidence marker; the choice made is right.

② Semver level

@objectstack/spec: minor, under Clause-②: yes. Rule on origin/main 8cf527f8e0 (AGENTS.md blob 8f1ec0d1…, 1099 lines), L1067-1068: "The declaration is Clause-②: yes|no plus at most one arm from the closed pair (widening)/(narrowing): yes takes at least minor, (narrowing) is BREAKING, no (widening) malformed." (The shared checkout's blob 126c139f…, 1075 lines, differs and carries the sentence at other lines — quoted from origin/main.) Declaration readable: PR body line 3 Clause-②: yes line-initial; card claim comment 5699630645 Clause-②: yes; node scripts/pm/check-clause2-carriers.mjs --pair 18492 (branch blob fe6601d5… = origin/main's) → exit 0, "readable in the fixed spelling and both carriers agree" (4 token reads, 0 writes). Re-derived on what ships: pnpm packobjectstack-spec-17.4.0.tgz, 2014 files; OLD spellings 0/0/0/0 in src/dist/json-schema/CHANGELOG; NEW "resolves to the form grid's full column count" 2/60/22/0, "form grid's current column count" 1/30/22/0, objectui#9253 1/30/0/0, 5e9b502fa4 0 everywhere; lit colSpan 5/852/88/3; dark zzz-no-such-token 0. Nothing but prose moves. By .claude/skills/pm-dispatch/SKILL.md:476's criterion (放宽接受集或扩大公开面) the honest declaration is no; yes is the safe direction (SKILL.md:639: a wrong no is an auditable false declaration) and minor is its consistent floor. Level stands as declared; not a blocker.

③ Boundary flags

  • F1 — ESCALATED, decides the verdict. The identity clause of item 2/3/7. Population on the head tree (wrapped-tolerant, excl. content/docs/releases/ and **/CHANGELOG.md): "identically to omitting the key" 2 files / 4 occurrences (view.zod.ts:2616, :2622; references/ui/view.mdx ×2); "pixel-identical to authoring nothing" in .changeset/17670-colspan-span-measured-behaviour.md:11 (the other two hits are [finding] absolute-colspan-discouraged asserts a misalignment that does not happen — measured in Chromium at all three surface widths, and its recommended alternative span: 'full' is the spelling that misrenders #17328's own lint changeset and lint test — history). Ships: dist 60/30, json-schema 22/12. Falsifier: form grid 3, section columns: 2, plain field, 720px — omitted 2/2 vs 'full' 1/2 at the pin. No test pins the clause (0 hits outside the two carriers). Required changes below; every prescribed string was run through the repo's gates here.
  • F2 — answered; recommendation, not blocking. Dangerous direction: at the .objectui-sha bump absorbing bd09957380, the pin-anchored sentences describe a state the repo no longer builds against, and nothing mechanical guards it: both new citations are in the HISTORICAL spelling, which check:objectui-pin-citations "records and does not check" by design (gate L43-53; run: 26 asserting match / 38 historical recorded, exit 0). A guard already exists in the repo and was tested: rewriting the TSDoc citation to the ASSERTING spelling — "At the pin we build against (.objectui-sha = 53ded82bf7) the renderer emits the widest tier's class only" — reads green today (27 asserting / 37 historical, exit 0) and reds with a non-pin sha (exit 1: "1 spec source(s) assert an objectui pin this repo does NOT build against … view.zod.ts:2615 cites bd09957380"), i.e. the bump itself would red on this line and force the re-read. Recommend the seat adopt this in the remediation round (or in its tracked-edge card). Dedupe: spanLadderFor, objectui#9253, span-full-tier-ladder, pin bump reconcile.
  • F3 — for the PM to file; outside the card's two sentences. The unchanged 'auto' clause in the same describe — "wide widgets like textarea/richtext/json/file/subform take the whole row": (a) json, file, subform are in no wide path — WIDE_FIELD_TYPES is 10 entries (textarea/markdown/html/grid/richtext, bare and field:-prefixed) at pin and main, and no other full-row path names them in plugin-form/src or the form renderer (grep 0; control: col-span-full 1 hit, section dividers only); (b) at the pin a wide widget in a 3-col grid emits @2xl:col-span-3 → 1 of 2 cells at @md — the same widest-tier-only defect this PR removes for 'full', at both 3- and 4-column grids. Population: 1 describe string → src 1, dist 30, json-schema 12, generated 2. Dedupe: WIDE_FIELD_TYPES, isWideFieldType, take the whole row, span: 'auto'.
  • F4 — noted, not blocking. Changeset's "part of objectui#9244" points at a 404 and ships via CHANGELOG.md (files[]). Suggested spelling: "objectui's own tests and changeset carry the -9244 suffix". The primary citation resolves.
  • F5 — answered, carried. Carrier 6 skills/objectstack-ui/rules/navigation.md:139 (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) and examples/app-showcase/src/ui/views/task.view.ts:363-366 (lint-discouraged 1 file) are the only surviving carriers outside quotations: "only lines up at" 2 files (navigation.md + the PR changeset's quotation), "whole row at any column count" 1 (quotation), "gated at the top breakpoint only" 0, "emits it as one container-query-scoped span class" 0, "it is not a whole row at every column count" 0; lit colSpan 35 files / 200; dark 0.
  • F6 — noted. "section's declared column count" survives in packages/lint/src/validate-form-layout.ts:27 (comment) and .changeset/17328-colspan-rule-withdrawn.md (lint's unconsumed changeset) — [finding] absolute-colspan-discouraged asserts a misalignment that does not happen — measured in Chromium at all three surface widths, and its recommended alternative span: 'full' is the spelling that misrenders #17328's own wording, imprecise (form grid's) but not falsified; outside the five carriers.
  • F7 — answered. CI on a3dd4bbd4a, latest run per check name (42 runs, 35 names, all head_sha = head): 31 success, 4 skipped (Auto Label, Check PR Size, Console Pin Gate [Console Pin Gate never runs on a packages/** change — so the one instrument AGENTS.md names for "did this removal break the pinned sibling?" is structurally absent on exactly the diffs that rule governs #17673], Packed-tarball smoke (opt-in)); no failure, no cancellation; Lint & Repo Gates success 17:18:39Z; all six Test Core shards and Type Check · workspace success.
  • F8 — noted. colSpan describe's "current column count" is per-tier at main and configured-count-at-its-tier at the pin (colSpan:4, 3-col, @md: pin 1/2 vs main 2/2); the sentence's falsifiable clauses hold at both, so not required.
  • F9 — disclosure. Shared state touched: git fetch in both shared checkouts updated remote-tracking refs/FETCH_HEAD (objectui origin/main ff29450a94..542718f457 and tags); a detached worktree was added under the scratchpad; no working-tree edit, no git stash, zero MCP GitHub calls, zero GitHub writes.

Implemented-by: claude/issue-17670-colspan-span-false-claims
Reviewed-by: session_01KB5PFtxuy1x3dcR5gxudx6

VERDICT: FAIL

Required changes for PASS

R1. packages/spec/src/ui/view.zod.ts:2622 (span describe) — replace exactly so at intermediate widths the field rendered identically to omitting the key (measured in Chromium at viewport widths 390, 720 and 1700). with exactly so at intermediate widths the field took a single cell, not the row (one of two at the 720px modal width; measured in Chromium at viewport widths 390, 720 and 1700).
R2. view.zod.ts:2616-2617 (TSDoc) — replace tier's class only, so at intermediate widths the field renders identically / to omitting the key (objectstack#17328). objectui#9253 (objectui with tier's class only, so at intermediate widths the field takes a single / cell, not the row (objectstack#17328: one cell of two at 720px). objectui#9253 (objectui.
R3. .changeset/17670-colspan-span-measured-behaviour.md:11 — replace so at the 2-column modal width it rendered pixel-identical to authoring nothing at all. with so at the 2-column modal width it took one cell of two, not the row — in the single 3-column section #17328 measured, pixel-identical to authoring nothing at all.
R4. pnpm --filter @objectstack/spec build, then check:generated --fix (only content/docs/references/** may move — measured here: only view.mdx moved; an api-surface/ "stale" without the rebuild is the build-input-hash prerequisite, not drift), then check:generated, check:docs, check:doc-authoring, check:objectui-pin-citations, check:changeset-no-major --base origin/main, check:empty-changeset --base origin/main, check:adr-0087-registration --base origin/main, and pnpm --filter @objectstack/spec test. All but the test suite were measured exit 0 on these exact strings here (doc-authoring and pin-citations also green on the regenerated tree); the test suite was NOT MEASURED against them.
R5. Report the counts both sides: "identically to omitting the key" → 0 on src/dist/json-schema/generated; "a single cell, not the row" → src 2 / dist 60 / json-schema 22 / generated 2 (generated measured 2 here); lit colSpan and dark control.
Recommended, not blocking: F2 (asserting spelling in the TSDoc) and F4.


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>

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 38c1c6ccfb7af6929d0fa2171880f4e178fc62f6

① Derived judgments

Accept set — 0 moves, PR characterisation RIGHT. colSpan stays z.number().int().min(1).max(4).optional() and span stays z.enum(['auto','full']).default('auto') on both sides of the diff (merge-base 85c6d76e → head); the only packages/spec file in the diff is view.zod.ts and the only tokens that move are two .describe() arguments and one TSDoc block. api-surface/ and authorable-surface/ are untouched. No export, key, enum member, default, error code or wire shape moves. The changeset's "Nothing an author writes moves … no validation, default or emitted class changes" is true.

Public surface — every change is published prose that states behaviour. Judged against the renderer EXECUTED at both states (ported helpers, 3-column container grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3; cells shown per tier @md · @2XL@4XL]):

probe eff pin 53ded82bf7 main dda8f38
span:'full' 3 @2xl:col-span-3 → 1/2 · 3/3 @md:col-span-2 @2xl:col-span-3 → 2/2 · 3/3
colSpan:4 3 identical to 'full' identical to 'full'
colSpan:2 2 @md:col-span-2 → 2/2 · 2/3 same
colSpan:1 1 (none) (none)
omitted, section columns:2 2 @md:col-span-2 → 2/2 · 2/3 same
'full', section columns:2 3 @2xl:col-span-3 → 1/2 · 3/3 2/2 · 3/3
4-col grid, 'full' 4 @4xl:col-span-4 → 1/2 · 1/3 · 4/4 2/2 · 3/3 · 4/4
field:json, omitted 1 (none) (none)

The pin column reproduces #17328's browser table (emitted classes and the 285px one-of-two modal reading) exactly.

  1. FormField.colSpan.describe() (src ×1 → generated view.mdx ×2, byte-equal) — RIGHT on both states: never exceeds a tier's columns (no bare class under a one-column base at either state); colSpan: 4 in a 3-column grid → 3; colSpan: 2 renders as authored; colSpan: 1 emits no class (gate colSpan && colSpan > 1, pin L2523 / main L2788). Soft only: "clamps it to the form grid's current column count" reads per-tier, and at the pin colSpan: 4 takes 1 of 2 at the @md tier, not 2 of 2; the sentence makes no tier claim, so nothing checkable is false. Scope qualifier: the clamp is plugin-form's resolveColSpan on the ObjectForm/applyAutoLayout path — the path spec FormView metadata takes and [finding] absolute-colspan-discouraged asserts a misalignment that does not happen — measured in Chromium at all three surface widths, and its recommended alternative span: 'full' is the spelling that misrenders #17328 measured; the raw components renderer reads colSpan unclamped (pin L2179), which is outside the spec's surface.
  2. FormField.span.describe() 'full' clauseRIGHT and correctly scoped. "resolves to the form grid's full column count" (resolveColSpan returns grid); "at the .objectui-sha pin 53ded82bf7 only the widest tier's class is emitted (@2xl:col-span-3 for a 3-column grid)" — probe; "at intermediate widths the field took a single cell, not the row" — holds in every configuration probed, including the section-columns: 2 falsifier that failed the previous head (1/2 vs omitted 2/2) and the 4-column grid (1/2 · 1/3); "one of two at the 720px modal width; measured in Chromium at 390, 720 and 1700" — [finding] absolute-colspan-discouraged asserts a misalignment that does not happen — measured in Chromium at all three surface widths, and its recommended alternative span: 'full' is the spelling that misrenders #17328 row: 285px of 586. No issue id in the describe (the doc-authoring regex would catch objectstack#17328; gate green on head via CI). The citation spelling .objectui-sha pin sha matches the gate's HISTORICAL regex — recorded, not checked, by design.
  3. TSDoc above spanRIGHT. objectui#9253 = merged 2026-09-12T06:24:38Z, merge commit bd0995738035…, trailer "Part of objectui#9244", introduces spanLadderFor plus 2 pin tests and .changeset/span-full-tier-ladder-9244.md; "ahead of that pin" — compare pin…ladder: ahead_by 726, behind_by 0, merge_base = pin; "one clamped class per multi-column tier, making 'full' the whole row at every multi-column tier" — probe main, 3- and 4-column; spanLadderFor byte-identical from bd09957380 to main. Whether the TSDoc ships in dist is NOT MEASURED here (no build).
  4. Generated content/docs/references/ui/view.mdxRIGHT as a regeneration: all four rows byte-equal their source describe after pipe-unescaping; no other file under content/docs/references carries the new text; OLD spellings 0 there (control colSpan present). check:docs/check:generated NOT run here; CI Lint and Repo Gates and Type Check · source gates are success on this head.
  5. content/docs/ui/views.mdx :434 comment and :449 span row — RIGHT on both states (resolution claim, pin-anchored tier claim, "since objectui#9253"); :450 colSpan row — RIGHT (same soft "current" note as item 1). "(preferred)" and "Legacy" gone.
  6. content/docs/protocol/objectui/layout-dsl.mdx :223 callout — RIGHT (only "legacy" removed); "Custom Span Widths" — RIGHT: resolution order verified at ObjectForm.tsx pin L1258-1260 (clampCol(schema.columns) ?? (declaredSectionCols.length ? Math.max(...declaredSectionCols) : inferColumns(approxInputs))) and the flat path (applyAutoLayout: schema.columns else inferColumns(fields.length)); tier sentence anchored to 53ded82bf7 / since objectui#9253.
  7. Preference removed, not reversedRIGHT and complete in scope: prefer \span`, Prefer this over the absolute, legacy absolute, only lines up at the width the author imagined, whole row at any column counteach survive on head only inside the PR's own changeset quotation (1 file each); 0 in the five carriers; lit controlcolSpan` 35 files, dark 0.
  8. Changeset bodyRIGHT: files[] carries dist, json-schema, src/**/*.zod.ts (verified); @object-ui/components 17.6.0 at pin, ladder and main (verified) and npm latest 17.6.0 published 2026-08-24, before both commits → "unreleased" holds ("0 tags contain the commit" NOT MEASURED here — shallow scratch clone); absolute-colspan-discouraged withdrawn by PR fix(lint)!: withdraw absolute-colspan-discouraged — its premise was measured false and its recommendation measured worse than the thing it warned about #17671 (merged, closed [finding] absolute-colspan-discouraged asserts a misalignment that does not happen — measured in Chromium at all three surface widths, and its recommended alternative span: 'full' is the spelling that misrenders #17328). "part of objectui#9244": issues/9244 → 404 (control: 9253 resolves); it is the commit trailer's own spelling and ships via CHANGELOG — spelling note, not a defect.
  9. Clause-② declaration yes (card claim comment and PR body line 3, both line-initial) — tested: the accept set is not loosened and no symbol or key is added; the public surface that moves is contract prose in a released package's files[], and the rewrite adds behavioural commitments (never overflows, colSpan: 1 emits no class, 'full' resolves to the grid). By SKILL.md:476's strict criterion the honest reading is no; by SKILL.md:639 a wrong no is an auditable false declaration and yes is the permitted conservative direction. Not a wrong yes that matters; recorded so the seat can see the direction.

② Semver level

.changeset/17670-colspan-span-measured-behaviour.md"@objectstack/spec": minor. Rule on origin/main AGENTS.md L1067-1068: "yes takes at least minor". Consistent with ① item 9; under a no the floor would be patch (L1064-1065), which minor also satisfies. CI Check Changeset success on this head (3 runs). Level stands. Consistent.

③ Boundary flags

Implemented-by: claude/issue-17670-colspan-span-false-claims
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Clause-② carriers cleared — provenance

Both carriers stripped in one stroke by the dispatching seat, session_01LvwGppdonww4zGLWZo5rho (domain:spec execution seat).

Review of record comment 5706759768 on this PR
Head judged 38c1c6ccfb7af6929d0fa2171880f4e178fc62f6
Served tier CONTRACT_REVIEW_TIER
Verdict PASS
Carriers cleared PR #18492 ✅ · card #17670 ✅ (both read back after the write)

How this verdict was produced. This seat's measured served model is claude-opus-5; CONTRACT_REVIEW_TIER is claude-fable-5-1 and the comparison is exact ⇒ the seat is not at tier. Per the downgrade fuse the verdict was routed to an isolated review subagent running at CONTRACT_REVIEW_TIER, fed only the card, its rulings and the PR itself, with an adversarial brief — ⛔ never the dispatch order, ⛔ never this seat's conclusions. The record is adopted verbatim. ⛔ Not an in-seat review and ⛔ not a quota downgrade.

Independence pair: a mode:subagent dev's branch against the seat session adopting the isolated reviewer's verdict ⇒ no SELF-REVIEW.

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 ✅ · ② check-clause2-carriers --pair 18492 re-run after the strip ✅ · ③ re-taken at landing time, latest-run-per-check-name ✅. Governed-surface predicate: 0 paths hit the register ⇒ ordinary queue landing.


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 17, 2026 00:55
@os-litant
os-litant added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 3a9ad22 Sep 17, 2026
58 checks passed
@os-litant
os-litant deleted the claude/issue-17670-colspan-span-false-claims branch September 17, 2026 08:05
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:ui size/s tooling

Projects

None yet

3 participants