From bbae5c70c28e9ac1c58412c0dba1ec32463d84e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 15:29:32 +0000 Subject: [PATCH 1/3] fix(scripts): apply the tenant-audit census's engine-door rule to inline 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 --- .../docs/permissions/tenant-audit-census.mdx | 81 +++++++--- ...08-tenant-audit-write-call-sites.counts.md | 30 ++-- scripts/tenant-audit-census.mjs | 139 ++++++++++++++++-- 3 files changed, 204 insertions(+), 46 deletions(-) diff --git a/content/docs/permissions/tenant-audit-census.mdx b/content/docs/permissions/tenant-audit-census.mdx index c4eb6d23387..3e5d380b8d1 100644 --- a/content/docs/permissions/tenant-audit-census.mdx +++ b/content/docs/permissions/tenant-audit-census.mdx @@ -71,6 +71,16 @@ as an engine when it declares a write door whose first parameter is named `IDataEngine` door signature. Interfaces that extend one inherit it; aliases that narrow one (`Partial>`) carry it. +**That rule is read off the TYPE, not off the type's name.** The engine type +index is keyed on declaration names, so a receiver whose declared type is an +inline type literal has no name to look up — and for two sites, both writing +under `{ context: { isSystem: true } }`, that alone subtracted a real engine +write from this population while the census printed the type text and said the +subtraction was probably wrong. The same door rule is now read off the type text +itself, so an unnamed type that states a write door places its site. That is +what moved this page's population from 225 to 227; nothing about the two sites +changed, only whether this instrument could see them. + **The expensive failure direction is a keyword.** Sites whose receiver the author typed `any` have no type to read, and there are 44 of them — just under a fifth of the population, concentrated in exactly the seed and bootstrap paths this @@ -86,7 +96,12 @@ Tenancy itself is enabled *by default* — `isTenancyDisabled()` reads `tenancy.enabled === false` and nothing else — so the object registry only has to find the opt-outs. Across 117 declared objects — the dated, ⛔ unenforced corpus-scale figure below — exactly two opt out (`sys_api_key`, -`sys_sso_provider`), and no write call site on this surface targets either. +`sys_sso_provider`), and exactly one write call site on this surface targets +either — `plugin-auth`'s legacy client-secret migration writes +`sys_sso_provider` under an elevated context. That is the +`declares tenancy off` row below reading **1** rather than 0: the object is +outside this control's reach by its own declaration, not by the census failing +to see the write. A **declared object** here is a top-level object declaration in a `*.object.ts(x)` file — `export const X = ObjectSchema.create({ name: … })` — @@ -106,7 +121,7 @@ are reported as `undecidable` rather than assumed either way. The same holds twice over for the context. An options argument spelled as a literal can be read; one spelled `options`, `{ ...opts }`, or handed through a -forwarding shim cannot, and **67 of the 225 sites are spelled that way**. A +forwarding shim cannot, and **67 of the 227 sites are spelled that way**. A context resolved from an inline literal or a local `const` can be tested for `isSystem`; one arriving from a helper call cannot. @@ -140,9 +155,21 @@ direction, on the very figure this page tells other cards to cite. `carries` is now three-valued, and an unreadable argument can never contribute to the provable count. -⭐ Both are the same shape as the failure this artefact exists for, wearing -opposite hats: one scored an unread thing as *nothing to report*, the other -scored an unread thing as *a finding*. `node +**A door rule keyed on names read an unnamed door as no door.** Two receivers +typed with an inline type literal that spells `insert` / `update` with an +`object: string` first parameter were subtracted from the population — by the +same instrument that printed their type text on every run and called the +subtraction probably wrong. Placing them moved the population **225 → 227** and +the elevated count **106 → 108**. ⭐ It moved the provable yield surface +(`9`) **not at all**: both sites thread an elevated context, and one of them +targets a tenancy-opt-out object. The repair is to the denominator and to what +this page can be read as having examined — ⛔ not to the count anything cites as +a violation. + +⭐ All three are the same shape as the failure this artefact exists for, wearing +different hats: one scored an unread thing as *nothing to report*, one scored an +unread thing as *a finding*, and one scored a thing it had read and printed as +outside its own population. `node scripts/check-tenant-audit-census.mjs --self-test` pins all of it — 18 classifier cases, 6 of which red against the old reading, driven from the gate's own self-test rather than from a flag on the generator. @@ -155,10 +182,10 @@ reproduce them. Where it disagrees, it disagrees on the page: | carried figure | where it survives | this census | | :--- | :--- | ---: | -| 175 write call sites | quoted in the merged changeset | **225** | +| 175 write call sites | quoted in the merged changeset | **227** | | 24 carrying no tenant context | quoted in the merged changeset | **9** provable and tenancy-enabled; **32** more whose options argument is unreadable | -| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **149 of 225** decidable, **76** undecidable | -| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 106 decidably elevated, 0 decidably not, 102 undecidable | +| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **151 of 227** decidable, **76** undecidable | +| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 108 decidably elevated, 0 decidably not, 102 undecidable | | 141 and 132, two independent re-derivations | the card that filed this work | — | **The differences are not reconciled, and deliberately so.** The old census's @@ -169,17 +196,17 @@ at any commit. Two structural facts do plausibly widen this reading against any hand or regex one, and both are counted in the generated tables below: the 44 sites reached -through an erased (`any`) receiver, and the 40 that name their object through a +through an erased (`any`) receiver, and the 41 that name their object through a `const` rather than inline. An instrument that read either the way a person does would report a smaller number and would not say so. The fourth row is the one worth flagging to anyone citing it. **The 135 / 77% figure has no surviving corroboration anywhere in the tree.** This census reads -106 of 225 (47%) as decidably elevated, with 102 more whose elevation is a +108 of 227 (48%) as decidably elevated, with 102 more whose elevation is a run-time fact — so the claim is neither confirmed nor refuted, and the honest answer is that a static reading cannot settle it. -⇒ **Cite `9 / 225`, and say what it is**: the sites whose options argument was +⇒ **Cite `9 / 227`, and say what it is**: the sites whose options argument was READ and holds no tenant context, against a decidably tenancy-enabled object. That is the control's provable yield surface. ⛔ Do not cite it as "the sites without tenant context" — **32 further sites** have an options argument this @@ -191,29 +218,29 @@ cannot read, and they are neither in nor out. | what | count | | :--- | ---: | -| write call sites on the application surface | **225** | -| …whose object name is statically decidable | 149 | +| write call sites on the application surface | **227** | +| …whose object name is statically decidable | 151 | | …whose object name is chosen at run time | 76 | -| …against an object with tenancy ENABLED | 149 | -| …against an object that declares tenancy off | 0 | -| threading a tenant context | 141 | +| …against an object with tenancy ENABLED | 150 | +| …against an object that declares tenancy off | 1 | +| threading a tenant context | 143 | | PROVABLY carrying none (options read, no context key) | **17** | | …of those, against a decidably tenancy-enabled object | **9** | | options argument UNREADABLE — may or may not carry one | 67 | | …of those, against a decidably tenancy-enabled object | 32 | -| threading a decidably ELEVATED (`isSystem`) context | 106 | +| threading a decidably ELEVATED (`isSystem`) context | 108 | | threading a context that is decidably NOT elevated | 0 | | threading a context whose elevation is a run-time fact | 102 | | how the instrument reached the site | count | | :--- | ---: | -| receiver carried a readable engine type | 181 | +| receiver carried a readable engine type | 183 | | receiver erased, placed by the object NAME | 18 | | receiver erased, placed by an `object: string` PARAMETER | 15 | | receiver erased, placed by an `UNTYPED_RECEIVERS` row | 11 | -| object name spelled inline | 109 | -| object name spelled through a `const` | 40 | +| object name spelled inline | 110 | +| object name spelled through a `const` | 41 | | object name is an `object: string` parameter | 19 | | object name is some other run-time expression | 57 | @@ -232,8 +259,14 @@ must not be spelled the same way as «read it, not an engine». | what | count | | :--- | ---: | -| write calls subtracted with no defensible reason | **3** | -| …whose declared type text states an engine door anyway | **2** | +| write calls subtracted with no defensible reason | **1** | +| …whose declared type text states an engine door anyway | **0** | + +⛔ The second row is **0 by construction**, not a tally that happens to be low. +An inline type literal stating a write door has no name for the engine type index +to be keyed on, so the door rule is read off the type text itself and the site is +PLACED — it is in the population above rather than subtracted here. A non-zero +value on that row means a door-shaped receiver reached the subtraction anyway. Every one of them is listed, by receiver and by the type text that could not be placed, in [`docs/audits/2026-08-tenant-audit-write-call-sites.counts.md`](https://github.com/objectstack-ai/objectstack/blob/main/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md). @@ -253,13 +286,13 @@ holds still. They are required to be HERE and to say WHEN they were true; their values are not compared. The reasoning, and the measurement behind it, are in `scripts/check-tenant-audit-census.mjs`. -Measured on 2026-09-18 at `02bdeaaf2`. +Measured on 2026-09-18 at `0ec81857a`. | corpus scale (not enforced) | count | | :--- | ---: | | tracked non-test sources scanned | 573 | | engine-shaped types recognised | 63 | | declared objects in the registry | 117 | -| same-named calls subtracted as non-engine | 146 | +| same-named calls subtracted as non-engine | 144 | {/* END GENERATED: tenant-audit-census */} diff --git a/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md b/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md index c988854b368..2ed5049e9bd 100644 --- a/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md +++ b/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md @@ -29,17 +29,17 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution. | Measure | Value | |---|---:| -| Write call sites | 225 | -| Object name statically decidable | 149 | +| Write call sites | 227 | +| Object name statically decidable | 151 | | Object name chosen at run time | 76 | -| Against a tenancy-enabled object | 149 | -| Against an object declaring tenancy off | 0 | -| Threading a tenant context | 141 | +| Against a tenancy-enabled object | 150 | +| Against an object declaring tenancy off | 1 | +| Threading a tenant context | 143 | | Provably carrying none | 17 | | …and decidably tenancy-enabled | 9 | | Options argument unreadable | 67 | | …and decidably tenancy-enabled | 32 | -| Threading a decidably elevated context | 106 | +| Threading a decidably elevated context | 108 | | Threading a decidably non-elevated context | 0 | | Threading a context of undecidable elevation | 102 | @@ -58,13 +58,17 @@ must not be spelled the same way as «read it, not an engine». | what | count | | :--- | ---: | -| write calls subtracted with no defensible reason | **3** | -| …whose declared type text states an engine door anyway | **2** | +| write calls subtracted with no defensible reason | **1** | +| …whose declared type text states an engine door anyway | **0** | + +⛔ The second row is **0 by construction**, not a tally that happens to be low. +An inline type literal stating a write door has no name for the engine type index +to be keyed on, so the door rule is read off the type text itself and the site is +PLACED — it is in the population above rather than subtracted here. A non-zero +value on that row means a door-shaped receiver reached the subtraction anyway. | file | receiver | verb | why | declared type | door | n | |---|---|---|---|---|---|---:| -| `packages/plugins/plugin-auth/src/audience-gate-test-support.ts` | `engine` | `insert` | anonymous-type | `{ insert: (name: string, data: any, options?: any) => Promise } \| null` | ⚠️ yes | 1 | -| `packages/plugins/plugin-auth/src/sso-client-secret.ts` | `e` | `update` | anonymous-type | `{ find(object: string, query: unknown): Promise[]>; update(object: string, data: unknown, options?: unknown): Promise; }` | ⚠️ yes | 1 | | `packages/plugins/plugin-hono-server/src/adapter.ts` | `this.app` | `delete` | type-not-in-corpus | `Hono` | no | 1 | ## Corpus scale — present and dated, ⛔ NOT enforced @@ -76,14 +80,14 @@ holds still. They are required to be HERE and to say WHEN they were true; their values are not compared. The reasoning, and the measurement behind it, are in `scripts/check-tenant-audit-census.mjs`. -Measured on 2026-09-18 at `02bdeaaf2`. +Measured on 2026-09-18 at `0ec81857a`. | corpus scale (not enforced) | count | | :--- | ---: | | tracked non-test sources scanned | 573 | | engine-shaped types recognised | 63 | | declared objects in the registry | 117 | -| same-named calls subtracted as non-engine | 146 | +| same-named calls subtracted as non-engine | 144 | ## Every site @@ -108,6 +112,7 @@ Measured on 2026-09-18 at `02bdeaaf2`. | `packages/plugins/plugin-auth/src/admin-user-endpoints.ts` | `insert` | `sys_audit_log` | enabled | elevated | 1 | | `packages/plugins/plugin-auth/src/admin-user-endpoints.ts` | `update` | `sys_user` | enabled | elevated | 1 | | `packages/plugins/plugin-auth/src/adopt-membership.ts` | `update` | `SystemObjectName.MEMBER` | undecidable | PROVABLY NONE | 1 | +| `packages/plugins/plugin-auth/src/audience-gate-test-support.ts` | `insert` | `sys_invitation` | enabled | elevated | 1 | | `packages/plugins/plugin-auth/src/auth-manager.ts` | `update` | `sys_account` | enabled | options unreadable | 1 | | `packages/plugins/plugin-auth/src/auth-manager.ts` | `update` | `sys_session` | enabled | options unreadable | 3 | | `packages/plugins/plugin-auth/src/auth-manager.ts` | `update` | `sys_two_factor` | enabled | options unreadable | 1 | @@ -129,6 +134,7 @@ Measured on 2026-09-18 at `02bdeaaf2`. | `packages/plugins/plugin-auth/src/reconcile-membership.ts` | `insert` | `sys_member` | enabled | context, elevation undecidable | 1 | | `packages/plugins/plugin-auth/src/scim-connection-service.ts` | `insert` | `sys_scim_connection_credential` | enabled | PROVABLY NONE | 1 | | `packages/plugins/plugin-auth/src/session-tombstone.ts` | `update` | `objectName` | undecidable | options unreadable | 1 | +| `packages/plugins/plugin-auth/src/sso-client-secret.ts` | `update` | `sys_sso_provider` | disabled | elevated | 1 | | `packages/plugins/plugin-email/src/attachment-reclaim.ts` | `update` | `sys_email` | enabled | elevated | 1 | | `packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts` | `insert` | `object` | undecidable | elevated | 1 | | `packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts` | `update` | `object` | undecidable | elevated | 2 | diff --git a/scripts/tenant-audit-census.mjs b/scripts/tenant-audit-census.mjs index ab418430523..40921ef8b3b 100644 --- a/scripts/tenant-audit-census.mjs +++ b/scripts/tenant-audit-census.mjs @@ -153,9 +153,30 @@ * run, carried per site in `--json`, and counted under ENFORCEMENT in both * artefacts -- so a type that leaves the index lands in the diff by name instead * of removing a site in silence. ⛔ No subtraction is withdrawn on this basis and - * ⛔ no exit code changed: three sites on a clean tree are undefended today, two - * of them with a write door in their own inline type text, and a gate that reds - * on arrival is a gate that gets weakened. + * ⛔ no exit code changed: one site on a clean tree is undefended today, and a + * gate that reds on arrival is a gate that gets weakened. + * + * ## ⭐ The door rule is applied to the TYPE, not to the type's NAME + * + * {@link memberIsEngineDoor} was applied to NAMED declarations only, so a receiver + * whose declared type is an inline type literal had no name for the index to be + * keyed on and was subtracted as `kind: 'other'` however plainly its own text + * stated a write door. Two sites on a clean tree were exactly that, both writing + * under an elevated context: `resolveInsertEngine()`'s + * `{ insert: (name: string, …) => … } | null` in `plugin-auth`, and + * `migrateLegacySsoClientSecrets`'s `engine as unknown as { find(object: string, …); + * update(object: string, …) }`. The census PRINTED both -- `doorShaped` in + * {@link nonEngineReason} is that diagnostic -- and subtracted them anyway. ⛔ A + * diagnostic that names a subtraction as probably wrong and then takes it is not a + * report, it is a deferral. + * + * ⇒ {@link inlineEngineDoorOrOther} closes it: when a declared type text names no + * indexed engine, the SAME door rule is read off the type text itself, and a + * write door there places the site. ⛔ Not a widening of the definition -- it IS + * the definition, applied where it had only been reported. The diagnostic stays, + * and its door-shaped count is now 0 BY CONSTRUCTION: a non-zero value there + * means a door-shaped receiver reached the subtraction anyway, i.e. this hole has + * reopened. * * ## Refusals, never quiet passes (#4690) * @@ -560,7 +581,7 @@ export function resolveReceiver(recvNode, sf, decls, index, depth = 0) { if (entry.type && /^(any|unknown)$/.test(entry.type.trim())) { return { kind: 'unresolved', how: `${how}:any`, detail: entry.type }; } - if (entry.type) return { kind: 'other', type: entry.type, how }; + if (entry.type) return inlineEngineDoorOrOther(entry.type, how); if (entry.init) { const t2 = nameOf(entry.init); if (t2) return { kind: 'engine', type: t2, how: `${how}/init` }; @@ -589,7 +610,7 @@ export function resolveReceiver(recvNode, sf, decls, index, depth = 0) { if (via.kind !== 'unresolved') return { ...via, how: `as-any/${via.how}` }; return { kind: 'unresolved', how: 'as-any', detail: receiverKey(r.expression, sf) }; } - return { kind: 'other', type: r.type.getText(sf), how: 'as' }; + return inlineEngineDoorOrOther(r.type.getText(sf), 'as'); } if (ts.isPropertyAccessExpression(r) && r.expression.kind === ts.SyntaxKind.ThisKeyword) { return fromEntry(decls.thisProps.get(r.name.text), `this.${r.name.text}`); @@ -608,7 +629,7 @@ export function resolveReceiver(recvNode, sf, decls, index, depth = 0) { if (mt) { const t = nameOf(mt); if (t) return { kind: 'engine', type: t, how: `member ${r.name.text}` }; - return { kind: 'other', type: mt, how: `member ${r.name.text}` }; + return inlineEngineDoorOrOther(mt, `member ${r.name.text}`); } } if (ts.isCallExpression(r)) { @@ -630,7 +651,7 @@ export function resolveReceiver(recvNode, sf, decls, index, depth = 0) { if (mt) { const t = nameOf(mt); if (t) return { kind: 'engine', type: t, how: `${fname}() return` }; - return { kind: 'other', type: mt, how: `${fname}() return` }; + return inlineEngineDoorOrOther(mt, `${fname}() return`); } } const entry = fname ? decls.fnReturns.get(fname) : null; @@ -692,6 +713,13 @@ const TYPE_SYNTAX_WORDS = new Set([ * those are reported per site, in both artefacts, by receiver and by the type text * it could not place. ⛔ A subtraction is never *withdrawn* on this basis: that * would be the census guessing in the other direction. It is DECLARED. + * + * ⚠️ `anonymous-type` is now the RESIDUE of a rule that runs first: an inline type + * literal whose own text states a write door is placed by + * {@link inlineEngineDoorOrOther} and never reaches here, so what lands on this arm + * is an unnamed type the door rule read and rejected. Its `doorShaped` flag is + * therefore 0 in both artefacts by construction, and is kept as that invariant's + * alarm rather than as a running count. */ export const NON_ENGINE_REASONS = Object.freeze({ 'builtin-import': 'the receiver is an identifier imported from a `node:` builtin', @@ -715,8 +743,11 @@ export const UNDEFENDED_REASONS = Object.freeze(['type-not-in-corpus', 'anonymou * * ⭐ A `true` here is the sharpest thing this diagnostic can say: the census * subtracted a write call whose receiver type satisfies its OWN definition of an - * engine, and the only reason it did is that the definition is applied to NAMED - * declarations while this type is spelled inline. It is reported, ⛔ not acted on. + * engine, and the only reason it did is that the definition was applied to NAMED + * declarations while this type is spelled inline. It is now ACTED ON -- + * {@link inlineEngineDoorOrOther} places such a receiver instead of subtracting it + * -- and this predicate is kept as that invariant's alarm: a door-shaped + * subtraction reaching the artefacts means the placement rule has a hole again. */ export function typeTextDeclaresEngineDoor(typeText) { if (typeof typeText !== 'string' || !/\b(insert|update|delete)\b/.test(typeText)) return false; @@ -736,6 +767,46 @@ export function typeTextDeclaresEngineDoor(typeText) { return door; } +/** + * What {@link runCensus} records as the engine type of a receiver placed by its + * own inline type literal rather than by a name in the index. + * + * Deliberately a SENTENCE and not a type name, because there is no name -- the + * index is keyed on names and this receiver has none. `--json` is where a reader + * navigating one site learns which rule placed it; the enforced artefacts + * aggregate by (file, verb, object, tenancy, context posture) and never render + * this text, so its wording cannot move a count. + */ +const INLINE_ENGINE_TYPE = 'inline type literal stating an engine door'; + +/** + * A declared type text that names no indexed engine, judged ONE more time -- by + * the door rule read off the text itself. + * + * {@link buildEngineTypeIndex} is keyed on declaration NAMES, so an inline type + * literal cannot be in the index however plainly it declares a write door. + * Answering `kind: 'other'` on that basis is the census answering "not an engine" + * to a question it never asked, and it fails in the expensive direction: the site + * is SUBTRACTED from the certified population. Two sites on a clean tree were + * exactly that, both writing under `{ context: { isSystem: true } }`. + * + * ⛔ This is not a second reading of "what an engine door looks like". It calls + * {@link typeTextDeclaresEngineDoor}, which calls {@link memberIsEngineDoor} -- the + * one function that answers that question for a named declaration too. Two + * spellings of the rule is how the two drift. + * + * ⚠️ Reached ONLY after {@link resolveReceiver} has failed to find an indexed name + * in the type text, so a named engine type still wins and still reports its own + * name. The `how` gains an `/inline-door` suffix so `--json` says which rule + * placed the site. + */ +function inlineEngineDoorOrOther(typeText, how) { + if (typeTextDeclaresEngineDoor(typeText)) { + return { kind: 'engine', type: INLINE_ENGINE_TYPE, how: `${how}/inline-door`, inlineType: typeText }; + } + return { kind: 'other', type: typeText, how }; +} + /** * Which arm of {@link NON_ENGINE_REASONS} this non-engine verdict rests on. * @@ -1447,6 +1518,12 @@ export function renderUndefendedSubtractions(census, heading, { withRows = true out.push(`| write calls subtracted with no defensible reason | **${rows.reduce((n, r) => n + r.count, 0)}** |`); out.push(`| …whose declared type text states an engine door anyway | **${doorShaped}** |`); out.push(''); + out.push('⛔ The second row is **0 by construction**, not a tally that happens to be low.'); + out.push('An inline type literal stating a write door has no name for the engine type index'); + out.push('to be keyed on, so the door rule is read off the type text itself and the site is'); + out.push('PLACED — it is in the population above rather than subtracted here. A non-zero'); + out.push('value on that row means a door-shaped receiver reached the subtraction anyway.'); + out.push(''); if (rows.length === 0) { out.push('None: every non-engine subtraction in this census rests on a named fact.'); return out; @@ -1912,6 +1989,46 @@ export function selfTest() { t('⭐ the same receiver, type NOT in the index, is a subtraction that says WHY', resolveIn([]), 'other/type-not-in-corpus'); + // ⭐⭐ THE DOOR RULE ON A TYPE WITH NO NAME. Same source shape, same index; the + // only variable is whether the receiver's inline type literal states a WRITE + // door. The index is keyed on names and an inline literal has none, so before + // `inlineEngineDoorOrOther` every one of these read `other/anonymous-type` -- + // including the two real sites, which the diagnostic printed as probably wrong + // and the classifier subtracted anyway. Pinned in BOTH directions, because a + // rule that places every inline literal would be the same failure mirrored. + const resolveInline = (typeText, indexNames = []) => { + const src = `declare const e: ${typeText};\ne.insert('sys_user', {}, { context: { isSystem: true } });\n`; + const sf = parseSourceFile('selftest.ts', src); + const decls = declaredTypesIn(sf); + const index = new Map(indexNames.map((n) => [n, { decls: ['probe.ts'], verbs: ['insert'] }])); + let out = 'NO-CALL'; + const visit = (node) => { + if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) + && WRITE_VERBS.includes(node.expression.name.text)) { + const res = resolveReceiver(node.expression.expression, sf, decls, index); + out = res.kind === 'other' + ? `other/${nonEngineReason(res, new Set()).reason}` + : `${res.kind}/${res.type ?? ''}`; + } + ts.forEachChild(node, visit); + }; + visit(sf); + return out; + }; + t('⭐ an inline type literal stating a write door is PLACED, with no name in the index', + resolveInline('{ insert(object: string, data: unknown): Promise }'), + `engine/${INLINE_ENGINE_TYPE}`); + t('⭐ …through a property-signature door in a union, which is how one real site is spelled', + resolveInline('{ insert: (name: string, data: any, options?: any) => Promise } | null'), + `engine/${INLINE_ENGINE_TYPE}`); + t('⛔ an inline literal whose only door is a READ door is still a subtraction', + resolveInline('{ find(object: string, query: unknown): Promise }'), 'other/anonymous-type'); + t('⛔ a same-named method whose first parameter is not an object name is no door', + resolveInline('{ delete(key: string): void }'), 'other/anonymous-type'); + t('⛔ an indexed NAME beside an inline literal still wins and reports itself', + resolveInline('IProbeEngine | { insert(object: string): Promise }', ['IProbeEngine']), + 'engine/IProbeEngine'); + const failed = cases.filter((c) => !c.ok); for (const c of failed) console.error(` ✗ ${c.name} -- ${c.detail}`); if (failed.length > 0) { @@ -1926,7 +2043,9 @@ export function selfTest() { + 'counts two, while `inlineColumns`, validation-rule, action, list-view and index names ' + 'in the same file count none -- and the TRACKED-ONLY criterion in both directions: one ' + 'receiver, one source text, and index membership the only variable, reading `engine` in ' - + 'the index and a subtraction that NAMES the unplaceable type out of it).', + + 'the index and a subtraction that NAMES the unplaceable type out of it -- and the door ' + + 'rule read off a type with no NAME at all, placing an inline literal that states a write ' + + 'door while still subtracting one that states none).', ); return 0; } From 6c23858e584dbdd2b8ef671ef80cf6fd01c69cc6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 15:37:45 +0000 Subject: [PATCH 2/3] docs(permissions): keep the census page's own claims true where the placement 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 --- content/docs/permissions/tenant-audit-census.mdx | 12 ++++++++---- scripts/tenant-audit-census.mjs | 7 ++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/content/docs/permissions/tenant-audit-census.mdx b/content/docs/permissions/tenant-audit-census.mdx index 3e5d380b8d1..c38745d44b4 100644 --- a/content/docs/permissions/tenant-audit-census.mdx +++ b/content/docs/permissions/tenant-audit-census.mdx @@ -164,15 +164,19 @@ the elevated count **106 → 108**. ⭐ It moved the provable yield surface (`9`) **not at all**: both sites thread an elevated context, and one of them targets a tenancy-opt-out object. The repair is to the denominator and to what this page can be read as having examined — ⛔ not to the count anything cites as -a violation. +a violation. It is pinned in **both** directions, because a rule that placed +every inline literal would be the same failure mirrored: an unnamed type that +states a write door places its site, one that states none is still a subtraction +that says why, and an indexed type name beside an inline literal still wins and +reports itself. ⭐ All three are the same shape as the failure this artefact exists for, wearing different hats: one scored an unread thing as *nothing to report*, one scored an unread thing as *a finding*, and one scored a thing it had read and printed as outside its own population. `node -scripts/check-tenant-audit-census.mjs --self-test` pins all of it — 18 classifier -cases, 6 of which red against the old reading, driven from the gate's own -self-test rather than from a flag on the generator. +scripts/check-tenant-audit-census.mjs --self-test` pins the first two — 18 +classifier cases, 6 of which red against the old reading — driven from the +gate's own self-test rather than from a flag on the generator. ## ⭐ Deviations from the carried figures, untrimmed diff --git a/scripts/tenant-audit-census.mjs b/scripts/tenant-audit-census.mjs index 40921ef8b3b..fd4cce1dadf 100644 --- a/scripts/tenant-audit-census.mjs +++ b/scripts/tenant-audit-census.mjs @@ -797,12 +797,13 @@ const INLINE_ENGINE_TYPE = 'inline type literal stating an engine door'; * * ⚠️ Reached ONLY after {@link resolveReceiver} has failed to find an indexed name * in the type text, so a named engine type still wins and still reports its own - * name. The `how` gains an `/inline-door` suffix so `--json` says which rule - * placed the site. + * name. The `how` gains an `/inline-door` suffix for anyone tracing a resolution; + * ⛔ `how` is not carried onto a PLACED site, so what `--json` shows for one of + * these is its `engineType`, the sentence above. */ function inlineEngineDoorOrOther(typeText, how) { if (typeTextDeclaresEngineDoor(typeText)) { - return { kind: 'engine', type: INLINE_ENGINE_TYPE, how: `${how}/inline-door`, inlineType: typeText }; + return { kind: 'engine', type: INLINE_ENGINE_TYPE, how: `${how}/inline-door` }; } return { kind: 'other', type: typeText, how }; } From 4b45249966b6e405d8e1597c78a44a2d795ca0d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 17:26:52 +0000 Subject: [PATCH 3/3] chore(docs): regenerate the tenant-audit census artefacts from the merged tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- content/docs/permissions/tenant-audit-census.mdx | 2 +- docs/audits/2026-08-tenant-audit-write-call-sites.counts.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/permissions/tenant-audit-census.mdx b/content/docs/permissions/tenant-audit-census.mdx index 1bbfd9c4550..b3856e810f9 100644 --- a/content/docs/permissions/tenant-audit-census.mdx +++ b/content/docs/permissions/tenant-audit-census.mdx @@ -290,7 +290,7 @@ holds still. They are required to be HERE and to say WHEN they were true; their values are not compared. The reasoning, and the measurement behind it, are in `scripts/check-tenant-audit-census.mjs`. -Measured on 2026-09-18 at `d4cb05cbf`. +Measured on 2026-09-18 at `30def652e`. | corpus scale (not enforced) | count | | :--- | ---: | diff --git a/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md b/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md index 72211d98ebd..1a37919af07 100644 --- a/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md +++ b/docs/audits/2026-08-tenant-audit-write-call-sites.counts.md @@ -84,7 +84,7 @@ holds still. They are required to be HERE and to say WHEN they were true; their values are not compared. The reasoning, and the measurement behind it, are in `scripts/check-tenant-audit-census.mjs`. -Measured on 2026-09-18 at `d4cb05cbf`. +Measured on 2026-09-18 at `30def652e`. | corpus scale (not enforced) | count | | :--- | ---: |