Skip to content

fix(tooling): a receiver's newline-separated type literal is CLASSIFIED, not a census takedown - #19290

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-19077-census-derived-parse-item-2
Sep 20, 2026
Merged

os-litant merged 1 commit into
mainfrom
claude/issue-19077-census-derived-parse-item-2

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #19077

Clause-②: no

Item 2 of #19077 — the census-side application. Item 1 (parseDerivedText, the
returnable door) landed as PR #19112, squash 956e0107a; nothing here re-does it.

Base: origin/main = 215840f4353, fetched 2026-09-20T09:07Z. Every reading below was
taken on that base in a dedicated worktree, never on the shared checkout.

The defect, and what it did

scripts/tenant-audit-census.mjs stores a declared type's text whitespace-collapsed and
re-parses it as a synthetic alias (type CensusReceiver = ...;) 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, 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

point landed
① the door rule takes the origin sf the type text came from readTypeTextDoor(typeText, origin); resolveReceiver hands its own sf at all four inlineEngineDoorOrOther call sites
② the synthetic alias goes through parseDerivedText readTypeTextDoor, one call site, the only synthesis in the file
③ on failure the report is printed against the SITE and the door reads false the located verdict rides on the site as derivedFailure; main() prints it under a per-site ::error:: line
④ a declared NON_ENGINE_REASONS arm, placed in UNDEFENDED_REASONS type-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-2008 invokes
scripts/check-tenant-audit-census.mjs and never the generator, and that gate's own
docblock says why (censusRefusals exists because runCensus's findings were read by
nothing on the way to a CI verdict). So on a corpus holding such a receiver:

  • todayrunCensus calls parseSourceFile, which exits 3; the gate dies with it, CI red.
  • with the repair and no gate arm — the site would be declared in the artefacts, the
    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.
  • as landed — the site is classified, declared in both artefacts, printed against its
    own file and line with the parse verdict under it, and refused by censusRefusals
    through 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 through
node 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 refusals
battery floor is raised 5 to 7 so the new cases cannot stop running unnoticed.

  1. A newline-separated inline type literal is CLASSIFIED. The case runs the REAL round
    trip — a source is parsed, declaredTypesIn collapses the declared type exactly as the
    census 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 the
    SAME literal is still PLACED as engine/inline type literal stating an engine door, so
    the 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 as
    located data naming the SOURCE it was derived from.
  2. A genuinely unparseable input is still REFUSED. A garbage type text lands on the
    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 refusal
    is pinned by ts-parse.mjs --self-test (landed with item 1) and enforced for every
    scripts/** caller by check: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, base 215840f4353:

  • before (2026-09-20T09:08Z): exit 0, 576 sources, 227 write call sites.
  • after (2026-09-20T09:20Z): exit 0, 576 sources — output byte-identical
    (diff exits 0), one undefended subtraction, reason type-not-in-corpus, door-shaped 0.

⚠️ The previous round measured 573 sources; main moved between the rounds. The escalation
trigger 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 HEAD empty).

  1. The door reverts to parseSourceFile — the self-test does not fail, it dies:
    exit 3, printing the card's own refusal for census-receiver-type.ts with
    1:78 ';' expected. That is the defect, reproduced from the acceptance case.
  2. The arm is removed from UNDEFENDED_REASONS (point ④'s own ablation) — RED, the
    declaration pin fails, 1 of 59.
  3. The gate's refusal loop is emptied — RED, 2 of 27 gate cases fail, which is the
    "quiet subtraction" costume failing to pass.

⚠️ Reported rather than quietly retried: ablation 2's first attempt was refused by the
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, derived
from this tree at ce1b5304101 (4 paths vs merge base 215840f43, three-dot): 64
families, 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, then
pnpm --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 lint repo-wide is CI's run. The narrowing here is DECLARED and carries all three
readings: (1) the reachable population comes from eslint's own config text —
eslint.config.mjs:328 records that this repo runs one config that never enables
type-aware linting (no parserOptions.project, no typed rules) for ANY file, measured
there with a positive control; (2) the targeted count is read from --format json: 4
files, 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 is private: true; (2) all 70
non-private workspace packages declare a files[] (lit control) and 0 of them have an
entry that climbs out of its own package directory; (3) none of the four changed paths is
under any package directory. ⇒ root scripts/**, content/docs/** and docs/audits/**
cannot be in any published payload. Clause-②: no follows: this moves a build-time gate's
failure convention, not packages/spec's accept set or any public surface.

⚠️ Declared deviation — three files outside the dispatched file surface

The dispatch declared scripts/tenant-audit-census.mjs and new tests under scripts/.
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.mdx and
    docs/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 way
    to edit them and is what check-tenant-audit-census.mjs demands. Point ④ puts the arm in
    the 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 does
carry scripts/ rows (#19024, #18414, #19153, #18723). Dark control:
scripts/zzz-no-such-file.mjs has no row.

⚠️ Also in the regenerated artefacts and NOT caused by this change: the unenforced
corpus-scale block re-stamps its date and sha and moves 573 to 576 tracked sources, because
main moved since it was last written.

Acceptance notes — noted, not filed

  • Triage's route ① (stop collapsing the declared type text at storage time) stays
    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.
  • The same defect class at the transpile door (transpileChecked on a lifted snippet) was
    already filed as its own card by the domain:spec#3 seat, with the boundary carried
    over. ⛔ No second card from here.
  • No path literal was added to scripts/tenant-audit-census.mjs or ts-parse.mjs;
    check:watch-hint-literal exits 0. Successor: none needed.

Generated by Claude Code

…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>
@os-litant os-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 20, 2026 — with Claude
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 20, 2026

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ce1b530410150de7f229a6ffb9dab0dd1608baf6

Isolated review. Every reading below was re-taken by this reviewer in two dedicated worktrees — the PR head ce1b53041 and its merge base with origin/main, 215840f43 (main has since moved to adf4b1877, touching none of the four paths) — at 2026-09-20T10:05Z–10:17Z, never on the shared checkout. Nothing in the PR body or the dev report was adopted without a reading of its own. Fixtures named below were git added into the census surface (packages/services/…, which collectSources enumerates by git ls-files) and removed again; both worktrees end with 0 status entries.

① Derived judgments

All four points of the sequenced hunk landed, and as specified.

point where it landed (head line numbers) reading
① door rule takes the origin sf readTypeTextDoor(typeText, origin) :836; resolveReceiver hands its own sf at all four inlineEngineDoorOrOther sites (:612, :641, :660, :682); nonEngineReason(res, typeNames, sf) at :1423 landed. The origin is the file's own parseSourceFile tree and decls are file-local (declaredTypesIn(sf) :1360), so the attribution is exact
② synthesis through parseDerivedText :839, the only census-receiver-type.ts synthesis in the file landed
③ failure printed against the SITE, door reads false the door returns { door: false, failure }; runCensus carries it as derivedFailure (:1438); generator main() prints a per-site ::error:: with the report under it landed. Measured: the report carries derived from packages/services/zz-review-probe/src/probe-receiver.ts — the site's file, not only the synthetic name
④ declared arm in UNDEFENDED_REASONS NON_ENGINE_REASONS :760, UNDEFENDED_REASONS :770–772, pinned by a self-test case landed

The acceptance inputs are a real pair; neither passes vacuously.

  • Input 1 — a newline-separated inline literal is CLASSIFIED. Re-taken on the real corpus with a fixture receiver whose literal separates insert(object: string, …) and find(…) by a newline only. Base: gate, gate --self-test and generator all die exit 3 naming census-receiver-type.ts:1:78 ';' expected — the card's defect, reproduced. Head: the site is classified type-text-not-round-trippable, located at probe-receiver.ts:6, with the parse verdict under it. Lit control held: the semicolon spelling of the same literal in the same file is PLACED — the certified population moves 227 → 228. (My first fixture named both parameters engine and both sites were refused; that is the pre-existing name-keyed locals map in declaredTypesIn conflating same-named parameters across scopes, not this PR — renamed, the control is clean. Noted as an out-of-scope observation.)
  • Input 2 — a genuinely unparseable input is still REFUSED. Two layers, both read. Corpus door: an unparseable SOURCE git added into the surface exits 3 on base and on head (gate and generator); node scripts/ts-parse.mjs --self-test exits 0 on head, 48 cases. Derived door: the GARBAGE_DOOR case lands on the refused arm with door: false. It cannot pass vacuously: a door that swallowed the failure would answer anonymous-type and fail that case; a door that rejected everything would fail the semicolon lit control.
  • The boundary pin is real: a newline-separated literal naming no write verb (remove/find) on a .delete() receiver reads anonymous-type on head, i.e. never reaches the synthesis — the verb gate runs first on both sides.
  • Self-test counts re-taken on head: census 49 → 59 cases, gate 24 → 27, all green; census refusals battery registers 8 cases against its raised floor of 7 — one case of slack, so "the new cases cannot stop running unnoticed" is one case over-stated. Nit.

Residual, stated so the seat can weigh it: the arm is a refusal that tells the author to restyle legal TypeScript (; separators or a named type) to work around the tool's own collapse at :480/:499/:562. That is what the sequenced hunk asked for and it is strictly better than the takedown it replaces, but the underlying limitation is still in the tree; the PR's "Not a defect in the tree" under-states it. Triage's route ① (stop collapsing) is the real repair. Whether it gets a card is the seat's call.

② Semver level

skip-changeset holds, on this reviewer's own reading over the workspace manifests at head (pnpm-workspace.yaml globs expanded, 81 manifests): root private: true; 11 private, 70 non-private packages, all 70 declare files[], 0 entries climb out of their package directory, and 0 of the four changed paths lies under any workspace package. Lit control: packages/spec/src/kernel/metadata-plugin.zod.ts IS under one. ⇒ root scripts/**, content/docs/** (docs site, not an npm payload; Build Docs green) and docs/audits/** cannot reach a published payload.

Clause-②: no is right: packages/spec is untouched, no accept set moves, and what changes is the failure convention of a repo-internal script. Check Changeset is skipped by the label per lint.yml:3279 design, as intended for this class.

③ Boundary flags

The floor is genuinely held, verified at its root, with one literal exception judged not a floor drop and one non-blocking defect.

Root. .github/workflows/lint.yml:2005-2008 is the only CI invocation of this census across all 37 workflow files: the GATE, twice (--self-test, then bare). The generator is invoked by no workflow, no package.json script and no turbo.json task. The gate's selfTest() calls censusSelfTest() (:1047), so the generator's 59 cases do run in CI. No other module imports this census's runCensus, typeTextDeclaresEngineDoor or nonEngineReason (check-platform-object-tenancy-census.mjs imports its own sibling's runCensus).

Measured exit codes with the newline-separated receiver tracked in the corpus:

invocation base 215840f43 head ce1b53041
check-tenant-audit-census.mjs 3 (takedown) 1 — [type-text-not-round-trippable] …probe-receiver.ts:6
check-tenant-audit-census.mjs --self-test 3 1
tenant-audit-census.mjs 3 1 — ::error:: per site, parse report under it
tenant-audit-census.mjs --json 3 1 — site carried with derivedFailure (count 1, line 1)
tenant-audit-census.mjs --write 3 0 — writes the row under ENFORCEMENT in both artefacts
gate AFTER --write (the "quiet subtraction costume") n/a 1 — exactly one problem, the refusal
corpus door: unparseable SOURCE in the surface 3 3

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. --write moves 3 → 0. The PR's sentence "Nothing that exits non-zero today exits 0 after this" is therefore over-broad. Judged not a floor drop: --write is the documented repair path and CI never runs it; on base it already returns 0 before the refusal loop for the two sibling classes (unledgered, staleLedgerRows; :2237–2248); and its product is the declared row, not silence. Nit: in --write mode the generator prints nothing about the site it just declared — an author learns of it from the artefact diff or from CI.

Non-blocking defect (measured, one-line fix). With the fixture present and artefacts regenerated, --self-test fails 1 of 27: the pre-existing control "a census with neither an unplaceable site nor a stale row is NOT a finding", because the old refuse helper (check-tenant-audit-census.mjs:979-980) spreads the live undefendedSubtractions, unlike the new refuseUndefended (:1005) which zeroes them. The gate's own docblock (:618–630) says the synthetic censuses exist so the self-test reads the same "no matter what today's tree happens to hold"; for the new class it no longer does. Consequence on such a tree: CI reds on the self-test line (bash stops there, so the bare gate's cleaner refusal never prints), with the located verdict visible only inside the control's detail text and mis-labelled as an instrument failure. Exit stays non-zero either way, so the floor is untouched. Fix: pass undefendedSubtractions: [] in refuse.

Declared deviation — three files outside the claimed surface. (a) scripts/check-tenant-audit-census.mjs: necessary, proved by construction — after --write the artefacts explain the row and only censusRefusals reds the gate (exit 1, 1 problem); without that arm the gate would have passed on a tree that reds today. Not scope creep. (b)+(c) the two artefacts: --write on the clean head is a no-op except the HEAD-sha stamp (215840f43ce1b53041 in my worktree), so they are exactly the generator's output; the arm's prose sits in the ENFORCED region the gate compares. Necessary. The 573 → 576 corpus-scale movement is the unenforced block, values not compared.

The generator main() non-zero judgement: sound, keep it. On base the generator exited 3 for this class; head returns 1 — the sign never changed, so this is not a new red on a code that was green, and it keeps the local run and the CI verdict agreeing on the same tree. The sibling undefended arms stay warnings by the documented reason at :2262–2270; the asymmetry is deliberate and correct for an arm that is a tool fault rather than a fact about the corpus.

CI at posting time: 31 latest-per-name checks, 0 failures; Lint & Repo Gates still in progress at step 60/183 with Tenant-audit census matches the tree (step 67) pending. The local runs above are this review's reading of that step.

Implemented-by: claude/issue-19077-census-derived-parse-item-2
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants