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