fix(tooling): a receiver's newline-separated type literal is CLASSIFIED, not a census takedown - #19290
Conversation
…ED, not a census takedown `tenant-audit-census.mjs` stores a declared type's text whitespace-collapsed and re-parses it as a synthetic alias to read the engine-door rule off it. A type literal may separate its members by a NEWLINE alone -- legal TypeScript -- and the collapse turns that separator into nothing, so the synthesis does not parse. Through `parseSourceFile` that did not fail the SITE: it ended the process, and every other site in the corpus lost its verdict with it. The synthesis now goes through `parseDerivedText` (the returnable door landed by PR #19112), and the verdict is acted on rather than swallowed: - `readTypeTextDoor` reads the door rule and reports the round trip separately, taking the origin tree the text was derived from -- so an unreadable SOURCE still cannot reach the returnable door. - `nonEngineReason` reads it once and classifies such a site on a new declared arm, `type-text-not-round-trippable`, placed in `UNDEFENDED_REASONS`. - the run prints the located parse verdict against the SITE and exits non-zero; `check-tenant-audit-census.mjs` refuses the same class through the one shared spelling, `notRoundTrippableSites`, because CI reads the gate and never the generator. The exchange is a loud process exit for a loud per-site refusal, never for a quiet subtraction. The corpus door is untouched: sources are still read through `parseSourceFile`, so a source that does not parse still ends the run. Census over the live corpus, before and after on `215840f4353`: exit 0, 576 sources, output byte-identical. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Isolated review. Every reading below was re-taken by this reviewer in two dedicated worktrees — the PR head ① Derived judgmentsAll four points of the sequenced hunk landed, and as specified.
The acceptance inputs are a real pair; neither passes vacuously.
Residual, stated so the seat can weigh it: the arm is a refusal that tells the author to restyle legal TypeScript ( ② Semver level
③ Boundary flagsThe floor is genuinely held, verified at its root, with one literal exception judged not a floor drop and one non-blocking defect. Root. Measured exit codes with the newline-separated receiver tracked in the corpus:
No CI path goes green: with the site declared in regenerated artefacts the gate still refuses it by name, and the site appears as an ENFORCED row in both artefacts. The exchange really is a process-wide takedown for a located per-site refusal. Literal exception. Non-blocking defect (measured, one-line fix). With the fixture present and artefacts regenerated, Declared deviation — three files outside the claimed surface. (a) The generator CI at posting time: 31 latest-per-name checks, 0 failures; Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #19077
Clause-②: no
Item 2 of #19077 — the census-side application. Item 1 (
parseDerivedText, thereturnable door) landed as PR #19112, squash
956e0107a; nothing here re-does it.Base:
origin/main=215840f4353, fetched 2026-09-20T09:07Z. Every reading below wastaken on that base in a dedicated worktree, never on the shared checkout.
The defect, and what it did
scripts/tenant-audit-census.mjsstores a declared type's text whitespace-collapsed andre-parses it as a synthetic alias (
type CensusReceiver = ...;) to read the engine-doorrule off it. A type literal may separate its members by a newline alone — legal
TypeScript — and the collapse turns that separator into nothing, so the synthesis does not
parse. Through
parseSourceFilethat did not fail the SITE: it ended the process, andevery other site in the corpus lost its verdict with it, under a refusal naming
census-receiver-type.ts, a file that does not exist in the tree.The repair — the four points of the minimal hunk, and where each landed
sfthe type text came fromreadTypeTextDoor(typeText, origin);resolveReceiverhands its ownsfat all fourinlineEngineDoorOrOthercall sitesparseDerivedTextreadTypeTextDoor, one call site, the only synthesis in the filefalsederivedFailure;main()prints it under a per-site::error::lineNON_ENGINE_REASONSarm, placed inUNDEFENDED_REASONStype-text-not-round-trippable, in both artefacts under ENFORCEMENT⛔ Point ④ and the floor — why this PR also edits the gate
The non-negotiable is that no importer ends up exiting 0 where it exits non-zero today.
Measured, rather than assumed:
lint.yml:2007-2008invokesscripts/check-tenant-audit-census.mjsand never the generator, and that gate's owndocblock says why (
censusRefusalsexists becauserunCensus's findings were read bynothing on the way to a CI verdict). So on a corpus holding such a receiver:
runCensuscallsparseSourceFile, which exits 3; the gate dies with it, CI red.author would regenerate, and the gate would go green. That is the loud process exit
traded for a quiet subtraction: the same floor drop in a different costume.
own file and line with the parse verdict under it, and refused by
censusRefusalsthrough
notRoundTrippableSites, the one spelling the generator and the gate share.main()counts it in its exit code too, for the local run.⇒ the exchange is a process-wide takedown for a located, per-site refusal. Nothing that
exits non-zero today exits 0 after this, and no site leaves the population in silence.
Acceptance — triage's two inputs, as a pair
Both run in
tenant-audit-census.mjs's own self-test, which CI drives throughnode scripts/check-tenant-audit-census.mjs --self-test(lint.yml:2007). Ten new cases;the file's self-test goes 49 to 59, the gate's 24 to 27, and the gate's
census refusalsbattery floor is raised 5 to 7 so the new cases cannot stop running unnoticed.
trip — a source is parsed,
declaredTypesIncollapses the declared type exactly as thecensus does, and the resolver reads the door off the stored text — and reads
other/type-text-not-round-trippable. Lit control: the semicolon spelling of theSAME literal is still PLACED as
engine/inline type literal stating an engine door, sothe first case is not a door that rejects everything. Two more: the arm is in
UNDEFENDED_REASONS(control: a defensible arm is not), and the failure is carried aslocated data naming the SOURCE it was derived from.
same declared arm and no door is read off it, so it is never placed. The corpus door is
untouched: sources are still read through
parseSourceFile, whose process-exit refusalis pinned by
ts-parse.mjs --self-test(landed with item 1) and enforced for everyscripts/**caller bycheck:parse-guard(exit 0 here).Boundary, pinned: the verb gate runs first, so a newline-separated literal naming no
write verb is never synthesised and cannot reach the new arm — the repair's reach is
exactly the defect's reach.
The census over the live corpus, before and after
node scripts/tenant-audit-census.mjs, base215840f4353:exit 0, 576 sources, 227 write call sites.exit 0, 576 sources — output byte-identical(
diffexits 0), one undefended subtraction, reasontype-not-in-corpus, door-shaped 0.mainmoved between the rounds. The escalationtrigger did not fire: exit 0 means no receiver of this shape exists in the tree, so the
card stays latent and the grading stays triage's.
Reverse verification — three ablations, each restored byte-identical
Driven through
scripts/ablation-replace.mjs, which proves the mutation landed on disk(anchor count fell, replacement count rose, blob hash changed) and proves each restore
(
blob == HEAD blob,git diff HEADempty).parseSourceFile— the self-test does not fail, it dies:exit 3, printing the card's own refusal forcensus-receiver-type.tswith1:78 ';' expected. That is the defect, reproduced from the acceptance case.UNDEFENDED_REASONS(point ④'s own ablation) — RED, thedeclaration pin fails, 1 of 59.
"quiet subtraction" costume failing to pass.
helper because the replacement text was a substring of the anchor, so the on-disk count
could not rise. It restored and never ran the command; the rerun used a dropping anchor.
Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derivedfrom this tree at
ce1b5304101(4 paths vs merge base215840f43, three-dot): 64families, all exit 0, each captured BEFORE any pipe, reconciled with
--ran:64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN— a DERIVED zero, every family carrying a code.Five of the 64 first answered
PREREQUISITE NOT MET(exit 3 / exit 1: not findings,nothing measured) and were re-run after building what they read —
pnpm --filter '@objectstack/lint...' build, thenpnpm --filter '@objectstack/client-react...' --filter '@objectstack/client...' build,both through
scripts/pm/os-verify-lock.sh(VERDICT command-exit 0). All five then exit 0.Outside that total by the tool's own accounting: 52 artifact-roster families, 11 declared
wide-population families, 14 pending-changeset families, 2 path-scheduled CI jobs.
pnpm lintrepo-wide is CI's run. The narrowing here is DECLARED and carries all threereadings: (1) the reachable population comes from eslint's own config text —
eslint.config.mjs:328records that this repo runs one config that never enablestype-aware linting (no
parserOptions.project, no typed rules) for ANY file, measuredthere with a positive control; (2) the targeted count is read from
--format json: 4files, 0 errors, exit 0 — the two markdown artefacts report "File ignored because no
matching configuration was supplied", i.e. eslint does not lint them at all; (3)
invariance — with no type-aware linting anywhere, a 4-file diff cannot move the verdict on
a file it does not contain. Taken at
ce1b5304101.Changeset
skip-changeset, on three readings: (1) the root package isprivate: true; (2) all 70non-private workspace packages declare a
files[](lit control) and 0 of them have anentry that climbs out of its own package directory; (3) none of the four changed paths is
under any package directory. ⇒ root
scripts/**,content/docs/**anddocs/audits/**cannot be in any published payload.
Clause-②: nofollows: this moves a build-time gate'sfailure convention, not
packages/spec's accept set or any public surface.The dispatch declared
scripts/tenant-audit-census.mjsand new tests underscripts/.This PR also touches:
scripts/check-tenant-audit-census.mjs— the gate-side half of point ④, argued above.Without it the repair lowers the floor, which the dispatch forbids outright.
content/docs/permissions/tenant-audit-census.mdxanddocs/audits/2026-08-tenant-audit-write-call-sites.counts.md— GENERATED regions,rewritten by
node scripts/tenant-audit-census.mjs --write, which is the only legal wayto edit them and is what
check-tenant-audit-census.mjsdemands. Point ④ puts the arm inthe enforced undefended table, so the table's prose had to name the third cause or the
artefact would explain a row it does not cover.
Measured before touching them, 2026-09-20T09:1xZ: 22 open PRs, 623 file rows (PR
#17076 paged past the 100-row cap so the negative is not a truncation artefact) — no open
PR holds any of the four paths, nor
scripts/ts-parse.mjs. Firing control: the map doescarry
scripts/rows (#19024, #18414, #19153, #18723). Dark control:scripts/zzz-no-such-file.mjshas no row.corpus-scale block re-stamps its date and sha and moves 573 to 576 tracked sources, because
mainmoved since it was last written.Acceptance notes — noted, not filed
available and would let such a receiver be PLACED rather than declared-undefended. It is
a much wider change — the collapsed text is also the display text, the index-lookup input
and the ledger key — and it is not what the sequenced hunk asked for. Successor: any
later card that revisits the census's stored type text. Not a defect in the tree.
transpileCheckedon a lifted snippet) wasalready filed as its own card by the
domain:spec#3seat, with the boundary carriedover. ⛔ No second card from here.
scripts/tenant-audit-census.mjsorts-parse.mjs;check:watch-hint-literalexits 0. Successor: none needed.Generated by Claude Code