Repository navigation
fix(census): scope the tenant-audit census on class-property and callable names - #19749
Conversation
…ble-name maps `thisProps` and `fnReturns` were keyed on the bare identifier, so two classes in one file sharing a property name were ONE entry and the first TYPED one decided for both. Measured on a synthetic pair, that admits a `Map.delete` to the certified tenancy population in one declaration order and SUBTRACTS a real engine write as `platform-type` in the other -- the silent direction the census's own header names as the one it cannot survive being wrong in. A class property is not a lexical name: `this.engine` in class B is B's. A callable's name belongs to whatever declares it -- a class body for a method, the enclosing block or file for a function. So `scopeChainOf` and `scopedNames` take the scope predicate, and all three name maps are scoped ones under the predicate that matches how the language scopes that kind of name. Each keeps the file-wide FLOOR, so nothing that resolved before stops resolving. The bare-key spelling is removed rather than guarded: `note()` now requires the declaring node, and no `.get(name)` lookup survives. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
…f-test Eight cases in the module's own self-test, in both declaration orders for each map, at both storage sites a class property reaches its type through, and through the `this.<base>.<member>` base. Two FLOOR cases hold the other direction: a `this.<prop>` no enclosing class declares, and a method called from outside its class, both still resolve file-wide, so nothing that resolved before this change stops resolving. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
…nsus-class-scope-keys
…ange `node scripts/tenant-audit-census.mjs --write`, the repo's own tooling, on the merged tree. Every ENFORCED figure is byte-identical -- 227 write call sites, 153 decidable, 9 tenancy-enabled sites provably carrying no tenant context and 34 more unreadable. What moved is the one line the gate deliberately does not compare: the unenforced corpus-scale block's provenance stamp, so the page now says the population was re-derived at a commit that carries the scoped keying. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
|
Served-tier: 89/89 Contract review of Method: Tier: ① Derived judgments1. Is the scoping correct? —
|
Seat disposition of the FAIL — accepted, confirmed at source, and going back to a dev. ⛔ Not landing as it stands.Written by the ⭐ Everything else in this round verified. The The blocking finding, re-derived by the seat at sourceAt head, against ⇒ a bare-identifier call — ⭐ The file's own docblock is what makes the defect legible, and it is worth quoting because it is right about the thing it is reasoning about:
That reasoning is about where a callable's NAME is declared — and it is correct for recording a declaration.
|
…here the name was declared
`opensCallableScope` is the predicate a callable's name is RECORDED under --
a method's name in its class body, a function declaration's in the block or
file. The lookup site used that same chain for every callee shape, so a bare
`getEngine()` was resolved through the enclosing class body, which the
language never does: a method lives on the prototype, not in lexical scope.
A `class C { getEngine(): Map<…> }` therefore shadowed a file-level
`function getEngine(): IDataEngine` at a call written `getEngine()` inside
`C` -- a real engine write subtracted under the DEFENDED `platform-type`
arm, which prints nothing and is counted nowhere.
`calleeScopeChain` picks the chain by callee shape: a bare identifier walks
the lexical scopes only, `this.m()` walks the class chain, and `x.m()` walks
neither and reads the file-wide floor, which is exactly what it resolved
through before. Recording is unchanged.
Four pins cover the three shapes: a bare call in both declaration orders
(the floor disagrees between them), `this.m()`, and `this.x.m()` from inside
a class with its own same-named method.
Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
`--write` moves exactly the two provenance lines and no other byte, which is the artefact-level statement that the callee-shape repair changes no published figure: the corpus has no call site whose classification it moves. The stamp now names a commit that carries the shipped instrument rather than one that carries the defective one. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
The
|
| reading | value |
|---|---|
workflow run 35787953113 |
status: completed, conclusion: cancelled, head bfd92001b1, event pull_request |
| its 15 jobs | 8 success · 1 cancelled · 5 skipped · 1 failure |
| the cancelled one | Test Core (1/6) — a shard, cancelled mid-run |
| the failed one | Test Core — the rollup, whose own log contains no error at all: it merged 4 shard timing captures and uploaded its artefact cleanly |
| the rollup's own timing table | 「Merged from 4 shard capture(s) (1/6, 2/6, 3/6, 4/6)」 — ⇒ the run did not reach all six |
⇒ the rollup reported failure because one of its needs: was cancelled, ⛔ not because a test failed. Zero test assertions failed in that run. And cancelled is NOT MEASURED — ⛔ neither green nor red.
Why it was cancelled is the ordinary reason: bfd92001b1 was superseded seconds later by a36551a559, and the concurrency group cancelled the older run. ⇒ this red belongs to a sha that is no longer this PR's head.
The reading that governs
The PR's current head is a36551a559. Read at 2026-09-22T21:49Z: 25 check names — 11 success, 7 skipped, 0 failure, 7 still running.
⇒ ⛔ No fix is pushed and no re-run is spent, and neither is a punt: there is no failure to root-cause. ⛔ Re-running a cancelled, superseded run would measure nothing — the run that matters is already in flight on the current head. This seat keeps the PR watched and will read that head's conclusions when they land.
Generated by Claude Code
|
|
Served-tier: 86/86 Contract reviewContract review of A record binds one head. The earlier record (comment Method. Tier. Why this review is owed. By the surface, not the lane: ① Derived judgments1. The callee-shape repair — each arm is correct, and the compiler agrees on every shape probedAt head ( Probed by running all three module versions —
Bare identifier → lexical only: X1, X2, E3 are the compiler's answers. 2. Does the repair introduce anything new? — ONE shape, in the silent direction, where
|
| probe | shape | origin/main |
bee92afe12 |
head | tsc |
|---|---|---|---|---|---|
| E7 | file-level function getEngine(): IProbeEngine; inside w() a const o = { getEngine(): Map… } and a bare getEngine().insert(…) |
engine | platform-type | platform-type | exit 0 (engine) |
| E7r | same, file-level function declared after w() |
platform-type | platform-type | platform-type | exit 0 (engine) |
At head a real engine write that origin/main PLACED is subtracted under the DEFENDED platform-type arm — the direction the census header names as the one it cannot survive — and where main was wrong in one declaration order the head is wrong in both for every bare call inside that block. It is not among the 79 pins, and the self-test's success line (:2621ff, "a bare f() resolves lexically and is never shadowed by a same-named method") and the calleeScopeChain docblock ("a BARE IDENTIFIER is a lexical name and nothing else") both claim the property this shape falsifies. The re-stamp commit's message ("the callee-shape repair changes no published figure") is true; the docblock's universal claim is not.
- Corpus exposure today: zero, with controls (head module, 576 sources, 22:56Z): 98 object-literal methods; 0 files in which an object-literal method shares a name with a same-file function declaration or arrow-const; 12 bare-identifier-callee write receivers, all
createHash()/createHmac()(node builtins, thenode-importarm); 0 exposed. Scanner firing control: a one-file synthetic tree with the E7 shape reports 1 pair and 1 exposed receiver. For calibration, the shape the earlier record blocked on has the same exposure: 1325 class methods, 0 files where a class method shares a name with a same-file function/arrow-const. Latent is the same defence it was last round, and it was not accepted then. - Repair, verified here on a variant of the head module (23:01:22Z):
opensCallableScopealso openingts.isObjectLiteralExpression(node)records an object-literal method under its literal, which no lookup chain walks, leaving it reachable only through the floor exactly as before. With that one line: E7 and E7r answerengine; X1–X4, E3, E5, E6 and the dark controls are unchanged;selfTest()still passes 79/79;runCensus({ root })over the head tree is byte-identical to the head's (sha2561e7fed94…). The finding wants that line plus a pin in both declaration orders.
Pre-existing shapes, unchanged by this PR and not findings against it (all three module versions agree): E8, a class property holding an arrow (getEngine = (): Map… ⇒ …) called as this.getEngine() is read from the floor (a same-named file-level function → engine, a Map admitted; tsc exit 2); E11, a bare call to a name declared only as a method resolves through the floor where the compiler reports TS2663; super.m() takes the floor (main's answer restored from the pre-fix head's).
3. The ZERO — a reading, with the right comparison and working controls
- Is one tree the right comparison? Yes, and for this PR it is also the only comparison there is: the census walks
packages/servicesandpackages/pluginsand indexespackages+examples, andgit diff --name-only main..head -- packages examplesis empty (22:56Z). The corpus is the same object under every instrument; only the instrument varies. Two checkouts would differ solely inmeasuredAt.ref, whichrunCensus({ root })reads from the root's HEAD. - Three instruments over the one head tree (22:56:17Z):
origin/main,bee92afe12and head modules each produce 112739 bytes, sha2561e7fed942bc2320893a926ed6299905547fcaadc7a97b13ab0accbe1d797d21f, byte-identical to each other and to the head CLI's own--json(22:53:11Z). Totals: 227 write call sites, 153 decidable, 74 undecidable, 576 sources, 63 engine types, 144 non-engine subtractions,platform-type3, 0 unresolved, 0 unledgered. - Firing control (one tracked file
packages/services/zz-review-x2-probe/src/probe.ts, X2 shape withIDataEngine):origin/mainandbee92afe12read 227 sites / 145 subtractions /platform-type4; head reads 228 sites / 154 decidable / 144 /platform-type3, and the only-in-head site is the probe'sgetEngine()receiver placedIDataEngineby declared type. The comparison moves for both main-vs-head and pre-fix-vs-head, for the reason claimed: the subtraction the repair removes. - Dark controls: instrument-vs-itself on the augmented tree is byte-identical (head/head, pre-fix/pre-fix); a single-declaration probe (no shadowing) reads 228 sites on all three instruments, byte-identical; the probe removed, head output returns byte-identical to the 22:56 reading; worktree
git status --porcelainempty afterwards.
4. The four ablations — the pins can fail, and the widened reach is composition, not a second job
Through scripts/ablation-replace.mjs (anchor count and blob hash prove the mutation, blob == HEAD 1298ffcb7085 and empty git diff HEAD prove each restore), wrapping node scripts/check-tenant-audit-census.mjs --self-test, 23:00:42–23:01:12Z; baseline 79 + 27 cases pass:
| ablation | mutation | reds |
|---|---|---|
| A0 | lookup(fname, callee, calleeScopeChain(callee)) → lookup(fname, callee) |
3 of 79: the two bare-call pins and the x.m() FLOOR pin, each got other/platform-type |
| A1 | the this. arm return opensClassScope → return null |
1 of 79: the this.m() pin, got engine/IProbeEngine |
| A2 | opensClassScope → return false |
6 of 79: the four class-scope pins, plus X2 and X3 |
| A3 | opensCallableScope → return false |
4 of 79: the two callable-scope pins, plus X2 and X3 |
No ⛔ FLOOR-labelled pin is among the reds under A2 or A3. A0 and A1 together red all four new pins, so none is vacuous. The widening is explained at source: under A2 opensCallableScope degenerates to opensScope, so a class method is RECORDED at the SourceFile scope where in X2 it is the first typed getEngine and wins over the later function (a recording-side red), while the this. chain is empty and X3 falls to the floor (a lookup-side red); under A3 every callable is recorded under the null scope, no chain reaches any entry, and both X2 and X3 are answered by the floor. That is one predicate composed into two places, each doing the job its name says — not the earlier defect, where a recording union was applied as the lookup chain for every callee shape. The gap that remains (item 2) is in what the recording predicate recognises, not in the two-jobs sense.
Self-test counts, from selfTest() called directly on each module: 67 at origin/main, 75 at bee92afe12, 79 at head (23:01Z).
5. The regenerated artefacts — generator output, stamp moved and nothing else, not on the os-regen roster
node scripts/tenant-audit-census.mjs --writein the head worktree (22:50:51Z):git diff --stat2 files / 2 insertions / 2 deletions; the only changed lines areMeasured on 2026-09-22 at \bfd9200`.→`a36551a55`.in each artefact. Restored withgit checkout HEAD -- THE_TWO_ARTEFACT_PATHS`; porcelain empty.node scripts/check-tenant-audit-census.mjs(local re-derivation, not the gate reading) exits 0: "227 write call sites certified (153 decidable; 9 tenancy-enabled sites PROVABLY carry no tenant context, 34 more unreadable), 23 prose figures held to the census, every write call site placed and everyUNTYPED_RECEIVERSrow matched."- The re-stamp commit
a36551a559diffs exactly the two stamp lines (90379e052→bfd92001b);bfd92001b1touches only the script.measuredAt()(:349–361) isgit rev-parse --short=9 HEADat generation time, so a commit that regenerates can only ever name its parent: the committed stamp namingbfd92001b(the last commit that changes the instrument) is the fixed point the body describes, andcheck-tenant-audit-census.mjsmasks that line whole (UNENFORCED_SCALE_ROWS/maskUnenforced, :488–554 atorigin/main) so its value is required to be present and dated, never equal. The argument is sound. git check-attr mergeat head (22:44:35Z):unspecifiedfor the script, both artefacts and the check script; firing controlpackages/spec/spec-changes.json,content/docs/permissions/system-context.mdx,content/docs/references/x.mdx→os-regen..gitattributesbyte-identical between main and head. TheLint & Repo Gatesjob (origin/main:.github/workflows/lint.yml, joblintat :164, named at :179) runscheck-tenant-audit-census.mjs --self-testand the check at :2245–2246.
6. The clause-② declaration and the changeset
The declaration Clause-②: no on the PR body and on the card's Claim: comment (5783415521, line-leading there) holds against the diff: no packages/** path, no published schema, key, type, default or export moves, no refusal is removed, and the diff removes no throw or process.exit. The changeset reading: 70 manifests under packages/ declare files[], 217 entries, 0 climb out of their package directory (23:02:37Z); the three changed paths are scripts/, content/docs/, docs/audits/ — under packages/: 0. No .changeset row is added or changed, so route 0 (only .changeset rows are CHANGED) does not apply; the gate's own text at origin/main:.github/workflows/pr-automation.yml:794–796 reads「2. It releases nothing (.github/, .claude/, skills/, docs/, content/, … -> apply the 'skip-changeset' label」, marked PREFERRED in that text. The label is present at both PR reads (≈22:51Z, 23:02Z), and the newest Check Changeset run on this head is skipped by the job's own if:. Who applied it and when is the seat's statement; this record has no timeline read and makes no GitHub write. The route is right.
7. The four acceptance notes — each correctly out of scope; note 1's new paragraph is right
- Class members not indexed — verified: N1 (two classes with
getEngine, called through parametersa: A,b: B) answersengine | engineon all three module versions withAfirst andplatform-type | platform-typewithBfirst, while the same members declared as interfaces are told apart (engine | platform-type). The new order-dependence paragraph is correct and reproduces: X4 answersenginewithAfirst andplatform-typewithBfirst at head,tscexit 0 in both orders, and the pre-fix head answersplatform-typein both — the pinned X4 case agrees with the compiler by the floor's first-typed-wins rule, not by readingA's members. Different mechanism (shapesindexes interfaces and type-literal aliases only, :740–751); a separate card is right. classifyHttpreads an absent header as zero — confirmed at source by reading, not by reproducing the live 403:origin/main:scripts/pm/label-write.mjs:344if ((status === 403 || status === 429) && Number(rateRemaining) === 0) return 'ratelimit';, withrateRemaining: nullproduced at :382 and :402/:407 when the header is absent;Number(null) === 0. A defect inscripts/pm/, not this file — out of scope, correctly noted.- The FLOOR is kept — a boundary, pinned in the FLOOR cases, which stay green under every ablation; correct.
- Note 2 not re-measured this round — a candid statement, consistent with item 2 above.
8. Check-runs on this head — read from GitHub, newest run per check name
First read ≈2026-09-22T22:51Z, final read 2026-09-22T23:02Z, identical: 39 check runs, all completed; none in_progress, none failure, none cancelled. The seat's body PATCH (PR updated_at 22:42:07Z) re-fired only the pr-automation family at 22:42:11Z — Auto Label, Check PR Size, Check Changeset newest skipped (older runs success / success / skipped), the four claim/part-of/single-writer checks success — and nothing heavier. Required contexts: Lint & Repo Gates success (run 35788706216, job 106952133704, 21:48:44Z–22:19:04Z), TypeScript Type Check success, Test Core success (shards 1–6 success), Dogfood Regression Gate success, Governed Surface Queue Guard success, Build Core skipped, Temporal Conformance (live PG + MySQL) skipped. Others: Type Check · workspace / source gates / debt ledger / consumer gates success, Build Docs success, Spec property liveness success, Check Documentation Links success, filter success; Console Pin Gate, Dogfood Verify CLI, Packed-tarball smoke, the Dogfood matrix shards skipped. The reds the thread mentions belong to the superseded sha bfd92001b1 and do not appear on this head.
② Semver level
None. Nothing this PR changes is inside any published package's files[] (70 / 217 / 0 climbing out; the three paths are outside packages/), so no tarball's bytes move and no package needs a bump; the gate's own prescription for that case is route 2, the skip-changeset label, which is present. The declaration no is consistent with that.
③ Boundary flags
- F1 (blocking, item 2): an object-literal method is recorded as a lexical callable of its enclosing block, so a bare
f()in that block resolves to the object's method; a real engine writeorigin/mainplaced is silently subtracted under the DEFENDEDplatform-typearm at head (E7), deterministically inside the block, in both declaration orders. Unpinned. Zero corpus exposure today (0 pairs, 0 exposed receivers, with a firing control) — the same exposure the earlier record's blocking shape had. Verified one-line repair:opensCallableScopeadditionally openingObjectLiteralExpression, plus a pin in both orders; the variant keeps 79/79 green and the population byte-identical. - F2 (pre-existing, not this PR's): a class property holding an arrow function, called as
this.m(), is read from the floor (E8) — the property map and the callable map do not meet;main, pre-fix and head agree. - F3 (pre-existing):
thisinside afunctionexpression or an object-literal method nested in a class method is not the instance, and the class chain — like the old flat map — still answers the class. - F4 (pre-existing, tooling):
node scripts/tenant-audit-census.mjs --self-testis not a mode the generator'smain()handles at head or atorigin/main: it prints the ordinary census report and exits 0, a false green for anyone wrapping the wrong command (this run did, and caught it by the absence of a verdict line). The self-test's only driver ischeck-tenant-audit-census.mjs --self-test. A follow-up card, not a rider. - F5 (disclosure): one ablation batch in this run was voided when a sibling worktree's
typescriptvanished (ts-parse: PREREQUISITE NOT MET, exit 3) and a second batch wrapped the generator instead of the check (F4); both are recorded in the scratch directory and neither is cited above. All cited ablation readings are the 23:00:42–23:01:12Z batch on the owned copy through the check. - F6: the earlier record's blocking finding (bare-call and
this.x.m()shapes) is cleared at this head by measurement (X1, X2, X4), not by inheritance. - Fallback / overload notices: none observed.
Implemented-by: claude/issue-19652-census-class-scope-keys
Reviewed-by: session_013RDBh5DqXd2xnLwvHLgLFr
VERDICT: FAIL — the head records an object-literal method as a lexical callable of its enclosing block, so a bare f() in that block resolves to the object's method and a real engine write that origin/main placed is silently subtracted under the defended platform-type arm (probe E7: main → engine/IProbeEngine, head → other/platform-type, tsc → engine), a second deterministic silent-subtraction shape in the certifying instrument, unpinned; the callee-shape repair itself, the zero and its controls, the four ablations, the artefacts and the changeset reading are otherwise verified as claimed, and the one-line repair is validated above.
Tier control — measured by the SEAT from the reviewer's transcript
- 86 assistant requests, 86 stamped
claude-fable-5-1— every per-request"message":{"model":…}. - 190/190 on the superset: 104
"model"plus 86"advisorModel", all the same value. - Dark control: empty.
- Fallback / overload sweep: 36 keyword hits, ⛔ none a notice — tool-schema prose, the dispatch order's own sentence, the reviewer's own "none observed", repo text about a
403fallback arm and a queue-unavailable fallback, and the reviewer's own shell echoFALLBACK to global typescript. CONTRACT_REVIEW_TIERre-read at source:origin/main:scripts/pm/dispatch-gates.mjs—export const CONTRACT_REVIEW_TIER = 'claude-fable-5-1';
5784504091, which binds head bee92afe12 and whose three blocking shapes (X1, X2, X4) this record measures as cleared at this head.
Generated by Claude Code
…ts block
`ts.isMethodDeclaration` is true of `const o = { getEngine() {…} }` as well as
of a class method, and an object literal opens no scope under
`opensCallableScope` -- so such a method was recorded as a lexical callable of
the enclosing BLOCK, and a bare `getEngine()` written in that block resolved to
the object's method. The language never does that: a name declared only that
way is `TS2304: Cannot find name`.
Measured at the previous head: a file-level `function getEngine(): IProbeEngine`
beside a block-local object literal declaring `getEngine(): Map<string, number>`
answered `other/platform-type` for a bare `getEngine().insert(…)` in that block,
in BOTH declaration orders, while `origin/main` answered the engine in one of
them and `tsc --noEmit --strict` answers the engine in both. That is a real
engine write subtracted under the DEFENDED `platform-type` arm, which prints
nothing and is counted nowhere -- the direction this census's header names as
the one it cannot survive.
`opensCallableScope` now also opens an object literal, so a member name is
recorded under ITS CONTAINER exactly as a class method's is. No lookup chain
walks a container (`calleeScopeChain` reaches the class for `this.m()` and
nothing for the rest), so the method stays reachable through the file-wide
FLOOR exactly as before and no receiver that resolved stops resolving.
Pinned in both declaration orders; the docblocks and the self-test success line
no longer claim a property the shape falsified.
Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
The second new pin was worded exactly like the class-scope one two cases above it, so an ablation that reds both prints the same line twice and the reader cannot tell which pin failed -- which is the whole value of a named case. Ablation A3 reds both, so this is not hypothetical. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
`node scripts/tenant-audit-census.mjs --write` at the instrument commit. The whole diff is the provenance stamp line in each artefact; every enforced figure is byte-identical, because the published population did not move -- 227 write call sites, 153 decidable, 74 undecidable over 576 sources, the same bytes the `origin/main` and pre-fix instruments produce over this tree. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewVERDICT: FAIL — one blocking finding, adopted by the seat verbatim and dispatched back for a third round. ⛔ Not softened, ⛔ not deferred to an acceptance note. Head reviewed: Reading moment: the reviewer's run closed and this record was written at 2026-09-23T00:09Z — a contract-review record is a snapshot bound to one head and one reading moment, ⛔ not a standing statement about CI. The authoritative reading for any landing is the seat's own, taken at the landing moment. Isolated at-tier subagent, worktree of its own, shared checkout never touched. Reviewed-by: isolated at-tier subagent dispatched by Tier — measured by the SEAT from the reviewer's own transcript, ⛔ not from its self-report
⇒ AT TIER. This seat is off-tier and ⛔ may not 自审; the record above is the isolated subagent's. F1 (BLOCKING) —
|
| probe | origin/main c120dbdfb8 |
pre-fix a36551a559 |
head b73fd40aa0 |
tsc --noEmit --strict |
|---|---|---|---|---|
| R1 — literal-first | engine/IProbeEngine | other/platform-type | other/platform-type | exit 0 (engine) |
| R1r — class method first | other/platform-type | other/platform-type | other/platform-type | exit 0 (engine) |
| R2 — the literal's method is the only one (floor) | engine | engine | engine | exit 0 |
Through the real census pipeline, ⛔ not only a probe harness: one tracked file carrying R1 with IDataEngine, three instruments over one tree via runCensus({ root }) — origin/main reads 228 sites / 154 decidable / platform-type 3 and places the write (placedBy: declared-type); pre-fix and head read 227 / 153 / 145 subtractions / platform-type 4 and the site is gone. Probe removed ⇒ git status --porcelain empty and the head reading returns byte-identical.
Corpus exposure today is 0 (186 this-rooted write receivers over 576 sources, none inside a this-rebinding function that is not a class member), with a scanner firing control: the same scan with the probe tracked reads 187 / 1 and names the file and binder.
⭐ Why blocking and not a boundary note. This PR introduced it at bfd92001b1 (round 2). The round-2 record filed it as F3 「pre-existing … all three module versions agree」 — true only in the class-first order. In the literal-first order origin/main places the site and this head subtracts it, which is the same shape the round-2 review blocked on for E7 (head wrong in both orders where main was right in one; deterministic; the silent direction; unpinned; zero exposure). It also contradicts the PR's own contract at head — declaredTypesIn's docblock reads 「⛔ The repair may not cost a single site that resolved before」 — so the body sentence 「nothing that resolved before stops resolving」 is false at head on this shape.
The reviewer carries a validated scratch repair (thisContainerOf(site), ~21 lines, selfTest() 81/81, R1 and R1r → engine, one movement at R7 it names). It is handed to the round as a proposal to judge, ⛔ not as a patch to paste, and the round owes pins in both declaration orders plus check-tenant-audit-census.mjs --self-test from the repo root.
Non-blocking — three body sentences the seat owes, since os-dev.md:56 makes the body the seat's hand
- The
e0f2e86741e4…hash binds to the9a24005710tree, ⛔ notb73fd40aa0; at this head the same JSON hashes075734a25f19…. The reviewer reproduced the round-2 record's1e7fed94…exactly ata36551a55, so its method is byte-exact. The population claim itself holds — three instruments over one tree, 112739 bytes, 227 / 153 / 74, platform-type 3, 0 unresolved, 0 unledgered. Prose precision only. - 「Four things were found and deliberately NOT fixed here」 heads a list of five.
- A
Lint & Repo Gatesrun id sits stale inside thea36551a559section; at this head it is run 35797018139, job 106978668423,success.
Verified as claimed — no finding
Round-2's F1 is closed, at no cost: object-literal shapes in both orders, as a call argument, nested, against a same-block arrow, in a class property initializer, and returned from an arrow all read engine at head with tsc agreeing; the floor still answers for a method called from outside its container. The union is closed by construction (the TS AST parents a MethodDeclaration only to ClassDeclaration / ClassExpression / ObjectLiteralExpression), with the corpus histogram matching. Ablations A0–A4 reproduce exactly (3 / 1 / 6 / 5 / 2 of 81), each with anchor-count and blob proofs on mutation and restore, and A3's fifth red is composition rather than breakage. Artefacts move only their two stamp lines. Scope is the card.
CI at this head, read by the reviewer at the end of its run
39 runs, all completed; 0 failure, 0 in_progress, 0 cancelled. Lint & Repo Gates — in progress at the seat's earlier read — completed success. The skipped rows are designed: Auto Label and Check PR Size are excluded by their own if: on the edited event (both were success on the 23:21 push), Check Changeset by the skip-changeset label, and six more by the path filter.
⇒ the head is green. It is the diff that fails review, which is the distinction this record exists to make.
Generated by Claude Code
`calleeScopeChain`'s `this.` arm returned `opensClassScope`, so `this.m()`
written inside an OBJECT LITERAL's method resolved to the method of whatever
class enclosed the literal. The language never does that: `this` inside an
object-literal method is the literal. Measured, that SUBTRACTED a real engine
write `origin/main` placed, under the defended `platform-type` arm -- the one
direction this census must never fail in.
`thisContainerOf` walks to the member that owns the site's `this` and hands
back ITS container: an arrow is transparent, a `function` expression or
declaration names no container at all (the floor), and a method, accessor,
constructor, property declaration or static block stops the walk at its own
parent -- a class for a class member, the object literal for a literal's
method. A plain `key: value` is not a stop, so `{ k: this.engine }` in a class
method still reads the class. A miss inside the named container falls to the
file-wide FLOOR and never outwards to a class that merely encloses the site.
Twelve pins, each shape in both declaration orders where the floor decides.
Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Co-authored-by: Claude <noreply@anthropic.com>
`node scripts/tenant-audit-census.mjs --write` at the instrument commit. Two lines in total -- the unenforced corpus-scale block's provenance stamp in each artefact. Every ENFORCED figure is byte-identical: the population did not move. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewVERDICT: PASS — nothing blocking. Head reviewed: Reading moment: this record was written at 2026-09-23T01:30Z. A contract-review record binds ONE head and one reading moment. Reviewed-by: isolated at-tier subagent dispatched by Tier — measured by the SEAT from the reviewer's transcript
What was proven
Four non-blocking findings — and what the seat did with each
⇒ The PR's own contract — 「the repair may not cost a single site that resolved before」 — holds on every probe the reviewer built, including the two that expose gaps in the new predicate's model. CI at this head39 check runs, all ⛔ This record is the review; the landing reading is the seat's own, taken separately at the landing moment. Generated by Claude Code |
Fixes #19652
Clause-②: no
scripts/tenant-audit-census.mjskeyed two of its three name maps on the bare identifier.thisPropsholds class property types;fnReturnsholds what a callable returns. Two classes in one file sharing a property name were ONE entry and the first TYPED one decided for both, and every consumer looked the name up by bare name too.The census builds
content/docs/permissions/tenant-audit-census.mdx, which ships undercontent/docs/**, so a wrong verdict publishes a call site to customers as tenancy-safe when it is not.The probe, re-taken on today's
origin/main— not inherited from the cardTaken at
origin/main@16d090ede0, 2026-09-22T20:17:28Z, before a line was changed. The card's probe is the #19300 dev's and the card says whoever takes this card re-takes it. Receiver verdicts in source order;IProbeEngineis in the engine type index, the other type is a Map keyed by string (MAP_OF_STRING_TO_NUMBER, spelled this way to keep the generic's angle brackets out of a GitHub body).engine, engine declared firstengine/IProbeEngine | engine/IProbeEngineengine/IProbeEngine | other/platform-typeother/platform-type | other/platform-typeother/platform-type | engine/IProbeEngineengine/IProbeEngine | engine/IProbeEngineengine/IProbeEngine | other/platform-typeengine/IProbeEngine | engine/IProbeEngineengine/IProbeEngine | other/platform-typeother/platform-type | other/platform-typeother/platform-type | engine/IProbeEngineP1 reproduces the card's prediction exactly: a
Map.deleteADMITTED to the certified tenancy population. P2 and P5 are the direction the card predicted only in prose and the census header names as the one it cannot survive — a real engine write SUBTRACTED asplatform-type, a DEFENDED arm that prints nothing and is counted nowhere. P3 is a storage site the card's probe does not reach.Controls, all taken on the same instrument in the same run, all unchanged before and after. FIRING: a lone Map-typed
this.engineis refused (other/platform-type) and a lone engine-typed one is accepted (engine/IProbeEngine) — so the door is not inert in either direction. DISCRIMINATION: the same two types under DIFFERENT property names, and under different function names, are told apart. DARK: the identical shape on PARAMETERS is already correct, because that is the lexical tier #19300 landed (PR #19650) — so the failure is specific to the two maps this card names. DARK: withIProbeEngineremoved from the index the same source answersother/type-not-in-corpus, so a verdict is not constant.The fix — the landed tier extended, not a second mechanism beside it
scopeChainOfandscopedNamesnow take the scope predicate. All three name maps arescopedNamesinstances under the predicate that matches how the language scopes that kind of name:thisProps—opensClassScope(class declarations and class expressions).this.enginein classBisB's property and can be nothing else.fnReturns—opensCallableScope, the lexical scopes PLUS the class. A method's name belongs to its class body; afunctiondeclaration's or aconst fn = …'s to the block or file that declares it. Not the class-only predicate: afunction getEngine()two lines above a class is not class-scoped, and a chain that saw only classes would lose it.locals— unchanged.Each keeps that structure's file-wide FLOOR, so a name no enclosing scope declares resolves exactly where it used to and nothing that resolved before stops resolving.
The bare-key spelling is REMOVED rather than guarded:
note()now requires the declaring node, and no.get(name)lookup survives on either map. There is no spelling left to reach for by accident.Before/after on the published population — it did not move, and that is a measurement
Full
--jsoncensus, before and after, on the same tree: 227 write call sites, 153 decidable, 9 tenancy-enabled sites provably carrying no tenant context, 34 more unreadable — byte-identical. Every array (sites,nonEngineCalls,undefendedSubtractions,engineTypes,declaredObjects) andnonEngineReasonsare identical too. Zero call sites change classification, in either direction.That zero is measured rather than assumed.
scopedNames().lookup(name, null)skips the scope chain and returns the file-wide tier, whose insertion rule is byte-for-byte the old bare-namenote()— so comparing it againstlookup(name, node)at every write-call receiver IS the before/after diff, on one code path:local, i.e. the tier tenant-audit census collapses declared type text before re-parsing it (route ①), and conflates same-named locals across scopes #19300 already landed. The kinds this card changes (this.PROP,this.BASE.member,fname()) diverge at 0 receivers. The 98 is the firing control: the same instrument, the same corpus, the same run, answering non-zero.The conflating shape is nevertheless already in the corpus — the defect was latent, not absent. The same instrument, applied at DECLARATION nodes, finds 73 shadowed declarations, 6 of them with genuinely conflicting declared types:
packages/services/service-messaging/src/http-outbox.ts:462code'DELIVERY_NOT_ELIGIBLE'packages/plugins/plugin-email/src/email-service.ts:644sendSendEmailResultTransportSendResultpackages/services/service-sms/src/sms-service.ts:113sendSendSmsResultSmsTransportSendResultpackages/services/service-settings/src/settings-service.ts:1509getT[K]packages/services/service-automation/src/engine.ts:3239executepackages/plugins/plugin-security/src/security-plugin.ts:5790isScalarIdNone of those six names is the receiver of a write call today, which is exactly why the population does not move. One write call through any of them and it would have.
Reverse verification — the pins can fail
Eight self-test cases were added (67 to 75, all pass). Both ablations ran through
scripts/ablation-replace.mjs, which proves the mutation landed on disk by anchor count and blob hash and proves the restore by blob-equals-HEAD plus an emptygit diff HEAD:scopedNames(opensClassScope)replaced by a predicate that opens nothing: exactly the 4 class-scope cases go red, each with thegotstring this PR measured atorigin/main(engine/IProbeEngine | engine/IProbeEngine). The two FLOOR cases stay green.opensCallableScope: exactly the 2 callable-scope cases go red. The FLOOR cases stay green.The regenerated page
node scripts/tenant-audit-census.mjs --writeon the merged tree, the repo's own tooling, never a hand edit. It rewrites two lines in total — the unenforced corpus-scale block's provenance stamp in each artefact. Every ENFORCED figure is byte-identical.node scripts/check-tenant-audit-census.mjsexits 0: "227 write call sites certified (153 decidable; 9 tenancy-enabled sites PROVABLY carry no tenant context, 34 more unreadable), 23 prose figures held to the census".Neither
content/docs/permissions/tenant-audit-census.mdxnordocs/audits/2026-08-tenant-audit-write-call-sites.counts.mdis routedmerge=os-regen(git check-attr mergeanswersunspecifiedfor both, and for the script).origin/mainwas merged in as an ordinary merge with zero path overlap, soscripts/pm/os-regen-merge.shhas nothing to do here.Verification
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 63 families from this worktree on the final tree; 63 run, 0 NOT-MEASURED, 0 UNRUN, and none exited 3, reconciled by--ranwith every exit code recorded (captured before any pipe). Four of them first answered PREREQUISITE NOT MET on an unbuilt worktree and named what they needed; after the builds they named (@objectstack/spec,@objectstack/formula,@objectstack/lint, then@objectstack/client,@objectstack/client-react) all four are green.pnpm lint— the repo-wideeslint . --no-inline-config, CI's own scope, not a narrowing — exits 0 atbee92afe12.Two artifact-roster families whose roster sits under
scripts/were run beyond the derived set:pnpm check:dts-closureexits 0 after the builds;pnpm check:published-readme-exportsremains exit 3 / NOT MEASURED, needing every package'sdistdeclarations, which a diff touching no package source cannot supply. It is not a pass and not a finding.Changeset
This PR needs the
skip-changesetlabel, and this branch cannot apply it — the dispatch order withholds label writes from this seat, so the seat that dispatched it owns that write.Check Changesetwill sit red until then: the job counts changesets added against the merge base and exempts only the label or the changesets release branch, and this PR adds none.The reading behind that route, rather than a guess: 70 package manifests declare a
files[], 217 entries in all, and 0 of them reach outside their own package directory. All three changed paths live at the repository root —scripts/,content/docs/,docs/audits/— outside every package directory, so no published tarball's bytes move. Positive control on the same matcher:packages/spec/dist/index.jsresolves as SHIPPED bypackages/spec. Dark control: the near-missscripts/tenant-audit-censuss.mjsresolves as shipped by nothing.The callee-shape repair — a call site is resolved by how the CALL is written
Commits
bfd92001b1anda36551a559, verified independently on heada36551a559by a later round that re-derived every number below rather than inheriting it.opensCallableScopeis the predicate a callable's name is RECORDED under: a method's name in its class body, a function declaration's in the block or file that declares it. The lookup site read that same union for every callee shape, so a baregetEngine()written insideclass Cresolved throughC's body — which the language never does, because a method lives on the prototype and not in lexical scope.calleeScopeChain(callee)now picks the chain by callee shape: a bare identifier walks the lexical scopes only,this.m()walks the class chain, and areceiver.m()call walks neither and reads the file-wide floor, which is exactly what it resolved through before. Recording is unchanged.The movement, measured on three trees
Receiver verdicts for the same synthetic sources, taken through the census's own exported
declaredTypesIn/resolveReceiver/nonEngineReasonon three checkouts of this repository.IProbeEngineis in the engine type index; the other type is a Map keyed by string (MAP_OF_STRING_TO_NUMBER, spelled this way to keep the generic's angle brackets out of a GitHub body).origin/mainc120dbdfb8bee92afe12a36551a559getEngine()inside a class, function declared firstengine/IProbeEngineother/platform-typeengine/IProbeEngineother/platform-typeother/platform-typeengine/IProbeEnginethis.getEngine()beside a same-named file-level functionengine/IProbeEngineother/platform-typeother/platform-typethis.x.getEngine(),xanAreturning an engine, inside aBwith its own same-named methodengine/IProbeEngineother/platform-typeengine/IProbeEngineX1 and X4 are the regression the at-tier review found: a real engine write that
origin/mainPLACED was SUBTRACTED at the pre-fix head under the DEFENDEDplatform-typearm, which prints nothing and is counted nowhere — the direction this census's header names as the one it cannot survive. X2 is an improvement over both heads. X3 is the class-scope half this PR already landed, and the repair leaves it standing.The language is the authority here, and it was asked.
tsc --noEmit --stricton the X1 source exits 0 — the bare call resolves to the file-level function, soinsertexists on it — and on the X3 source exits 2 withTS2339: Property 'insert' does not exist on type 'MAP_OF_STRING_TO_NUMBER', so athis.call does read the class's own method. The two verdicts this head gives are the two the compiler gives.The new pins can fail — four ablations, each proven on disk
All four ran through
scripts/ablation-replace.mjs: anchor count plus blob hash prove the mutation landed, blob-equals-HEAD plus an emptygit diff HEADprove the restore. There is no build leg to reach —scripts/tenant-audit-census.mjsis executed from source, soablation-dist-preflighthas nodistto preflight for it.lookup(fname, callee)got other/platform-type, exactly the pre-fix answerthis.arm returns the floor instead of the class chaingot engine/IProbeEngineopensClassScopeopens nothingopensCallableScopeopens nothingA0 and A1 together red all four new cases, so not one of them is vacuously true. The four FLOOR cases stay green under A2 and A3, as they did before. A2 and A3 now red more cases than they did before
bfd92001b1, and that is the repair being load-bearing rather than a regression:calleeScopeChainhands back those two predicates, so neutralising either one now also breaks the lookup that reads it. The earlier "exactly the 4" and "exactly the 2" readings in this body describe the tree atbee92afe12and no longer describe this head.Self-test case count: 67 on
origin/main, 75 atbee92afe12, 79 here.node scripts/check-tenant-audit-census.mjs --self-testexits 0.The published population, re-measured — still zero, and still a measurement
runCensus({ root })threads its root through every enumeration the module makes, so both instruments were run against one tree rather than two trees believed equal: the pre-fix module frombee92afe12and this head's module, both over this head's checkout. The--jsonbytes are identical — 112739 bytes,sha256 1e7fed942bc2320893a926ed6299905547fcaadc7a97b13ab0accbe1d797d21f— at 227 write call sites, 153 decidable, 74 undecidable, 576 sources, 144 non-engine subtractions, 0 unresolved receivers. No call site changes classification, in either direction.The zero has controls, on the same comparison:
platform-type 4; this head reports 228 sites, 154 decidable,platform-type 3; and the diff names the added site. The subtraction the repair removes is exactly the one the review predicted.Across the two checkouts the only differing byte in the whole census object is the provenance
refit records (a36551a55againstbee92afe1), which is what the re-stamp commit exists to move.The two re-stamped artefacts
node scripts/tenant-audit-census.mjs --writeat this head rewrites the provenance stamp line and no other byte in either artefact: re-run on a clean tree, the entiregit diffis two lines, each aMeasured on 2026-09-22 at .... Every enforced figure is what the generator produces at this head, and the tree was restored to its HEAD blobs afterwards.node scripts/check-tenant-audit-census.mjsexits 0: "227 write call sites certified (153 decidable; 9 tenancy-enabled sites PROVABLY carry no tenant context, 34 more unreadable), 23 prose figures held to the census". The committed stamp namesbfd92001b, the last commit that changes the instrument — stamping the re-stamp commit itself would need a further commit that would in turn be unstamped, so this is the fixed point, and the gate deliberately does not compare that value.Verification, re-derived from this head
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsfrom a worktree ata36551a559derives 63 families from 3 changed paths (248 changed lines, under the 5000 human-merge threshold). All 63 ran with each exit code captured before any pipe and reconciled by--ran: 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, none exited 3. One family first answered exit 3 / PREREQUISITE NOT MET —check:skill-examples, which needs built@objectstack/clientand@objectstack/client-reactdeclarations; after building those packages' dependency closure it exits 0.Lint & Repo Gatesreported success ona36551a559(run 35788706216) and again onb73fd40aa0(run 35797018139, job 106978668423,success). ⛔ Each reading names the commit it was taken on and is about no other: on93555d0fcdit reported success too (run 35802103477, 00:27:18Z → 00:57:46Z).node scripts/check-tenant-audit-census.mjsand the same script's--self-test— exit 0 each.The object-literal repair — a member name is not a lexical one on the RECORDING side either
Commits
9a24005710,496d65dc79andb73fd40aa0, answering the at-tier contract review's blocking finding F1.The callee-shape repair kept the class body off a bare call's lookup chain. That is only half of "a bare
f()is never shadowed by a method": a name is reachable through whatever scope it was RECORDED at.ts.isMethodDeclarationis true of an OBJECT LITERAL's method as well as a class one, and an object literal opened no scope underopensCallableScope— so such a method was recorded as a lexical callable of the enclosing BLOCK, and a baregetEngine()written in that block resolved to the object's method. The language never does that.opensCallableScopenow also opensts.isObjectLiteralExpression, so a member name is recorded under ITS CONTAINER exactly as a class method's is. No lookup chain walks a container —calleeScopeChainreaches the class forthis.m()and nothing for the rest — so an object-literal method stays reachable through the file-wide FLOOR exactly as before, and no receiver that resolved stops resolving. ThethisProps/opensClassScopehalf, thethis.arm and the floor are untouched.origin/mainc120dbdfb8a36551a559b73fd40aa0tscfunction getEngine(): IProbeEngine; insidew()aconst o = { getEngine(): MAP_OF_STRING_TO_NUMBER … }and a baregetEngine().insert(…)in that blockengine/IProbeEngineother/platform-typeengine/IProbeEnginew()other/platform-typeother/platform-typeengine/IProbeEngineRe-taken here rather than inherited (2026-09-22T23:11:48Z at
a36551a559, 23:13:37Z after the fix), through the census's owndeclaredTypesIn/resolveReceiver/nonEngineReason. Controls on the same instrument and run: FIRING — the same block with no object literal readsengine/IProbeEngine, so the door is not inert; DISCRIMINATION — the same literal under a DIFFERENT member name readsengine/IProbeEngine; DARK — withIProbeEngineout of the index the fixed instrument readsother/type-not-in-corpus, so the verdict is not constant.tsc --noEmit --strict(6.0.3,--ignoreConfig) exits 0 on both E7 sources; on a third source where the object-literal method is the ONLY declaration it exits 2 withTS2304: Cannot find name 'getEngine'— the compiler does not put an object-literal method in lexical scope at all.The union is now closed for this corpus, measured rather than argued. Over all 576 sources at this head, every identifier-named
MethodDeclarationis parented byClassDeclaration(1325) orObjectLiteralExpression(98), and everyFunctionDeclarationbySourceFile(1493),Block(9) orModuleBlock(1). Both method containers are now on the predicate and all three function containers already were, so no recorded callable in this tree has an innermost recording scope the predicate does not open.The new pins can fail — a fifth ablation, and the earlier four re-read
All through
scripts/ablation-replace.mjs: anchor count plus blob hash prove each mutation landed, blob-equals-HEAD plus an emptygit diff HEADprove each restore. Baselinenode scripts/check-tenant-audit-census.mjs --self-testexits 0 at 81 of 81 cases (79 + the two new ones) plus the check's own 27.|| ts.isObjectLiteralExpression(node)deletedgot other/platform-type, the pre-fix answer; nothing else moveslookup(fname, callee)this.arm returns the flooropensClassScopeopens nothingopensCallableScopeopens nothingA3's extra red is the new reversed-order pin, and it is composition rather than breakage: with
opensCallableScopereturning false nothing is recorded under any scope, every name is answered by the FLOOR, and in that declaration order the floor holds theMap. No ⛔ FLOOR-labelled pin reds under A2 or A3. This supersedes the3 / 1 / 6 / 4 of 79reading above, which describes the tree ata36551a559.The published population, re-measured — still zero, with both controls
Three instruments over one tree via⚠️ A hash of that JSON identifies neither a tree nor an instrument on its own, so the reading above is stated with the tree AND the day it was taken:
runCensus({ root })(2026-09-22T23:15:58Z at the9a24005710tree): theorigin/mainc120dbdfb8module, the pre-fixa36551a559module and this head's module each produce 112739 bytes, byte-identical — at 227 write call sites, 153 decidable, 74 undecidable, 576 sources, 63 engine types, 144 non-engine subtractions,platform-type3, 0 unresolved, 0 unledgered.measuredAt()returns{ date: new Date().toISOString().slice(0,10), ref }andrunCensuscarries BOTH, so the same tree and the same three instruments hash differently on two different days. Measured: the9a24005710tree hashese0f2e86741e4…dated 2026-09-22; theb73fd40aa0tree hashes075734a25f19…dated 2026-09-22 andc18ad4dc836a…dated 2026-09-23, and maskingmeasuredAt.datealone makes those two byte-identical. The DRIFT gate is unaffected — it masks the unenforced corpus-scale block out of its comparison on purpose.IDataEngineand the comparison MOVES in the predicted direction:origin/mainreads 228 sites / 154 decidable /platform-type3 and PLACES the write; the pre-fix head reads 227 / 153 / 145 subtractions /platform-type4 and SUBTRACTS it under the defended arm; this head reads 228 / 154 /platform-type3, byte-identical toorigin/main. The diff names the site.git status --porcelainis empty and the comparison returns to the byte-identical 227.No real call site moves: 0 of 227 change classification in either direction.
The prose the shape falsified
Both docblocks and the self-test success line claimed a property E7 disproved.
opensCallableScope's now names the object literal as a member-name container beside the class and says why omitting it cost the same site;calleeScopeChain'sf()bullet records that excluding a container from the lookup chain is only half of the property, the other half being that the recording predicate put every member name under its container; and the success line states the pinned property in both orders for both containers.The two re-stamped artefacts
node scripts/tenant-audit-census.mjs --writeat the instrument commit rewrites two lines in total — the provenance stamp in each artefact,bfd92001b→496d65dc7. Every ENFORCED figure is byte-identical, because the population did not move.node scripts/check-tenant-audit-census.mjsexits 0: "227 write call sites certified (153 decidable; 9 tenancy-enabled sites PROVABLY carry no tenant context, 34 more unreadable), 23 prose figures held to the census". Neither artefact was hand-edited.Verification, re-derived at
b73fd40aa0node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsfrom a worktree at this head derives 63 families from the same 3 changed paths (294 changed lines, under the 5000 human-merge threshold). All 63 ran with each exit code captured BEFORE any pipe and reconciled by--ran: 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, none exited 3. Five first refused on an unbuilt worktree and named what they needed —check:doc-formula-expressions,check:doc-security-posture,check:docs-transcript-drift(built@objectstack/lint), speccheck:docs(built spec /gen:schema), speccheck:skill-examples(built@objectstack/clientand@objectstack/client-react) — and all five are green after the two builds, both run throughscripts/pm/os-verify-lock.sh(VERDICTcommand-exit 0, waited 0s).That zero has controls on the reconciler itself: handed the PRE-BUILD record the same tool reads "63 derived, 59 run, 4 NOT-MEASURED"; handed a record with one family omitted it reads "1 of 63 derived families UNRUN" and exits 1.
pnpm lint— the repo-wideeslint . --no-inline-config, CI's own scope and not a narrowing — exits 0 atb73fd40aa0. The edited file is a tool script, so its own suite is owed beyond the derived set: no*.test.tsin the repository namestenant-audit-census, and the module's only importer isscripts/check-tenant-audit-census.mjs, which is in the derived set and green. The siblingscripts/platform-object-tenancy-census.mjscarries no scope machinery at all, so this shape does not exist there.The
this-container repair —thisis not "the enclosing class"Commits
8f8828bf66and93555d0fcd, answering the at-tier contract review's blocking finding F1.The object-literal repair put every member name under its container on the RECORDING side. The
this.m()LOOKUP still read "the chain of enclosing classes":calleeScopeChain'sthis.arm returnedopensClassScope, and boththisProps.lookupsites defaulted to the same predicate. The language never resolvesthisthat way.thisinside an object literal's own method is the LITERAL, whatever classes enclose the literal; inside afunctionexpression it is whatever the call supplies; an arrow has none of its own. Measured, reading athis.getEngine()written in an object-literal method through the enclosing class SUBTRACTED a real engine writeorigin/mainhad PLACED — under the DEFENDEDplatform-typearm, which prints nothing and is counted nowhere, the direction this census's header names as the one it cannot survive.thisContainerOf(site)walks the parents: an arrow is TRANSPARENT; afunctionexpression, afunctiondeclaration and the source file returnnull, which is the file-wide FLOOR alone; and a method, accessor, constructor, property declaration or class static block STOPS the walk and hands back ITS OWN parent — a class for a class member, the OBJECT LITERAL for a literal's method. ⛔ A plainkey: valueproperty assignment is not a stop, so{ k: this.engine }written in a class method still reads the class. Thethis.arm and boththisProps.lookupsites now pass a chain reaching EXACTLY that container, and a miss inside it falls to the FLOOR — never outwards to a class that merely encloses the site.The movement, measured on three trees
Receiver verdicts for the same synthetic sources, through the census's own exported
declaredTypesIn/resolveReceiver/nonEngineReason, taken from three module versions in one run.IProbeEngineis in the engine type index; the other type is a Map keyed by string (MAP_OF_STRING_TO_NUMBER, spelled this way to keep the generic's angle brackets out of a GitHub body).tscis--noEmit --strict --ignoreConfigon 6.0.3, the ORACLE: exit 0 means the compiler resolvedinserton the receiver.origin/main544767da3bb73fd40aa093555d0fcdtscthis.getEngine()in an object-literal method nested in a class member; the literal's method declared firstgetEngine(floor)this.engineinside afunctionEXPRESSION in a class methodTS2683thislacks the name, the floor holding the MapTS2339this.getEngine()straight in a class method, beside a same-named file-level functionTS2339this.getEngine()TS2339{ k: this.engine }in a class method, two classesTS2339R1 is F1 exactly as the review stated it, and R1r is the same shape in the order where
origin/mainwas wrong too. Controls on the same instrument and run, all UNCHANGED between the pre-fix head and this one:this.PROPin a class method and the same inside an ARROW readengine, platform-typein both; R9 is the dark control (the fix is name-specific); R3 and R12 are the class half this PR already landed, and the repair leaves both standing, agreeing with the compiler.R7b's movement is acceptable, and it is the direction the repair must take — not a regression. Three readings, all on the same instrument and run. ① The shape does not compile:
tscanswersTS2339: Property 'getEngine' does not exist, because an inner class expression does not inherit the lexically-enclosing class's members throughthis. A tracked corpus that type-checks cannot contain it, and the corpus scan below reads 0 either way. ② The pre-fix head's answer is the LANGUAGE ERROR, not a capability being lost: walking outwards to the enclosing class invents a resolution TypeScript does not have, and the repair declines to invent it. ③ The new answer ISorigin/main's answer, so this PR's own contract — 「The repair may not cost a single site that resolved before」 — holds exactly. A regression here would be a site moving from RESOLVED to unresolved relative toorigin/main; this one moves from an invented resolution back to main's. It is pinned in both declaration orders rather than left to prose.The new pins can fail — eight ablations, each proven on disk
All through
scripts/ablation-replace.mjs: anchor count plus blob hash prove each mutation landed, blob-equals-HEAD plus an emptygit diff HEADprove each restore, andgit diff HEAD --name-onlywas asserted empty after every leg. There is no build leg to reach — the module is executed from source. Baselinenode scripts/check-tenant-audit-census.mjs --self-testexits 0 at 93 of 93 cases (81 + 12 new) plus the check's own 27.b73fd40aa0lookup(fname, callee)this.arm returns the flooropensClassScopeopens nothingopensCallableScopeopens nothingopensCallableScopethis.arm reverted toopensClassScope— the pre-fix spellingthisProps.lookupreverted to its default chainthisContainerOfstops at an arrow instead of passing through itA5 is the direct ablation of this repair and every one of its six reds carries the pre-fix
gotstring. Each earlier count grew, and that is composition rather than breakage: the new pins read the chaincalleeScopeChainhands back, so neutralising a predicate it depends on now breaks them too. This supersedes the3 / 1 / 6 / 5 / 2 of 81reading above, which describes the tree atb73fd40aa0. No ⛔ FLOOR-labelled pin reds under A2 or A3; two of them DO red under A0 and A5, so the floor pins are not vacuously true either.The published population, re-measured — still zero, with three controls
Three instruments over one tree via⚠️ a tree whose HEAD was the fix commit
runCensus({ root })—8f8828bf66, ⛔ not the re-stamp93555d0fcd, as the at-tier review measured from the JSON's ownmeasuredAt.ref: theorigin/main544767da3bmodule (byte-identical toc120dbdfb8for this file andts-parse.mjs), the pre-fixb73fd40aa0module, and this head's. All three produce 112739 bytes,sha256 4c35ebc1b26c876b48fa91e9e53d044ea4a159fd049624dca14ef5520abdb8cbat that tree (the same bytes at93555d0fcdon 2026-09-23 hash71eb74aa2070af3c…, differing only inmeasuredAt.ref— acceptance note 6's hazard, this time in thereffield) — byte-identical — at 227 write call sites, 153 decidable, 74 undecidable, 576 sources, 63 engine types, 144 non-engine subtractions,platform-type3, 0 unresolved, 0 unledgered. 0 of 227 change classification, in either direction.IDataEngineand the comparison MOVES exactly as the review predicted:origin/mainreads 228 sites / 154 decidable /platform-type3 and PLACES the site withplacedBy: declared-type; the pre-fix head reads 227 / 153 / 145 subtractions /platform-type4 and the site is GONE; this head reads 228 / 154 /platform-type3, byte-identical toorigin/main. The--jsonnames the file and the line.thisrooted on a CLASS MEMBER instead of a literal's method is byte-identical on all three instruments at 228 sites, so the movement is attributable to the shape and not to the added file. Probe removed,git status --porcelainis empty and the comparison returns to the byte-identical 227.this-rooted receiver number 184 by the round's own scanner —this.), so treat the total as definition-dependent, and 0 of them sit where the container theirthisnames is not the nearest enclosing class. That zero is the whole exposure of this repair. Firing control with the probe tracked: +1 site, 1 exposed, naming the file; dark control, the same file with a class-memberthis: +1 site, 0 exposed — the review reproduced both at 187 / 1 and 187 / 0. ⭐ The load-bearing readings are the 0 and the +1, and both reproduce under every definition.The prose the shape falsified
opensClassScope's docblock said "the chain for a property is the chain of enclosing classes"; it now says it is a RECORDING predicate and namesthisContainerOfas the lookup-side one, with the two shapes that break the old claim.calleeScopeChain'sthis.m()bullet no longer says "the enclosing class". The existing pin named 「athis.METHOD()call DOES read the enclosing class」 now reads 「…the class itsthisnames」 — the same case, the claim narrowed to what it actually holds. The self-test success line states the pinned property for both containers, through an arrow, inside afunctionexpression, and for a plainkey: value.The two re-stamped artefacts
node scripts/tenant-audit-census.mjs --writeat the instrument commit rewrites two lines in total — the provenance stamp in each artefact,496d65dc7→8f8828bf6. Every ENFORCED figure is byte-identical, because the population did not move.node scripts/check-tenant-audit-census.mjsexits 0: "227 write call sites certified (153 decidable; 9 tenancy-enabled sites PROVABLY carry no tenant context, 34 more unreadable), 23 prose figures held to the census". Neither artefact was hand-edited.Verification, re-derived at
93555d0fcdnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsfrom a worktree at this head derives 63 families from the same 3 changed paths (446 changed lines, under the 5000 human-merge threshold). All 63 ran with each exit code captured BEFORE any pipe and reconciled by--ran: 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, none exited 3. Five first refused on an unbuilt worktree and named what they needed —check:doc-formula-expressions,check:doc-security-postureandcheck:docs-transcript-drift(built@objectstack/lint), speccheck:docsand speccheck:skill-examples(built@objectstack/spec, then@objectstack/clientand@objectstack/client-react) — and all five are green after the two builds, both run throughscripts/pm/os-verify-lock.sh(VERDICT command-exit 0; waited 64s and 229s).That zero has controls on the reconciler itself: handed the PRE-BUILD record the same tool reads "63 derived, 59 run, 4 NOT-MEASURED"; handed a record with one family omitted it reads "1 of 63 derived famil(ies) UNRUN" and exits 1.
pnpm lint— the repo-wideeslint . --no-inline-config, CI's own scope and not a narrowing — exits 0 at93555d0fcd. The edited file is a tool script, so its own suite is owed beyond the derived set: no*.test.tsin the repository namestenant-audit-census, andgit grep "from '.*tenant-audit-census.mjs'"returns exactly one hit,scripts/check-tenant-audit-census.mjs, which is in the derived set and green.Acceptance notes
Eight things were found and deliberately NOT fixed here.
1. A class's member types are not indexed, so
x.method()still conflates across two classes.declaredTypesIn'sshapesmap indexes interfaces and type-literal aliases only, neverclassdeclarations. Two classes each declaring a methodgetEngine, called from outside both, answerengine/IProbeEngine | engine/IProbeEngine— aMap.deleteadmitted. CONTROL: the identical member types declared as INTERFACES are told apart correctly (engine/IProbeEngine | other/platform-type), so the instrument can discriminate; what it cannot do is read a class's members. CONTROL: a lone class member is still correctly subtracted. Measured to PRE-EXIST this change and be untouched by it: under the callable-scope ablation the same probe answers identically. Different mechanism from this card's keying, so a separate card rather than a rider.Re-measured on this head, and the pin's agreement with the compiler is order-dependent. With
Adeclared first the census answersengine/IProbeEngine, which is also whattsc --noEmit --strictanswers (exit 0). With the two classes in the other order the same source answersother/platform-type, whiletscstill answers the engine (exit 0). So the file-wide FLOOR reaches the right verdict in the pinned case by its first-typed-wins rule and not by readingA's members — which is exactly the card this note names. Measured to be untouched by the callee-shape repair: the pre-fix head answersother/platform-typein BOTH orders, so the repair moved the first order and left the second where it was.2.
classifyHttpreads an ABSENT rate-limit header as zero remaining, inscripts/pm/label-write.mjs. The guard is(status === 403 || status === 429) && Number(rateRemaining) === 0, andNumber(null)is0, so a 403 carrying nox-ratelimit-remainingheader at all classifies asratelimit. ThroughstopSignalFromthat verdict writes a 30-minute STOP MARKER refusing EVERY write by that token key, whatever path failed. Observed live in this container:scripts/pm/with-fleet.sh --readtook the egress proxy's 403 on the App-token mint path and wroteSTOP MARKER written for token key f665bb8f6447 until 2026-09-22T21:39Z. CONTROLS: with the header present at 4999 the same status classifiesrefusalandstopSignalFromreturns null; with it genuinely 0 the intendedratelimitarm still fires; a 404 near miss classifiesrefusal. The producer is honest —fleet-token.mjs'srateRemainingOfdocumentsnullas "absent" — and the consumer misreads it.3. A known boundary, not a defect. The file-wide FLOOR is kept on all three maps, so a
this.PROPno enclosing class declares, and a method called from outside its class, still resolve file-wide. Removing the floor would LOSE sites, which is the direction this census must never fail in, so it is pinned in both new FLOOR cases rather than closed.4. Acceptance note 2 was not re-measured this round. The
classifyHttpreading above was taken in an earlier round of this branch and is recorded here unchanged; this round neither reproduced nor refuted it, and its own card carries it.5. Two self-test cases share one name verbatim.
⭐⭐ …and in the other declaration order, the real engine write is no longer subtracted as a language globalnames both a two-parameters case and a two-classes case; ablation A2 reds both and prints the same line twice, so a reader cannot tell which pin failed. Pre-existing — both predate this round. This round hit it by writing a third collision of its own and removed that one (496d65dc79); the pre-existing pair is left alone as out of scope. Its own card or the next PR in this file, not a rider.6. A census
--jsonhash names a DAY as well as a tree.measuredAt()returns{ date: new Date().toISOString().slice(0,10), ref }andrunCensuscarries both, so asha256over--jsonchanges at midnight UTC on a tree nobody touched. Measured: theb73fd40aa0tree hashes075734a25f19…dated 2026-09-22 andc18ad4dc836a…dated 2026-09-23, identical apart from that one field. Not a defect in the census — the date is deliberate (「tell a reader how old the unenforced numbers are」) and the DRIFT gate already masks the whole unenforced block out of its comparison — but a prose-citation hazard, and it has now produced one wrong hash in this body. The ad-hoc three-instrument byte comparison these rounds use is the thing that should maskmeasuredAt.date, never the artefact. Carrier: the next PR to touchscripts/tenant-audit-census.mjs. Noted, not filed.7.
thisContainerOftreats everyfunctionexpression as naming no container, and one compiling shape disagrees. TypeScript contextually typesthisinside afunctionexpression that is an object literal'skey: valueas that literal. The at-tier review's probe X1 (const o = { getEngine(): IProbeEngine …, z: function () { this.getEngine().insert(…) } }inside a class whose owngetEnginereturns a Map) compiles (tsc --noEmit --strictexit 0) while this head reads the floor and subtracts the write. ⛔ Not a cost of this PR —origin/mainand the pre-fix head answer identically, and the corpus exposure is 0 (firing control 1 with the probe tracked). A one-clause repair (aFunctionExpressionwhose parent is aPropertyAssignmentin anObjectLiteralExpressionnames that literal) plus a pin in both orders. Carrier: the next PR to touchscripts/tenant-audit-census.mjs. Noted, not filed.8. The R7b ruling's third argument carries it; the first two over-generalise. An inner class expression that
extendsthe outer class inherits the member and does compile (probe R7x); there the pre-fix head placed the write, andorigin/mainand this head both subtract it. ⇒ 「walking outwards invents a resolution TypeScript does not have」 is false for the inheriting case — the pre-fix answer was right by the accident of lexical enclosure, and the census models no inheritance at all (pre-existing, identical atorigin/main). The same family: afunction (this: C)annotation. ⛔ Neither is a cost againstorigin/main— ③, 「the new answer isorigin/main's」, is the load-bearing leg and it stands. Exposure 0 in both (firing control 1 each). Carrier: the same next PR. Noted, not filed.🤖 Generated with Claude Code
https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr
Generated by Claude Code
Generated by Claude Code