Uh oh!
There was an error while loading. Please reload this page.
test(lint): census the SYSTEM_FIELDS consumer population by module graph, not by grep - #10475
Conversation
…aph, not by grep (#8999) Every sweep of the blanket `SYSTEM_FIELDS` union has been scoped by a `SYSTEM_FIELDS.has` grep, which structurally cannot see a consumer that spreads the union into a rule-local set — nor one that imports that laundered set from another rule file, since such a file contains the token zero times. Measured across seven historical refs of `packages/lint/src`, the blind fraction is not a closed historical accident: it has been FIVE rule files on every ref since the union was created (2026-07-31), i.e. half the population. Two of those five carry no occurrence of the token at all. So the enumeration moves to the module graph. `system-fields-consumers.test.ts` propagates the union's VALUE lineage over the static import/export graph with the TypeScript parser, and pins the result against a ledger that also records, per consumer, whether it asks the #8116 provenance question — verified mechanically, with `false` a legitimate answer that must state its reason. The analyzer is proved against synthetic fixtures before it judges the tree: a matcher that stopped matching yields the same green as a clean tree (#8892). Those fixtures pin the spread form and a two-hop re-export chain specifically, and assert positively that the old `.has` grep finds zero of them. No new CI job: it runs inside the package's existing `pnpm test` (3.9s). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. |
os-zhuang
commented
Aug 21, 2026
PM review — ⭐ accepted. The census reframes the card, and the instrument guards against its own failure mode.Reviewed against the diff, not the report. ⭐ The finding is bigger than the card claimedThe card presented this as three consumers missed across three sweeps — three historical accidents, with #8996 having closed them. Your census over seven historical refs says otherwise: the ⇒ "Three sweeps were pointed at an instrument with a standing 50% blind spot and happened to notice three of the five." That is a different and more serious claim than the card made, and it is the one the evidence supports. Two of the five contain the token zero times — no textual search of any spelling reaches them, so no amount of grep-tuning was ever going to close this. ⭐ The ruling states the strongest argument against itselfBuild-it rulings are easy to write badly. You recorded the counter-evidence in the gate's own file: arrival rate is one consumer in 20 days, and that one arrival used the What outweighs it is specific and checkable: 3 of 10 rule files launder via spread today, including the two most-copied rules, so a new rule written next to either inherits the invisible shape. A measured trade, not a hunch. ⭐ It guards against its own failure mode — the part I would not have thought to ask forThe analyzer originally seeded taint from ⇒ A zero-consumer answer can no longer be a green. That is the anti-vacuity rule applied reflexively, and it is the best thing in this PR. What I verified myself
Follow-ups, both correctly handled#10474 — the SORT axis ( CI still finishing. I will flip ready and arm once it is green — ⛔ nothing for you to change. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes: #8999
The ruling first: a gate is warranted, and here is the measurement
The card asked for a decision, not a deliverable, and explicitly welcomed "not worth
building". The numbers came out the other way — but the number that decided it is not the
one the card or the hold reasoned from.
Method. The consumer population is enumerated by propagating the union's value
lineage over the static import/export graph with the TypeScript parser — not by any
textual search. Deliberately narrow: propagating through arbitrary expressions (a function
that merely reads the union) taints 44 of 151 files and destroys the signal. What
propagates is the set of bindings a rule can ask
.has()of.Run over seven historical refs of
packages/lint/src(viagit archiveat each ref),against the
SYSTEM_FIELDS.hasgrep on the identical trees:.hasgrep seesd4a687a66^(pre-#8340)d4a687a66(post-#8340)b849e6911(post-#8404)192213f66^(pre-#8996)192213f66(post-#8996)42d899071(post-#9314)origin/main2026-08-20The card records "three consumers missed across three sweeps" and reads that as three
historical accidents that #8996 closed. It is not that. The blind fraction is a constant
five — half the population — on every ref since the union was created (2026-07-31,
#4330 / #4339), and it did not move when #8996 landed. The sweeps did not miss three
consumers; they were pointed at an instrument with a standing 50% blind spot and happened
to notice three of the five.
Two of those five carry no occurrence of the token at all —
validate-flow-node-writes.tsand
validate-action-body-writes.tsboth return0forgrep -c SYSTEM_FIELDS. Notextual search of any spelling reaches them; only the module graph does.
Arrival rate, same method: the family was born at 9 consumers and has taken exactly one
since —
validate-sortable-fields.ts(2026-08-17, #9314), the arrival that released thiscard's hold. So the ledger costs about one line per three weeks.
Cost. Zero new CI job, zero new workflow line, zero root
package.jsonedit (the#9465 fence is untouched, and so is
.github/workflows/lint.yml, which open PR #10441edits). It is a vitest file in
packages/lint/src, so it runs inside the package'sexisting
pnpm test— measured 3.9s for 14 tests, inside a suite that already takes 45s.The mechanism is the card's own named precedent,
rule-id-barrel-exports.test.ts.Against. One arrival in 20 days is a low rate, and that arrival used the
.hasform,so the old instrument would have caught it. Recorded honestly. What outweighs it: three of
the ten rule files launder the union through a spread today, including the two largest and
most-copied (
validate-hook-body-writes,validate-translation-references), so a new rulewritten next to either inherits the invisible shape — which is the AI-authorability axis
the card leads with, now with a denominator.
What landed
One file,
packages/lint/src/system-fields-consumers.test.ts. It pins the transitiveconsumer population against a ledger that records, per consumer, how it reaches the
union (
direct/derived/transitive/barrel/namespace) and whether it asksthe #8116 provenance question — verified mechanically against the provenance API, with
falsea legitimate answer that must state its reason. A new consumer fails with its fullpath to the union:
Anti-vacuity: the analyzer is proved before it judges
A matcher that stopped matching yields the same green as a clean tree (#8892), so the
positive controls run on synthetic fixtures first, and they pin the two shapes the real
sweeps missed:
new Set([...SYSTEM_FIELDS, '_id', 'name', 'space']));construction, so the fixture runs origin to spread to barrel to consumer, and the test
asserts positively that neither transitive file contains the token;
.hasgrep run over the same fixtureset finds zero of the three transitive consumers the module graph names;
Two guards against a false green on real data:
the census is anchored to a real export. The first draft seeded taint fromsystem-fields.ts::SYSTEM_FIELDSwithout checking the export exists — so a renamedunion would still have matched every import of the old name and reported a confident
population for a union that was not there. That is the gate: a rule that SPREADS the blanket SYSTEM_FIELDS union into a rule-local set is invisible to every sweep this family has run — three consumers missed across three sweeps, the third reached it by re-export #8999 failure mode one level up,
and the fixture that caught it is now a test.
the union stays off the published surface. Scanningsrc/is a complete censusonly while the union is unreachable from outside the package. It is re-exported from
neither barrel today; the day one does, this file would go on reporting a confident wrong
number, so that day it reds instead.
Also pinned:
validate-flow-node-writes.tsby name as the card's live positive control,and a shape-coverage floor so a half-broken analyzer that lost one propagation path cannot
pass.
Ablation
Both laundering shapes injected into
packages/lint/src, mutation confirmed on disk beforeany verdict was read (files matching the spread marker went 4 to 5; the transitive leg's
grep -c SYSTEM_FIELDS= 0,IMPLICIT_FIELDS= 2):old instrument named zero of the two, including the leg that imports
SYSTEM_FIELDSdirectly but calls
.hason its own set.ledger by name and crashed with
TypeErroron an un-ledgered file, burying the oneactionable message. Fixed, re-ablated — now exactly one failure with one message.
to the baseline 4,
ABLATION_IMPLICIT0 occurrences), tree byte-clean, 14/14 green.No
dist/leg: the test reads.tssource off disk and imports nothing from a builtpackage, so there is no stale-artifact path for the mutation to hide in.
Gates
Derived from the real diff with
node scripts/pm/dispatch-gates.mjs(no path arguments),not from the dispatch's guesses — it named nine families where I had guessed four. All nine
green on
f840122388, quoting each gate's own verdict line:check:cross-package-test-inputs—OK: 12 package(s) read outside themselves, all declared…check:slot-lookup—✓ slot-lookup ratchet holds: 107 unswept site(s)… none newscripts/check-cross-package-test-inputs.mjs—OK: 12 package(s)…scripts/docs-audit/check-affected-docs.mjs— exit 0, no outputcheck:query-options-erasure—✓ ratchet holds: 67 unswept non-test site(s)… none newcheck:engine-double-contract—OK — 340 pinned, 133 in the DEBT ledger, 2 exemptcheck:where-matcher—✓ 266 matcher(s) discovered, 266 answer the combinator battery correctlycheck:type-check-coverage—OK — 64/77 workspace packages type-checkedcheck:type-check-debt --re-measure(on the built closure) —OK — 33 ledger entr(ies) re-measured in 353.1s, 1924 raw tsc error(s) total, none above its recorded numberPackage suite:
pnpm --filter @objectstack/lint typecheck(tsc --noEmit, clean) andvitest run— 78 files, 2156 passed, 5 skipped.Changeset:
skip-changeset, by verified publish surface@objectstack/lintis a published package (private: false,publishConfig.access: public) — so this was checked rather than assumed in either direction. It shipsfiles: ["dist", …]andtsup.config.tsbuilds onlysrc/index.tsandsrc/runtime.ts.Built the package and grepped the output:
censusUnionConsumersappears indist/0times. The publish surface is unchanged, so there is nothing to announce in a release
note. An empty-frontmatter changeset is not the alternative here —
check-empty-changesetrejects newly added ones — so the label is the repo's declaration for this shape.
Out of scope, filed not fixed
The census surfaced two consumers that make no #8116 provenance call. Both are recorded in
the ledger with reasons rather than silently exempted, and the question of whether either
is a real hole is filed separately as #10474: the SORT axis (
validate-sortable-fields.ts,landed after the #8996 sweep) reads the union but asks no provenance question, while the
SEARCH axis on the identical index does — and
declaredFieldTargetdoes not excludeexternal objects, so the branch is reachable.
validate-translation-references.tsis thesecond row, recorded as deliberately not asking (a bundle supplies a label, never reads the
value). Notably this means the hold's premise ("no known remaining hole") no longer holds
literally, though neither row is a proven defect.
Governed surface
Nothing under
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.mdwastouched. The card's routing question (devx vs skills) was already answered by the claim
comment and needs nothing here.
Generated by Claude Code