Skip to content

Commit 60bcfe7

Browse files
feat(plugin-security,rest,spec)!: retire the permission-set overlay discard (ADR-0131 cutover stage 7-pre) (#22776)
Part of #15204 Clause-②: no (narrowing) Stage **7-pre** of the ADR-0131 cutover (claim amendment 6105462924; stage 0 item U1 of 6095755866). #15204 stays open: this is one stage of its plan, and the later stages (6b-2, 7a, 7b, 8) are not addressed here. ## U1, measured on `main` `a18c514965` (re-read after merging `490cb6d9fa`): the action retires **1. What an "overlay" is now.** An active, environment-wide `sys_metadata` row (`organization_id` null) of type `permission` or the legacy `permissions`, stored under the name of a permission set a code package ships (`classifyPackagedPermissionSet` verdict `packaged`). It is not a `sys_permission_set` row. The removed module found it with `findActiveOverlayRows` and gated it on that classifier. **2. Can any caller-reachable path still create one? No.** Every door was measured: - **Metadata door.** `saveMetaItem` asks `refusePackagedBaseOverride`, which refuses an in-place write over a packaged item of a type with no overlay channel (`isSealedManagedItem`). `permission` is registered `allowOrgOverride: false`. ADR-0131 D6: the `OS_METADATA_WRITABLE` hatch "is not consulted" for managed content, on every topology. `SysMetadataRepository.assertAllowed` repeats the refusal at the store (`packages/metadata-protocol/src/protocol.ts`, `packagedBaseRefusal` and its header). - **Data door on `sys_metadata`.** The object is `apiMethods: ['get', 'list']` (`packages/metadata-core/src/objects/sys-metadata.object.ts`), so it is read-only over the data API. - **Data door on `sys_permission_set` and Setup.** `packaged-permission-set-lock.ts` refuses a save over a packaged set with 403 `NOT_OVERRIDABLE`, naming Clone ("No silent overlay row is ever minted again"). The metadata-door half of that lock is `packaged-permission-set-lock-gate.ts`. - **Package install.** A package that declares a name the environment catalog holds is refused with 422 `NAMESPACE_CONFLICT`, on a hot install and on a cold boot (`refuseSecurityCatalogNameConflicts`; `BUILT_IN_SECURITY_CATALOG_NAMES.permission` is empty, so no permission-set name is exempt). - **Rows stored before those refusals.** The cold boot refuses the deployment right after `sys_metadata` hydrates: `ObjectQLPlugin.refuseEnvironmentHeldSecurityCatalogNames` reads the registry's `environmentHeldSecurityCatalogConflicts` (ADR-0048 N.3, registered as `security-catalog-environment-overlay-refused`; pinned by `packages/runtime/src/standalone-stack-security-catalog-one-holder.test.ts`, "artifact boot over one database …"). So a server that could answer the discard route never holds an overlay. The remedy that exists for those rows is the offline `os migrate security-catalog-overlays [--apply]` (maintainer ruling letter B on #22371). **3. What the discard did, and whether the resolver reads it.** It deleted the overlay row(s) under `SYSTEM_CTX`, then re-projected the `sys_permission_set` row through `projectPermissionMutation`. An overlay row would be read: hydration puts it in the registry's bare slot, which the security catalog reader serves. But no running server holds one (point 2). The re-projected row is not read for grants: since stage 1 the resolver reads the security catalog, and since stage 2a/2b the services readers do too. **Decision rule → RETIRE.** No path creates an overlay. The operator workflow for existing databases is already served by the offline step, so no product choice is left open. **Four axes** (`.claude/skills/pm-dispatch/SKILL.md`, 升级与决策): - **Real business need (measured):** none at runtime. On this tree the action's target state cannot exist on a running server. The pull it served (field-reported forks) is served by `os migrate security-catalog-overlays`, which runs where the action cannot: before boot. - **Long-term soundness (leads):** one remedy, offline, ahead of the boot that refuses, instead of a second in-server path that can no longer fire. A name-keyed rewrite would build a transition piece for a state the cutover removes (#22601 → B). - **AI-error-proofing:** a declared route, Setup action and contract member that can never succeed invite callers to rely on them. Removing them leaves one prescription, named by the cold-boot refusal itself. - **Startup focus:** retire now, with no deprecation window; about 1.5k lines leave the tree. ## What changed - **Deleted:** `plugin-security/src/permission-set-overlay-discard.ts` and its test, and the dogfood eligibility test. - **plugin-security:** the service wiring and the five exports go. The **Discard Overlay** action on `sys_permission_set` goes (translations regenerated). The `drift_status` field description, the drift diagnostic's `overlay_shadow` detail and its comments lose their reference to the action (deletions in a 6b-2 module). The boot reading `reportPackagedPermissionSetOverlays` (`packaged-permission-set-overlay-detection.ts`) now names the offline step instead of the action. Comment references in the lock, the projection and `write-refusals.ts` are trimmed. - **rest:** the route and its route-ledger row go, and a RETIRED note takes the row's place. Its rows in two rest ledger tests go. - **spec:** - `ISecurityService.discardPermissionSetOverlay` and `PermissionSetOverlayDiscardResult` are removed, with their contract test. - `ERROR_CODE_LEDGER['@objectstack/plugin-security']` drops `INVALID_STATE` and `NOT_FOUND`: the removed action was their only producer in that package. Both stay under `@objectstack/rest`, so the union is unchanged. The `NOT_FOUND` waiver reason and the ledger test follow. - New ADR-0087 semantic entry `security-permission-set-overlay-discard-retired`. - `api-surface/` and `export-origins/` are regenerated. - **Kept, per the stage plan:** `packaged-permission-set-lock` and its gate. `packaged-permission-set-overlay-detection` keeps its consumer: the boot reading in `security-plugin.ts` calls it, and the discard never imported it. - **Docs:** `permission-sets.mdx` (the "Overlay shadow" remedy), `system-context.mdx` (row 14 removed; census counts regenerated), `metadata-lifecycle.mdx` and `environment-variables.mdx` (the permission-set exception), and `tenant-audit-census.mdx` with its counts ledger (one write site fewer). Nothing under `content/docs/releases/`. - **Gate ledgers:** `engine-double-contract.pinned.json` (regenerated; 2 rows of the deleted test) and `objectql-double-limit.baseline.json` (its entry, shrink-only). - **Changeset:** `spec`, `plugin-security` and `rest` are `minor`, BREAKING (the epic's launch-window convention), with a removed → instead table and the ADR-0087 `registered` marker. ## Acceptance notes - **Contract review owed.** This touches non-test `packages/spec/src/**` (the contract, the error-code ledger and the migration entry), so a same-head contract-tier PASS is owed before `pr_ready`. - **Two pending changesets name the action** (not edited here; `check-empty-changeset` refuses an edit to another PR's note, and correcting a pending release note is the seat's call): - `.changeset/22307-cold-boot-catalog-refusal.md` tells an operator to use Discard Overlay "on the release you run now", before upgrading. That stays true of the release they run, where the action exists. - `.changeset/15206-reads-environment-only.md:26` says a packaged-set fork "keeps its own ruling (detection reading and Discard Overlay) and is still served". After this PR the "Discard Overlay" half is false. The suggested correction is "(the detection reading)". - `.changeset/22719-rest-write-hook-refusal-sentence.md:16` (landed on `main` while this PR was open) lists `POST /security/permission-sets/:id/discard-overlay` among the routes it repaired. Once this PR lands, that route no longer exists. The suggested correction is to drop it from that row. - **objectui pin: NOT MEASURED.** No checkout of `../objectui` is available here. The removed surface is a server route, a declarative action and a contract type. An objectui file importing `PermissionSetOverlayDiscardResult` or naming `discard-overlay` would be the Console Pin Gate's red. - Row 14 of `system-context.mdx` leaves a gap in its numbering (13 → 15). Renumbering would move every later row's key that other text cites. ## Verification (head `8f992cde3d`: the change plus one no-rebase merge of `origin/main` `490cb6d9fa`) - **Package suites:** - `plugin-security`: test exit 0 (4003 passed, 45 skipped); typecheck exit 0. - `rest`: test exit 0 (5235 passed, 326 skipped); typecheck exit 0. - `spec`: test exit 0 (19220 passed); typecheck exit 0. - `client`: test exit 0 (653 passed); typecheck exit 0. Each was run as `pnpm --filter PKG test` and `typecheck` behind `os-verify-lock`, after `pnpm install --frozen-lockfile` and a closure build. - **Targeted:** - The runtime cold-boot refusal pin `standalone-stack-security-catalog-one-holder.test.ts`: 6/6. - Dogfood `route-ledger-live-mount-parity`, `permission-set-lock-row-provenance` and `permission-set-clone-boot-unowned-warning`: 25/25. - The full `runtime` and `dogfood` suites are left to CI. This is a declared narrowing: neither package's source is in the diff, and the one dogfood test file in the diff is deleted. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran` reports 131 derived, 131 run, 0 NOT MEASURED, 0 UNRUN, and every family exits 0. That includes `check:generated` for spec, `check:i18n`, `check:i18n-stale-fill`, the system-context and tenant-audit censuses, `check:adr-0087-registration`, `check:empty-changeset`, `check:engine-double-contract` and `check:objectql-double-limit`. `check-route-ledger-census` also exits 0. - **Census edits, from the measurement:** - `execctx-consumer-census.test.ts`: 63 → 62 sites, 78 → 77 mentions, 47 → 46 bare. - The hook-refusal ledger: the write-row floor goes 45 → 44, and the repaired rows 20 → 19. --- _Generated by [Claude Code](https://claude.ai/code/session_019TtY6pnoZSemcQRzbRUm5e)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 23419ba commit 60bcfe7

47 files changed

Lines changed: 228 additions & 1587 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/plugin-security': minor
4+
'@objectstack/rest': minor
5+
---
6+
7+
feat(plugin-security,rest,spec)!: the permission-set "Discard Overlay" action, its REST route and its contract member are retired; no door creates the overlay it discarded, and a database holding one is refused at cold boot (ADR-0131 cutover, ADR-0048)
8+
9+
Clause-②: no (narrowing)
10+
11+
<!-- adr-0087: registered security-permission-set-overlay-discard-retired -->
12+
13+
**BREAKING** — a narrowing of the public surface: one HTTP route, one Setup action and one optional contract member are removed. Shipped as `minor` under the launch-window convention for breaking changes.
14+
15+
The action deleted the environment-wide `sys_metadata` row that shadowed a package-declared permission set, then re-projected the set's `sys_permission_set` row. Neither half has anything left to do:
16+
17+
- No door creates such an overlay. The metadata door refuses a save over a packaged permission set on every topology, whatever `OS_METADATA_WRITABLE` says. The data door refuses an edit of one (clone it instead). The data API cannot write `sys_metadata`. A package that declares a name the environment holds is refused at install.
18+
- An overlay stored before those refusals makes the cold boot refuse the deployment with `422 NAMESPACE_CONFLICT`, so a server that could answer the route never holds one.
19+
- The authorization resolver reads the security catalog, not `sys_permission_set` rows, so the re-projected row granted nothing.
20+
21+
### What was removed
22+
23+
| removed | what to use instead |
24+
| --- | --- |
25+
| `POST /api/v1/security/permission-sets/:id/discard-overlay` | `os migrate security-catalog-overlays` with the flags and environment the deployment boots with lists every overlay row with no server running; `--apply` deletes them (a history tombstone per row). |
26+
| the **Discard Overlay** action on `sys_permission_set` in Setup | the same offline step. To keep a customization of a packaged set, clone it under a new name (the **Clone** action) and assign the clone. |
27+
| `ISecurityService.discardPermissionSetOverlay` and `PermissionSetOverlayDiscardResult` (`@objectstack/spec/contracts`) | nothing: delete the call and the import. `tsc` names each one. |
28+
| `discardPermissionSetOverlay`, `PermissionSetNotFoundError`, `PermissionSetOverlayStateError` and the `PermissionSetOverlayDiscardDeps` / `PermissionSetOverlayDiscardResult` types (`@objectstack/plugin-security`) | nothing: delete the import. |
29+
30+
**The one-line fix:** stop calling the route or the member, and clear any overlay offline with `os migrate security-catalog-overlays --apply` before the first boot on this release.
31+
32+
`ERROR_CODE_LEDGER['@objectstack/plugin-security']` no longer lists `INVALID_STATE` or `NOT_FOUND`: the removed action was their only producer in this package. Both codes stay registered under `@objectstack/rest`, and the union of codes is unchanged.
33+
34+
The permission-set drift diagnostic no longer names the action in its `overlay_shadow` detail, and the boot reading of shadowed packaged sets names the offline step instead.

‎content/docs/concepts/metadata-lifecycle.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ A permission set and a position also have a switch, outside the refusal: `POST /
144144

145145
The switches are operator-only where one install serves several organizations. The refusal names the same path whichever layer answers it: `403 NOT_OVERRIDABLE` for a write that names no base, and `403 ITEM_LOCKED` for one that names the read-only package. Which writes are refused, and with which code, is unchanged.
146146

147-
An environment row stored over a managed item of a type sealed against overlays (a flow, action, hook, object or datasource, for example — any type whose registry entry admits no overlay) before the seal — written through the `OS_METADATA_WRITABLE` hatch — is **not served** (ADR-0131 D6): every read, and boot, takes the package's definition, and the row stays at rest, untouched. Boot names each such row, per type (`[metadata_sealed_overlay_unserved]`), with the two remedies: re-express the change as a new item under a new name, or delete the stored row. A stored fork of a code-declared permission set is the one exception: it keeps the detection reading and the operator's Discard Overlay action it already had.
147+
An environment row stored over a managed item of a type sealed against overlays (a flow, action, hook, object or datasource, for example — any type whose registry entry admits no overlay) before the seal — written through the `OS_METADATA_WRITABLE` hatch — is **not served** (ADR-0131 D6): every read, and boot, takes the package's definition, and the row stays at rest, untouched. Boot names each such row, per type (`[metadata_sealed_overlay_unserved]`), with the two remedies: re-express the change as a new item under a new name, or delete the stored row. A stored fork of a code-declared permission set is the one exception: the deployment does not boot while it holds one (ADR-0048), and `os migrate security-catalog-overlays` lists and, with `--apply`, deletes it.
148148

149149
---
150150

‎content/docs/deployment/environment-variables.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ that bypassed write hooks, `rebuildSearchCompanion` (from
306306
| `OS_MARKETPLACE_CACHE` | enum | `on` | `off` disables the in-memory marketplace listing cache. |
307307
| `OS_MARKETPLACE_PUBLIC_BASE_URL` | url | — | Public base URL of the marketplace registry (proxied from this runtime when set). |
308308
| `OS_ALLOW_UNMASKED_OBJECT_METADATA` | boolean | `false` | Escape hatch for the metadata-plane field-level security mask ([ADR-0106](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0106-metadata-plane-fls-object-schema-masking.md) D8). By default every object schema served by `/meta` and `/metadata` is projected onto the fields the **calling user** may read, so a field they cannot read does not appear at all — not its name, label, type, picklist options, formula, `visibleWhen` predicate, `defaultValue`, or the `requiredPermissions` capability guarding it. Set to `1` to serve the full schema to every authenticated caller, as releases before this one did. This changes **disclosure only**: the data plane still masks values and refuses forbidden writes either way, and the console reads field affordances from `/auth/me/permissions`, so toggling it never changes UI correctness. The REST layer also honours a per-server `metadata.maskObjectFields: false`; this variable is the deployment-wide knob and covers the runtime `/metadata` dispatcher, which has no REST config to read. |
309-
| `OS_METADATA_WRITABLE` | csv | — (none) | Comma-separated metadata type names (e.g. `hook,job`) granted a runtime escape hatch that treats them as `allowOrgOverride: true` for items **no managed package ships**: it opens runtime creation of a type whose registry entry allows none. It never opens an organization-scoped write: since [ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md) D6 the metadata protocol refuses every organization-scoped write with `403 NOT_OVERRIDABLE`. It **never opens an item a managed package ships**: managed content is sealed ([ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md) D6), so an overlay or a removal of a shipped flow, object, field, permission set, position or any other type without an environment overlay is refused with `403 NOT_OVERRIDABLE` whether the hatch is set or not. Customize managed content through its type's own route instead: an environment overlay for `view`, `dashboard`, `report`, `translation` and `email_template`; disable, or clone under a new name, for a flow; clone for a permission set. An overlay row this hatch wrote before over a managed item is kept at rest but **not served**: every read takes the package's definition, and boot names each such row, per type (`[metadata_sealed_overlay_unserved]`), with the two remedies — re-express the change as a new item under a new name, or delete the row. A stored fork of a packaged permission set is the one exception: it is still served, with its detection reading and Discard Overlay. Removing that permission-set row restores the package's definition. A stored row over a managed item of the other types that merge overlays at read (`position`, `page`, `app`, `dataset`, `book`, `tool`, `skill`) can still be removed, but removing it changes nothing that is served: no read serves it, so the package's definition is already served. See [ADR-0005](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0005-metadata-customization-overlay.md). |
309+
| `OS_METADATA_WRITABLE` | csv | — (none) | Comma-separated metadata type names (e.g. `hook,job`) granted a runtime escape hatch that treats them as `allowOrgOverride: true` for items **no managed package ships**: it opens runtime creation of a type whose registry entry allows none. It never opens an organization-scoped write: since [ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md) D6 the metadata protocol refuses every organization-scoped write with `403 NOT_OVERRIDABLE`. It **never opens an item a managed package ships**: managed content is sealed ([ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md) D6), so an overlay or a removal of a shipped flow, object, field, permission set, position or any other type without an environment overlay is refused with `403 NOT_OVERRIDABLE` whether the hatch is set or not. Customize managed content through its type's own route instead: an environment overlay for `view`, `dashboard`, `report`, `translation` and `email_template`; disable, or clone under a new name, for a flow; clone for a permission set. An overlay row this hatch wrote before over a managed item is kept at rest but **not served**: every read takes the package's definition, and boot names each such row, per type (`[metadata_sealed_overlay_unserved]`), with the two remedies — re-express the change as a new item under a new name, or delete the row. A stored fork of a packaged permission set is the one exception: the deployment does not boot while it holds one (ADR-0048); `os migrate security-catalog-overlays` lists such rows and, with `--apply`, deletes them, which restores the package's definition. A stored row over a managed item of the other types that merge overlays at read (`position`, `page`, `app`, `dataset`, `book`, `tool`, `skill`) can still be removed, but removing it changes nothing that is served: no read serves it, so the package's definition is already served. See [ADR-0005](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0005-metadata-customization-overlay.md). |
310310

311311
---
312312

‎content/docs/permissions/permission-sets.mdx‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -382,15 +382,13 @@ either can be live on one row, and they need different remedies:
382382
(from a Studio save made while overlaying was still permitted, or through
383383
the `OS_METADATA_WRITABLE` escape hatch) is re-projected onto the record on
384384
every boot, unconditionally — the overlay wins forever, regardless of what
385-
the package ships next. **Remedy:** the record's **Discard Overlay** Setup
386-
action (`POST /api/v1/security/permission-sets/:id/discard-overlay`,
387-
tenant-admin only) removes the stale overlay and resyncs the record to the
388-
current artifact immediately. It refuses on any set that no installed code
389-
package ships (a set created in this environment, a clone, or a set saved
390-
into a writable runtime package), so it can never destroy a genuinely
391-
environment-authored set. **Discard it before you upgrade:** from the
385+
the package ships next. No door creates such an overlay any more: the
386+
metadata door refuses a save over a packaged set, and so does the data door
387+
(clone the set instead). **Remedy:** delete the overlay row. From the
392388
release that adds ADR-0048's cold-boot check, a deployment that still holds
393-
such an overlay does not boot, so the action can no longer reach it.
389+
one does not boot: `os migrate security-catalog-overlays` lists every such
390+
row with no server running, and `--apply` deletes them, after which the
391+
package's definition is served again.
394392
- **Provenance skip.** The record's `managed_by` column predates package
395393
provenance tracking (a legacy insert without `managed_by: 'package'` —
396394
typically a database first initialized on an older release line), so boot

‎content/docs/permissions/system-context.mdx‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ the seed loader replaying package fixtures, a plugin's boot reconciler, a
1010
service self-write, a migration.
1111

1212
This page is **the authority** for what that flag actually does. It exists
13-
because the flag is not one concept: it is a single boolean read at **123
13+
because the flag is not one concept: it is a single boolean read at **122
1414
distinct sites across 20 packages**, and knowing three of those behaviours gives
1515
no hint that the other hundred-and-four exist. Every documented app-side bug
1616
traced to `isSystem` had the same shape — the metadata was complete and correct,
@@ -112,7 +112,6 @@ that silently does not happen.
112112
| 11b | Position write-through skipped — a `sys_position` write is not mirrored into the environment ledger | plugin-security | Get: the seeders and the package door write rows for definitions that already have their home, untouched. Lose: a system-written row has no environment definition, so the security catalog read does not resolve it until a data-door edit, or the one-time row-only position backfill, gives it one | `packages/plugins/plugin-security/src/position-write-through.ts#createPositionWriteThrough` |
113113
| 12 | Session-resolution middleware skipped | plugin-auth | Get: no session lookup attempted | `packages/plugins/plugin-auth/src/auth-plugin.ts#start` |
114114
| 13 | Per-request performance timings disclosed | observability | Get: timing headers a normal caller cannot pull | `packages/observability/src/perf-timing.ts#isPerfDisclosurePrincipal` |
115-
| 14 | Permission-set **overlay discard** skips the tenant-admin assertion | plugin-security | Get: an overlay can be discarded with no authenticated tenant administrator | `packages/plugins/plugin-security/src/permission-set-overlay-discard.ts#assertTenantAdmin` |
116115
| 15 | MCP stdio bridge skips the object API-exposure gate | mcp | Get: the bridge reaches objects whose `apiEnabled` / `apiMethods` would refuse an external caller | `packages/mcp/src/stdio-data-bridge.ts#enforceApiExposure` |
117116
| 16 | **Read-audit rows are not written** | plugin-audit | Lose: the "a person opened this record" trail. `sudo()` keeps the caller's `userId`, so this flag is the only thing separating a human read from a platform one | `packages/plugins/plugin-audit/src/read-audit.ts#installReadAuditWriter` |
118117
| 17 | Approval snapshot payload redaction skipped, and so is the snapshot query guard | plugin-approvals | Get: the whole snapshot on `find` / `findOne` — the audit/replay channel — and a filter, sort or grouping by it on any read. Lose: field-visibility redaction over approval payloads, and the refusal of a query over the snapshot for a reader withheld a field of the objects it can reach | `packages/plugins/plugin-approvals/src/payload-redaction-middleware.ts#bindSnapshotRedactionMiddleware`, `packages/plugins/plugin-approvals/src/payload-predicate-guard.ts#bindSnapshotPredicateGuard` |
@@ -143,7 +142,7 @@ that silently does not happen.
143142

144143
### 3. Sharing (`plugin-sharing`)
145144

146-
The largest single consumer — **17 of the 123 sites**.
145+
The largest single consumer — **17 of the 122 sites**.
147146

148147
| # | Behaviour when `isSystem` | What you get / what you lose | Anchor |
149148
|:--|:---|:---|:---|
@@ -286,7 +285,7 @@ Ownership injection, `readonly` bypass and sharing materialisation are
286285
independent decisions, and a seed loader plausibly wants the first two but not
287286
the third. The concept is nevertheless **staying as one boolean**:
288287

289-
- **Shipped semantics.** `isSystem` is a published contract with 123 read sites
288+
- **Shipped semantics.** `isSystem` is a published contract with 122 read sites
290289
in 20 packages. Splitting it is a breaking contract change across all of them.
291290
(The ruling was taken when the census read 80 sites in 18 packages; the count
292291
has grown, which strengthens rather than weakens the argument.)
@@ -360,16 +359,16 @@ still holds equal to the census on every pull request:
360359
| Appearances of the bare identifier `isSystem` in non-test sources | 813 | — |
361360
| — parsed as a declaration | 28 | ✅ |
362361
| — parsed as an object-literal / type key (producers and option objects) | 310 | — |
363-
| — parsed as a property **read** | 129 | ✅ |
362+
| — parsed as a property **read** | 128 | ✅ |
364363
| — parsed in some other syntactic position (a local, a cast, a conditional) | 9 | ✅ |
365364
| — the remainder: text inside comments and string literals | 358 | — |
366365
| Of those reads: reads of one of the unrelated metadata fields | 6 | ✅ |
367-
| Of those reads: reads of `ExecutionContext.isSystem` | **123** | ✅ |
368-
| — behaviour-bearing (rows 1–61 above) | 120 | ✅ |
366+
| Of those reads: reads of `ExecutionContext.isSystem` | **122** | ✅ |
367+
| — behaviour-bearing (rows 1–61 above) | 119 | ✅ |
369368
| — carry the flag onward only (rows 62–64 above) | 3 | ✅ |
370369
| Packages containing at least one elevation read | **20** | ✅ |
371-
| Files containing at least one elevation read | 58 | ✅ |
372-
| — the distinct symbols those reads live in — what this page anchors | 105 | ✅ |
370+
| Files containing at least one elevation read | 57 | ✅ |
371+
| — the distinct symbols those reads live in — what this page anchors | 104 | ✅ |
373372
| — of those files, the ones holding more than one read in one symbol | 8 | ✅ |
374373

375374
The six rows marked — are a **dated decomposition, not a live claim**: they were
@@ -433,7 +432,7 @@ same resolver, and the same registration shape, that holds `docs/adr/**`.
433432
Renaming a symbol is now a loud red instead of a silent misdirection.
434433

435434
⚠️ **The precision that costs, priced here rather than buried.** A symbol anchor
436-
cannot say WHICH read inside a function it means, and **8** of the **58**
435+
cannot say WHICH read inside a function it means, and **8** of the **57**
437436
anchored files hold more than one read inside a single symbol. So the population
438437
check runs per file at symbol granularity: every file the census finds a read in
439438
must be anchored, and the set of symbols this page cites into that file must

0 commit comments

Comments
 (0)