Skip to content

fix(tooling): a derived parse's verdict comes BACK, so one unparseable synthesis stops taking the whole run - #19112

Merged
os-elon-musk merged 2 commits into
mainfrom
claude/issue-19077-ts-parse-refusal-localisable
Sep 18, 2026
Merged

os-elon-musk merged 2 commits into
mainfrom
claude/issue-19077-ts-parse-refusal-localisable

Conversation

@os-elon-musk

@os-elon-musk os-elon-musk commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Refs #19077 (item 1 — the returnable derived-parse door; the census-side application is item 2, sequenced after PR #19073)

⚠️ Seat edit at 2026-09-18T20:27Z, Seat: domain:spec#3, session_019srGWGCBBCBHqcDoRZpQRh — the closing keyword was changed to a reference, and that is the only change to this body. The dispatched dev reported status: rework deliberately and per its dispatch word: acceptance input 1's census-level half ("that receiver is CLASSIFIED") cannot be delivered from this surface, because it needs scripts/tenant-audit-census.mjs, which this seat serialised behind open PR #19073. ⇒ this PR makes the repair available and provable; it does not apply it. A Fixes keyword would have closed card #19077 on merge while the defect it describes was still live on main — the charter's partial-landing rule (SKILL.md: 部分落地 PR 带 Refs #N (item k),⛔ 不 Fixes) is exactly this case. ⛔ Nothing about the diff, its tests or its declarations was touched by this edit.

Clause-②: no — measured, not assumed; the three readings are under "Clause ② and the changeset" below.

⚠️ Notation: type texts in this body are written with [...] where the source has angle
brackets (Promise[void] is Promise of void in the real fixture). The body sanitizer
eats tag-shaped fragments, backticked ones included, so the real spelling cannot be written
here. The fixtures in scripts/ts-parse.mjs carry the real characters.

What was wrong

scripts/ts-parse.mjs answers one question — "could I read this tree?" — and answers it by
ending the process. That is right for a tree the gate's own author controls, and the module's
header argues it at length: a throw is swallowable, and try { parse } catch { continue } is
already written in this repo.

tenant-audit-census.mjs was asking a different question through that same door. It
reads a receiver's declared type out of a source that parsed, stores the type's text
whitespace-collapsed (:459, getText(sf).replace(/\s+/g, ' ')), and later re-parses the
stored text as a synthetic type alias (:723,
parseSourceFile('census-receiver-type.ts', ...)) to ask whether it declares a write door.
A type literal may separate its members by a newline alone. The collapse turns that
separator into nothing, the synthetic alias does not parse, and the census takes
EXIT_UNPARSEABLE for the whole run — with a refusal naming a file that does not exist in
the tree.

Reproduced on this branch's base (origin/main@07c6f822e) before any edit:

STORED: "{ insert(object: string, data: unknown): Promise[void] find(object: string, query: unknown): Promise[void] }"
LIT CONTROL typeTextDeclaresEngineDoor = true      [the semicolon spelling, from the card]
about to classify the newline-only spelling
x  ts-parse — REFUSING to scan a source that does not parse.
    file       census-receiver-type.ts
    errors     1 parse diagnostic(s) from TypeScript 6.0.3
      census-receiver-type.ts:1:78  ';' expected.
REPRO EXIT=3

And the shape itself is legal TypeScript — so the collapse is the defect, not the receiver:

type text alias parses?
authored, members separated by a newline yes, 0 diagnostics
the same text after .replace(/\s+/g, ' ') NO, 1: ';' expected.
the semicolon spelling (the card's lit control) yes, 0 diagnostics

The refusal's own reasoning is false for this call: "a file the gate could not read, reported
as a file with nothing to report"
. The gate read the file. What did not round-trip is the
gate's own re-serialisation of a fragment of it — a fact about the synthesiser, never about
the corpus.

The repair

parseDerivedText(origin, fileName, text, scriptKind) answers the synthesis question and
hands the verdict back, so one unanswerable site costs one site instead of every site.

The floor is held by construction rather than by review. The door requires an origin
a ts.SourceFile this module has already certified — and the only two doors that certify one
(parseSourceFile, createProgramChecked) exit on a source that does not parse. So a
source the gate could not read cannot reach the returnable door: it hits the refusal
first. An uncertified origin is itself a refusal (EXIT_DERIVED_MISUSE, exit 4), an exit and
not a throw for the reason the module already gives.

Two deliberate divergences from packages/lint/src/checked-parse.ts, the repo's existing
ruling on this same axis (it reports rather than refuses "because a scripts/** gate audits
a tree its own author controls"), both in the strict direction:

  • no tree comes back on failure (sourceFile: null), where the sibling always returns the
    recovered one. There is no recovered wreckage to walk and no reading of the result that says
    "nothing to report". A caller that forgets to branch gets a TypeError — loud, non-zero.
  • a rejection is counted as a rejection, never as a refusal. parseCensus() gains
    derived and rejections. A refusal ended the run, so every later source went unread and
    the numerator is short by an unknown amount; a rejection left the run going. One combined
    total would make the report whose job is to say how much was measured say it wrong.

⭐ Deliverable 1 — the dependency census: who depends on the fact that it EXITS

Instrument: TypeScript AST over every git-tracked .mjs/.cjs/.js/.mts/.cts/.ts/.tsx
file in the repo (6873 scanned), resolving each module specifier to an absolute path and
counting call expressions per imported binding. ⛔ No head -N anywhere in the pipeline.
Measured at b09c8f550.

population count
files whose text mentions ts-parse at all 45
A — a real module edge to scripts/ts-parse.mjs (import / re-export / dynamic) 34
B — of A, CALL a door that ends the process ⇒ depend on the exit 34
C — of A, name only non-refusing exports ⇒ do not depend on it 0
D — text mentions with NO module edge (prose / payload literals) 11
files that RE-EXPORT anything from it (a second-order importer would hide here) 0
files the instrument could not parse (its own blind spot) 0

Lit control, as triage required. scripts/tenant-audit-census.mjs provably calls
parseSourceFile — it is the subject of this card — and the instrument counts it, at
parseSourceFile x12. Verified independently: grep -n parseSourceFile on that file shows
one import at :177 and 12 call sites (:319 :363 :416 :723 :860 :1189 :1686 :1738 :1757 :1778 :1827 :1897). The two numbers agree.

A second, discriminating control. The instrument tells the three doors apart, which a
text match on parseSourceFile cannot: check-published-readme-exports.mjs is counted at
createProgramChecked x1 and calls parseSourceFile zero times;
platform-object-tenancy-census.mjs at transpileChecked x1, likewise zero.

⭐ The seat's 35 is 34. scripts/check-parse-guard.mjs matches
from './ts-parse.mjs' as text — at :555 inside its own failure message, and at
:694 :697 :733 inside self-test fixture strings — and imports nothing from the module.
A git grep -l on the specifier counts it; an AST edge does not. It is row D, not row B.

Reachable radius, and what is outside it. The instrument sees static
import/export ... from, namespace imports, and dynamic import() with a literal
specifier, over git-tracked source files, one hop, resolved by path. ⛔ Outside it:
a specifier built at runtime from a variable; a createRequire/require edge; an edge
through node_modules; untracked files; and any second-order importer reached through a
re-exporter — that last one measured to be vacuous here, because the re-exporter count is
0. Also outside it: scripts/pm/dispatch-gates.mjs deliberately does not fan
watch-hints out to the importers of the five shared utilities (ts-parse.mjs is one), so
the derived gate list for this diff does not name those 34 families, by that file's own
measured decision. That is why the floor below is argued mechanically and not from the
gate list.

Acceptance — triage's two inputs, both pinned

Both live in scripts/ts-parse.mjs's own --self-test, which CI runs
(.github/workflows/lint.yml:507). 30 cases before, 48 after; five new batteries
declaring 18 cases, and the roster floor rose 12 to 17 so deleting one reds.

① the newline-separated inline type literal. The fixtures are the census's own round
trip, spelled the way the census spells it — a receiver authored across lines, put through
.replace(/\s+/g, ' '), wrapped in the alias :723 builds:

  • ⛔ the collapsed receiver still exits through parseSourceFile (the refusal is untouched);
  • ⭐ the same text through parseDerivedText returns a located verdict and the run
    continues (status 0, tree null, failure.count 1, failure.line 1);
  • and that verdict names the source the text was derived from, not only the synthetic name;
  • lit control: the semicolon spelling — the card's own control — parses through the
    same door and returns a usable tree;
  • lit control: so does the authored spelling. Without these two the first case is
    indistinguishable from a door that rejects everything.

② a genuinely unparseable input is still REFUSED. Merge-conflict markers and a truncated
body, each through both doors: parseSourceFile still ends the run, and parseDerivedText
hands back no tree with failure.count at least 1. Triage's words are the reason this leg
exists — without it "fixed" and "the check was switched off" are the same green.

⛔ Floor — nothing exits 0 where it exited non-zero

Every deleted line in the whole diff, all 9 of them — git diff 07c6f822e..b09c8f550,
439 insertions, 9 deletions:

-const SELF_TEST_BATTERY_FLOOR = 12;
-/** Parses attempted, the distinct file names, and the refusals. */
-const census = { parses: 0, programs: 0, transpiles: 0, files: new Set(), refusals: 0 };
- * `parses` counts every source that reached the parser through ANY of the three
- * `programs` and `transpiles` say which door they came through.
-        + `(${c.programs} program(s), ${c.transpiles} transpile(s)); ${c.refusals} refusal(s)\n`,
-        && counted.out === '{"parses":3,"programs":0,"transpiles":0,"files":2,"refusals":0}',
-        && countedAll.out === '{"parses":3,"programs":1,"transpiles":1,"files":3,"refusals":0}',
-      + `included, and a caller’s try/catch cannot swallow any of it).`,

not one of them is in a refusal path. Every line from each door's diagnostics read
through its process.exit is byte-identical:

580:  if (diagnostics.length > 0) {      671:  if (rows.length > 0) {       720:  if (rows.length > 0) {
581:    census.refusals += 1;            672:    census.refusals += 1;      721:    census.refusals += 1;
582:    ...refusalReport(...)            673:    ...programRefusalReport     722:    ...transpileRefusalReport
583:    process.exit(EXIT_UNPARSEABLE);  674:    process.exit(EXIT_UNPAR…)  723:    process.exit(EXIT_UNPAR…)

The only change inside an existing door is one vouchedSources.add(...) on each success
path — after the refusal has already been decided.

The covering mechanism for all 34, rather than 34 readings. The new door has zero
call sites anywhere in the tree outside the module that declares it
(git grep for parseDerivedText, EXIT_DERIVED_MISUSE, derivedMisuseReport,
derivedFailureReport over all tracked files: 0 files outside scripts/ts-parse.mjs). No
importer reaches it, no refusal path changed ⇒ no importer's behaviour on an unparseable
source can have moved.

Direct per-door reading at b09c8f550, each fed a truncated body:

door exit returned?
parseSourceFile 3 no
createProgramChecked 3 no
transpileChecked 3 no

The census's live behaviour is unmoved. node scripts/tenant-audit-census.mjs over the
live corpus: exit 0, 573 sources scanned, identical counts — the repair adds a capability
and adopts it nowhere (see Acceptance notes).

The new cases can fail — three ablations, each on a different limb

Every mutation was confirmed on disk before the reading (anchor count to 0, injected marker
counted), driven through scripts/ablation-replace.mjs, and every restore verified
byte-identical (blob == HEAD blob be83f4e875df, git diff HEAD empty).

ablation on-disk proof self-test which cases
the door EXITS instead of returning the verdict anchor 1 to 0; process.exit(EXIT_UNPARSEABLE) 3 to 4 RED, exit 1 5 — the rejection cases only. ⭐ Both lit controls stayed green, because they parse and never reach that path
the origin-vouch guard deleted vouchedSources.has(origin) 1 to 0 RED, exit 1 6 — the whole unreachability battery
the recovered tree handed back instead of null sourceFile: null, 1 to 0 RED, exit 1 3 — the two "no tree" acceptance legs plus the verdict case

Checks

scripts/pm/dispatch-gates.mjs derived the families from the changeset itself (1 path vs
merge base 07c6f822e, three-dot). 29 derived, 29 run, 0 NOT-MEASURED, 0 UNRUN, and the
reconciliation's zero is DERIVED — every one of the 29 carries a recorded exit code and none
of them is 3. Every family exit 0, including check:parse-guard, check:nul-bytes,
check:watch-hint-literal, check:scripts-symbol-anchors, check:self-test-wired,
check:self-test-workflow-commands, check:cross-package-test-inputs and
check:pm-dispatch-gates. node scripts/ts-parse.mjs --self-test exit 0, 48 cases.

Outside the derived total and therefore CI's, as that tool prints: the 53 artifact-roster
families, the 11 declared wide-population families, the 14 pending-changeset families and
the 1 path-scheduled job.

pnpm lint was narrowed, and the narrowing is a measurement. The repo-wide
eslint . --no-inline-config run is CI's. Three readings, all three required:
① the reachable population comes from eslint's own config text, not a guess —
eslint.config.mjs:328 records that "this repo runs one eslint.config.mjs, which never
enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules)
for ANY file", measured there with a positive control; ② the targeted run's file count is
read from --format json: 1 file, 0 errors, 0 warnings, exit 0; ③ invariance — with
no type-aware linting enabled anywhere, a one-file diff cannot move the verdict on any file
it does not contain. The targeted run is at the final commit, b09c8f550.

Clause ② and the changeset

skip-changeset, and the reading behind it. Nothing published moves.
① the root package is private: true; ② of the 70 non-private workspace packages,
0 have a files[] entry that climbs out of its own package directory, so root
scripts/** cannot be in any published payload; ③ the new symbols
(parseDerivedText, EXIT_DERIVED_MISUSE, derivedFailureReport, derivedMisuseReport)
appear in 0 files under packages/, apps/ or examples/ — with the same grep lit as
a control, finding 48 files for a symbol that is published
(MetadataTypeSchema, reached by packages/spec's src/**/*.zod.ts entry).

Clause-②: no, on the same three readings plus two more: the accept set of all three
refusing doors is byte-identical (above), and the new door has zero adopters, so nothing in
this tree accepts anything it previously refused. ⭐ The judgement left for the seat, stated
rather than buried: this does add four exports to a module's surface — that surface is
measured unpublished, which is why the answer is no, and the seat can overrule without
re-deriving anything.

Acceptance notes

  • ⚠️ The census is NOT wired to the new door in this PR, and that is deliberate.
    scripts/tenant-audit-census.mjs is serialised behind PR fix(scripts): apply the tenant-audit census's engine-door rule to inline type literals (225 -> 227) #19073 (card [finding] tenant-audit census applies its engine-door rule to NAMED declarations only, so two real engine writes with inline type-literal receivers are subtracted from the certified population — placing them moves it 225 to 227 and rewrites both artefacts #18944) and the
    dispatch holds it: if the sound repair also has to edit the store/re-parse pair, stop and
    report the minimal hunk rather than edit it. It does. The minimal hunk, for the
    sequenced PR:
    in typeTextDeclaresEngineDoor, take the already-parsed sf of the source
    the type text came from as the origin, call parseDerivedText(origin, 'census-receiver-type.ts', ...) instead of parseSourceFile, and on failure print
    failure.report against the site and return false; then add one NON_ENGINE_REASONS
    arm (suggested: type-text-not-round-trippable) and put it in UNDEFENDED_REASONS, so
    the site is declared in both artefacts instead of dropping out of the population
    silently. Until that lands, the defect on main is unchanged — this PR makes the repair
    available and provable, it does not apply it.
  • Discharge check, as triage required. PR fix(scripts): apply the tenant-audit census's engine-door rule to inline type literals (225 -> 227) #19073's diff was read first. It touches
    scripts/tenant-audit-census.mjs, content/docs/permissions/tenant-audit-census.mdx and
    docs/audits/2026-08-tenant-audit-write-call-sites.counts.md, and it does not change
    the collapse at :459 or the re-parse at :723. What it does is route four more
    kind: 'other' returns through inlineEngineDoorOrOther, which calls
    typeTextDeclaresEngineDoor — i.e. it enlarges the set of type texts that reach the
    defect
    without creating it. The seat's reading is confirmed; the finding is not
    discharged.
  • All three exit sites, answered. :485 (parseSourceFile) gets the derived
    counterpart. :575 (createProgramChecked) needs none: a Program is built from root
    paths on disk, and its one caller passes real paths — and its source files are
    certified, so a Program-based gate can use the new door for text it synthesises. :617
    (transpileChecked) is not answered here; see the next note for why, and it is a
    finding rather than silence.
  • Out of scope, reported not fixed: two of transpileChecked's three callers are already
    in the same derived-text position as the census.
    check-objectql-double-limit.mjs:535 and check-where-matcher-conformance.mjs:543 both
    transpile a lifted snippet, named ${candidate.file}#L${candidate.line}.lifted.ts
    synthesised text, not a file. A legal source whose lifted span is not independently
    parseable would take either gate down process-wide, exactly as the census. Not fixed here:
    it is a different call site with a different acceptance input (that span), and adding a
    second new door while check-parse-guard.mjs — the gate that polices which doors
    scripts/** may use — is out of this card's surface is worse than adding one.
    (platform-object-tenancy-census.mjs:226 transpiles a real file's source, so the
    refusal is correct there.)
  • Escalation reading, taken because it fell out of the work. node scripts/tenant-audit-census.mjs over the live corpus exits 0 — no such receiver exists
    in the tree today, so the card stays latent. ⭐ The zero is lit on the same subject:
    the run reached typeTextDeclaresEngineDoor and that function returned true for two real
    sites (plugin-auth/src/audience-gate-test-support.ts:72,
    plugin-auth/src/sso-client-secret.ts:303, both reported as door-shaped subtractions), so
    the instrument was not silently skipping the code path. ⛔ Grading is triage's — this
    changes no priority.
  • No path literal was added to scripts/ts-parse.mjs, on purpose.
    scripts/pm/dispatch-gates.mjs records that the five shared utilities it follows declare
    no path literals at all, and it follows them one hop; a literal here would fan a watch-hint
    into every one of the 34 importer families — the precision loss that file explicitly
    refuses. So the door's safety is the origin-vouch rather than a roster with a scan.
    check:watch-hint-literal exits 0.

🤖 Generated with Claude Code

https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh


Generated by Claude Code


Generated by Claude Code

…e synthesis stops taking the whole run

`scripts/ts-parse.mjs` answers "could I read this tree?" by ending the
process, which is right for a tree the gate's own author controls. A gate
that re-parses text it SYNTHESISED from a source it already read is asking
a different question, and it was getting that answer:
`tenant-audit-census.mjs` stores a declared type's text
whitespace-collapsed and re-parses it as a synthetic type alias, so a
receiver whose members are separated by a newline alone -- legal
TypeScript, 0 diagnostics as authored -- collapses to an unparseable alias
and takes the entire census down with EXIT_UNPARSEABLE, naming a file that
does not exist in the tree.

`parseDerivedText` answers the synthesis question and returns the verdict.
The floor is held by construction rather than by review: it requires an
`origin` this module has certified, and the only two doors that certify one
exit on a source that does not parse -- so a source the gate could not read
cannot reach the returnable door. An uncertified origin is itself a
refusal. On failure no tree comes back, so there is no recovered wreckage
to walk and no reading of the result that says "nothing to report".

The three existing refusals are untouched: every line from each
diagnostics read through its `process.exit` is byte-identical, and the only
change inside an existing door is one `vouchedSources.add` on a success
path, after the refusal has already been decided.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
…, both lit controls, and the floor

Five batteries, 18 cases, on the fixtures the census itself produces: a
receiver type authored across lines, put through `.replace(/\s+/g, ' ')` --
the collapse the census performs -- and wrapped in the synthetic alias it
builds.

  • the collapsed receiver STILL exits through `parseSourceFile`, and
    through `parseDerivedText` returns a located verdict with the run
    surviving. Two lit controls hold the fixture family capable of
    SUCCEEDING: the semicolon spelling (the card's own control) and the
    authored spelling both parse, so the defect is the collapse and not the
    shape.
  • a genuinely unparseable input is still refused at every door: an exit
    at `parseSourceFile`, and NO TREE back from `parseDerivedText`, so
    there is nothing to walk and no result that reads as "nothing to
    report". Without this leg a repair and a switched-off check are the
    same green.
  • the fourth door is unreachable from an uncertified origin -- a raw
    tree, null, undefined, a string -- and that refusal is not swallowable
    either; a source file a checked Program was built over IS certified.
  • the knobs are compared across the two doors rather than asserted in
    prose, and the census counts a rejection as a run that continued.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

Ready for the queue: green, non-governed, and its mergeable_state: blocked is EXPECTED rather than a blocker — measured, with a landed control. Seat: domain:spec#3 · 2026-09-18T20:50Z

Seat: domain:spec#3

Flipped draft → ready at 2026-09-18T20:48Z via POST .../pulls/19112/ccr/ready_for_review, verified by GET (⛔ the status code does not count): draft: false.

reading value
CI on head b09c8f55, latest run per check name 31 checks · 0 still running · 0 non-green
governed predicate, re-derived on the FINAL file list 0 of 1 path(s) hit the register ⇒ NOT governed
clause-② carrier pair --pair 19112 exit 0 — governing claim 5735173485, claim.clause2-line: DECLARED no; ⛔ no needs:contract-review on either carrier
mergeable / mergeable_state true / blocked, on two consecutive passes

⭐ Why blocked here is not a defect and ⛔ needs no push

The discriminating measurement: the check-name set on this head is 31, while a sibling PR of this seat (#19073, clean) has 32. The one name present there and absent here is Spec property liveness.

.github/workflows/spec-liveness-check.yml explains it in its own words: its pull_request trigger is path-filtered to packages/spec/**, packages/qa/dogfood/**, content/docs/**, docs/audits/** — and this PR touches exactly one file, scripts/ts-parse.mjs, so the job never reports on the PR head. The same file states the other half:

Merge queue (see ci.yml for the full note). merge_group has no paths support, so queue builds run this unconditionally — acceptable: the whole job is ~a minute, and the queue result is the next main.

⇒ a required check that is path-filtered on pull_request and unconditional on merge_group reads as not reported before the queue and runs inside it.

Landed control, so this is not a reading of the workflow's intentions: PR #19072 merged today with none of those four trigger paths in its diff — Spec property liveness runs on its PR head: 0 — and its timeline shows ready_for_review 2026-09-18T17:14Z → added_to_merge_queue 2026-09-18T17:25Z → merged 2026-09-18T17:55Z. ⇒ a PR in exactly this shape enqueues and lands.

What this PR owes, and to whom

  • Nothing from the dev or this seat. The head is green, non-governed, the carrier pair is clean, and Refs #19077 (item 1 …) is deliberate: the census-side half is serialised behind PR fix(scripts): apply the tenant-audit census's engine-door rule to inline type literals (225 -> 227) #19073 and this PR must ⛔ not close that card.
  • It owes one enqueue action, which this session's write classifier refuses ([Merge Without Review]) — the same wall as this seat's other six ready+green PRs. ⛔ This seat does not self-approve, direct-merge or switch accounts.

Generated by Claude Code

@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit 956e010 Sep 18, 2026
44 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-19077-ts-parse-refusal-localisable branch September 18, 2026 22:10
os-litant pushed a commit that referenced this pull request Sep 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants