Skip to content

fix(scripts): apply the tenant-audit census's engine-door rule to inline type literals (225 -> 227) - #19073

Merged
os-elon-musk merged 4 commits into
mainfrom
claude/issue-18944-census-inline-receiver-placement
Sep 18, 2026
Merged

os-elon-musk merged 4 commits into
mainfrom
claude/issue-18944-census-inline-receiver-placement

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #18944

Clause-②: no

The census applied its engine-door rule to named declarations only. A write
call site whose receiver type is an inline type literal had no name for the
engine type index to be keyed on, so it was classified kind: 'other' and
subtracted from the certified population — however plainly its own text stated
an ObjectQL write door. The census already printed both such sites on every
run and called the subtraction "probably WRONG"; it then took the subtraction
anyway. A diagnostic that names a subtraction as wrong and still takes it is a
deferral, not a report.

JOB ONE — the crux, answered before the classifier was touched

Both sites PASS. This is a classification repair, not a live violation.

Measured with a probe that calls the census's own exported functions
(resolveReceiver, resolveObjectNameArg, declaredObjects,
tenantContextOf) on the unmodified module, i.e. before any edit to the
classifier. Sites selected by symbol — file + verb + receiver text — never by
the card's line numbers.

site (re-derived by symbol) object tenancy context elevated violation?
plugin-auth/src/audience-gate-test-support.ts · engine.insert · re-derived at :72 sys_invitation enabled options.context yes no
plugin-auth/src/sso-client-secret.ts · e.update · re-derived at :303 sys_sso_provider disabled (declared opt-out) options.context yes no

Controls, because a "no" needs one:

  • corpus control (HITS) — both files are in the census corpus on this tree
    (collectSources().includes(...) → true for each). A "no violation" from a
    file the instrument never reads would be void.
  • predicate control (HITS) — the same violation predicate
    (tenancy === 'enabled' && carriesTenantContext === false) lights on 9
    other sites of 227, three named in the report
    (auth-plugin.ts, scim-connection-service.ts,
    datasource-admin-plugin.ts). The instrument can say "yes"; it said "no"
    here.
  • what would have changed the reading: either site dropping its
    { context: { isSystem: true } } argument (→ carries: false), or
    sys_invitation declaring tenancy.enabled: false.

⇒ The figure downstream cards cite does not move. 9 / 225 becomes
9 / 227: the numerator is unchanged and the denominator becomes truthful. The
repair is to what this page can be read as having examined — ⛔ not to any count
that anything cites as a violation.

On triage's still-open escalation branch
(「被减掉的两个写点里任一个在生产路径上执行过 ⇒ 升 p1」): still not run, and
it still needs a runtime reading. What this PR adds is that the branch's safety
premise is spent for these two sites either way — once placed, neither breaks
the rule the population exists to enforce. One of them is nonetheless
production-shaped (migrateLegacySsoClientSecrets, a boot-path migration), so
the branch is reported as open rather than closed.

The fix

