Skip to content

Commit 67e8792

Browse files
feat(plugin-audit): sys_comment gains a writer-stamped object_name + record_id pair with a backfill, and the comment-thread read gate leafs over it (ADR-0139 E7) (#23881)
Fixes #23857 Clause-②: yes ADR-0139 E7, comments half, by the ruling on #23732 (record `6212058353`, batch #354 item 2: letter A on Q1, A on Q2, maintainer 「同意」). `sys_comment` gains a writer-stamped parent pair, and the comment-thread read gate narrows by it with one `ReadableIds` leaf per parent object, with no pre-scan. #23732 remains open; the seat handles it as the ruling's item 6 says. ## Rollout (the changeset's first paragraph) **Run `os migrate comment-parent-pair --apply` at once after upgrading.** Until it runs, the comments written before the upgrade carry no pair, and the gate answers them by the bounded thread probe it always had: at most 2,000 candidate rows per read, fail closed. So a broad comment list (one not scoped to a single record's thread) can omit comments the reader may see, and its `total` agrees with the short list. A record's own thread is unaffected. The door copies `os migrate activity-changed-fields` (E6, PR #23814) one for one: `--database-url`, `--apply`, `--yes`, `--force`, `--json`; dry run by default; idempotent. Same measured difference as E6: the columns arrive with the upgraded deployment's schema sync, which `--apply` performs itself, so a dry run against a database no upgraded process has opened cannot read them and exits 1. ## What changed - **The pair** (`objects/sys-comment.object.ts`). `object_name` and `record_id` are `Field.text`, `readonly`, not required, and indexed together as `(object_name, record_id)`. `thread_id` stays the authored key. There is no ADR-0087 conversion and no writer change. - **One planner**, `planCommentParentPair` (`comment-access-hooks.ts`). It parses the thread with `parseCommentThreadId`, the parse every comment gate authorizes by. A thread that names no record gets `null` / `null`. - **Who can write the pair:** - Create: a `beforeInsert` hook stamps every row, for every caller, the system included, and overwrites any value the writer sent. The engine's create-side readonly strip skips `sys_` objects, so this hook is the guard, as it is for `is_edited`. - The re-point rule: an update that names `thread_id` is re-stamped from it. - An update that names either column without a thread: the update stamp re-derives the pair from the row's stored thread, for every caller, and drops it when there is no stored row. It does not rely on the engine's strip, which under `preserveAudit` (the REST import's `treatAsHistorical`) keeps a readonly value. A multi-row update of this kind over rows whose threads name different records is refused whole by the stamp itself, not by the engine's key-set divergence check (`VALIDATION_ERROR` / 400, nothing written), because one `SET` clause cannot carry two pairs. Over rows sharing one thread, it stores that thread's pair. Pinned on a real engine and through the REST import (patch rounds 1–2, addenda `6216192076` and `6217048956`). - The backfill's SYSTEM by-id update: the stamp re-derives its pair from the thread the row holds at write time. That narrows the backfill's race to the one engine call; it does not close it. - Both hooks stamp only when the registered `sys_comment` declares the pair. This is E6's `declaresActivityStampColumns` precedent, for harnesses that register their own `sys_comment`. - **The gate** (`installCommentReadVisibility`). It is now `computeParentRecordFilter` from `parent-record-read-gate.ts`. That function is imported, not edited, so no third copy of the D5 classification is written. It runs over the comment's pair with a `commentThreadGate()` config: - The parent set is every registered object except `sys_comment`. All of `AUDIT_EXCLUDED_OBJECTS` is named, because a comment can hang off any record. This is the same set `parseCommentThreadId` accepts. - D5's three classes are routed to the bounded probe by name and up front, never by catching a refusal. So are the two whole-read states: no `canReadObject` (no leaf is written without it) and an engine that cannot classify. - Rows with no stamped pair get one more branch, ORed in: the old bounded thread probe, restricted to `object_name IS NULL`. Its branch carries that conjunct, so it never selects a stamped row. Rows the backfill has not reached are probed, never leafed. - A `sys_comment` that declares no pair keeps the thread probe over every row, and the gate says so once. - `next()` sits outside the gate's `try`, so a composition refusal (on `driver-mongodb`, a 500) is the read's own failure (D3). - `AuditPlugin` passes the security source. - **Reactions are unchanged** (Q2, D5.2). They are recorded on #23857 with the reading member 1,763 of 2,300 and administrator `total` 2,000 of 2,601. - **The backfill**, `backfillCommentParentPair` (`comment-parent-pair-backfill.ts`, exported from `src/index.ts`): - It works in E6's shape: a SYSTEM keyset walk on `object_name $null`, then a single-id SYSTEM update of both columns. - It updates under `preserveAudit` with the row's stored `updated_at`, so no comment's `updated_at` moves. Measured through the door. - A thread that names no record is counted as `unparented` and left unstamped. - A refused page read is counted, so the door exits 1. - **The door** `os migrate comment-parent-pair`, registered in the one-shot family table, the JSON-purity family and the absent-database roster. - **Docs and generated files:** - `deployment/cli.mdx`: a section and a table row. - `platform-checklist/areas/cli.json`: 17 → 18 subcommands. - The plugin-audit i18n bundles, regenerated and translated. - The tenant-audit census, regenerated: 226 → 227 write sites, from the backfill's update. The hand-written prose figures follow. - The checklist item `attachments-storage.read-inherits-parent-rls`, revision 5, carries the two notes from `6206614195`. Landing places: everything is in `plugin-audit`, the CLI door and its three family registrations, dogfood, docs, and the generated files the gates regenerate. None of `writeAudit`, `packages/spec`, `objectql`, `core`, any driver, `parent-record-read-gate.ts`, `migrate/organization-ownership*`, `apply.ts`, `docs/adr/**` or objectui is touched. ## Evidence Local readings at head `e499adaf6` (with `origin/main` `34b740f2d` merged in), on the shared box, under `scripts/pm/os-verify-lock.sh`. Patch rounds 1–2 (to `7d41db652`) change only the update stamp, docblocks, the changeset and tests. The read gate is untouched. The dev re-read these numbers at `7d41db652` (supplement `6217097662`), and they are unchanged: real engine member 2,303 of 2,303 and administrator `total` 2,605 of 2,605; the stack served 2,050 of 2,050. The rounds' own gates, suites and ablations are in addenda `6216192076` and `6217048956`. - `@objectstack/plugin-audit`: tests 53 files / 863 passed; `typecheck` exit 0 (test-typecheck ledger OK). - `@objectstack/cli`: unit project 301 files / 4,496 passed; `typecheck` exit 0. - CLI integration, run locally because the diff touches it: - the new door suite plus `data-commands.absent-database` and `schema-migrate.one-shot-family`: 147 / 147; - `json-stdout-purity.e2e` (nightly tier): 62 / 62. - `@objectstack/dogfood` typecheck exit 0. Dogfood: the new pin plus the eight suites that read `sys_comment` or share the gate (the E6 pin among them): 9 files / 79 passed. - **The bound case, before and after.** "Before" is ablation A0 below: main's whole-read thread probe put back. | reading | before | after | |:--|--:|--:| | real engine + `AuditPlugin` + SQLite (`comment-parent-pair.integration`, 2,607 rows): member served / total, of 2,303 visible | 1,766 / 1,766 | 2,303 / 2,303 | | same: administrator `total`, of 2,605 | 2,000 | 2,605 | | real `SecurityPlugin` + `AuditPlugin` stack at the HTTP door (dogfood, 4,151 comments, one thread each): member served, of 2,050 visible | 981 | 2,050 | The administrator's newest-20 read is served 20 rows, with a total past 2 × 2,000. An unreadable parent's comments stay hidden: another member's record, a vault object, a deleted record, and a thread naming no record. The thread-scoped read of one of them answers 0 rows and `total` 0. - **No SYSTEM-composed read or leaf.** On the real stack, no SYSTEM read of `sys_comment` carries a leaf. The only SYSTEM reads are the two bounded probes: `thread_id` over `object_name $null`, and the D5 pair probe. **Ablations.** Each negative pin was run red, then restored. Every mutation went through `scripts/ablation-replace.mjs`, and every restore was proven blob == HEAD with `git diff HEAD` empty. Unless noted, the pins are the two plugin-audit suites, run from `src`. | leg | mutation | red | |:--|:--|:--| | A0 | main's whole-read thread probe put back | src 7 of 17. Dogfood through `dist`: marker present (`ablation-dist-preflight` exit 0), 4 of 5 red. Restore rebuilt and the marker proven absent | | A1 | the stamped branch admits every stamped row | 11 | | A2 | unstamped rows not probed | 5 | | A3 | the unstamped probe's branch loses its `object_name IS NULL` conjunct | 2 | | A4 | a leaf written without `canReadObject` | 1 | | A5 | the consumer catches the read's refusal into deny-all | 2 (unit and real engine) | | A6 | the stamp keeps a writer's own pair | 7 | | A7 | the re-point not re-stamped | 2 | | A8b | a D5.2 middleware-gated parent leafed (temporary mutation of `planParentReads`, not committed) | 9 | | A9 | a delegated caller leafed (same file, not committed) | 2 | | A10b | a refused page read not counted | 1 | | A11 | a thread naming no record stamped | 7 | | A12 | an update naming the pair without a thread not re-derived (patch round 1) | src 3. Dogfood through `dist`: marker present (`ablation-dist-preflight` exit 0), 2 red. Restore rebuilt and the marker proven absent | | A13 | with no stored row, the writer's pair kept (patch round 1) | 1 | | A14 | the multi-row pair-divergence refusal removed (patch round 2) | 2 (real engine and unit) | Void legs, declared: - The first A0 leg was a no-op: its replacement contained the anchor, the tool refused it, nothing landed, and its readings are discarded. - A8 and A10 were refused the same way first (the replacement text already occurred), and were rerun as A8b and A10b. - A4, A5 and A9 were rerun after a test-only restructure of the unit stand-in, with the same results. **Gates.** - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 103 commands at `e499adaf6`: 23 paths, 2,276 changed lines, under the 3,000 threshold. All 103 exit 0. `--ran` reports 「103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN」. - `pnpm check:adr-symbol-anchors` exits 0. ADR-0139's anchors `comment-access-hooks.ts#READ_SCAN_LIMIT` and `#installCommentReadVisibility` still resolve. - The first union had three real reds, all fixed: - the census prose figures; - `check:engine-double-contract` and `check:objectql-double-limit`, both on my unit stand-in. The stand-in was restructured; no ledger was touched. - Three exit-3 prerequisites (missing `dist`, a shallow clone) re-ran green after a build and a fetch. ## Acceptance notes - **No redaction and no query guard over the pair.** The pair is a parse of `thread_id`, which every reader of a row is already served and may already filter by, so it discloses nothing new. The pair gets no entry in `parent-field-query-guard.ts`; that guard covers value-bearing columns of the activity stream and the ledger, and `sys_comment` has no such column. - **A wider read.** An unpinned comment read builds one branch per registered object: E5's activity cost, now on comments too. Every comment read also runs one SYSTEM probe over `object_name IS NULL`. After the backfill that probe finds nothing; until then it finds at most 2,000 rows. - **Unparented threads.** A comment whose thread names no record keeps `null` / `null` forever, so the unstamped probe always scans it. It stays hidden, as before. - **The comments checklist item.** The card asks for "the comments item's revision" to carry the same two notes, but no checklist item covers comment read inheritance: the comment-related items in `records-forms.json` are about feeds and posting. So only the attachments item was revised, and no item was invented. - **Stale prose outside this card's surface.** `parent-record-read-gate.ts`'s `no-object-admission` warning still cites the activity stream's withheld-update re-read, and the comment gate now prints it too. The file is #22788's, so it is not touched here. Carrier: #22788. _Seat edit · `domain:services` seat 2 · `session_018VqJPGA1NDNSCZsnecsChP` · 2026-10-16T13:10Z: "Who can write the pair" bullets 3–4 and ablations A12–A13, per patch round 1 (addendum `6216192076`)._ _Seat edit · `domain:services` seat 2 · `session_018VqJPGA1NDNSCZsnecsChP` · 2026-10-16T13:56Z: bullets 3–4 again, ablation A14 and the Evidence scope line, per patch round 2 (addendum `6217048956`)._ _Seat edit · `domain:services` seat 2 · `session_018VqJPGA1NDNSCZsnecsChP` · 2026-10-16T13:58Z: the Evidence re-read at `7d41db652` and bullet 3's refusal source, per supplement `6217097662`._ --- _Generated by [Claude Code](https://claude.ai/code/session_01NX4Woa8XhvMs6xpqQpx8kK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e9301e8 commit 67e8792

23 files changed

Lines changed: 2431 additions & 50 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/plugin-audit': minor
3+
'@objectstack/cli': minor
4+
---
5+
6+
feat(plugin-audit): `sys_comment` rows carry a writer-stamped parent pair, `object_name` + `record_id`, and the comment-thread read gate narrows by it with one `ReadableIds` leaf per parent object, with no pre-scan (ADR-0139 E7)
7+
8+
**Run `os migrate comment-parent-pair --apply` at once after upgrading.** After the upgrade, and until it runs, the `sys_comment` rows written before the upgrade carry no pair, and the read gate answers them as it answered every comment before: by a SYSTEM pre-scan of at most 2,000 candidate rows per read, fail closed. So a broad comment list — one not scoped to a single record's thread — can omit comments the reader may see, and its `total` agrees with the short list; a record's own comment thread is unaffected. The door is `os migrate comment-parent-pair`, dry run by default, `--apply` to write, idempotent, and it stamps both columns in one run, keeping each row's `updated_at`. Its flags are `os migrate activity-changed-fields`'s, one for one (`--database-url`, `--apply`, `--yes`, `--force`, `--json`). The columns arrive with the upgraded deployment's schema sync, which an `--apply` run performs itself; a dry run against a database no upgraded process has opened cannot read them and exits `1`.
9+
10+
Clause-②: yes
11+
12+
`sys_comment` gains two `readonly` text columns and an index over them. They are derived from `thread_id` (`{object}:{record_id}`, split on the first colon) by one planner, `planCommentParentPair`, and `thread_id` stays the key authors write: nothing changes for a writer. A `sys_comment` before-insert hook stamps every inserted row, whoever writes it, over whatever value the writer sent; an update that names `thread_id` (a re-point) is re-stamped from it; and an update that names either column without a thread has the pair re-derived from the row's stored thread, for every caller — a historical import (`preserveAudit`) included, where the engine's update-side `readonly` strip keeps a value — so no writer's value for either column lands. A multi-row update that names either column without a thread is refused whole (`VALIDATION_ERROR` / 400, nothing written) when its rows' threads name different records, since one write cannot store each row's own pair; over rows sharing one thread it stores that thread's pair. A thread that names no record stamps `null` / `null`, and such a comment stays hidden from every non-system reader, as before.
13+
14+
The comment-thread read gate (`installCommentReadVisibility`) is now the parent-record gate the activity stream and the compliance ledger use, over the comment's own pair: one branch `{ object_name: P, record_id: { $in: ReadableIds.of(P) } }` per registered object other than `sys_comment`, ORed and ANDed into the read's `where`, on `find`, `findOne`, `count` and `aggregate`. A broad comment read is served every comment the reader may read, on any table size, with a true `total`. What keeps the bounded 2,000-row fail-closed probe, chosen up front by name, never by catching a refusal:
15+
16+
- ADR-0139 D5's three classes: a delegated (on-behalf-of) caller (the whole read); a parent narrowed by an object-scoped read middleware (`sys_attachment`, `sys_comment_reaction`, `sys_approval_request`, …); a parent bound to another datasource;
17+
- a security service that answers no `canReadObject`, or none at all (no leaf is written without it), and an engine that cannot classify a parent — each sends the whole read to the probe and is said once;
18+
- a row with no stamped pair (until the backfill reaches it): probed by its `thread_id`, restricted to rows with no pair, never leafed;
19+
- a composition whose registered `sys_comment` declares no pair: the thread probe over every row, said once.
20+
21+
**The cost:** a deployment whose driver compiles no `ReadableIds` leaf — `driver-mongodb`, until ADR-0139's O3 — now fails a non-system comment read with `INTERNAL_ERROR` / 500 naming the driver in the log, as the activity, ledger and attachment reads already do (ADR-0139 D3); the gate does not catch that refusal into an empty list.
22+
23+
Comment reactions are unchanged: their only parent is `sys_comment`, which this gate narrows by an object-scoped middleware, so the reaction gate keeps the bounded probe (ADR-0139 D5.2).
24+
25+
`installCommentReadVisibility` takes an optional third argument, the security source whose `canReadObject` admits a leaf (`ParentObjectReadAdmissionSource`); without it every comment read is the bounded probe, as before. Its first parameter widens from `CommentAccessEngine` to `ParentRecordGateEngine` (`CommentAccessEngine` plus three optional members: a registry view, `hasObjectMiddleware` and `resolveEffectiveDatasource`); every existing caller's engine still satisfies it. Both types reach the published declaration through that signature. `CommentAccessEngine` gains an optional `getSchema`. `@objectstack/plugin-audit` exports the backfill, `backfillCommentParentPair` (with `COMMENT_PARENT_PAIR_BACKFILL_PAGE_SIZE` and its report and engine types), the planner `planCommentParentPair` (with `COMMENT_PARENT_OBJECT_COLUMN`, `COMMENT_PARENT_RECORD_COLUMN` and the `CommentParentPair` type).

‎content/docs/deployment/cli.mdx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,7 @@ where the data lives.
11941194
| `os migrate comment-reactions` | Carry the emoji reactions a 17.x deployment stored in the retired `sys_comment.reactions` column into `sys_comment_reaction` records, list every stored value that does not read, and record the verified run that lets `os migrate apply --allow-destructive` drop the column |
11951195
| `os migrate organization-ownership` | The v18 ownership ceremony (ADR-0131 D10): write a plan of what each table's rows will become, then `--apply` that reviewed plan. A v18 server refuses to start until it has run. See [below](#os-migrate-organization-ownership) |
11961196
| `os migrate activity-changed-fields` | Stamp the two read-rule columns (`changed_fields`, `milestone_key`) on the `sys_activity` rows written before they existed. Until it runs, those rows are withheld from every reader but the system, so run it at once after upgrading. No flag; idempotent |
1197+
| `os migrate comment-parent-pair` | Stamp the parent pair (`object_name`, `record_id`) on the `sys_comment` rows written before it existed. Until it runs, a broad comment read past 2,000 candidate rows omits comments the reader may see, and its total agrees with the short list, so run it at once after upgrading. No flag; idempotent |
11971198
| `os migrate meta --stored` | Replay the metadata conversion chain over this deployment's `sys_metadata` rows and rewrite the ones still carrying a pre-protocol shape. Hygiene, not a gate — nothing depends on it having run |
11981199
| `os migrate security-catalog-overlays` | List the environment-wide permission-set and position rows (the legacy plurals `permissions` / `positions` included) stored over a name a configured package holds — the rows a v18 cold boot refuses — each with the package that holds its name; `--apply` deletes them, one history tombstone per row. Run it before the first v18 boot, with the flags and environment the deployment boots with: the platform security plugin's sets are held only where `os serve` composes it (an auth secret set, and the `auth` tier on), and the step takes `serve`'s `--preset` and `--dev` with `serve`'s meaning. Adopts nothing; exits 1 while it lists a row |
11991200
| `os migrate duplicates` | Report business identifiers already minted twice across the organization partitions, and the rows blocking the boot-time NULL-safe index tightenings — a read-only inventory as JSON on stdout. Renumbers nothing and writes no row and no schema; run it before the boot-time tenancy repair, which overwrites part of the evidence |
@@ -1499,6 +1500,36 @@ os migrate organization-ownership --dev … # boot as `os ser
14991500
the security and audit plugins), the development `.env*` files, and the
15001501
`telemetry` sibling. Pass it for a database that `os dev` serves.
15011502
1503+
#### `os migrate comment-parent-pair`
1504+
1505+
A comment is served to a reader only when the reader can see the record its
1506+
thread names. Since this version that check reads two columns the platform
1507+
stamps on every new comment from its `thread_id`, `object_name` and
1508+
`record_id`, and narrows a read in one statement, on any number of comments.
1509+
Rows written before the upgrade carry no pair, and the platform answers them
1510+
the way it answered every comment before: it examines at most **2,000
1511+
candidate rows per read** and hides the rest (it never shows a comment the
1512+
reader may not see). So until this command runs, a broad comment list — one
1513+
not scoped to a single record — can omit comments the reader may see, and its
1514+
total agrees with the short list. A record's own comment thread is unaffected.
1515+
1516+
```bash
1517+
os migrate comment-parent-pair # Dry run: counts, writes no rows
1518+
os migrate comment-parent-pair --apply # Stamp the rows (prompts)
1519+
os migrate comment-parent-pair --apply --yes --json # CI / scripts
1520+
```
1521+
1522+
Each row is stamped from its own `thread_id`, by the rule the platform stamps
1523+
new comments with; the row's `updated_at` is kept. **A comment whose thread
1524+
names no record stays unstamped**, and stays hidden from every reader but the
1525+
system, as before. The report counts those rows.
1526+
1527+
**Idempotent**: only unstamped rows are read, so a second run stamps nothing,
1528+
and re-running is how an interrupted run is finished. The columns arrive with
1529+
the upgraded deployment's schema sync, which an `--apply` run performs itself:
1530+
a dry run against a database no upgraded process has opened cannot read them,
1531+
reports the failed read and exits `1`. A failed read or write exits `1`.
1532+
15021533
#### A database created by this version needs no migration
15031534
15041535
A deployment whose database the platform **creates from empty** records these

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122122

123123
The same holds twice over for the context. An options argument spelled as a
124124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125-
forwarding shim cannot, and **53 of the 226 sites are spelled that way**. A
125+
forwarding shim cannot, and **53 of the 227 sites are spelled that way**. A
126126
context resolved from an inline literal or a local `const` can be tested for
127127
`isSystem`; one arriving from a helper call cannot.
128128

@@ -187,10 +187,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
187187

188188
| carried figure | where it survives | this census |
189189
| :--- | :--- | ---: |
190-
| 175 write call sites | quoted in the merged changeset | **226** |
190+
| 175 write call sites | quoted in the merged changeset | **227** |
191191
| 24 carrying no tenant context | quoted in the merged changeset | **2** provable and tenancy-enabled; **32** more whose options argument is unreadable |
192-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **155 of 226** decidable, **71** undecidable |
193-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 122 decidably elevated, 0 decidably not, 96 undecidable |
192+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **156 of 227** decidable, **71** undecidable |
193+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 123 decidably elevated, 0 decidably not, 96 undecidable |
194194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195195

196196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -201,17 +201,17 @@ at any commit.
201201

202202
Two structural facts do plausibly widen this reading against any hand or regex
203203
one, and both are counted in the generated tables below: the 38 sites reached
204-
through an erased (`any`) receiver, and the 51 that name their object through a
204+
through an erased (`any`) receiver, and the 52 that name their object through a
205205
`const` rather than inline. An instrument that read either the way a person does
206206
would report a smaller number and would not say so.
207207

208208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209209
figure has no surviving corroboration anywhere in the tree.** This census reads
210-
122 of 226 (54%) as decidably elevated, with 96 more whose elevation is a
210+
123 of 227 (54%) as decidably elevated, with 96 more whose elevation is a
211211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212212
answer is that a static reading cannot settle it.
213213

214-
⇒ **Cite `2 / 226`, and say what it is**: the sites whose options argument was
214+
⇒ **Cite `2 / 227`, and say what it is**: the sites whose options argument was
215215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217217
without tenant context" — **32 further sites** have an options argument this
@@ -223,29 +223,29 @@ cannot read, and they are neither in nor out.
223223

224224
| what | count |
225225
| :--- | ---: |
226-
| write call sites on the application surface | **226** |
227-
| …whose object name is statically decidable | 155 |
226+
| write call sites on the application surface | **227** |
227+
| …whose object name is statically decidable | 156 |
228228
| …whose object name is chosen at run time | 71 |
229-
| …against an object with tenancy ENABLED | 154 |
229+
| …against an object with tenancy ENABLED | 155 |
230230
| …against an object that declares tenancy off | 1 |
231-
| threading a tenant context | 165 |
231+
| threading a tenant context | 166 |
232232
| PROVABLY carrying none (options read, no context key) | **8** |
233233
| …of those, against a decidably tenancy-enabled object | **2** |
234234
| options argument UNREADABLE — may or may not carry one | 53 |
235235
| …of those, against a decidably tenancy-enabled object | 32 |
236-
| threading a decidably ELEVATED (`isSystem`) context | 122 |
236+
| threading a decidably ELEVATED (`isSystem`) context | 123 |
237237
| threading a context that is decidably NOT elevated | 0 |
238238
| threading a context whose elevation is a run-time fact | 96 |
239239

240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
242-
| receiver carried a readable engine type | 188 |
242+
| receiver carried a readable engine type | 189 |
243243
| receiver erased, placed by the object NAME | 28 |
244244
| receiver erased, placed by an `object: string` PARAMETER | 5 |
245245
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
246246

247247
| object name spelled inline | 104 |
248-
| object name spelled through a `const` | 51 |
248+
| object name spelled through a `const` | 52 |
249249
| object name is an `object: string` parameter | 7 |
250250
| object name is some other run-time expression | 64 |
251251

@@ -297,12 +297,12 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-10-16 at `c53c594e7`.
300+
Measured on 2026-10-16 at `caebc6a7b`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |
304-
| tracked non-test sources scanned | 654 |
305-
| engine-shaped types recognised | 77 |
304+
| tracked non-test sources scanned | 655 |
305+
| engine-shaped types recognised | 78 |
306306
| declared objects in the registry | 116 |
307307
| same-named calls subtracted as non-engine | 165 |
308308

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
3333

3434
| Measure | Value |
3535
|---|---:|
36-
| Write call sites | 226 |
37-
| Object name statically decidable | 155 |
36+
| Write call sites | 227 |
37+
| Object name statically decidable | 156 |
3838
| Object name chosen at run time | 71 |
39-
| Against a tenancy-enabled object | 154 |
39+
| Against a tenancy-enabled object | 155 |
4040
| Against an object declaring tenancy off | 1 |
41-
| Threading a tenant context | 165 |
41+
| Threading a tenant context | 166 |
4242
| Provably carrying none | 8 |
4343
| …and decidably tenancy-enabled | 2 |
4444
| Options argument unreadable | 53 |
4545
| …and decidably tenancy-enabled | 32 |
46-
| Threading a decidably elevated context | 122 |
46+
| Threading a decidably elevated context | 123 |
4747
| Threading a decidably non-elevated context | 0 |
4848
| Threading a context of undecidable elevation | 96 |
4949

@@ -90,12 +90,12 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-10-16 at `c53c594e7`.
93+
Measured on 2026-10-16 at `caebc6a7b`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
97-
| tracked non-test sources scanned | 654 |
98-
| engine-shaped types recognised | 77 |
97+
| tracked non-test sources scanned | 655 |
98+
| engine-shaped types recognised | 78 |
9999
| declared objects in the registry | 116 |
100100
| same-named calls subtracted as non-engine | 165 |
101101

@@ -117,6 +117,7 @@ Measured on 2026-10-16 at `c53c594e7`.
117117
| `packages/plugins/plugin-approvals/src/backfill-platform-row-organizations.ts` | `update` | `objectPlan.object` | undecidable | context, elevation undecidable | 1 |
118118
| `packages/plugins/plugin-audit/src/activity-changed-fields-backfill.ts` | `update` | `sys_activity` | enabled | elevated | 1 |
119119
| `packages/plugins/plugin-audit/src/auth-event-audit.ts` | `insert` | `sys_audit_log` | enabled | options unreadable | 1 |
120+
| `packages/plugins/plugin-audit/src/comment-parent-pair-backfill.ts` | `update` | `sys_comment` | enabled | elevated | 1 |
120121
| `packages/plugins/plugin-audit/src/read-audit.ts` | `insert` | `sys_audit_log` | enabled | options unreadable | 1 |
121122
| `packages/plugins/plugin-audit/src/stored-metadata-body-migration.ts` | `update` | `object` | undecidable | elevated | 1 |
122123
| `packages/plugins/plugin-auth/src/admin-import-users.ts` | `insert` | `sys_audit_log` | enabled | options unreadable | 1 |

0 commit comments

Comments
 (0)