Skip to content

fix(scripts): make the tenant-audit census say when it could not place a receiver's type - #18943

Merged
os-elon-musk merged 2 commits into
mainfrom
claude/issue-18548-census-unresolved-type-diagnostic
Sep 18, 2026
Merged

os-elon-musk merged 2 commits into
mainfrom
claude/issue-18548-census-unresolved-type-diagnostic

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #18548
Clause-②: no

The defect, in one sentence

scripts/tenant-audit-census.mjs builds its engine type index from git ls-files
and nothing else, so a receiver typed with a declaration that enumeration cannot
see — untracked, generated, or a dependency's — has no type the index can find.
That site resolved to kind: 'other', was subtracted from the certified
population
, and printed nothing. It was spelled exactly like "I read the
declared type and it is a Set": same row, same exit 0, no diagnostic.

⛔ The enumeration is unchanged and stays tracked-only. Acceptance item 3 is
explicit that reading untracked files would make the repository's verdict a
function of somebody's working tree, and this PR does not do it. Acceptance
item 4 is what landed instead: the criterion is now the script's own, written
down at CORPUS_TYPE_DECL, and pinned in both directions.

Acceptance item 1 — the reading, re-measured on this tree

origin/main = 02bdeaaf2. 129 write calls reach kind: 'other' through a
declared type or a constructed value; 17 more arrive through an
UNTYPED_RECEIVERS row. 116 of the 146 subtractions are made because the
receiver's declared type is not in the engine type index
— the path the card
asks about. Broken out by the fact each one now rests on:

why the subtraction stands sites
constructed-locally — the census watched new X being constructed 92
ledger-row — an UNTYPED_RECEIVERS row says what the receiver is 17
corpus-type — a type THIS corpus declares; the door rule read it and said no 15
builtin-import — the receiver is imported from a node: builtin 13
platform-type — a language global this corpus cannot declare 6
⚠️ anonymous-type — an inline type literal: no name for the index to be keyed on 2
⚠️ type-not-in-corpus — no declaration of that name exists in the tracked corpus 1

⇒ 3 undefended subtractions on a clean tree, and 2 of them are provably
wrong
: their own inline type text states an ObjectQL write door
(insert / update with a first parameter named object or name and typed
string), which is the census's own definition of an engine, applied to named
declarations only:

  • packages/plugins/plugin-auth/src/audience-gate-test-support.ts:72 —
    engine.insert('sys_invitation', row, { context: { isSystem: true } })
  • packages/plugins/plugin-auth/src/sso-client-secret.ts:303 —
    e.update(SSO_PROVIDER_OBJECT, patch, { context: { isSystem: true } })

Both are real engine writes, and both are subtracted from the population today.
⛔ This PR does not re-place them — that moves a security-relevant population
other cards cite, and acceptance item 2 says diagnostics before behaviour. It
makes them loud and names them for a follow-up card.