inlineEngineDoorOrOther() judges a declared type text one more time when no
indexed name is found in it, by reading the same door rule off the text
itself. It calls typeTextDeclaresEngineDoor → memberIsEngineDoor — the one
function that already answers that question for a named declaration — so there
is no second spelling of the rule to drift from. Applied at all four
resolveReceiver returns that carry a declared type text (fromEntry, an
as cast, a member access, a call's return type), not only at the two that
happen to fire today. A named engine type still wins and still reports its own
name; an inline literal with no write door is still a subtraction that says
why.

⛔ Not a widening of the definition: it IS the definition, applied where it had
only been reported.

Before / after, with the ablation proof

Same tree, same command (node scripts/tenant-audit-census.mjs):

before after
write call sites 225 227
object name decidable 149 151
tenancy enabled 149 150
declares tenancy off 0 1
threading a tenant context 141 143
provably none AND tenancy-enabled 9 9
options unreadable AND tenancy-enabled 32 32
decidably elevated 106 108
non-engine calls subtracted 146 144
subtractions the census could NOT defend 3 1
…of those, type text states an engine door 2 0

Ablation, through scripts/ablation-replace.mjs so the write is verified
against the disk rather than against an exit code (anchor: the helper's own
guard, replaced with a dead one):

anchor hits 1 time(s), as declared     anchor 1 -> 0, replacement 0 -> 1
blob  40921ef8b3b3 -> f76872fe8b03     (git hash-object, before -> under mutation)
under mutation: census reads 225; anonymous-type 2; undefended 3; doorShaped 2
under mutation: check-tenant-audit-census.mjs EXIT=1
  committed : | write call sites on the application surface | **227** |
  census    : | write call sites on the application surface | **225** |
restore: git checkout HEAD -- PATH   (the explicit-HEAD form, absolute path)
  blob after restore 40921ef8b3b3f66bb657c24097d357ad26ce68b8
  blob at HEAD       40921ef8b3b3f66bb657c24097d357ad26ce68b8
  git diff HEAD      empty

Predicted direction before running it: reverting the classifier turns the gate
red. Observed: exactly that — and the census reverts to the pre-fix reading in
every one of the eleven rows above. So the artefacts in this PR are held to the
fixed classifier, and the file that was edited is the file that ran.

Both artefacts AND the prose

Both generated artefacts were rewritten by the census's own tooling
(node scripts/tenant-audit-census.mjs --write), ⛔ never by hand.

The page's hand-written prose was re-read against the new numbers. One claim
was falsified by the placement and is corrected:

exactly two opt out (sys_api_key, sys_sso_provider), and no write call site
on this surface targets either.

One now does — the legacy client-secret migration writes sys_sso_provider
under an elevated context, which is why the generated declares tenancy off row
reads 1 rather than 0. sys_api_key still has no write site on this surface
(measured: no site in --json names it). Nine further hand-written figures
moved and are updated (the gate holds every one of them: 23 prose figures).

Two things stated rather than rewritten on a guess:

  • "18 classifier cases, 6 of which red against the old reading" — could not
    be verified. The generator's self-test reports 44 cases before this change
    and 49 after, and the gate's own reports 24; none is 18, so the claim counts
    some subset this PR cannot identify. It is left as written and its sentence
    is narrowed to the two over-claims it was written for, rather than being
    silently extended over the third instance this PR adds.
  • the (47%) → (48%) parenthetical is hand-written and unenforced (the
    gate requires digits-then-%, not a value). 108/227 = 47.58%, so floor gives
    47 and nearest gives 48. Both surviving data points on the page (77% and 47%)
    round the same way under either rule, so the convention is undetermined from
    the page itself; nearest-integer was used and is flagged here for the seat.

Verification

Gate families derived from the real change set after the final commit:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
derived at 6c23858e5, the final commit (3 paths vs merge base 0ec81857a;
the tool's own repo+commit declaration is in its stderr, and the list is
byte-identical to the derivation taken before the last commit).

All 62 commands it printed were run on that HEAD, each exit code captured
before any pipe: 62 run, 62 exit 0.
Five of them first refused as
PREREQUISITE NOT MET in a fresh worktree — @objectstack/spec,
@objectstack/lint, @objectstack/formula, then @objectstack/client-react
were not built — so those packages' closures were built and the families were
re-run rather than reported unmeasured. The per-command table is in the report
comment on #18944.

NOT MEASURED, and named rather than counted as passes: the 14 families that
apply only once a changeset exists (this PR declares none, see below), the 11
that declare a whole-root population no path narrows, the 2 that take a value
from the workflow, the 2 path-scheduled CI jobs (Test Core, Build Docs) and
the 52 artifact-roster families whose silent verdict is a fact about a list
rather than about these paths.

scripts/check-tenant-audit-census.mjs is this census's gate and it judges:

✓ check-tenant-audit-census: OK -- 227 write call sites certified (151 decidable;
  9 tenancy-enabled sites PROVABLY carry no tenant context, 32 more unreadable),
  23 prose figures held to the census, every write call site placed and every
  `UNTYPED_RECEIVERS` row matched.

⛔ No gate was weakened: no ratchet raised, no floor lowered, no ledger row
deleted, no test skipped. The doorShaped diagnostic is kept — now 0 by
construction — as the alarm for this hole reopening, and five new self-test
cases pin the rule in both directions.

Changeset

No changeset, and the skip-changeset label is owed but not applied by this
PR
— label management was withheld by this dispatch. Measured, not assumed:
no package directory contains any of the three files (30 manifests inspected;
the repo-root manifest is private: true), so nothing any package's files[]
ships can move. Positive control: packages/spec is published and its files[]
is non-empty, so the instrument can say "shipped" when it is. An
empty-frontmatter changeset is explicitly not an option here (the gate rejects
newly added ones).

Acceptance notes

  • to file (class (a), pre-existing, ⛔ not fixed here) — the census aborts
    with a ts-parse refusal (exit 3) on a receiver whose inline type literal
    separates its members by a newline only: declaredTypesIn collapses
    whitespace, so the separator is lost and the synthetic
    type CensusReceiver = ... re-parse fails with "';' expected". Reproduced
    directly; control lit (the semicolon-separated form of the same literal
    returns true). Live on main through the anonymous-type arm, which has
    called typeTextDeclaresEngineDoor on the same collapsed text since PR
    fix(scripts): make the tenant-audit census say when it could not place a receiver's type #18943; this PR widens the same call to brace-free type texts that mention a
    write verb. No such receiver exists in the corpus today (573 sources, exit 0).
  • noted, not filed: NON_ENGINE_REASONS is exported and read by nothing — its
    keys are produced by nonEngineReason and its descriptions are documentation
    only, so its docblock's "closed set of seven" is closed by review, not by a
    gate. True today. Successor: the next author of this classifier.
  • noted, not filed: the module docblock's "There are 45 of them" for erased
    (any) receivers reads 44 on this tree (18 + 15 + 11), a pre-existing
    one-off drift in a comment, untouched here.

Generated by Claude Code

…ine type literals

The census's engine type index is keyed on declaration NAMES, so a write call
site whose receiver type is an inline type literal had no name to look up and
was classified `kind: 'other'` -- subtracted from the certified population --
however plainly its own text stated an ObjectQL write door. Two sites on a clean
tree were exactly that, both writing under an elevated context:
`resolveInsertEngine()`'s `{ insert: (name: string, ...) => ... } | null` and
`migrateLegacySsoClientSecrets`'s `engine as unknown as { find(object: string,
...); update(object: string, ...) }`. The census already PRINTED both, per site,
on every run, and called the subtraction probably wrong -- then took it anyway.

`inlineEngineDoorOrOther()` reads the same door rule off the type text when no
indexed name is found in it, reusing `typeTextDeclaresEngineDoor` ->
`memberIsEngineDoor` rather than spelling the rule a second time. A named engine
type still wins and still reports its own name; an inline literal with no write
door is still a subtraction that says why.

Population 225 -> 227. The figure downstream cards cite -- sites provably
carrying no tenant context against a tenancy-enabled object -- is UNCHANGED at
9: both sites thread `{ context: { isSystem: true } }`, and one targets
`sys_sso_provider`, a declared tenancy opt-out. The repair is to the denominator
and to what this page can be read as having examined, not to any violation count.

Both artefacts regenerated with `--write`. The page's hand-written prose is
re-read against the new numbers, including the claim it falsified: "no write
call site on this surface targets either" opt-out object is no longer true, and
the generated `declares tenancy off` row now reads 1.

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

Two corrections to text this change would otherwise leave subtly false:

- the generator docblock claimed the `/inline-door` suffix on `how` is what
  `--json` shows for a placed site. It is not -- `how` is carried onto
  diagnostics, never onto a placed site's record, whose `engineType` is the
  observable. The unused `inlineType` payload goes with it.
- inserting a third instance ahead of the page's "pins all of it" sentence
  silently extended the unverifiable "18 classifier cases, 6 of which red"
  claim over the new one. That sentence now names the two it was written for,
  and the door rule states its own both-directions pin.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Sep 18, 2026
@os-elon-musk os-elon-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 18, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

Check Changeset red on 6c23858e584dbdd2b8ef671ef80cf6fd01c69cc6 — cleared by the seat, ⛔ not by the dev, because the remedy is a label and labels are the seat's write. Seat: domain:spec#3 · taken 2026-09-18T16:01Z

What was failing, in the gate's own words

This PR adds no changeset. FIRST: if its only .changeset rows are CHANGED, not added, this PR corrects somebody else's pending release note — do NOT apply skip-changeset … Otherwise, if it releases nothing … apply the skip-changeset label; otherwise run pnpm changeset and name the packages.

Read from the job log (105666980728), ⛔ not from the check-run summary, which is empty on this check.

Route 0 does not match — measured, because it is the one case where the label is REFUSED

Route 0 fires when a PR's only .changeset rows are CHANGED (correcting someone else's pending note — ruling ② B on #18375, which says the label is not applied there). This PR's diff carries zero .changeset rows of any status:

file status lines
content/docs/permissions/tenant-audit-census.mdx modified +64/−27
docs/audits/2026-08-tenant-audit-write-call-sites.counts.md modified +18/−12
scripts/tenant-audit-census.mjs modified +130/−10

⇒ route 0 is out by the diff, which the gate says is how it should be decided: 「Route 0 … is answered by the diff, not by judgement」.

"Releases nothing" — the premise, with a control, because it is what licenses the exemption

  • pnpm-workspace.yaml globs are packages/*, packages/apps/*, packages/drivers/*, packages/plugins/*, packages/qa/*, packages/triggers/*, packages/services/*, packages/adapters/*, packages/connectors/*, apps/*, examples/*. None of content/, docs/ or scripts/ at the repo root is a workspace member.
  • Every tracked package.json scanned (83 files, 0 unreadable): 0 declare a files[] entry naming content/, scripts/ or docs/.
  • Lit control, same instrument: 70 of those packages declare a files[] at all, and all 70 name a dist entry. ⇒ the zero above is a reading, ⛔ not a dead scan.

⚠️ First attempt at that control was spoiled by my own head -3 breaking the pipe and reporting "3" — re-run without truncation. Recorded because a truncated enumeration reads exactly like a small population.

⇒ this PR publishes nothing, so route 2 applies, which the gate's own log marks <<< PREFERRED, and skip-changeset is now on the PR (four-step write with read-back: documentation, size/m, skip-changeset).

Why the dev did not do this

The dispatch forbade the dev any label write and anticipated exactly this case in writing — a repo-root script may owe skip-changeset, and if so it is the seat's. ⛔ No fault of the dev's, and ⛔ nothing for it to push.

What to expect on the check itself

Per the same log: 「applying it now still fires a labeled event and that run goes green — but the red left here does not clear itself.」 ⇒ the new run is the verdict; this run stays red as a historical artifact. ⛔ Do not read the stale red as a live failure, and ⛔ do not re-run it to tidy the display.

The dev's round is still open; this comment records the seat's act so the dev's report is not written against a red it neither caused nor may clear.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Correction to this seat's own reading in comment 5732653264 — the post-label run is skipped, ⛔ not success, and「green」was the wrong word. Seat: domain:spec#3 · taken 2026-09-18T16:08Z

The implementing dev re-read the live state instead of taking the seat's word for it and caught this. Verified here by reading both check runs directly, ⛔ not from an aggregate:

check run started status conclusion
105666980728 15:57:22Z completed failure (pre-label; ⛔ does not clear itself)
105668291851 16:01:15Z completed skipped (the labeled run; the job-level if: short-circuits on the live label)

⇒ the exemption is expressed as a SKIP, not as a pass. This seat's earlier sentence 「reads green on the latest run per check name」 came from an instrument that buckets success, skipped and neutral together as not-red — fine for 「is anything failing」, wrong for 「is this check satisfied」. The dev was right to refuse the word.

Does a skipped required context satisfy the required set here? — measured by behaviour, because the config is unreadable

GET /branches/main/protection returns 403 Resource not accessible by integration to this seat, so the configuration cannot be read. The behavioural control instead — three PRs that carry skip-changeset and merged, each read on its own head:

PR merged Check Changeset runs on that head, in order
#19026 2026-09-18T15:16:38Z success 12:15:25 → skipped 12:15:43
#18998 2026-09-18T15:16:08Z success 10:28:57 → skipped 10:29:31
#19050 2026-09-18T15:17:05Z success 14:22:17 → skipped 14:22:43

⇒ a skipped latest conclusion did not block a merge in three measured cases. That is the reassuring half.

⚠️ What the control does NOT establish, stated rather than glossed: in all three the earlier run on the head was success, while on this PR the earlier run is a failure. So 「skipped supersedes an earlier failure on the same head」 has zero measured cases here. GitHub evaluates the latest run per check name, which predicts it is fine, and the merge queue re-runs checks on its own test-merge commit anyway — but that is a prediction, ⛔ not a reading, and it is named as such.

What this changes operationally

Nothing to do now: the PR is a draft awaiting the landing act that this session's write classifier refuses ([Merge Without Review]), and the queue will produce its own fresh run. If an arm ever fails naming this check, that is the measurement the control above is missing, and it should be recorded here rather than re-derived.

⛔ No re-run of the stale failure — re-running to tidy a display is exactly what the gate's own log warns against.


Generated by Claude Code

…ement

Conflict set: exactly ONE line in each generated artefact — the deliberately
UNENFORCED `Measured on <date> at <ref>` marker, which sits inside the census
page's generated region and in the counts file's corpus-scale block. Both sides
were stale; main's side is kept so this merge never asserts a marker older than
what landed, and the generator rewrites it in the commit that follows.

Everything else text-merged with BOTH intents intact, verified rather than
assumed:

- scripts/tenant-audit-census.mjs auto-merged with no conflict and now carries
  #19075's corrected `merge=os-regen` rationale (the emitted "no
  `.gitattributes` row names it ... Root-level tooling is no obstacle by
  itself") AND this branch's inline-type-literal placement
  (inlineEngineDoorOrOther, 9 occurrences). Zero conflict markers.
- the counts file holds #19075's new rationale paragraph and this branch's
  population (227, `declares tenancy off` 1, both new site rows, the
  0-by-construction paragraph); #19075's superseded rationale is gone.
- the census page holds every hand-written prose edit from this branch,
  including the claim the placement falsified. Measured: main's ONLY change to
  that page between this branch's base and origin/main is the one in-region
  marker line, so there is no hand-written text of main's to carry over.

Main still reads population 225 on both artefacts, so #19075 re-baselined
nothing and this branch's 225 -> 227 is still the whole change. Regeneration
from the merged tree follows as its own commit, per the ordering rule: running
a generator while the tree is in MERGE state stamps the PRE-MERGE tip, because
`measuredAt()` reads `git rev-parse HEAD`.

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

`node scripts/tenant-audit-census.mjs --write`, run AFTER the merge commit so
`measuredAt()` reads the merged HEAD rather than the pre-merge tip.

The staged diff is exactly two lines — the deliberately UNENFORCED
`Measured on ... at ...` marker in each artefact, now naming the merge commit
`30def652e` instead of main's `d4cb05cbf`. Nothing else moved, which is the
proof the merge resolution was byte-faithful: the generator, run against the
merged tree, reproduces both hand-resolved artefacts exactly. Had the semantic
merge dropped or mangled a row, a total or #19075's rationale paragraph, this
commit would carry it.

That also reconciles the two rules that govern this file. `os-regen-merge.sh`
requires a non-driver-managed generated artefact to be hand-resolved for the
merge commit and ⛔ not regenerated mid-merge (a mid-merge recompute describes
a half-merged tree); the file's own header requires a conflict to be resolved
BY REGENERATION and ⛔ never by hand. Both hold when the order is: resolve,
commit the merge, regenerate, and let the regeneration diff certify the
resolution.

#19075's corrected rationale re-measured on the merged tree with its own
instruments and still true: 0 `.gitattributes` rows name this census (fire
control: `strictness-ledger` returns 1, so the zero is not a dead reading),
`git check-attr merge` reads `unspecified`, 0 manifests name it in a `scripts`
entry, and the lint workflow runs the gate directly.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
@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 ab41a8f Sep 18, 2026
33 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-18944-census-inline-receiver-placement branch September 18, 2026 21:42
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…e synthesis stops taking the whole run (objectstack-ai#19112)

Refs objectstack-ai#19077 (item 1 — the returnable derived-parse door; the census-side
application is item 2, sequenced after PR objectstack-ai#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 objectstack-ai#19073. ⇒ this PR makes the repair available and provable; it
does **not** apply it. A `Fixes` keyword would have closed card objectstack-ai#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@07c6f82`) 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
07c6f82..b09c8f5`,
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 be83f4e`, `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 objectstack-ai#19073 (card
objectstack-ai#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 objectstack-ai#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.com/claude-code)

https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh

---
_Generated by [Claude
Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

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

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