Skip to content

Commit 191e043

Browse files
feat(plugin-webhooks,runtime)!: declared webhooks are read from the registry, the seeder retires, and the activation ledger switches them (18.x step 1, 8b-0g′) (#24588)
Closes #24529 Clause-②: yes (narrowing) 18.x line, step 1 (8b-0g′). Ruling #24435 Q2 A (6259141885), under ruling records 6266750393 and 6267391172 on #15194. Stage detail: plan amendment 13 (6259616282) on #15212. **Merge gate:** this PR stays a draft until #24522's upgrade harness has merged; it then merges `main` and must be green on R1–R4. ## What changed **Declared webhooks are read from the registry.** `bootstrapDeclaredWebhooks` is deleted. A fresh boot writes zero `sys_webhook` rows for a declared webhook. ADR-0131 D2's sentence "No seeder writes them into any table" is now true of webhooks. - `plugin-webhooks/src/declared-webhooks.ts` (new). `DeclaredWebhookSource` reads the `webhook` registry items, falling back to the metadata service, as the seeder did. It parses each with `WebhookSchema` (an invalid item warns once per name) and resolves the activation-ledger switch. - `AutoEnqueuer` takes a `declaredWebhooks` reader. On every refresh it projects each declaration that is switched on into a cached subscription through `parseRow`, so the trigger drift-guard and the `webhook/without-triggers` refusal apply as they do to a row. The secret and headers come from the declaration in memory and are never stored. The subscription id and every delivery's `ref_id` are `declared:NAME`. - The plugin binds the reader as the `declaredWebhooks` service, on the data engine alone (no realtime needed). The dispatcher, the redelivery veto and the activation door share it. **The switch: regime C (ADR-0131 D6), as `sharing_rule` did in #24428.** - `sys_metadata_activation` rows of type `webhook` override the declared `isActive` in either direction. A webhook shipped inactive is switched on the same way. - The door is `runtime`'s catalog activation door, `POST /api/v1/security/_activation/webhook/:name`. `webhook` is admitted next to `sharing_rule`, and the two service-backed types now share one lookup table. The authority tiers are unchanged. The door answers `404` for an undeclared name, and `503` when no reader is bound or the read fails. - A ledger write refreshes the dispatcher in the same request, through a third realtime subscription on the ledger. - The redelivery veto judges a `declared:NAME` delivery by its declaration: not declared any more, or switched off, is refused. **The organization dimension (measured, then fixed in this PR).** On `main` (18.0, after 8b), the seeded `sys_webhook` row is stamped with the Default Organization. Measured on a fresh `main` showcase: `organization_id: org_…`. So once activated, it delivered under `single`, because record events there carry that organization too. An org-less declaration would have been refused by `admitsOrganization`, which is a silent loss. - Under a posture with no wall (the `tenancy` service, read as the sharing plugin reads it), a declared webhook is **deployment-wide**. It receives every organization's events, and each delivery is stamped with the event's organization, as the 18.0 seeded copy's deliveries were. - Under a wall, the org-less refusal (the 2026-09-07 ruling) stands. An absent or unreadable posture reads as walled. ## 18.0-row behaviour (expand first: nothing is moved or deleted) The 18.0 seeder's rows stay. A `sys_webhook` row holding a declared name is read the way the 18.0 seeder would have treated it on this boot (`classifyDeclaredNameHolder`). **Choice, measured: the seeded row is recognised and ignored in favour of the registry entry when it is pristine, and the registry entry defers to it when it is not.** | row | 18.0 seeder did | this release | |---|---|---| | pristine mirror (`managed_by: 'package'`, not `customized`) | overwrote it from the declaration every boot | ignored; the declaration answers. One delivery, never two. It follows the declaration's URL and switch. | | `customized: true` (an admin edited the seeded row) | never touched it | answers as itself; the declaration is withheld (`warn` once per name). An admin's `active: false` keeps holding. | | `managed_by: 'admin'` under a declared name | skipped with a warning | answers as itself; the declaration is withheld. | | pristine mirror whose declaration is gone | left it dispatching | keeps dispatching. | | an old delivery of a pristine mirror, redelivered | judged by the row | judged by the declaration that now answers for it | The 18.0-shaped fixture is `seeded18_0Row` in `declared-webhooks.test.ts`, byte-for-byte the seeder's `mapWebhookToRow` plus provenance. `webhook-secret-at-rest.test.ts`'s `seed18_0Rows` writes the same rows through the real `ObjectQL` engine, so both credentials land encrypted as they did. ## Ceremony debt (step 5), table by table - **`sys_webhook`:** delete the pristine declared mirrors (`managed_by` in `package`/`platform`, `customized` not true, name still declared). Settle the customized seeded rows, as the sharing-rule ceremony settles its customised copies: a copy under a new name plus the declaration switched off. Then turn the `organization-ownership-inventory.ts` row's `declared` category into a `mirror-deletion` departure (ADR-0131 D10 fate 2). That edit is ceremony behaviour (the apply executes departures), so this PR does not make it. - **`sys_http_delivery`:** re-source the deleted mirrors' tails from `ref_id = MIRROR_ROW_ID` to `declared:NAME`, so history and redelivery follow the declaration. - **`sys_secret`:** delete the cipher rows the deleted mirrors' `signing_secret` / `headers_secret` referenced. They become orphans. - **`sys_webhook` columns `managed_by` / `customized`:** the mirror-row edit path "goes with the mirrors" (Q2 A). Once no `package` row exists, `customized` and the `package`/`platform` values of `managed_by` describe nothing. Their help text still mentions boot seeding (left so the i18n bundles are not churned here). - **Code owed at step 7:** the dual read (`classifyDeclaredNameHolder`, the mirror skip, the guard's mirror branch), the provenance stamp, and `migrateLegacyWebhookSecrets`'s package-row half. ## Tests Run at head `6b3042a8` (the final commit before this body). - `@objectstack/plugin-webhooks`: 19 files, 212 tests, pass. `typecheck` exit 0 (`check:test-typecheck` OK). - `declared-webhooks.test.ts` (new, 24): the fresh-install shape, including a plugin boot that records zero `sys_webhook` writes; the ledger switch both ways; the ledger self-heal; a failed ledger read; the posture dimension; the 18.0 dual read (seven cases); the redelivery veto. - `webhook-secret-at-rest.test.ts`: a declared webhook signs byte-identically with no row and no cipher stored, and needs no CryptoProvider. The at-rest, sweep and fail-closed pins now run over 18.0-seeded rows. - Removed with the seeder: the four re-seed / rotate-in-code tests, `bootstrap-declared-webhooks.test.ts`, and the gate test of the seeder's own write path. - `@objectstack/runtime`: full suite, 422 files, 6652 passed, 19 skipped. `typecheck` exit 0. `catalog-activation-door.test.ts` gains 7 `webhook` pins. - Dogfood ADR-0054 proof `webhook-materialization.dogfood.test.ts`: 1/1 pass. It now asserts no `sys_webhook` row, plus a dispatch through the bound `declaredWebhooks` service with `object`→`object_name` applied. - `check:liveness` passes. The ledger is re-anchored to the registry read. `label` and `description` are now `dead`: their only reader was the seeded Setup row. **Ablations** (`scripts/ablation-replace.mjs`; each leg restored, blob == HEAD, `git diff HEAD` empty; the tests import `src/` relatively): | ablated | red | |---|---| | the mirror skip in `doRefresh` | 3 | | the withheld-declaration rule | 3 | | the ledger override in `DeclaredWebhookSource.list` | 6 | | the ledger self-heal handler | 1 | | `deploymentWide` in `admitsOrganization` | 2 | | the plugin's posture read | 1 | | the guard's `declared:` branch | 2 | | the guard's pristine-mirror branch | 1 | | the door's `declaredWebhooks` lookup | 5 | **Fresh showcase boot** at `6b3042a8`, `single`: - `GET /data/sys_webhook` returns 0 rows. - `POST /security/_activation/webhook/showcase_task_changed {enabled:true}` returns 200, and one ledger row (`package_id: com.example.showcase`). - An undeclared name returns 404. - A task create gives one `sys_http_delivery` row with `ref_id: declared:showcase_task_changed`, stamped with the Default Organization. - Switched off, a second create adds no delivery. The same steps on a fresh `main` showcase gave the seeded row (`organization_id` stamped) and its delivery (`ref_id: whk_…`, same organization). ## Gates Run locally at `6b3042a8`. The commands are the 130 that `dispatch-gates --commands` derived, plus the 50-family artifact-roster block, 178 in all. Exit codes were captured to a file before any pipe. - **173 exited 0.** This includes the families this diff touched: `check:liveness`, `check:tenant-audit-census`, `check:engine-double-contract`, `check:objectql-double-limit`, `check:where-matcher`, `check:adr-0087-registration`, `check:changeset-no-major`, `check:platform-checklist`, and the spec `check:generated` set. - `--ran` reports 130 derived and 130 run, with 0 NOT-MEASURED. - **NOT MEASURED, five:** - `check-closing-target-claim`, `check-single-claim-paths` and `check-governed-queue-guard` need PR or event context, which a local run does not have. CI runs them. - `check-partof-closing-keyword` needs the same. Run with `PR_BODY` set to this body, it is reported in the os-dev report on #24529. - `check-plugin-teardown-shape --self-test` needs the unshallowed history its fixture is pinned to. It is checker health only, not a PR verdict. ## Acceptance notes - **Files outside the claimed surface.** Each is a consequence of deleting the seeder, and none is #24522's harness, an ADR or a releases page: - `packages/spec`: liveness ledger rows, comments, and the `WebhookSchema` doc block; - `packages/qa/dogfood`: the webhook proof and its fixture (not #24522's harness); - `docs/qa/platform-checklist` (revision 8); - `content/docs/automation/webhooks.mdx`, plus `tenant-audit-census.mdx` and its counts file (regenerated, 231 to 229: the seeder's two writes); - the showcase docblock; - four `scripts/*` ledgers, re-keyed for the renamed test file or shrunk for the deleted source. - `organization-ownership-inventory.ts` is unchanged: see the ceremony debt above. - **Not added: a 409 name guard.** Nothing refuses a new `sys_webhook` row under a declared name, as `sharing_rule` has. Expand-first adds no new refusal. Such a row answers as itself and the declaration is withheld, which is what the 18.0 seeder did with it. - `label` / `description` of a declared webhook have no reader. Enforce-or-remove (ADR-0049) is owed: a Setup surface that lists declared webhooks, or retirement. --- _Generated by [Claude Code](https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bfc1539 commit 191e043

42 files changed

Lines changed: 1958 additions & 1144 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/plugin-webhooks': minor
3+
'@objectstack/runtime': minor
4+
'@objectstack/spec': patch
5+
---
6+
7+
feat(plugin-webhooks,runtime)!: declared webhooks are read from the registry — the boot seeder retires, no `sys_webhook` row is written for one, and it is switched through the activation ledger
8+
9+
Clause-②: yes (narrowing)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) no spec key, export, option or stored metadata shape is added, removed or renamed, so `objectstack migrate meta` has nothing to rewrite; the rows an 18.0 seeder wrote stay in place and keep working, and what changes is where a declared webhook lives and how it is switched -->
12+
13+
**BREAKING** (a narrowing of where a declared webhook can be edited), shipped as `minor` under the launch-window convention for breaking changes.
14+
15+
ADR-0131 D2 says no seeder writes declared items into a table, and D6 charts behavioural types under regime C. A webhook declared with `defineStack({ webhooks })` now follows both, as declared sharing rules already do: the dispatcher reads the declaration from the metadata registry on every subscription-cache refresh, and `bootstrapDeclaredWebhooks` no longer runs.
16+
17+
What changes:
18+
19+
- **No `sys_webhook` row for a declared webhook.** Before: every boot wrote or refreshed one `managed_by: 'package'` row per declared webhook, shown and editable in Setup → Integrations → Webhooks. Now: a fresh install writes none, and Setup lists only the webhooks an organization authors. The declared webhook still delivers, with the same URL, method, triggers, `timeoutMs`, signing secret and headers; its secret and headers are code, held in memory, and never stored. Its deliveries carry `ref_id = declared:<name>`.
20+
- **A declared webhook is read-only, and switched through the activation ledger.** Before: an administrator edited the seeded row in Setup (`customized: true`). Now: switch it on or off for the deployment with `POST /api/v1/security/_activation/webhook/<name>` and `{ "enabled": true | false }` — one `sys_metadata_activation` row of type `webhook`, which overrides the declared `isActive` in either direction, so a webhook shipped inactive is switched on the same way. The door is the declared-sharing-rule door with `webhook` admitted: `manage_metadata`, then the platform operator under a wall; `404` for a name nothing declares; `503` when no declared-webhook reader is bound or it cannot be read. The dispatcher refreshes in the request that wrote the row. Remedy for a change beyond on/off: author a copy under a new name as a `sys_webhook` row, and switch the declared one off.
21+
- **`label` and `description` on a declared webhook are no longer shown anywhere.** The seeded row was their only reader. They are display-only, so no delivery changes; the liveness ledger marks them dead, owed enforce-or-remove.
22+
23+
On a database an 18.0 install created (expand first — nothing is moved or deleted):
24+
25+
- The rows the 18.0 seeder wrote stay where they are. A pristine one (`managed_by: 'package'`, not `customized`) is ignored in favour of its declaration, so a declared webhook is never delivered twice; it follows the declaration's URL and switch, as the seeder's rewrite on every boot made it do.
26+
- A seeded row an administrator edited (`customized: true`), or a row an administrator authored under a declared name, answers as itself, and the declaration is not dispatched beside it — an administrator's `active: false` on a noisy webhook keeps holding. This is logged at `warn` once per name per process, with the remedy.
27+
- A seeded row whose declaration was removed keeps delivering, as it did on 18.0.
28+
- Redelivering an old delivery of a pristine seeded row is judged by the declaration that now answers for it, so a webhook switched off through the ledger is not re-sent.
29+
30+
Deleting the seeded rows, and re-pointing their `sys_http_delivery` history at the declaration, is a step of the ownership migration in a later 18.x release.

‎content/docs/automation/webhooks.mdx‎

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,38 @@ Two persisted objects underpin the runtime, owned by different packages.
8080
Both are defined as `ObjectSchema.create()` schemas so they participate in
8181
CRUD, permissions, audit, and Studio UI without bespoke code.
8282

83+
### 3.0 Declared webhooks (code) and `sys_webhook` rows
84+
85+
A webhook reaches the dispatcher one of two ways:
86+
87+
- **Declared in code** — `defineStack({ webhooks })` / `defineWebhook()`. The
88+
declaration is read from the metadata registry on every subscription-cache
89+
refresh, and **no `sys_webhook` row is written for it** (ADR-0131 D2). Its
90+
signing secret and headers are code too, held in memory with it and never
91+
stored. A declared webhook is read-only: switch it on or off for the
92+
deployment with
93+
`POST /api/v1/security/_activation/webhook/:name` and body
94+
`{ "enabled": true | false }`, which writes one `sys_metadata_activation` row
95+
of type `webhook` and overrides the declared `isActive` in either direction
96+
(ADR-0131 D6, regime C — the door, its authority tiers and its refusals are
97+
the ones the declared-sharing-rule switch uses). To change one, author a copy
98+
under a new name as a `sys_webhook` row and switch the declared one off.
99+
Deliveries of a declared webhook carry `ref_id = declared:<name>`.
100+
- **Authored by an organization** — a `sys_webhook` row (§3.1), switched by its
101+
own `active` field.
102+
103+
On a database an 18.0 install created, the rows that release's boot seeder
104+
wrote for declared webhooks (`managed_by: 'package'`) are still there. Until the
105+
ownership migration removes them, a pristine one is ignored in favour of its
106+
declaration, so the webhook is never delivered twice; one an administrator
107+
edited (`customized: true`) answers as itself, and the declaration of the same
108+
name is not dispatched beside it — an administrator's `active: false` keeps
109+
holding.
110+
83111
### 3.1 `sys_webhook`
84112

85-
The subscription record. One row per "I want webhook X to fire for object Y".
113+
The subscription record an organization authors. One row per "I want webhook
114+
X to fire for object Y".
86115
The authored envelope — the per-attempt `timeoutMs` and the rest of the
87116
`Webhook` config — is carried in `definition_json`, a serialised `Webhook` JSON
88117
(canonical schema: `WebhookSchema`, exported from
@@ -149,8 +178,9 @@ readable through an ordinary `GET /api/v1/data/sys_webhook`. They now go to
149178
`signing_secret` and `headers_secret`; existing rows are migrated on boot by one
150179
idempotent sweep that moves both passengers and strips them from the blob in a
151180
single update. Authoring does not change — `defineWebhook({ secret, headers })`
152-
is written exactly as before, and the boot materializer routes each value to its
153-
own column.
181+
is written exactly as before; a declared webhook's values are now read from the
182+
declaration itself and stored nowhere (§3.0), and an authored row's go to their
183+
own columns.
154184

155185
A row the sweep has not converted — for example a pre-v17 row on a runtime with
156186
no `CryptoProvider` wired, where the encrypted write is refused and the row is
@@ -366,9 +396,9 @@ UNIQUE constraint.
366396

367397
For each incoming event the subscriber:
368398

369-
1. Loads `sys_webhook` rows where `active = true` AND `object_name`
370-
matches the event's object AND the event's action is in the row's
371-
`triggers`.
399+
1. Loads `sys_webhook` rows where `active = true`, and the declared
400+
webhooks switched on (§3.0), whose `object_name` matches the event's
401+
object AND whose `triggers` include the event's action.
372402
2. For each match, builds the payload (§5).
373403
3. Enqueues a `sys_http_delivery` row (`source = 'webhook'`).
374404

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

Lines changed: 20 additions & 20 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 **52 of the 232 sites are spelled that way**. A
125+
forwarding shim cannot, and **50 of the 230 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

@@ -150,8 +150,8 @@ now **0**: nothing on this surface threads a context that provably lacks the fla
150150

151151
**"No tenant context" counted sites it had not read.** An options argument the
152152
walker could not parse was folded into the same bucket as one it had read and
153-
found empty. That published **60 sites "carrying no tenant context at all"**
154-
when 8 said so and 52 were simply unread — an over-claim in the *alarming*
153+
found empty. That published **58 sites "carrying no tenant context at all"**
154+
when 8 said so and 50 were simply unread — an over-claim in the *alarming*
155155
direction, on the very figure this page tells other cards to cite. `carries` is
156156
now three-valued, and an unreadable argument can never contribute to the
157157
provable count.
@@ -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 | **232** |
191-
| 24 carrying no tenant context | quoted in the merged changeset | **2** provable and tenancy-enabled; **31** more whose options argument is unreadable |
192-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **158 of 232** decidable, **74** undecidable |
193-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 129 decidably elevated, 0 decidably not, 95 undecidable |
190+
| 175 write call sites | quoted in the merged changeset | **230** |
191+
| 24 carrying no tenant context | quoted in the merged changeset | **2** provable and tenancy-enabled; **29** more whose options argument is unreadable |
192+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **156 of 230** decidable, **74** undecidable |
193+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 129 decidably elevated, 0 decidably not, 93 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,20 +201,20 @@ 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 41 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 49 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-
129 of 232 (56%) as decidably elevated, with 95 more whose elevation is a
210+
129 of 230 (56%) as decidably elevated, with 93 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 / 232`, and say what it is**: the sites whose options argument was
214+
⇒ **Cite `2 / 230`, 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
217-
without tenant context" — **31 further sites** have an options argument this
217+
without tenant context" — **29 further sites** have an options argument this
218218
cannot read, and they are neither in nor out.
219219

220220
{/* BEGIN GENERATED: tenant-audit-census (scripts/tenant-audit-census.mjs) — DO NOT EDIT */}
@@ -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 | **232** |
227-
| …whose object name is statically decidable | 158 |
226+
| write call sites on the application surface | **230** |
227+
| …whose object name is statically decidable | 156 |
228228
| …whose object name is chosen at run time | 74 |
229-
| …against an object with tenancy ENABLED | 157 |
229+
| …against an object with tenancy ENABLED | 155 |
230230
| …against an object that declares tenancy off | 1 |
231231
| threading a tenant context | 172 |
232232
| PROVABLY carrying none (options read, no context key) | **8** |
233233
| …of those, against a decidably tenancy-enabled object | **2** |
234-
| options argument UNREADABLE — may or may not carry one | 52 |
235-
| …of those, against a decidably tenancy-enabled object | 31 |
234+
| options argument UNREADABLE — may or may not carry one | 50 |
235+
| …of those, against a decidably tenancy-enabled object | 29 |
236236
| threading a decidably ELEVATED (`isSystem`) context | 129 |
237237
| threading a context that is decidably NOT elevated | 0 |
238-
| threading a context whose elevation is a run-time fact | 95 |
238+
| threading a context whose elevation is a run-time fact | 93 |
239239

240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
242-
| receiver carried a readable engine type | 191 |
242+
| receiver carried a readable engine type | 189 |
243243
| receiver erased, placed by the object NAME | 31 |
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 | 107 |
248-
| object name spelled through a `const` | 51 |
248+
| object name spelled through a `const` | 49 |
249249
| object name is an `object: string` parameter | 7 |
250250
| object name is some other run-time expression | 67 |
251251

@@ -297,7 +297,7 @@ 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-19 at `030483e7f`.
300+
Measured on 2026-10-19 at `fa6a837e7`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |

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

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

3434
| Measure | Value |
3535
|---|---:|
36-
| Write call sites | 232 |
37-
| Object name statically decidable | 158 |
36+
| Write call sites | 230 |
37+
| Object name statically decidable | 156 |
3838
| Object name chosen at run time | 74 |
39-
| Against a tenancy-enabled object | 157 |
39+
| Against a tenancy-enabled object | 155 |
4040
| Against an object declaring tenancy off | 1 |
4141
| Threading a tenant context | 172 |
4242
| Provably carrying none | 8 |
4343
| …and decidably tenancy-enabled | 2 |
44-
| Options argument unreadable | 52 |
45-
| …and decidably tenancy-enabled | 31 |
44+
| Options argument unreadable | 50 |
45+
| …and decidably tenancy-enabled | 29 |
4646
| Threading a decidably elevated context | 129 |
4747
| Threading a decidably non-elevated context | 0 |
48-
| Threading a context of undecidable elevation | 95 |
48+
| Threading a context of undecidable elevation | 93 |
4949

5050
## Subtractions the census could NOT defend — enforced
5151

@@ -90,7 +90,7 @@ 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-19 at `030483e7f`.
93+
Measured on 2026-10-19 at `fa6a837e7`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
@@ -183,8 +183,6 @@ Measured on 2026-10-19 at `030483e7f`.
183183
| `packages/plugins/plugin-sharing/src/sharing-service.ts` | `delete` | `sys_record_share` | enabled | elevated | 2 |
184184
| `packages/plugins/plugin-sharing/src/sharing-service.ts` | `insert` | `sys_record_share` | enabled | elevated | 1 |
185185
| `packages/plugins/plugin-sharing/src/sharing-service.ts` | `update` | `sys_record_share` | enabled | elevated | 1 |
186-
| `packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts` | `insert` | `sys_webhook` | enabled | options unreadable | 1 |
187-
| `packages/plugins/plugin-webhooks/src/bootstrap-declared-webhooks.ts` | `update` | `sys_webhook` | enabled | options unreadable | 1 |
188186
| `packages/plugins/plugin-webhooks/src/migrate-webhook-secrets.ts` | `update` | `subscriptionsObject` | undecidable | options unreadable | 1 |
189187
| `packages/services/service-automation/src/builtin/crud-nodes.ts` | `delete` | `objectName` | undecidable | context, elevation undecidable | 1 |
190188
| `packages/services/service-automation/src/builtin/crud-nodes.ts` | `insert` | `objectName` | undecidable | context, elevation undecidable | 1 |

0 commit comments

Comments
 (0)