Skip to content

Commit 9dac9eb

Browse files
fix(scripts): the tenant-audit census reads this.X through a function's declared this host (#25222) (#25660)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuCEzLkAB12FfRHL9ipwfKPd)_ Part of #25222 Clause-②: no Metadata-protocol: none Ruling-ref: 6384677413 Sales's ruling B (comment 6384677413) puts this census-reader PR before slice 8 of the `engine.ts` split. Its claim is comment 6384913796. It changes one file, `scripts/tenant-audit-census.mjs`. The census now reads a `this.X` receiver through the host type its function declares for `this`, so a method body moved out of its class keeps the verdict it had inside the class. On `main`, no census reading changes. ## Why - Slice 8 moves 21 `AutomationEngine` bodies. They hold 33 `this.FIELD.delete(` sites, all on the class's own `Map` and `Set` fields. - A moved body's function declares `this` as a `Pick` of a host interface, with each member typed `AutomationEngine['FIELD']`. That is the shape slices 1 to 7 use. - Before this PR, the census read `this.X` only through a class property declared in the same file. Inside a `function`, `this` names no container, so the lookup fell to the file-wide floor, and a host module declares no class. So every moved site would have been unreadable. Each one was either refused as `[untyped-receiver]` or placed by its `name: string` parameter as an engine write, which turns 238 sites into 239. ## What changed - **`resolveReceiver`, the `this.X` branch.** Some functions declare `this` as their first parameter (`this: …`). When that is the function that binds the site's `this`, the census answers through the declaration (`hostMemberEntry`). It takes three steps: 1. the type, with `Pick` and the other narrowing wrappers peeled off, names an interface or a type-literal alias at the top level of the file; 2. that shape declares the member as a property with a type; 3. the member type is read as written. If it is `C['x']`, an indexed access on a class, the census reads the class's own declaration of `x` instead, in the file that declares the class (`classDeclarationIn`, `scopedNames.own`). When the host reading resolves, it wins, because it is what the compiler checks the body against. When it cannot be read, the site stays on the floor it had, so no receiver that resolved before stops resolving. That covers these cases: - no declared `this`; - a host that is not a top-level shape; - a class the census cannot load; - an indexed access on an interface. - **Across files.** The census reaches a class in another file through the site file's named import of it. - `declaredTypesIn` records named imports and takes an optional `loadModule`. - `runCensus` passes a loader. The loader resolves a relative specifier (`moduleFileFor`: `./engine.js` to `engine.ts`). - It reads only files in the census's own corpus, the same tracked, non-test list it scans. It never reads `node_modules` or an untracked file. - **`fromEntry`**, the rule that turns one declaration into a verdict, is lifted out of `resolveReceiver` unchanged as `receiverFromEntry`, and `nameOf` becomes `indexedNameIn`. This lets the census read a class's declaration against that class's own file. The two closures in `resolveReceiver` now call them. - **Not changed:** `UNTYPED_RECEIVERS`, the two rescue rules, `check-tenant-audit-census.mjs`, the census page and the counts file. ⛔ No new gate, ledger row, CI step or `check:` script. `skip-changeset`, because the diff publishes nothing. ## Self-test Eight new cases (93 to 101), in a new battery whose sources import each other: | Case | Verdict | With the host reading ablated | | --- | --- | --- | | ⭐⭐ a host member typed off a class's `Map` in another file | `other/constructed-locally` | fails, `unresolved/` | | ⭐⭐ positive control: a host member typed off the same class's engine | `engine/IProbeEngine` | fails, `unresolved/` | | the class in the same file, after a decoy class whose same-named properties are typed the other way round | `other/constructed-locally \| engine/IProbeEngine` | fails, the decoy answers: `engine/IProbeEngine \| other/platform-type` | | a host member typed directly (`readonly engine: IProbeEngine`) | `engine/IProbeEngine` | fails, `unresolved/` | | an arrow inside the host function | `other/constructed-locally` | fails, `unresolved/` | | ⛔ a `function` expression inside the host function | `unresolved/` | passes | | ⛔ a host member typed off a class the census cannot load, with a same-named typed property on the floor | `engine/IProbeEngine`, from the floor | passes | | ⛔ an indexed access on an interface | `unresolved/` | passes | The ablation is one line, `const host = null;` in the `this.X` branch, made in a scratch checkout. 5 of the 8 new cases fail, including both fixtures the ruling names. The 3 that pass pin the fallback to the floor, which is the same with or without the host reading. ## Readings The base is `49653f91`, this branch's merge base with `main`, in a worktree of that commit. `main` has since moved to `baae6ed1`, and nothing that landed in between touches the census files. The head is this PR's commit, `9a4c20ea`. Both were read in this container. - **The 33 sites.** I built one probe module per `this.FIELD.delete(` site in `engine.ts`, next to it. Each probe imports `AutomationEngine` with `import type` from `./engine.js`, declares a host member typed `AutomationEngine['FIELD']`, and calls `FIELD.delete` from a function whose `this` is a `Pick` of that host. All 33 probes get the same verdict as their in-class site: 32 `constructed-locally` and 1 `corpus-type` (`store?: SuspendedRunStore`). The 21 members that hold the sites hold no other `insert`, `update` or `delete` call. - **On `main`, the new branch answers no site.** I instrumented a scratch copy to print every write-call receiver that reaches a declared `this`, and over the whole corpus it printed none. 45 non-test files in the corpus declare such functions. Their only `this.X.delete(` lines (`this.readAt.delete` in `auth-manager-password-session-policy.ts`) sit inside a class. - **Outputs**, at both ends: | Output | Base sha256 | Head sha256 | | --- | --- | --- | | `node scripts/tenant-audit-census.mjs`, stdout and stderr | `5a3e8991ecd717edd4e123e9b9439d09d4d7d3b0b3c14125650b6696c08bda7e` | `5a3e8991ecd717edd4e123e9b9439d09d4d7d3b0b3c14125650b6696c08bda7e` | | `node scripts/tenant-audit-census.mjs --json`, with `measuredAt` removed | `d189877c2ae1afb94324f88fc9bef24925feda79b1897d5b246b509b6fa7055a` | `d189877c2ae1afb94324f88fc9bef24925feda79b1897d5b246b509b6fa7055a` | | `node scripts/check-tenant-audit-census.mjs` | `7f0cc90798085180772c7255aa4f21d35b3b6a6bc6f041f4181234fd10cd1172` | `7f0cc90798085180772c7255aa4f21d35b3b6a6bc6f041f4181234fd10cd1172` | - The raw `--json` differs on one line: `measuredAt.ref`, which is the HEAD sha (`49653f918` and `9a4c20ea7`). - The gate's verdict at both ends: 238 write call sites certified (162 decidable; 2 provably without tenant context, 29 more unreadable), 23 prose figures held, every site placed and every `UNTYPED_RECEIVERS` row matched. - I also ran `--write` at each end and then reverted it. At both ends it rewrites the same three lines in each artefact, against the committed page and counts file. The base and head results differ only in the sha on the unenforced `Measured on … at` line. - **`node scripts/check-tenant-audit-census.mjs --self-test`** exits 0 at both ends. Its output differs on one line: the census self-test's summary, whose count goes from 93 to 101 and whose sentence gains a clause for the new cases. The gate's own line (27 cases) is identical. Every earlier case still passes. - This corrects item 2 of the ruling record: a self-test that counts its cases cannot stay byte-identical once it gains cases. The default output, the `--json` output and the gate output can, and do. ## Gates - **Derived.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on this commit derives 32 families from the one changed path. All 32 ran in this container on this commit, with each exit code captured before any pipe, and all 32 exit 0. `dispatch-gates.mjs --ran` reconciles: 32 derived, 32 run, 0 NOT-MEASURED, 0 UNRUN. - **`check-governed-merges --branch`** on this commit: 0 of 1 path on the register, NOT governed, 356 changed lines (+317 / -39). - **ESLint** on the file passes. - **Open PRs.** I re-listed them before opening this one: 19. I diffed each one's head against its merge base with `origin/main`. None touches `scripts/tenant-audit-census.mjs`, `scripts/check-tenant-audit-census.mjs`, `scripts/ts-parse.mjs`, the census page or the counts file. ```text node scripts/check-ci-filter-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs --self-test :: exit 0 node scripts/check-position-name-fold-loaders.mjs :: exit 0 node scripts/check-position-name-fold-loaders.mjs --self-test :: exit 0 node scripts/check-scripts-symbol-anchors.mjs :: exit 0 node scripts/check-scripts-symbol-anchors.mjs --self-test :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-tenant-audit-census.mjs :: exit 0 node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:bash32-floor :: exit 0 pnpm check:cli-command-ids :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:pnpm-flag-spellings :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` ## Next When this merges, slice 8 moves the 21 site-bearing bodies. Its census reading is the one the 33-site probe above already takes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Mnj4nkWuhJ18xEdZvb2cjE Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1286eea commit 9dac9eb

1 file changed

Lines changed: 317 additions & 39 deletions

File tree

0 commit comments

Comments
 (0)