The firing control (the card's git add experiment, reproduced)

packages/services/service-settings/src/settings-service.types.ts declares
SettingsEngine. Untracking it with git rm --cached leaves the bytes on disk
untouched (blob 90c0966 before and after) and removes only the declaration the
index can see. Measured four times on this tree:

census tracked declaration untracked
pre-change (02bdeaaf2) 225 sites / 146 subtracted, exit 0, stderr 0 bytes 223 sites / 148 subtracted, exit 0, stderr 0 bytes
this PR 225 / 146, exit 0, 3 named warnings 223 / 148, exit 0, 6 named warnings, type-not-in-corpus 1 to 4

The four names the diagnostic prints under the control are SettingsEngine (2
sites), SettingsSecretStore (1) and Hono (1). Pre-change, the same run wrote
zero bytes to stderr in both columns — which is the defect the card describes,
measured rather than relayed.

What changed

  • corpusTypeNames() — every type NAME the tracked corpus declares, collected
    from the same git ls-files enumeration as the index, so the diagnostic can
    never see more of the tree than the index it reports on. 6520 files, 103 MB,
    8154 names, 0.6 s. Zero names refuses.
  • NON_ENGINE_REASONS — a closed set of seven. Five name a fact that defends
    the subtraction; two admit the census could not place the receiver's type.
  • typeTextDeclaresEngineDoor() reuses memberIsEngineDoor, so "an engine door
    in its own text" is one rule with one implementation, not two spellings.
  • The undefended ones are printed per site on every run
    (::warning::[receiver-type-not-placed]), carried per site in --json, and
    counted under enforcement in both artefacts with the rows in the audit
    ledger. A type that leaves the index now lands in the diff by name.
  • The reason breakdown deliberately stays OUT of the enforced artefacts: every
    commit that adds a Map moves it, which is the ambient churn with no safety
    content the maintainer's 2026-08-31 enforced/unenforced ruling exists to keep
    out. The undefended rows move only when the population's own boundary moves, so
    they belong on the enforced side by that ruling's own criterion.

⚠️ Nothing got quieter and nothing moved. 225 sites, 146 subtracted, 0
unresolved receivers, generator exit 0 and gate exit 0 — before and after, on a
clean tree. No subtraction is withdrawn, no verdict is recomputed, no exit code
changed. The only committed-number movement is in the two unenforced rows a
regeneration always touches: tracked non-test sources scanned 570 to 573 and
the dated marker 2026-09-16 at 11daf7f69 to 2026-09-18 at 02bdeaaf2.

⚠️ The generator warns rather than refuses, deliberately: this class is not empty
on a clean tree, so refusing would red main for findings nobody has ruled on,
and a gate that reds on arrival is a gate that gets weakened.

Acceptance item 4 — the criterion, pinned in both directions

The census self-test goes 30 to 44 cases. The load-bearing pair is one receiver,
one source text, and index membership as the only variable:

  • type IS in the index ⇒ engine/IProbeEngine
  • type is NOT in the index ⇒ other/type-not-in-corpus, naming IProbeEngine

Both arms are also pinned at the classifier: a name the corpus declares reads
corpus-type; the same name absent from the corpus reads type-not-in-corpus.
The five defensible arms are pinned too, so the diagnostic cannot decay into
"everything is undefended" — a pile that flags all 146 hides the 3 that matter
exactly as effectively as flagging none.

Both ablations fired (scripts/ablation-replace.mjs, on-disk blob evidence,
restore verified against HEAD):

mutation result
const unseen = ids.filter(...) to const unseen = [] (collapse the tracked-only distinction) ✗ 2 of 44 — exactly the two criterion pins
door = true to door = false in the door reader ✗ 1 of 44 — the door pin

Restored: blob back to b87aade77de1, git diff HEAD empty, 44 of 44 green.

Premises tested

  1. The relayed headline reproduces exactly. The card relays 223 / 146 to 148 versus 225 / 146 from card [finding] tenant-audit 普查器的 ERROR 方向送不到 CI —— 门禁一次都没读 unledgered / staleLedgerRows #18211's dev and states it did not reproduce
    them. They reproduce on this tree, to the digit, with the file named above.
  2. The enumeration claim holds, with one correction. On 02bdeaaf2:
    buildEngineTypeIndex is at :215, trackedTs at :163, the ls-files
    call at :164, and grep -c "ls-files" is 1 — one mouth, no second path.
    ⚠️ But trackedTs( appears 5 times: one definition and four
    consumers (collectSources:171, buildEngineTypeIndex:217,
    widenIndexThroughAliases:313, declaredObjects:638). The card's control
    proves there is no second way in; it does not show that four separate answers
    are derived from that one way. This PR adds a fifth consumer on purpose, for
    exactly that reason.
  3. It is subtraction, not an under-reported row. Under the control the
    population itself moves 225 to 223 and object name chosen at run time moves
    76 to 74: the two sites leave census.totals entirely rather than being
    counted and mis-verdicted. Confirmed structurally too — kind === 'other'
    increments nonEngineCalls and never pushes into sites.
  4. origin/main merged before opening this PR: already up to date at
    02bdeaaf2, and every reading above is from this tree.

Acceptance notes

维护者速读(草稿)

改了什么 —— tenant-audit 普查器在把一个写调用点判成「非引擎调用」并从被认证总体里减掉时,
现在必须说出它凭的是哪条事实。七个理由构成一个封闭集:五个是站得住的(node: 内建、就地
new 构造、语言全局、本仓声明且门规则读过后否掉的类型、台账行),两个是坦白「我没能把这个接
收者的类型放到位」。⛔ 枚举口没动,仍然只从 git ls-files 建 —— 这是设计,不是缺陷。

为什么改 —— 原来这两类印出来完全同形:总体静默变小、exit 0、零诊断。本席实测:把一个声明
引擎接口的文件 git rm --cached(磁盘字节不变),改前的普查器给出 223 个站点(原本 225)、
减法 148(原本 146),而 stderr 一个字节都没有。⇒ 这个仪器的裁决确实取决于你有没有先跑
git add,而它对此一言不发。

风险与代价(含回滚) —— 本次没有任何判定、计数或退出码移动(225/146/0,改前改后一致),
没有撤回任何一次减法。代价是两份产物各多一节「无法辩护的减法」并进入强制比对:今后一个类型离开
索引会让门禁红着报出该类型的名字,而不是安静少算一个站点。回滚 = 单 commit revert 再跑一次
node scripts/tenant-audit-census.mjs --write,无数据迁移、无发布面。

席位意见 ——

你要做的 —— 无需动作;这是仓内普查脚本,不发布任何东西(声明行在正文首部)。⚠️ 若关心那两个
「自己的类型文本就写着引擎门、却仍被减掉」的站点(audience-gate-test-support.ts:72、
sso-client-secret.ts:303),它们会独立立卡:把它们放回总体会把 225 推到 227 并改写页面里手写
的偏差表,该由一次专门的评审决定。

🤖 Generated with Claude Code

https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh


Generated by Claude Code

…e a receiver's type

`buildEngineTypeIndex` enumerates through `git ls-files`, so a receiver typed
with an untracked, generated or dependency-owned declaration has no type the
index can find. Such a site resolved to `kind: 'other'`, was subtracted from the
certified population, and printed nothing -- indistinguishable from "read the
declared type, it is a `Set`". The population could shrink at exit 0 and the part
that shrank was invisible.

The enumeration is unchanged and stays tracked-only, deliberately: a census whose
verdict moved with the working tree would be measuring the working tree. What
changes is that the subtraction now states the fact it rests on.

- `corpusTypeNames()` collects every type NAME the tracked corpus declares, from
  the SAME `git ls-files` enumeration as the index, so the diagnostic can never
  see more of the tree than the index it reports on (6520 files, 0.6s).
- `NON_ENGINE_REASONS`: a closed set of seven. Five name a fact that DEFENDS the
  subtraction (`node:` builtin, locally constructed, language global, a type this
  corpus declares and the door rule rejected, an `UNTYPED_RECEIVERS` row); two
  admit the census could not place the type (`type-not-in-corpus`,
  `anonymous-type`).
- The undefended ones are printed per site on every run, carried per site in
  `--json`, and counted under ENFORCEMENT in both artefacts, with the rows in the
  audit ledger. A type that leaves the index now lands in the diff by name.
- `typeTextDeclaresEngineDoor()` reuses `memberIsEngineDoor`, so an inline
  receiver type that states a write door says so. Two sites on this tree do.

No verdict, count or exit code moves: 225 sites, 146 subtracted, 0 unresolved,
before and after. No subtraction is withdrawn -- it is declared.

The self-test pins the criterion in BOTH directions: one receiver, one source
text, index membership the only variable, reading `engine` in the index and a
subtraction that NAMES the unplaceable type out of it (30 -> 44 cases).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Sep 18, 2026
@os-elon-musk os-elon-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 18, 2026 — with Claude
@os-elon-musk
os-elon-musk marked this pull request as ready for review September 18, 2026 07:17
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit 512191e Sep 18, 2026
34 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-18548-census-unresolved-type-diagnostic branch September 18, 2026 08:11
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…he tree still shows (objectstack-ai#19075)

Fixes objectstack-ai#19007

Clause-②: no

Three same-cause prose statements were left behind when objectstack-ai#13585 loosened
owner resolution.
Two of them are replaced here with statements this tree still shows; the
third is left
alone, because it is the evidence rather than the defect. **Prose only —
no behaviour
change**, and ⛔ no `merge=os-regen` registration is added, ⛔ no
`.gitattributes` row is
touched.

## 1. `scripts/tenant-audit-census.mjs` — the reason emitted INTO the
generated counts file

Before (verbatim, as `renderCountsFile()` emitted it):

```
merge cleanly and WRONG. This file is deliberately NOT `merge=os-regen`: that
driver resolves an artefact's `gen:`/`check:` scripts in `@objectstack/spec`
only, and these are root-level tooling. The gate is the backstop — a wrongly
```

After:

```
merge cleanly and WRONG. This file is NOT `merge=os-regen`: no `.gitattributes`
row names it, so `git check-attr merge` over it reads `unspecified`. Routing it
would take a `REGEN_ARTIFACTS` row whose `gen:`/`check:` names exist in the
manifest that row declares as owner, and no manifest declares such a pair for
this census — the gate runs straight from the lint workflow. Root-level tooling
is no obstacle by itself: the driver resolves those names in whichever manifest
the row names, the root one included. The gate is the backstop — a wrongly
```

The FACT is kept (this file is not routed). The REASON is replaced, and
every clause of
the new one is measurable on this tree:

| new clause | how it was measured | reading |
| :--- | :--- | ---: |
| no `.gitattributes` row names it | `grep -n tenant-audit
.gitattributes` | 0 hits |
| `git check-attr merge` reads `unspecified` | `git check-attr merge --
docs/audits/2026-08-tenant-audit-write-call-sites.counts.md` |
`unspecified` |
| a row needs `gen:`/`check:` in its owner's manifest |
`reconcileScripts()` fails a row whose names are missing — fired below |
red on demand |
| no manifest declares such a pair for this census | every tracked
`package.json`, `scripts` keys AND values | 0 of 601 entries in 83
manifests |
| the gate runs straight from the lint workflow |
`.github/workflows/lint.yml`, step "Tenant-audit census matches the
tree" | `node scripts/check-tenant-audit-census.mjs` |
| root-level tooling is no obstacle | `REGEN_ARTIFACTS` rows declaring
`owner: ROOT_OWNER`, and what git says about them | 2 rows, both `merge:
os-regen` |

Fire control for the 0-hit row: the same instrument searching for
`strictness-ledger` —
the neighbouring `.counts.md` that IS registered — returns 2 hits
(`gen:` and `check:`
in `packages/spec`). Reachable radius: manifest `scripts` text. A known
target outside
it: a manifest script that reaches the census through an indirection
that never spells
its name (`"gen:x": "node scripts/wrapper.mjs"` where the wrapper spawns
it) — this
instrument reads manifests, it never follows a spawn.

## 2. `scripts/git-merge-regen.mjs` — "every row is spec-owned"

Before (verbatim):

```
 * `reconcileScripts` above is green on this tree for the same reason it was green
 * before objectstack-ai#13585: every row is spec-owned, so it exercises exactly one manifest and
 * would keep passing if the loosening were reverted.
```

After (verbatim, plus the paragraph break the replacement adds):

```
 * `reconcileScripts` above no longer rests on a single manifest. Rows that declare
 * no owner default to `DEFAULT_OWNER`; rows that declare `ROOT_OWNER` send the
 * lookup to the root manifest instead (measured on this tree: 18 rows — 16
 * defaulted, 2 root-owned, none naming `DEFAULT_OWNER` explicitly). The four
 * `gen:`/`check:` names those two root-owned rows carry exist ONLY in the root
 * manifest, so reverting objectstack-ai#13585's loosening would now turn that reconciliation RED
 * rather than leave it green.
 *
 * What a live tree still cannot show is the RULE. Which manifests get exercised is
 * a property of whatever the table happens to hold, and a table that fell back to a
 * single owner would stop touching the root path with nothing saying so.
```

What the paragraph exists to explain — why the pinned owner-resolution
cases are needed
even though the live tree is green — is kept, and now rests on something
true: the
composition that exercises the root path is a property of today's table,
not of the rule.

## 3. `scripts/git-merge-regen.mjs:769-777` — deliberately NOT touched

That block quotes objectstack-ai#13585 correctly. It is the evidence the two
statements above drifted
away from, and the diff contains zero lines from it.

## The registry re-count, taken from the registry's own data structure

The card reported `4 ROOT_OWNER + 19 named + 41 default = 64`.
Re-counted by importing
`scripts/regen-artifacts.mjs` and bucketing with the module's own
`ownerOf(entry) = entry.owner ?? DEFAULT_OWNER`, at merge base
`0ec81857a`:

| table | rows | `owner: ROOT_OWNER` | named package owner | no owner,
defaults to `DEFAULT_OWNER` |
| :--- | ---: | ---: | ---: | ---: |
| `REGEN_ARTIFACTS` (registered; what `reconcileScripts` iterates) | 18
| 2 | 0 | 16 |
| `NOT_DRIVER_MANAGED` (deliberately not registered) | 33 | 2 | 19 | 12
|
| both tables | 51 | 4 | 19 | 28 |

Each row reconciles: 2+0+16 = 18, 2+19+12 = 33, 4+19+28 = 51.

Three consequences:

1. The card's `4 + 19` is a BOTH-TABLES count. The registration table
declares
   `ROOT_OWNER` twice, and both of those rows really are routed
   (`content/docs/permissions/system-context.mdx` and
`scripts/platform-object-tenancy-census.json`, each `merge: os-regen`).
The card's
other two `ROOT_OWNER` rows are `NOT_DRIVER_MANAGED` entries — reading
`unspecified`
   is those rows working as recorded, not a failure to route.
2. `41`, and the `64` total it implies, reproduce under NEITHER
instrument: structurally
the defaulted bucket is 16 (registration table) or 28 (both tables),
against totals of
   18 and 51.
3. `reconcileScripts` itself prints the refutation of "exercises exactly
one manifest":
`all 36 gen:/check: names resolve in their declared owner
(@objectstack/spec
(packages/spec/package.json), @objectstack/spec-monorepo
(package.json))` — 36 = 18
   rows x 2 names, across TWO manifests.

## Evidence

**Behaviour leg — the generated artefact changes in the prose and
nowhere else.**
`node scripts/tenant-audit-census.mjs --write`, then both the committed
and the
regenerated counts file put through the gate's own `maskUnenforced()`
(the mask
`scripts/check-tenant-audit-census.mjs` applies to both sides of its
byte comparison):
the masked diff is exactly the replaced paragraph, 3 lines out, 7 lines
in, nothing else.

The raw diff carries one more line, the `Measured on ... at ...` marker,
in the counts
file and in the census page. That marker is **not** attributable to this
edit: a
regeneration on the PRISTINE tree, with zero source edits, already moved
exactly those
two lines and nothing else. The gate masks its value deliberately
(`UNENFORCED_MEASURED_AT`: "The date and the ref are deliberately NOT
compared to
anything"), its self-test pins "a stale measurement date passes", and
the last commit to
touch these artefacts (objectstack-ai#18943) carried the same marker bump. Both
artefacts are committed
as the generator wrote them.

**`reconcileScripts` leg, with a fire control.** The card said it had
not been measured
whether that reconciliation is still green under today's owner
distribution.

- `node scripts/git-merge-regen.mjs --self-test` → exit 0, including
  `✓ all 36 gen:/check: names resolve in their declared owner`.
  Its CI spelling `pnpm check:merge-driver` → exit 0.
- Fire control (a green that can go red, and proof the ROOT manifest is
really read):
through `scripts/ablation-replace.mjs`, the root manifest's
`gen:system-context-census`
  key was renamed. The anchor hit once, the blob moved
(`57ab936beaf0` to `1074c54635`), and the self-test went red naming the
row:
`content/docs/permissions/system-context.mdx → gen:system-context-census
[owner
@objectstack/spec-monorepo, package.json]`. A spec-only resolution could
not produce
that failure — which is the measured form of "reverting the loosening
would now turn it
  red".
- Restored and proven restored: blob back to `57ab936beaf0` = the HEAD
blob,
  `git diff HEAD` empty, `git status --porcelain` empty.

**Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`
derived 64 families from this tree; all 64 were run with the exit code
captured before any
pipe, and `--ran` reconciles: `64 derived, 64 run, 0 NOT-MEASURED, 0
UNRUN`, every one
exit 0. Five of them first exited on a PREREQUISITE NOT MET (unbuilt
`@objectstack/lint`,
`@objectstack/formula`, `@objectstack/client-react`, and `gen:schema`);
the closures were
built and all five then passed — none was a finding. `pnpm lint` over
the whole repo:
exit 0 in 1m27s. `pnpm check:nul-bytes`: exit 0, plus a direct
control-character scan of
both edited files: 0 hits.

**Changeset.** Measured rather than assumed: each of the 4 changed paths
against every
tracked manifest's `files[]`. The root manifest is `private: true`, and
all four paths lie
outside every publishable package directory, so nothing already
published moves — hence
`skip-changeset`. Fire control: the same instrument scores
`packages/spec/src/index.ts` as
inside a published surface.

## Acceptance notes

Observed while working, out of scope here, not filed:

- The docblock this PR edits sits immediately above ANOTHER docblock, so
it documents no
declaration; the pinned cases it describes (`reconcileOwnership`) are
about 330 lines
below. `pnpm check:scripts-symbol-anchors` is green on it, so the repo's
own instrument
does not read this as a violation. Successor: the next PR editing these
self-test blocks.
- The `NOT_DRIVER_MANAGED` disposition for `docs/audits/**` explains
itself in terms of
hand-written ledgers and names the strictness `.counts.md` as "the ONE
exception", while
a second generated `.counts.md` — this census's — sits under the same
prefix. Whether
that one ought to route is the question objectstack-ai#18960 deliberately left open,
so this is that
  neighbour, not a new card.
- No manifest script names this census, so its generator is invisible to
the generator
disposition reconciliation — a bound that reconciliation states about
itself in its own
docblock, and `pnpm check:ratchet-remedy-authority` (which owns that
population) is
  green.

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

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ine type literals (225 -> 227) (objectstack-ai#19073)

Fixes objectstack-ai#18944

Clause-②: no

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

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

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

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

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

Controls, because a "no" needs one:

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

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

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

## The fix

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

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

## Before / after, with the ablation proof

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

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

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

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

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

## Both artefacts AND the prose

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

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

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

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

Two things stated rather than rewritten on a guess:

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

## Verification

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

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

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

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

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

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

## Changeset

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

## Acceptance notes

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants