Uh oh!
There was an error while loading. Please reload this page.
fix(lint): the three ADR-0120 uniqueness rules name the object in the where slot (#9600) - #9908
Conversation
…ere` (#9600) `AuthoringFinding` declares two location slots with different jobs — `where` ("human-readable location", e.g. `object "leave_request"`) and `path` ("config path", e.g. `objects[3].sharingModel`). Three registry adapters set the first from the second (`where: f.path`), so every CLI command printed the same positional string twice: • objects[44].indexes[1]: "sys_account" declares index [provider_id, … rule: unique/unscoped-declared-index at objects[44].indexes[1] That index is a position in the merged object array, which appears in no file the author wrote. The three rules now state `where` themselves — the producer is the only place still holding the object it walked — and their return type becomes `LocatedLintIssue`, a `LintIssue` with a REQUIRED `where`, so a fourth rule joining the family cannot reach the adapter without one. A `f.where ?? f.path` fallback at the adapter was rejected for that reason. Display text only; `path` is untouched and stays positional. Measured over the 45 object declarations platform-objects and metadata-core ship, the registry produced 1050 findings from the same 5 rules before and after, with bare-config- path `where` values going 72 to 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
📓 Docs Drift CheckThis PR changes 1 package(s): 7 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 4f064934bc6fb689faf71e564991c8ca486ed4e4 && git checkout 4f064934bc6fb689faf71e564991c8ca486ed4e4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 597a4660e57ccdea37c92255be9d39ce04b21a36 dffca71dea869e365108e8e277d47f3c840373ca && git checkout -B drift-repro 597a4660e57ccdea37c92255be9d39ce04b21a36 && git merge --no-ff dffca71dea869e365108e8e277d47f3c840373ca
node scripts/docs-audit/affected-docs.mjs --json 597a4660e57ccdea37c92255be9d39ce04b21a36
|
PM review — ACCEPT. You falsified the card's own title, and proved "no widening" with the exact number I asked for. Arming.Verified at Ruling 1 — right-shaped, and you said why in contract termsThe draft fixes at the producer (the three rule functions state
That is the whole argument, and it is the right one. A tolerant fallback fixes today's three instances and guarantees the fourth arrives undetected — the defect returns wearing the fix as camouflage. Making the field required means the compiler refuses the shape. ⭐ The two corrections — and one of them falsifies the card's own title
The card is titled "…so Studio renders the location as ⇒ The defect is real, live and user-visible, but it is CLI-scoped, not Studio-scoped, so its severity is lower than the title implies. I have corrected the card title. A card that overstates its own blast radius distorts every triage decision downstream of it, and this one had already survived a triage pass and a dead run with the claim intact. Inheriting a draft and finding that the card — not the draft — carries the false claim is the harder direction to look in. H1 — the failure mode named precisely
The path is dereferenced and rendered on every line, and names a position in the merged object array that appears in no file the author wrote. That is the distinction H1 asked for — cosmetic / wrong-match / wrong-message — answered with the rendered output rather than a claim about it. ⭐ Ruling 3 — proven, not asserted
This is exactly what I asked for and it is rarely produced. "I didn't change behaviour" is usually an intention; here it is two identical numbers on either side of the diff, with the one number that should have moved moving to zero. Nothing further to check. H2 — three is the right number, with the denominators41 rules in And the static registry-wide guard means a fourth cannot arrive quietly — which is the half a fix usually omits. H3Unfixed impl + new test → Changeset — correct call
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9600
Verified at
dffca71dea— the union below was run on that exact head.What was wrong
AuthoringFindingdeclares two location slots with different jobs:where— "Human-readable location", e.g.object "leave_request"path— "Config path", e.g.objects[3].sharingModelThree registry adapters set the first from the second (
where: f.path), so everyCLI command printed the same positional string twice and the only human-readable
slot said nothing the
atclause did not already say.H1 — measured, not assumed. Produced on the unfixed tree over the corpus the
card measured (44 objects from
@objectstack/platform-objectsplus one from@objectstack/metadata-core), rendered the wayos validaterenders it:So the answer to "wrong message, wrong match, or nothing at all" is wrong
message: the path is dereferenced and rendered on every line, but it is a
position in the merged object array, which appears in no file the author wrote.
It is not a wrong location — it is a correct path spent in the slot reserved
for the readable one. Not inert, and not a wrong match.
What it renders now
An index is identified by its
namewhen it has one, otherwise by the columns theauthor actually wrote — both searchable in their source, which a bare ordinal is not.
Shape
whereis stated by the rule functions rather than reconstructed in theadapter, because only the rule still holds the object it walked. Their return type
is now
LocatedLintIssue— aLintIssuewith a requiredwhere, newlyexported — so a fourth rule joining this family cannot reach the adapter without
one. A
f.where ?? f.pathfallback at the adapter was rejected for exactly thatreason: a tolerant consumer would let the positional spelling ship again silently.
pathis untouched and stays positional, and a test pins that it does.No population change
The rules match the same code they matched before; only the string changed. Over
the same 45 shipped object declarations, across
validate+build+lint:wherethat is a bare config pathSweep — is three the right number?
Yes. The registry holds 41 rules; across 195
where:assignment sites in 61non-test source files of
packages/lint/src, exactly 3 carried the shape, andall 3 are fixed here. No rule builds a positional
wherein its own module. Astatic guard in the new test runs the whole registry and fails if any adapter maps
wherefrom apathagain, so the class is closed rather than the instances patched.Behaviourally on the real corpus only 2 of the 3 fire —
unique/double-declarationcarries the shape but its trigger (a column with both a field-level
uniqueand asingle-column unique index) does not occur in the platform objects.
One correction to the card's premise
The card's title says Studio renders the location. It does not, today. All three
rules are
surfaces: CLI_ONLY, andruntime-gate.tsdispatches only rules whosesurfacesincluderuntime-publish, so they never reachSaveMetaItemResponseSchema.advisories. The card predates the #4716 split, whichcrossed the five gating object rules and deliberately left the six advisory-tier
ones — these among them — behind the door. The defect is real but CLI-scoped;
the header comment the draft carried repeated the Studio claim and has been
corrected to what is measurable. The guard is what keeps this fixed if that door
later opens.
Verification, all at
dffca71deapnpm --filter @objectstack/lint test— 76 files, 2106 tests passedpnpm --filter @objectstack/lint typecheck— clean;@objectstack/cli, the direct consumer, also cleanrules is 6 failed / 1 passed, and with the fix 7 passed. The one that passes
in both is the guard pinning
pathpositional, which is state-independent by design.check:nul-bytes,check:cross-package-test-inputs,check:slot-lookup,check:changeset-gate-self-tests,check:objectui-changeset,check:empty-changeset,check:query-options-erasure,check:engine-double-contract,check:where-matcher,check:type-check-coverage,check:type-check-debt(re-measured on a built closure —33 ledger entries, none above its recorded number),
check-adr-0087-registration,check-changeset-no-major,docs-audit/check-affected-docs— all pass.A changeset is included:
packages/lintis published and this changes its CLI output.Generated by Claude Code