Repository navigation
fix(lint)!: withdraw absolute-colspan-discouraged — its premise was measured false and its recommendation measured worse than the thing it warned about - #17671
Conversation
…asured false and its recommendation measured worse (#17328) The rule fired on every authored `colSpan`, `colSpan: 1` included, and asserted that a fixed span "only aligns at one width". Browser measurement at all three surface widths the message names shows the renderer clamps the span to the section's column count: overflow is 0px in every configuration, including `colSpan: 4` in a 3-column section. The hint's recommended alternative, `span: 'full'`, compiles to the same class as `colSpan: 4` and renders pixel-identical to authoring nothing at the modal width. With no authored colSpan shape left that misbehaves there was nothing to re-ground, so the rule is withdrawn rather than narrowed. `colSpan` itself is untouched and still parses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
…ed (#17328) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 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 2c3789e443e5768cf0e06d4bfc6f6e2f162f2136 && git checkout 2c3789e443e5768cf0e06d4bfc6f6e2f162f2136
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7880c184427baec39e5cdd1166f497bf9870a652 70e35f4beb63f073a950cf201d2222af1f2819cc && git checkout -B drift-repro 7880c184427baec39e5cdd1166f497bf9870a652 && git merge --no-ff 70e35f4beb63f073a950cf201d2222af1f2819cc
node scripts/docs-audit/affected-docs.mjs --json 7880c184427baec39e5cdd1166f497bf9870a652
|
…ured citations, and fold in the two-code sdui-parser port (objectstack-ai#17645) (objectstack-ai#19398) Fixes objectstack-ai#17429 Part of objectstack-ai#17645 — that card's deliverable (the two-code port) lands here; the director seat ends card 17645 with the landing record after the merge (a seat-written record, not a closing reference). The card's take-over from the offline `domain:devx` seat is the maintainer's instruction (「18723 os-try-charles 已下班,你也接手」), recorded on the card; the carrier reader's cross-author row (C9) cannot be cleared without the offline holder's own `Release:`, so the card is carried as `Part of`, not as a closing reference. Clause-②: yes Corrected from `no` under ruling batch objectstack-ai#201 item 1 (maintainer 「201 A」, record 5753263180 on objectstack-ai#17645): this PR now also carries objectstack-ai#17645's two-code sdui-parser port, so the save gate's diagnostic set grows (`inert-quick-add`, `member-type-mismatch`); the pin bump and the 27 re-measured citations alone moved no accept set. Both landed on this branch: the fold-in commit `6ef8344645` and the citation-spelling fix `fc25a5925d` (the head); the lockstep record needed no re-recording (see the fold-in section). (Pin-bump half, as originally declared:) Declared from the diff, not from the card: nothing in the pin bump adds, removes or renames an authorable key. The two `packages/spec` edits with a published face are `.describe()` SENTENCES (`Dataset` measure `format`, `FormField.span`); `check:authorable-surface` and `check:api-surface` both report their artefacts current with no regeneration, and `pnpm --filter @objectstack/spec check:generated` reports all 15 artefacts up to date after `gen:docs` and `gen:migration-registry` ran. The same authored metadata is accepted and refused as before, so neither arm of the closed pair applies. ## What moved `.objectui-sha`: `53ded82bf7a494f54e344e19099dbf00854b8694` → `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` (objectui `origin/main` tip read 2026-09-20; `cfcc17d9dd04` — the revision PR objectstack-ai#18723 ported from — is an ancestor of it, `git merge-base --is-ancestor` exit 0). Produced by `scripts/bump-objectui.sh --no-commit 87af769e9a3e`, never by hand. Everything the pin travels with: | Artefact | How | |---|---| | `.changeset/console-87af769e9a3e.md` | the bump script's own digest — 584 releasing changesets of 891 added across 1156 non-merge commits, `@objectstack/console: minor`, 98 declared-breaking entries listed | | `sdui.manifest.json` + `scripts/sdui-manifest.record.json` | `node scripts/gen-sdui-manifest-node.mjs`. The manifest is BYTE-IDENTICAL (sha256 `49211fee7792…`, 57 components): the pinned commit declares the same published `@object-ui` `17.6.0` the old pin did, so only the provenance record moves. `check:sdui-manifest` exit 0 | | `packages/sdui-parser/objectui-lockstep.json` | `pnpm gen:sdui-lockstep` against a worktree parked at the new pin (`--update` refuses off the pin). See the RED below | | 27 `.objectui-sha` citations under `packages/spec/src/**` | each re-MEASURED against the new pin's tree — see the table | | `content/docs/references/ui/{dataset,view}.mdx` | `gen:docs`, from the two corrected describes | | `.changeset/17429-pin-bump-describe-corrections.md` | `@objectstack/spec: patch` for those two sentences | Lockfile / override state: `pnpm install` in this worktree produced no `pnpm-lock.yaml` change, and the bump script writes none — the console SPA is vendored as a built `dist/`, not as a dependency, so this bump moves no dependency edge in this repo. ## Fold-in: the two-code sdui-parser port (ruling batch objectstack-ai#201 item 1, maintainer 「201 A」) Commit `6ef8344645` brings PR objectstack-ai#18723's port onto this branch **verbatim**. Eight files, each byte-identical to the blob on `claude/issue-17645-sdui-lockstep-port` (`git hash-object` here vs `git rev-parse claude/issue-17645-sdui-lockstep-port:PATH` there — all eight SAME, no hunk rewritten): | path | blob | |---|---| | `packages/sdui-parser/src/kanban-quick-add.ts` | `20f9f74b9f05` | | `packages/sdui-parser/src/validate.ts` | `156a264073f7` | | `packages/sdui-parser/src/types.ts` | `567bc1259bea` | | `packages/sdui-parser/src/index.ts` | `2ea35795b9cf` | | `packages/sdui-parser/src/codegen.ts` | `76a7d654f1fd` | | `packages/sdui-parser/src/__tests__/inert-quick-add.test.ts` | `723cad21333a` | | `packages/sdui-parser/src/__tests__/member-type-mismatch.test.ts` | `4b87cadaa067` | | `.changeset/17645-sdui-parser-lockstep-port.md` | `7c171fddfc43` | Deliberately NOT brought from that branch: `.objectui-sha`, `scripts/sdui-manifest.record.json` (both already this branch's, at `87af769e9a3e`) and `packages/sdui-parser/objectui-lockstep.json`. ### The lockstep record was neither text-merged nor re-recorded — and needed neither This branch already carries the record **taken at the live pin** in the pin-bump round: `objectui.rev` = `recordedAgainstPin` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2`, 26 diagnostic codes including `inert-quick-add` and `member-type-mismatch`. `--update` re-reads objectui's side only, so a re-record here would have rewritten the same bytes. Attempted anyway, and it REFUSED, which is the generator working: `pnpm gen:sdui-lockstep` exit 1, `[head-off-pin] the objectui checkout is at c2f0f48329de, and .objectui-sha names 87af769e9a3e`; record file byte-unchanged across the attempt (`e0a6988b1c9a` before and after). The remedy it prints — parking the sibling checkout on the pin — was not taken: that is a git write inside a read-only checkout this container shares with other agents, and the record it would produce is the one already committed. **Result — `check:sdui-lockstep` exit 0** on the head: `OK — this copy is byte-identical to objectui@87af769e9a3e over 214 grammar line(s) [blob 0131f27cf86d] and agrees on all 26 diagnostic code(s), across 8 non-test source(s)`. `--self-test` exit 0 alongside. `check:sdui-manifest` exit 0 (manifest intact, sha256 `49211fee7792`, recorded at the live pin). ### The type-check red PR objectstack-ai#18723 carried is closed by the pin, not by a code change The red was `check:objectui-pin-citations` inside the `Type Check · source gates` job. It reds exactly on the two heads of objectstack-ai#18723 that claimed `.objectui-sha` = `cfcc17d9dd04`; on this branch, with the live pin, the gate is exit 0 (27 asserting citations match, 48 historical recorded and not checked, 7 anchor content assertions verified). Reverse-verified here through `scripts/ablation-replace.mjs`: rewriting `.objectui-sha` to `cfcc17d9dd04` turns the gate RED with five anchor-content mismatches; restored and proven (blob back to `035b6937d641`, `git diff HEAD` empty). So the fix is the fold direction itself: the port lands on the branch whose citations are already re-measured at the pin. No edit was made inside `packages/sdui-parser/**` for it, and no `@ts-ignore` or `any` was added anywhere; `tsc --listFiles` confirms both new test files are inside the package program. ### Ported from `cfcc17d9`, re-verified against the LIVE pin objectui DID move `packages/sdui-parser` between `cfcc17d9dd04` and `87af769e9a3e`: one unrelated feature, objectui#6771's retired `body` child-list dialect (`body-dialect.ts` +99, `index.ts` +1, `validate.ts` +31/−7, its own test). It touches neither ported code and stamps only `not-a-container` and `unknown-prop`, both already in the 26-code set — which is why `26 = 26` holds at the live pin with it unported. Against objectui at `87af769e9a3e` the whole non-comment divergence of the five ported sources is four items, three pre-existing (`ValidationResult` vs `ManifestValidationResult`; the `code: 'inert-quick-add'` inline literal forced by `check:dispatcher-error-vocabulary`, pinned equal to the constant by a test row; `provenance.ts` and `assertFullyLoaded` / `RegistryConfigLike.lazy` absent here) and one new since `cfcc17d9` (`body-dialect.ts` absent here — see Acceptance notes). The port strictly REDUCES divergence in every file it touches: `validate.ts` 146 → 73, `index.ts` 114 → 92, `codegen.ts` 30 → 3, `types.ts` 96 → 85 diff lines vs objectui at the live pin. ### Ablations — re-run on this tree Three, each through `scripts/ablation-replace.mjs` (anchor ×1, mutation proven on disk by anchor count and blob change, restore proven by `blob == HEAD` AND an empty `git diff HEAD`): (1) `checkKanbanQuickAdd` stamps nothing — **5 failed / 8 passed** of 13; (2) the validator's member check reads no declared arms — **7 failed / 14 passed** of 21; (3) the codegen narrows no member type — **3 failed / 18 passed** of 21. Baselines 13 and 21 passed; in each leg the failures are the SUBJECT rows and every control row stays green. Both suites import `../index.js`, a relative intra-package specifier, so no rebuild is in the resolution path. ## Citation spelling fix (`fc25a5925d`) CI's `Lint & Repo Gates` went red on `check-issue-citations` at `6ef8344645`: two issue-number citations on docblock lines this branch rewrote. `packages/spec/src/ui/view.zod.ts` — `objectstack#17328` respelled bare `objectstack-ai#17328` (the issue exists, closed completed by merged PR objectstack-ai#17671; the gate probes only unqualified citations, so the this-repo-qualified spelling was judged against a board never asked for it). `packages/spec/src/ui/component.zod.ts` — `objectstack-ai#10274` measured **deleted** by `--probe-cause` (gone from the board and 404 on the web endpoint, not transferred); no replacement guessed — the sentence now states in prose that the number no longer resolves and names the live record (that block plus `check:objectui-pin-citations`). `node scripts/check-issue-citations.mjs` exit 0 at the head (31 judged, 0 findings); `check:generated` unchanged (TSDoc, not `.describe()`). ## Post-landing smoke evidence (triage's condition, issue comment 5690507317) ``` bash scripts/pm/os-verify-lock.sh -c 'bash scripts/build-console.sh' os-verify-lock: VERDICT command-exit 0 · held the lock 653s (10m53s) · waited 0s ✓ 8786 modules transformed. ✓ Bundle canary 'import/jobs' present — framework client is in the bundle. ✓ Console bundle carries THIS tree's @objectstack/spec, and only it. ✓ @objectstack/console dist ready (60228 KB) from objectui@87af769e9a3e ``` | Reading | Value | |---|---| | exit code | 0 | | `packages/console/dist` size | 60228 KB — 51,702,779 bytes, 3,285 files | | content hash | `sha256 259d2dfecaa12507fd3166a3f91c66c52581990e5ccac3899757027fa6ac108d` over `find … -type f \| sort \| xargs sha256sum \| sha256sum` | | provenance stamp | `packages/console/dist/.objectui-sha` = `87af769e9a3ee28ace099fdd653d3ebd79fe82e2` | | `pnpm check:console-sha` | exit 0 — "Console dist matches the objectui pin" | The console build is GREEN at the new pin. 584 commits of objectui change came with it and none of them broke it. ## The citations — re-MEASURED, never re-pointed 27 asserting citations, all of them stale at the new pin. Each was re-derived by reading objectui at BOTH shas and comparing content, not by arithmetic on a line number. `pnpm --filter @objectstack/spec check:objectui-pin-citations` exit 0; `--verify-anchors` exit 0 with 7 of 7 content assertions verified against objectui at `87af769e9`. ### Corpus-absence records (7 citations + 6 generated copies) These cite a zero-hit count over the pinned tree, not a file:line. Re-counted with `git grep -F -o` at both shas; the METHOD was calibrated first by reproducing the old pin's own numbers exactly (6409 tracked files, `useState` 2304). | Record | Cited | Reads at `87af769e9` | Verdict | |---|---|---|---| | `18.kernel-health-check-and-hot-reload-durations-unit-in-key.ts:48` | 13 exports of `plugin-lifecycle-advanced.zod.ts` + `debounceDelay` = 0 / 6409 files; controls `objectstack` 10171, `@objectstack/spec` 3479 | all still 0 / **8228** files; controls **12966**, **4997** | unchanged verdict, counts re-measured | | `18.kernel-runtime-config-timeout-unit-in-key.ts:45` | `resourceLimits.timeout` 0; controls `timeout` 832, `RuntimeConfig` 236, `resourceLimits` 2 | 0; **1075**, **240**, 2 — and both `resourceLimits` hits are still prose in `packages/app-shell` | unchanged | | `18.logging-durations-unit-in-key.ts:48` | 4 names 0; controls `useState` 2304, `timeout` **702** | 0; **2383**, **1075** | unchanged —⚠️ but `timeout` 702 was WRONG AT THE PIN IT NAMED: that corpus reads 832. An error since written, found only by re-measuring, corrected here | | `18.system-metrics-jsdoc-durations-unit-in-key.ts:70` | 6 def names + 2 keys 0; controls `window` 2710, `timeout` 832, `period` 160, `interval` 156, `metrics` 301 | all 0; **3464**, **1075**, **170**, **170**, **324** | unchanged | | `18.system-tracing-otel-exporter-durations-unit-in-key.ts:66` | 37 exports of `tracing.zod.ts` + 4 key names 0; `Span` 404 / `SpanSchema` 40 unrelated; controls 10171, 3479 | all 0; **486** / **53**; **12966**, **4997** | unchanged | | `18.tenant-schema-cache-ttl-unit-in-key.ts:31` | `schemaCacheTTL` 0; controls `TTL` 112, `tenant` 819 | 0; **156**, **976** | unchanged | | `migrations/registry.ts` ×6 | the generated concatenation of the six above | regenerated with `pnpm --filter @objectstack/spec gen:migration-registry` | follows its entries | ### Read-point records (20 citations + 7 content assertions) | Record | Anchor, as cited | At `87af769e9` | Verdict | |---|---|---|---| | `data/api-methods-batch-conformance.test.ts:64` | `ObjectGrid.tsx:3538-3553` | `:3940-3955` | MOVED, span byte-identical (`git hash-object` `6133933199…` both sides) | | | `hooks/useBulkExecutor.ts:284-289` | `:298-303` | MOVED, span byte-identical (`0108334833…`);⚠️ the FILE is no longer byte-identical (+36/-22), so the record's old identity argument is gone and the span was re-read | | `ui/component.zod.ts` tab-item `icon` + `ui/component.test.ts:376` | `containers.tsx:730-736` | `:853-859` | MOVED, 7 lines byte-identical | | | `containers.tsx:789` (registration `items` input) | `:912` | MOVED **and the LINE CHANGED** — it lost `label: 'Tabs'`, gained `of: 'object'` and a longer description; the member list the record cites is unchanged | | `ui/component.zod.ts` accordion-item `icon` + `component.test.ts:286` | `containers.tsx:919-925` | `:1069-1075` | MOVED, byte-identical | | | `containers.tsx:966` | `:1116` | MOVED **and the LINE CHANGED**, same way | | `ui/component.zod.ts` button `icon` + `component.test.ts:3489` | `form/button.tsx:43`, `:72`, `:74` | unchanged | UNCHANGED | | | `form/button.tsx:85-102` (inputs), `:103-107` (defaultProps) | `:85-97`, `:98-102` | MOVED **and SHRANK** — every input dropped its `label` and `defaultValue`; the four input names and the ABSENCE of an `icon` input, which is what the record asserts, both hold | | | `action/resolve-icon.ts:129-132` (`resolveIcon`) | `:322-328` | MOVED **and REWRITTEN** — the tail no longer indexes `lucide-react`'s `icons` record; it asks `recordIconName` and hands the pair to `lazyIconComponent`. Accept/reject behaviour unchanged | | | `resolve-icon.ts:117-120`, `:100-105`, `:90-92` | `:302-305`, `:153-158`, `:143-145` | MOVED, byte-identical | | | `lib/lazy-icon.tsx:66-92` | `:98-124` | MOVED, byte-identical | | `ui/component.zod.ts` object-metric `icon` | `plugin-dashboard/src/index.tsx:204` | `:237` (registration now opens at `:227`) | MOVED **and the LINE CHANGED** — the input's `label: 'Icon (Lucide name)'` is GONE; the key is still published, now as a bare `{ name: 'icon', type: 'string' }` | | | `ObjectMetricWidget.tsx:142` / `:474`; `MetricWidget.tsx:312-321` / `:373-382`; `lazy-icon.tsx:66-80` | `:174` / `:483`; `:351-360` / `:412-421`; `:98-112` | MOVED, byte-identical | | `ui/component.zod.ts` kanban `limit` ×2 + `component.test.ts:3360` | `ObjectKanban.tsx:264` (`$top: schema.limit ?? DEFAULT_KANBAN_LIMIT`) | `:676` (`$top: resolveRowLimit(schema.limit, DEFAULT_KANBAN_LIMIT)`) | MOVED **and REWRITTEN** — objectui#9925 put a refusal in front of it (a contract-refused cap is dropped and reported at `:553`). The pinned fact, that `limit` lowers into the query's top-level `$top`, holds | | | `ObjectKanban.tsx:71`; `plugin-kanban/src/index.tsx:395-398` | `:84`; `:447-450` | MOVED, byte-identical | | | `ObjectKanban.tsx:143` + `plugin-kanban/src/types.ts:134` (`KanbanSchema.limit?: number`) |⚠️ **VANISHED** | `KanbanSchema` was RETIRED at this pin (maintainer ruling 2026-09-09) and `types.ts` declares the member no more. Re-derived rather than re-pointed: the published twin is `ObjectKanbanSchema`, imported at `ObjectKanban.tsx:10`, declaring `limit?: number` at `packages/types/src/objectql.ts:3735` | | | `ElementDataSourceGate.tsx:236-241` | `:316-331` | MOVED **and EXTENDED** — the branch gained objectui#9899's presence-is-not-authorship test and a `describeDisplacedRowLimit` report | | `ui/component.zod.ts` kanban `quickAdd` + `component.test.ts:3427` | `ObjectKanban.tsx:931`; `plugin-kanban/src/index.tsx:196`; `KanbanImpl.tsx:355` / `:368` | `:1563`; `:313`; `:621` / `:634` | MOVED, byte-identical. The absence re-counted at this pin: `quickAdd` / `onQuickAdd` 0 each in `ObjectKanban.tsx`, against 11 (was 6) for the sibling `onCardClick` | | `ui/dataset.zod.ts:207` measure `format` | `dataset-format.ts:185-198`, `date-display.ts:131-164` / `:117`, `:195` | `:229-264` (routed at `:370`), `:198-233` / `:152`, `:260-262` |⚠️ **SUBSTANCE REVERSED** — see below | | `ui/view.zod.ts:2945` `FormField.span` | the widest-tier-only class claim at `53ded82bf7` | `spanLadderFor`, `form/form.tsx:204-231` |⚠️ **SUBSTANCE REVERSED** — see below | | `ui/view.zod.ts` `ListMapConfig`, 7 content assertions | `ListView.tsx:113` / `:67`; `ObjectView.tsx:1381`; `objectql.zod.ts:562` / `:313`; `ObjectMap.tsx:373` / `:378` | `:146` / `:85`; `:1764`; `:1574` / `:734`; `:385` / `:390` | all 7 MOVED;⚠️ one LOST ITS SYMBOL — `FLAT_MAP_CONFIG_KEYS` became the total `FLAT_MAP_CONFIG_SPELLING` map (objectui#9950), so `style` now DOES reach the flat product, as `mapStyle`. The quote was re-read, not re-pointed | ### The two records whose SUBSTANCE the range reverses Both are claims about what the SHIPPED renderer does, so the pin is what dates them, and both were re-stated from the new tree. Each also carried a published `.describe()` saying the same now-false thing, which this bump is what falsifies — so both sentences are corrected here and `content/docs/references/ui/{dataset,view}.mdx` regenerated from them. That is the `@objectstack/spec: patch` changeset in this PR. - **`Dataset` measure `format`** said "a datetime value ignores it". objectui#8352 is inside the range: `formatMeasureDate`'s datetime arm now SELECTS a formatter — `relative` to `formatRelativeDate` (`:260`), `short` to `formatDateTime(v, { locale, style: 'compact' })` (`:261`), everything else to the bare `formatDateTime(v, { locale })` (`:262`). A date PATTERN is still ignored on both arms, which is the half that survives. - **`FormField.span`** said only the widest container-query tier's class is emitted, so a `'full'` field was one cell of two at 720px (objectstack-ai#17328). objectui#9244 / objectui#9253 (`bd09957380`) are inside the range: `spanLadderFor` emits one clamped col-span class per multi-column tier. ⭐ The previous revision of this block asked in writing to be re-read at the bump that absorbed it; this is that re-read. The `'auto'` half — textarea, markdown, html, richtext and repeater — re-measured UNCHANGED. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (122 commands for this 19-path, 757-line change set). Run locally, exit codes captured before any pipe: ``` pnpm check:sdui-lockstep EXIT 0 (after the fold-in: 26 = 26 codes, grammar region byte-identical) pnpm --filter @objectstack/spec check:objectui-pin-citations EXIT 0 (27 asserting citations match, 7/7 anchors verified) … --verify-anchors EXIT 0 node scripts/check-sdui-manifest.mjs EXIT 0 pnpm check:objectui-bump EXIT 0 pnpm check:objectui-changeset EXIT 0 pnpm check:console-sha EXIT 0 pnpm --filter @objectstack/spec check:generated EXIT 0 (15/15 artefacts current) MANIFEST=… check:react-declaration-parity EXIT 0 (reading unmoved: the manifest is byte-identical) node scripts/check-adr-0087-registration.mjs --base origin/main EXIT 0 node scripts/check-changeset-no-major.mjs --base origin/main EXIT 0 node scripts/check-empty-changeset.mjs --base origin/main EXIT 0 pnpm lint EXIT 0 (repo-wide, eslint . --no-inline-config) pnpm --filter @objectstack/spec test EXIT 0 (503 files, 14710 tests) pnpm --filter @objectstack/spec --filter @objectstack/sdui-parser typecheck EXIT 0 ``` plus 27 further derived families, every one exit 0: `check:nul-bytes`, `check:merge-driver`, `check:watch-hint-literal`, `check:comment-mask-adoption`, `check:comment-mask-corpus`, `check:doc-frontmatter`, `check:docs-section-name`, `check:scripts-symbol-anchors`, `check:spec-docblock-symbol-anchors`, `check:ci-filter-parity`, `check:keyed-text-bounds`, `check:undeclared-dep-imports`, `check:section-landing-index`, `check:liveness`, `check:duration-unit-keys`, `check:llms-txt`, `check:spec-changes`, `check:upgrade-guide`, `check:strictness-ledger`, `check:export-origins`, `check:api-surface`, `check:authorable-surface`, `check:docs`, `check:doc-anchors`, `check:docs-spec-enumerations`, `check:quick-reference-counts`, `check:corpus-claim-drift`, `check:changeset-gate-self-tests`, `check:pm-widening-tells`, `check:published-files`, and both changeset self-tests. The rest of the 122 are left to CI, which runs the whole farm. `origin/main` moved 7 commits while this was built and touches NONE of this diff's 19 paths, so no merge was taken; the queue's rebuilt generation is what validates the join. ## Acceptance notes - `check-issue-citations` blind spot (measured on objectstack-ai#17328 at `6ef8344645`): a citation qualified with this repository's own name (`objectstack#N` or `owner/repo#N`) is recognised as naming THIS repo and resolved against the board, but the board is only asked for unqualified citations and `--probe-cause`'s guard is `!cite.qualifier` too — a live issue reads `allocated-but-absent` and NOT MEASURED under `--probe-cause`. Not fixed here (the gate is not this PR's surface); recorded for the next author of `scripts/check-issue-citations.mjs`. - objectui's `packages/sdui-parser/body-dialect.ts` (objectui#6771, landed between `cfcc17d9dd04` and the live pin) has no counterpart in this copy: for an authored `body` key the two copies stamp different codes (`not-a-container` under a non-container there, the bare `unknown-prop` here), invisible to `check:sdui-lockstep`'s code-SET comparison because both codes are already in the 26-set. Both severities are `warning`, so `compile().ok` and the save verdict are unchanged. A third code path the ruling does not name; carrier: the next sdui-parser port round. - `provenance.ts` and `assertFullyLoaded` / `RegistryConfigLike.lazy` remain absent from this copy (zero diagnostic codes; objectstack-ai#18723's acceptance notes already record `provenance.ts`). `pnpm gen:sdui-lockstep` is unrunnable in this container because the shared sibling checkout sits off the pin and the generator's remedy is a git write in someone else's tree; it cost nothing this round and will bite the next pin bump, which must park its own checkout. - `.changeset/18516-span-auto-wide-types.md` (pending, not yet released) describes the `FormField.span` describe text this PR corrects, including "At this pin only the widest tier's class is emitted". `check:empty-changeset` refuses any diff that MODIFIES a changeset present at the merge base, so it is deliberately left untouched here and reported instead. - The `element:button` `icon` `.describe()` still says the renderer resolves through `lucide-react`'s `icons` map. At the new pin that is one hop indirect (`recordIconName` + `lazyIconComponent`) but the glyph set and the accepted spellings are unchanged, so it was left as written rather than churned. Original author: the director seat's `os-dev` rounds (pin bump; fold-in `6ef8344645`; citation fix `fc25a5925d`), session_012GcsUbuqFGBibkEDMRC1eE; body maintained by the director seat, `session_012GcsUbuqFGBibkEDMRC1eE`. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Clause-②: no
Closes #17328
The decision: retire, not re-ground — and the evidence that decided it
Triage's ruling was "withdraw or re-ground the rule, and fix the hint", with the recommendation named as the harmful half. The choice between the two branches is settled by asking what a narrowed rule would still be warning about. Every candidate answer is empty:
colSpan: 1colSpan: 2in a 2- or 3-column sectionspan: 'full'nor omitting it cancolSpan: 4in a 3-column section@2xl:col-span-3; 0px overflow, the case that would overflow if the clamp did not workspan: 'full'(the recommended alternative)colSpan: 4and measures byte-identically; at the modal width it is pixel-identical to authoring nothingOverflow is 0px in every configuration at every width in the card's table. With no authored shape that misbehaves, there is nothing left to re-ground, and a narrowed rule could not be given a positive control — the brief's own test for keeping it. A rule that reports 0 and a rule correctly narrowed look identical on a clean tree, so the honest branch is withdrawal.
The corollary matters for anyone tempted to soften the message instead: the rule warned about
colSpan: 4and recommendedspan: 'full', and they are the same emitted class. There is no wording that makes that coherent.Re-verification of the ruling's premise, on this branch's base (5ddd5d3)
Re-read first-hand rather than taken from the dispatch. All three hold, plus a fourth the card and triage did not name:
colSpanvalidate-form-layout.ts:203—if (colSpan != null) {, no further discrimination;colSpan: 1included:210-213—…so a fixed span only aligns at one widthspan: 'full':214-215—Prefer span: 'full' (whole row at any column count). The renderer clamps colSpan to the current column count.:13— "an over-wide colSpan is clamped", one paragraph above the claim the clamp falsifies:18-21— the module docblock repeats the same false premise and the same harmful recommendationReverse-read sweep — which existing sentences does this change make false?
Swept tree-wide with
git grep. Reported in full, including the zeros.only aligns at one width(exact): 1 hit —validate-form-layout.ts:212, the retired message itself. Zero elsewhere in the tree.lines up at the one width: 3 hits —validate-form-layout.ts:20(the docblock carrier, rewritten here) and twoCHANGELOG.mdfiles. Changelogs are an immutable record of what a past release shipped and are not edited.one width(broad, 14 hits): everything else is an unrelated sense — identifier column widths, SQL storage ceilings, string-family widths. One real hit:skills/objectstack-ui/rules/navigation.md:139. See "carriers left standing" below.span: 'full'(13 hits): the two carriers in this file (both fixed); two.changeset/CHANGELOGhistory rows;content/docs/ui/views.mdx×4;packages/spec/src/ui/view.zod.ts:2494;skills/objectstack-ui/rules/navigation.md:136; and three pure fixtures (examples/app-showcase/.../task.view.ts:368,packages/lint/src/showcase-shape.fixtures.ts:298, the test file) which assert nothing about width and are unaffected.colSpantree-wide: ~100 hits, almost all*.form.tsbuiltin form definitions authoring a colSpan — untouched and still valid, which is the point. The hits that say something about it are the ones tabled above.FORM_COLSPAN_ABSOLUTE: after this change, zero code references — only tombstone comments invalidate-form-layout.ts,index.ts,authoring-rules.tsand the re-judged tests.Two zeros worth stating as readings rather than silences:
FORM_COLSPAN_ABSOLUTEin any.md/.mdxoutsideCHANGELOG.md: zero hits — no README or doc page advertises the removed export, so nothing else had to move with it.packages/lint/src: zero hits — the only consumers were this package's own index and tests.Carriers left standing, deliberately — filed as #17670
The sweep found the same two false sentences in five more places outside this PR's fence: the
.describe()onFormField.colSpanand onFormField.spaninpackages/spec/src/ui/view.zod.ts(which generatecontent/docs/references/ui/view.mdx), two hand-written docs pages, andskills/objectstack-ui/rules/navigation.md— a governed surface, and the prescriptive one an AI author reads first. Each is outside this card's land point and pulls in a different gate family (spec generated artifacts) or a human-merge surface. Filed unassigned as #17670 rather than fixed here or left unsaid.content/docs/releases/v12.mdx:73-76and:82also name the rule; release notes are release-owned history and are correct as history. Not edited, not filed.What changed
packages/lint/src/validate-form-layout.ts— the(b)emission block and theFORM_COLSPAN_ABSOLUTEconstant are gone. The module docblock's colSpan bullet is replaced by a tombstone carrying the measurement, the reason the claim was false, the reason the recommendation was worse, and a standing instruction that reintroducing a colSpan warning takes a browser measurement plus a positive control — not source reading.packages/lint/src/index.ts—FORM_COLSPAN_ABSOLUTEis off the public surface, with the reason recorded at the export site: a rule id on the public surface reads as a check the platform performs (Prime Directive chore: version packages #10), and the compiler is the precise channel for telling a consumer that suppressed it.packages/lint/src/authoring-rules.ts— the registry comment describedvalidateFormLayoutas covering "an absolutecolSpanunder a per-surface derived column count". That becomes false with this change, so it is corrected in place (it now names thesection.grouprule it always ran and never mentioned).FormField.colSpaninpackages/specis untouched and still parses. Only the diagnostic is gone.The pins: re-judged in place, never deleted
Each carries a comment saying what it used to assert, what changed and why. Each re-judged pin that would otherwise assert an empty result is paired with a live finding on the same fixture, because
expect([]).toEqual([])would keep passing if the walk stopped reaching the site at all.validate-form-layout.test.ts"discourages absolute colSpan and steers to span"span: 'full'form-field-unknownon the identicalsections[0].fields[1]colSpanon the entry does not suppress, duplicate or relocate the reference finding it earns[FORM_COLSPAN_ABSOLUTE]— the colSpan rule was the only thing that could speak on an unresolvable binding[], paired with the identical sections under a resolvable binding reporting both dangling namesgroupsbucket too"colSpan: 3stays on the fixture — it is what proves the entry-object shape is read herecolSpanon a list-bound container is silent, a dangling one is still reported — #16168's fix still holding, now with nothing riding alongside itauthoring-rule-wiring.test.ts"the form-layout rule really runs, and really finds something"['absolute-colspan-discouraged', 'form-field-unknown']['form-field-unknown']; thecolSpan: 2stays on the fixture and the test's whole point — the registry entry'srunreturns a real finding on all three commands — is carried intactVerification
pnpm --filter '@objectstack/lint^...' build— exit 0.pnpm --filter @objectstack/lint test— 101 files / 3724 passed, 5 skipped, exit 0.pnpm --filter @objectstack/lint typecheck— exit 0 (tsc --noEmit+check:test-typecheck).pnpm lint(eslint . --no-inline-config, the whole repo, not narrowed) — exit 0.scripts/pm/dispatch-gates.mjsagainst the real change set and reconciled with--ran: 59 derived, 56 run green, 0 unrun, 3 NOT MEASURED at exit 3 / PREREQUISITE NOT MET (check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt— each needs the whole-repo build closure; declared to CI).check:docs-transcript-driftfirst exited 3 for the same reason and passes afterpnpm --filter '@objectstack/lint...' build.check-adr-0087-registration(accepted theno-migration-prescriptiondisposition),check-changeset-no-major,check-empty-changeset,check-changeset-fixed.70e35f4beb.Reverse verification (one-time ablation, not left on the tree)
Prediction before running: red. Re-emitting the withdrawn finding was injected into the committed source; the mutation was proved on disk by marker count and by
git hash-objectdiffering from theHEADblob (8ddaf813…against0c354a4d…), and the two test files were run against the mutated source.Result: 7 failed / 48 passed — every one of the seven re-judged pins goes red, and nothing else does. So the pins assert the withdrawal, not nothing.
Restore leg:
git checkout HEAD -- packages/lint/src/validate-form-layout.tsunder anEXIT INT TERMtrap using absolute paths, verified by blob hash equal toHEAD(0c354a4d…) and an emptygit diff HEAD— not by an exit code.AGENTS.md Post-Task Checklist item 4 — does this removal break the pinned sibling checkout?
This PR removes a published export (
FORM_COLSPAN_ABSOLUTEfrom@objectstack/lint), which is exactly the class item 4 names. TheConsole Pin Gatejob does not answer it on this PR, and that is not a pass:if: ${{ !cancelled() && needs.filter.outputs.console != 'false' }}(.github/workflows/ci.yml, theconsole-pinjob). Theconsolepaths-filter lists exactly seven paths —.objectui-sha,scripts/build-console.sh,scripts/check-console-sha.mjs,scripts/check-console-injection.mjs,scripts/console-spec-probes.mjs,scripts/assert-console-spec-injection.mjs,.github/workflows/ci.yml. No path underpackages/is among them, so apackages/lintdiff never triggers it, and a skipped check reads as success in branch protection. On this question CI is NOT MEASURED.So it was measured directly instead, on the pinned tree.
The tree that was read
.objectui-shaat this PR's head is53ded82bf7a494f54e344e19099dbf00854b8694. Fetched exactly asscripts/build-console.shmode 3 does (git init+fetch --depth 1 originat that SHA fromhttps://github.com/objectstack-ai/objectui.git), checked out detached.git rev-parse HEADequals the pin; 6409 tracked files.Reading 1 — what the pinned sibling imports, zeros included
Identical probe (
git grep -Fon the symbol), same tree, absent symbols beside their positive controls:FORM_COLSPAN_ABSOLUTEabsolute-colspan-discouragedvalidateFormLayoutFORM_FIELD_UNKNOWNFORM_SECTION_GROUP_UNKNOWNPAGE_SOURCE_CLASSNAME— positive controlvalidatePageSourceStyling— positive controlvalidateCapabilityReferences— positive controlvalidateSecurityPosture— positive controlThe controls are not arbitrary: the pinned sibling really does depend on
@objectstack/lint—apps/console/package.json:94andpackages/app-shell/package.json:85both declare"@objectstack/lint": "^17.0.0"— so the zeros above are a reading taken on a tree where this exact package is demonstrably present and imported, not on an empty clone.Every site that reaches into the package, exhaustively — five, and only four symbols between them:
apps/console/src/__tests__/sdui-preview-page-source-styling.test.ts:28—import { validatePageSourceStyling, PAGE_SOURCE_CLASSNAME } from '@objectstack/lint';packages/app-shell/src/preview/capabilityLint.ts:50—await import('@objectstack/lint'), feature-detectingvalidateCapabilityReferencesand nothing elsepackages/app-shell/src/preview/securityPostureLint.ts:94—await import('@objectstack/lint'), feature-detectingvalidateSecurityPostureand nothing elseapps/console/vite.config.tsNamespace / wildcard imports of
@objectstack/lint: 0 hits — so there is noimport * as lintwhose dynamic member read a grep for the symbol could miss. The two dynamic sites read two named functions, by name, and both are still exported here (checked against the export list inpackages/lint/src/index.ts, not against a comment that merely mentions the name).Reading 2 — the gate could not see this change even if the symbol were there
scripts/build-console.shinjects exactly two framework packages into the pinned build. Those twoexportlines are the only ones in the whole script::171export OBJECTSTACK_CLIENT_DIST="$CLIENT_PKG":215export OBJECTSTACK_SPEC_DIST="$SPEC_PKG"@objectstack/lintis not injected. The pinned tree is installed withpnpm install --frozen-lockfile --prefer-offline --prod=falseagainstregistry.npmjs.org, and objectui's own lockfile resolves'@objectstack/lint@17.2.0'— a published, immutable version. No change to this repo'spackages/lintworking tree can move what the Console Pin Gate builds against, at PR time or at merge time.Verdict
Item 4 is satisfied without a sibling fix and without a pin bump: the pinned sibling does not import the removed symbol, and the gate resolves the package from npm rather than from this tree. This ships alone.
.objectui-shais untouched — that pin belongs to #17429, not to this PR.Declared NOT MEASURED: the
Console Pin Gatebuild itself was not executed locally — it installs objectui's full dependency tree and CI allows it 45 minutes, beyond this session's foreground budget. What is reported above is the two readings that decide item 4's question, not a substitute build. If a reviewer wants the build run anyway, its trigger is a.objectui-shatouch, which is #17429's to make.Changeset — measured, not defaulted
@objectstack/lintshipsfiles: ["dist", "README.md", "CHANGELOG.md"], and this change moves whatdistcontains: one export is gone and one class of finding is no longer emitted. A changeset is genuinely owed;skip-changesetis not applicable and no such label is applied. Gradedminorwith a**BREAKING**banner, per this repo's launch-window convention (majoris refused bycheck-changeset-no-major; breaking-ness is carried by the banner plus the ADR-0087 disposition, not by the level). Disposition:not-required (no-migration-prescription)— nothing an author writes moves, and the one surface that does move has no metadata representation forobjectstack migrate metato reach.Clause-②
Clause-②: no, as the claiming seat judged on the card. Nothing here widens a contract: a lint warning is withdrawn,packages/spec/src/**is untouched, and the rule's severity waswarningthroughout. One thing the seat asked to be told: this implementation removes a public export (FORM_COLSPAN_ABSOLUTEfrom@objectstack/lint). It adds no new export and no new rule constant — the movement is strictly subtractive — but the line is the seat's to re-judge, not mine.New evidence for the PM: the
span: 'full'halfNot filed here, and no objectui change attempted — this session's repository access is
objectstack-ai/objectstackonly. Two readings from this branch worth having when that half is triaged:packages/spec/src/ui/view.zod.ts:2494describesspan: 'full'as "whole row at any column count" and tells authors to "prefer this over the absolutecolSpan". That.describe()is the source the public reference docs are generated from, so the recommendation outlives this PR by a wide margin (carried in [finding] The colSpan / span: 'full' claim #17328 measured false is still on the tree in five more carriers — including the spec .describe() that generates the public reference docs #17670).span: 'full'andcolSpan: 4both compile to@2xl:col-span-3— a hard-coded span of the declared maximum gated at the top breakpoint only. Sospan: 'full'is not "relative" in the emitted CSS at all; it is an absolute span of 3 with a single container-query gate, which is exactly why it collapses to one column of two at the modal width. Whatever the fix is, "full" needs a class at each breakpoint, not one at the last.验收备注
packages/cli/test/build-warning-truncation-notice.test.ts:65,131uses the stringabsolute-colspan-discouragedas synthetic fixture data for a warning-truncation test; it constructs the findings by hand and asserts nothing about this rule. Nothing there becomes false, so it is left alone rather than pullingpackages/cliinto this diff. Noted, not filed.(a)/(c)comment labels insidevalidateFormLayoutnow have a gap where(b)was. The labels are referenced from test comments, so they are left stable rather than renumbered. Noted, not filed.packages/lint's own module docblock said "the two that matter" while three rule ids existed (form-section-group-unknownwas added later and documented only on its constant). Corrected as a side effect of the rewrite, since the sentence had to be rewritten anyway.🤖 Generated with Claude Code
https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU