Repository navigation
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 |
…and span: 'full', drops the deprecated claim (objectstack-ai#18674) Fixes objectstack-ai#18496 Clause-②: no One bullet in one published rule file. `skills/objectstack-ui/rules/navigation.md` (the 「Field width」 bullet, `:136–:139` on `origin/main` `30be2ac0b`) prescribed `span: 'full'` as "the whole row" and told authors not to use `colSpan` because "it only lines up at one width and is deprecated". objectstack-ai#17328 measured both rendering claims false in Chromium at 390 / 720 / 1700, PR objectstack-ai#18492 removed them from `packages/spec` and the generated reference page, and nothing in `packages/spec` deprecates `colSpan`. This was the last prescriptive carrier in the tree, and the one `npx skills add objectstack-ai/objectstack/skills` ships to customer projects. The bullet now says what the spec declares and the measurement found; it prescribes no preference between the two keys, because the spec does not (PR objectstack-ai#18492: 「The preference between the two keys is removed, not reversed」). ## The bullet, before and after Before (`origin/main` `30be2ac0b`, `:136–:139`): ```markdown - **Field width.** Use the relative **`span: 'full'`** to make a field take the whole row; otherwise **omit it** (`auto` sizes by widget type × current columns — textarea / rich-text / file take the row automatically). Do **not** use the absolute `colSpan` — it only lines up at one width and is deprecated. ``` After (`7155de6ac`, `:136–:141`): ```markdown - **Field width.** **Omit it** (`auto` sizes by widget type × current columns — textarea / rich-text / file take the row). Both keys are valid: `colSpan` (1–4) is clamped to the current column count, grid-aligned at every width; `span: 'full'` resolves to the form grid's full column count, but the shipped renderer emits it at the widest tier only (a 3-column form: one cell of two in a modal). ``` Two restatements in the same section are deleted to pay the token ratchet (below); no other line moves: - 「Overlay width」 `:142–:143` → `:144`: the parenthetical 「(they can't be chosen without knowing the client viewport)」 is gone. It restated the section intro `:127–:128` 「because an author (especially an AI) cannot know the client's screen」. The 「`width` / `drawerWidth` are deprecated」 clause it hung on stays, and it is true: `packages/spec/src/ui/view.zod.ts:3539–:3540` carries `@deprecated` on `drawerWidth` and the describe opens `[DEPRECATED → size buckets]`. - 「Column count」 `:146–:147` → `:147`: the closing 「; the columns adapt themselves」 is gone. It restated the bullet's own first sentence, 「Not authored. The form grid follows its real rendered width via container queries」. ## What the spec declares — `packages/spec/src/ui/view.zod.ts` at `30be2ac0b` - `:2664` `colSpan: z.number().int().min(1).max(4).optional()`, describe: 「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: 4` in a 3-column grid renders as 3); a `colSpan` within the column count renders as authored, and `colSpan: 1` emits no span class at all.」 - `:2676` `span: z.enum(['auto', 'full']).default('auto')`, describe: 「'auto' (default — omit it): the renderer sizes the field from its widget type × the current column count (wide widgets like textarea/richtext/json/file/subform take the whole row). '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-sha` pin `53ded82bf7` only the widest tier's class is emitted (`@2xl:col-span-3` for a 3-column grid), 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).」 - `:2665–:2675` the TSDoc above `span`: objectui#9253 (objectui `bd09957380`, 2026-09-12) is 「ahead of that pin」 and the block says 「re-read this block at the pin bump that absorbs it」. - Nothing deprecates `colSpan`, measured over `packages/spec/src`: `.deprecated(` → 0 files; `retiredKey(` calls naming `colSpan` → 0; `UNKNOWN_KEY_GUIDANCE` (7 files) and `RETIRED_KEY_GUIDANCE` (8 files) → 0 `colSpan` lines inside them; the ADR-0087 registries `packages/spec/src/conversions/*` and `packages/spec/src/migrations/*` → 0 `colSpan` lines (positive control: `namespace` / `tableName` / `sourceView` hit `conversions/registry.ts` ×3, `apply.ts` ×2, `types.ts` ×2, `stored.ts` ×1); `colSpan` lines co-occurring with `deprecat|retire|legacy` → 0. The changeset PR objectstack-ai#18492 landed (`.changeset/17670-colspan-span-measured-behaviour.md`) states 「Nothing an author writes moves. Both keys are unchanged, both still parse」. - The generated reference page `content/docs/references/ui/view.mdx:201–:202` and `content/docs/ui/views.mdx:449–:450` say the same two sentences; `content/docs/protocol/objectui/layout-dsl.mdx:254–:262` (「Custom Span Widths」) says 「widest tier only at objectui `53ded82bf7` … one clamped class per tier since objectui#9253」. ## What the measurement found — objectstack-ai#17328, Chromium at 390 / 720 / 1700 From the card's geometry table (hotcrm at `427c98535d`, `@objectstack/*` 17.4.0, one 3-column section, `helpful_count` cell): | authored | 390 (1 col) | 720 (2 cols) | 1700 (3 cols) | emitted class | |:--|:--|:--|:--|:--| | none | 358 | 285 | 495 | — | | `colSpan: 2` | 358 | **586** (2 of 2) | 1006 (2 of 3) | `@md:col-span-2` | | `span: 'full'` | 358 | **285** (1 of 2 — pixel-identical to none) | 1517 | `@2xl:col-span-3` | | `colSpan: 4` | 358 | 285 | 1517 | `@2xl:col-span-3` (identical to `span: 'full'`) | | `colSpan: 1` | 358 | 285 | 495 | none | Overflow 0px in every configuration at every width. So: `colSpan` is clamped and grid-aligned at every width (the old bullet's 「only lines up at one width」 is contradicted at all three), and `span: 'full'` at the pinned renderer is one cell of two at the modal width (the old bullet's 「the whole row」 is false there). The bullet's parenthetical 「a 3-column form: one cell of two in a modal」 is that row; it is qualified to a 3-column form because a 2-column form grid resolves `'full'` to 2 and emits `@md:col-span-2`, which is the row at 720. **Is the pin state still the shipped state on `30be2ac0b`?** `.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694`, untouched since PR objectstack-ai#18492 merged (`git log 3a9ad22..HEAD -- .objectui-sha` is empty). In the objectui clone (`git rev-parse --is-shallow-repository` = false): `git merge-base --is-ancestor bd09957380 53ded82bf7` → **exit 1**; control leg `PIN~5` (`83c77dc30`, 2026-09-05) → exit 0; reverse `is-ancestor 53ded82bf7 bd09957380` → exit 0; `git tag --contains bd09957380` → 0 tags. The span-ladder fix is ahead of the pin and unreleased, so 「the shipped renderer emits it at the widest tier only」 is true today. It stops being true at the pin bump that absorbs `bd09957380` — the same event the spec TSDoc `:2675` tells its own reader to re-read at; see acceptance note 3. ## Why no preference is prescribed The dispatch left the direction to the measurement: 「If the measurement says neither spelling is to be discouraged, say so; if it says `colSpan` is the safer primitive, say that」. At the pin, `colSpan` is the one that does what it says at every width, and a rule could say so — but the spec (the protocol, per 「我们的项目以objectstack 协议为准,文档应该以实际实现为准」) declares both keys without preference after PR objectstack-ai#18492's at-tier contract review, and the renderer half is already fixed ahead of the pin (objectui#9253), after which `'full'` is the whole row at every multi-column tier and a `colSpan`-first rule would need reversing again. So the bullet states each key's declared behaviour and the measured caveat, and lets the author choose; it calls neither deprecated because the spec does not. ## Token ratchet — `scripts/check-skills-token-ratchet.mjs:472` `['skills/objectstack-ui/rules/navigation.md', 2273]`, unchanged Convention `ceil(utf8 bytes / 4)`. Before: 9092 bytes → 2273 tokens (ceiling 2273, headroom 0). After (`7155de6ac`): 9091 bytes → **2273 tokens (ceiling 2273; headroom 0)** — the gate's own line: 「✓ check-skills-token-ratchet: skills/objectstack-ui/rules/navigation.md is 2273 tokens (ceiling 2273; headroom 0).」 Arithmetic: bullet 319 → 411 bytes (+92); the two deletions −61 and −32 (−93); net −1 byte. Row not raised, not lowered. File 162 → 162 lines; widest line in the section 84 (file-wide 181, a code example, unchanged). ## Sweep — `git grep -z -l -F`, excluding `content/docs/releases/` and `**/CHANGELOG.md`, files | phrase | before (`30be2ac0b`) | after (`7155de6ac`) | |:--|:--|:--| | `only lines up at` | 2 — `skills/objectstack-ui/rules/navigation.md`, `.changeset/17670-colspan-span-measured-behaviour.md` | 1 — the changeset only | | `at any column count` | 1 — the changeset | 1 — the changeset | | `lint-discouraged` | 1 — `examples/app-showcase/src/ui/views/task.view.ts` (carrier 2, boundary) | 1 — unchanged, not this PR's | | `is deprecated` | 40 files; in `navigation.md` 1 line | 39 files; in `navigation.md` 0 | | files carrying both `colSpan` and `is deprecated` | 3 — `view.zod.ts`, `references/ui/view.mdx`, `navigation.md` | 2 — `view.zod.ts`, `view.mdx` (there 「is deprecated」 is on other keys; 0 same-line hits with `colSpan`) | | lit control `colSpan` | 35 | 35 | | dark control `zzz-no-such-token` | 0 | 0 | The changeset carries the two old phrases as quotations of the wording it removed — a historical record by construction, consumed into `CHANGELOG.md` at release (which the exclusion set already exempts); it is not a prescription and is not touched. ## Gates (worktree at `7155de6ac`) `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` — change set derived from git, 1 path vs merge base `30be2ac0b` — derived 22 commands; every one run with redirect-then-`$?` capture; `--ran` reconcile: 「22 derived, 22 run, 0 NOT-MEASURED, 0 UNRUN」, exit 0. ```text node scripts/check-ci-filter-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 node scripts/check-skills-token-ratchet.mjs :: exit 0 node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:corpus-claim-drift :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:role-word :: exit 0 pnpm check:skill-compatibility :: exit 0 pnpm check:skill-frame-sync :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 ``` Named verdicts: `check:doc-authoring` 「44 published skill files clean — no internal issue-id references」; `check-skills-token-ratchet --self-test` 「65 cases pass」; `check:skill-identifier-liveness --self-test OK` and the gate exit 0; `check:skill-compatibility` 「10 SKILL.md file(s) reconciled against 81 workspace packages」; `check:nul-bytes` exit 0. `check:doc-formula-expressions` was run after `pnpm --filter '@objectstack/lint...' build` under `scripts/pm/os-verify-lock.sh` (「VERDICT command-exit 0 · held the lock 186s · waited 0s」). Not owed locally: no ① dependency-closure build and no ② package test (the diff touches no package); the 51 artifact-roster, 11 wide-population and 2 workflow-valued families are CI's own. **Governed**: `node scripts/pm/check-governed-merges.mjs --test skills/objectstack-ui/rules/navigation.md` → **exit 3**, 「GOVERNED — a human merge is the review record for this PR … skills/** ×1 — the published skills catalog」. Recorded as GOVERNED, not as a failure. Draft; no seat flips it ready, queues it or arms auto-merge; the maintainer's approval lands it (Prime Directive objectstack-ai#14). **Control characters**: `grep -naP` over the touched file → no match (exit 1). ## Changeset — `skip-changeset`, by measurement 83 tracked `package.json` manifests read: **0** carry a `files[]` entry naming a `skills` path (positive control: 70 name `dist`); `skills/*/package.json` → 0; the catalog ships from the GitHub tree (`skills/README.md:9` `npx skills add objectstack-ai/objectstack/skills`; `packages/create-objectstack/src/skills-install.ts:62` `SKILLS_CATALOG = 'objectstack-ai/objectstack/skills'`), never inside an npm tarball; `.github/workflows/pr-automation.yml:799` names `skills/**` among the releases-nothing paths. ## Acceptance notes Out of this PR's scope; nothing is filed by this PR. 1. noted, not filed — 承接者: the card's own routing round. Carrier 2, `examples/app-showcase/src/ui/views/task.view.ts:363–:366` (「'full' = whole row at any / derived column count — the primary primitive; absolute colSpan / is legacy and lint-discouraged」, wrapped across comment lines; the lint rule it names was withdrawn by objectstack-ai#17328). objectstack-ai#18496 records it as a boundary, not an expansion; left exactly as found, and the sweep row above keeps it visible. 2. noted, not filed — 承接者: the pin-bump PR that absorbs objectui `bd09957380` (PR objectstack-ai#18492's acceptance note 5 names the same event; dedupe words `spanLadderFor`, `objectui#9253`, `span-full-tier-ladder`). The new bullet's 「the shipped renderer emits it at the widest tier only」 is anchored to the current pin the way the spec describe is, but no gate watches `skills/**` for pin-state prose (`packages/spec/scripts/check-objectui-pin-citations.ts` scans `packages/spec/src` only), so that PR must re-read `navigation.md:139–:141` alongside `view.zod.ts:2665–:2676`. 3. noted, not filed — 承接者: whoever next edits that interface block (PR objectstack-ai#18492 note 4 says the same). `content/docs/protocol/objectui/layout-dsl.mdx:850` lists a `FormField` interface with `colSpan` and no `span` — incompleteness, not an error; the card's own refs exclude it. 4. noted, not filed — 承接者: 无. `.changeset/17670-colspan-span-measured-behaviour.md` still carries 「only lines up at」 and 「at any column count」 as quotations of the removed spec wording; release consumes it into `CHANGELOG.md`, where the exclusion set already expects them. ## 维护者速读(草稿) **改了什么**:`skills/objectstack-ui/rules/navigation.md`「Record Presentation」一节的「Field width」要点重写:不再规定「用 `span: 'full'` 占满整行、别用 `colSpan`,它只在一种宽度对齐且已弃用」,改为按 `packages/spec` 的声明陈述两个键——默认省略(`auto`);两个键都合法;`colSpan`(1–4)被渲染器夹到当前列数、在每个宽度都对齐网格;`span: 'full'` 解析为表单网格的满列数,但当前出货的渲染器只在最宽一档发出它(3 列表单在弹窗里只占两格中的一格)。为了守住 token 棘轮(该文件上限 2273、余量 0),同一节删掉两处重复表述:「Overlay width」里的括号原因句(节首已说过同一理由)和「Column count」末尾的「the columns adapt themselves」(该要点首句已说)。棘轮行不动,实测仍是 2273。 **为什么改**:objectstack-ai#17328 在 Chromium 三个宽度实测:`colSpan` 被夹紧、每个宽度都对齐;而被推荐的 `span: 'full'` 在弹窗宽度只占一格、与不写一样。PR objectstack-ai#18492 已把这两句从 spec 与生成的参考页移掉,并明确「两键之间的偏好是移除、不是反转」。这一条是树里最后一处仍在**规定**旧说法的载体,而且随 `npx skills add` 出货到客户项目;「deprecated」一句更是独立于实测就是假的——spec 里没有任何东西弃用 `colSpan`(`.deprecated()` 0、`retiredKey` 0、ADR-0087 转换表 0、`UNKNOWN_KEY_GUIDANCE` 0)。 **风险与代价(含回滚)**:① 新要点的「当前出货的渲染器只在最宽一档发出」锚定在 `.objectui-sha` `53ded82bf7`(objectui 修复 `bd09957380` 在 pin 之后、尚未发布,双腿实测);pin 升级吸收该修复时这句要跟着重读,spec 的 TSDoc 对自己也写了同一句提醒,但 `skills/**` 没有门禁盯 pin 状态的散文——已记在验收备注 2,承接者是 pin 升级 PR。② 不再给作者一个「首选拼法」,由作者按两句事实自选;这与 spec 现状一致。③ 同节另两处删除只删重复表述,不删事实。回滚 = revert 单个提交(单文件,无发布物,无 changeset)。 **席位意见**: **你要做的(一个动作)**:批准并合并本 PR(受管面 `skills/**`,需你点合并)。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_ Co-authored-by: Claude <noreply@anthropic.com>
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