diff --git a/.changeset/27685-manifest-liveness-cell.md b/.changeset/27685-manifest-liveness-cell.md new file mode 100644 index 00000000000..a398504ad3a --- /dev/null +++ b/.changeset/27685-manifest-liveness-cell.md @@ -0,0 +1,10 @@ +--- +'@objectstack/spec': patch +--- + +docs(spec): the liveness ledger README's `manifest` cell names the type's dead set as the ledger holds it, and says why `packaging` is `planned` + +Clause-②: no +Metadata-protocol: none + +The `manifest` row of `liveness/README.md` said "Dead 19", counting the per-child rows of the retired `capabilities` and `configuration` containers and a `loading` row that has since left the ledger. The cell now partitions the dead set the ledger carries (the ten dead `contributes` members and the three retired containers, one tombstone row each), which sums to the count in the generated `liveness/state-counts/manifest.md` shard. It also gains the missing sentence for `packaging`, the type's only `planned` row: `bundled` is the one strategy, `manifest-deps` is refused by name, and `os plugin build` reads the key only to echo it. No ledger status, evidence, schema or accepted shape changes: only the README prose moves. diff --git a/docs/design/plugin-distribution.md b/docs/design/plugin-distribution.md index 5728a5b4bbe..8fa58e0094f 100644 --- a/docs/design/plugin-distribution.md +++ b/docs/design/plugin-distribution.md @@ -111,15 +111,13 @@ A `tar.gz`: -.osplugin ├── objectstack.plugin.json # compiled manifest (below) ├── dist/ # pre-built ESM bundle (tsup); @objectstack/* externalized -├── package.json # only for the manifest-deps strategy -├── pnpm-lock.yaml # only for the manifest-deps strategy ├── assets/ README LICENSE icon └── SIGNATURE # detached signature + publisher cert chain ``` Compiled manifest adds four blocks to the existing `ObjectStackManifest`: `engines` (protocol + platform ranges), `runtime` (`node|sandbox|worker`), -`packaging` (`bundled|manifest-deps`), `permissions` (services/hooks/network/fs +`packaging` (`bundled`, the one strategy and the default — §4.3), `permissions` (services/hooks/network/fs + RLS data scopes), and `integrity` (per-file hashes). ### 4.2 UI plugin (`runtime: 'ui'` — ADR-0026 §3.1) @@ -135,15 +133,27 @@ client-scoped `permissions` (data read/write via SDK, network → CSP, navigatio > `ManifestSchema` and `PluginSchema` refuse `ui-plugin`; this section and > worked example C in §10 use `ui`. -### 4.3 Packaging strategies (default: bundled) - -- **Bundled (default).** `tsup --noExternal` bundles third-party JS into `dist`; - no npm at install time. **`@objectstack/*` is externalized** as a host-provided - peer-dep singleton (bundling it forks the engine — duplicate registries / broken - `instanceof` / double Zod validation). Native addons disallowed. -- **manifest-deps (opt-in).** Ships `package.json` + lockfile; install runs - `pnpm install --frozen-lockfile --ignore-scripts`. For native addons / huge - deps only; disallowed for unverified publishers. +### 4.3 Packaging strategy: bundled only + +- **Bundled — the one strategy, and the default.** `os plugin build` inlines + every dependency the entry imports into `dist/index.mjs`, so install is unpack + + verify with no npm or toolchain at install time: `os plugin install` and the + boot-time loader import `dist/index.mjs` as packed, and the artifact carries + neither `package.json` nor a lockfile. **`@objectstack/*` is externalized** as a + host-provided peer-dep singleton (bundling it forks the engine — duplicate + registries / broken `instanceof` / double Zod validation). A native (`.node`) + addon is refused by name at build, before any artifact is written. + +> **Note (2026-10-29, #27679).** This section used to list a second, opt-in +> strategy, `manifest-deps`, which shipped `package.json` + a lockfile for an +> install-time `pnpm install`. Nothing ever ran that install, so an artifact +> built with it could not load the dependencies it left out. It is retired +> (ADR-0025 v2 §3.3): `PluginPackagingSchema` +> (`packages/spec/src/kernel/manifest.zod.ts`) accepts `bundled` alone and +> refuses `packaging: 'manifest-deps'` by name with its migration prescription +> (delete the key), and `os plugin build` no longer forks on the value. A plugin +> that needs a native addon or a very large dependency is a fresh ADR-0025 M3 +> decision, not a strategy here. §4.1, §5, §6 and §8 name no second strategy. ## 5. Build → Sign → Publish @@ -164,8 +174,8 @@ os plugin build → os plugin sign → os plugin publish `sys_plugin_version` (`status: pending_review → published`). - **Server gates:** schema validation; secret/malware/known-vuln scan; **permission audit** (sensitive permissions → human review); **publish-time - tier enforcement** (an unverified publisher cannot ship `runtime: 'node'` or - `manifest-deps`); marketplace **counter-signs** with the platform key on + tier enforcement** (an unverified publisher cannot ship `runtime: 'node'`); + marketplace **counter-signs** with the platform key on approval. - Listed in the **contribution catalog** (not the consumer App Marketplace), with a "contains code" badge and a permission-disclosure screen. @@ -191,8 +201,8 @@ resolve → compat check → permission consent → download+verify → material are wired to the KV secret store (ADR-0007) — never stored in the artifact. 4. **Download + verify.** Verify signature (publisher + marketplace keys) and each file against `integrity`; reject on mismatch. -5. **Materialize.** Unpack to `/plugins////`; - manifest-deps runs `pnpm install --frozen-lockfile --ignore-scripts` here. +5. **Materialize.** Unpack to `/plugins////`. No + dependency install runs here: the bundle is imported as packed (§4.3). 6. **Register.** Write `sys_plugin_installation` (env_id, plugin_id, version_id, granted_permissions incl. RLS scopes, status, is_preview). Persist to a local store so installs survive restarts (the ADR-0016 §9.7 disable-state pattern). @@ -242,8 +252,9 @@ Reuses existing components, adds two compositions: - **Secrets.** `configuration` secret fields → KV secret store (ADR-0007) via secret-refs; the artifact never carries credentials; the plugin reads them through `PluginContext` gated by the network/service permission. -- **Supply chain.** Lockfile + per-file integrity; server-side secret/vuln scans; - SBOM on the version row; **always `--ignore-scripts`** (no `postinstall`). +- **Supply chain.** Per-file integrity; server-side secret/vuln scans; SBOM on + the version row; **no install-time package manager at all** (bundled only, + §4.3), so no dependency's `postinstall` ever runs. - **Server-authoritative UI data.** UI plugins read/write only via the client SDK under the user's session → every access re-checked by server RLS; manifest `permissions.data` is a UI hint/guard, not the boundary. U1 iframes cannot read diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index c5ac57e82dd..e8afa6ee9a7 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -1716,7 +1716,7 @@ "title": "os plugin build enforces the manifest at the packaging boundary: an invalid manifest is refused with located errors, `bundled` is the one packaging strategy (the retired `manifest-deps` is refused at the manifest, a native addon is refused by name), and the compiled manifest the artifact carries is readable back", "since": "v15", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "cli", "personas": [ @@ -1787,8 +1787,8 @@ "negative": [ "an artifact written from a manifest that failed the schema, or from one missing id/version, is the FAIL this item exists for — the refusal is worthless if the build still produces something publishable", "a build that accepts `manifest-deps` — writes an artifact, or packs package.json and keeps a dependency external — is a FAIL of the bundled clause: that fork was retired because nothing ever installed the dependencies it left out, so its artifact could not load them. Likewise a native-addon refusal that prints and still writes the .osplugin hands os plugin sign the very artifact it refused", - "⛔ the `integrity` map's PRESENCE is not evidence of an integrity CHECK. os plugin build computes it and writes it into the compiled manifest, and nothing re-verifies it at unpack — #11331 is open on exactly that. Scoring the printed 'Integrity entries' count as a working integrity guarantee is the false-compliance shape the enforce-or-remove ledger exists for, and this item asserts only that the map is PRODUCED", - "⛔ likewise the plugin trust tier: build and publish both print `runtime: `, and nothing in this repo dispatches on it — #11330 is open. A run that reads that printed line as isolation has scored a log line, not a capability", + "⛔ the `integrity` map's PRESENCE is not evidence that this item scored an integrity CHECK. os plugin build computes the map and writes it into the compiled manifest, and this item asserts only that the map is PRODUCED. The check is another door's: os plugin install re-hashes every unpacked entry against the map and refuses the install by name (`integrity_mismatch`) before it writes anything (packages/cli/src/commands/plugin/install.ts#PluginInstall, through packages/core/src/security/plugin-artifact-integrity.ts#verifyIntegrity), and os plugin publish runs the same verifier as its own preflight — no clause here scores either. Reading the printed 'Integrity entries' count as that check scores a log line for a capability this item never exercised", + "⛔ likewise the plugin trust tier: build and publish both print `runtime: `, and neither dispatches on it. The dispatch is at load — the boot-time loader passes every plugin os plugin install installed through packages/runtime/src/installed-plugin-loader.ts#dispatchPluginRuntime before importing it (`node` loads in process, which is not isolation; `sandbox` and `worker` are refused by name, `runtime_not_dispatched`; an undeclared tier is refused, `runtime_undeclared`), and os plugin install warns when the next boot will refuse the tier. This item scores none of that: a run that reads the printed line as isolation has scored a log line, not a capability", "a lint warning that fires on a manifest-less metadata fragment is a FAIL of the scoping half even though the rule itself works" ], "traps": [ @@ -1799,6 +1799,9 @@ "packages/cli/src/commands/plugin/build.ts#formatZodErrors — manifest read + named refusal; ManifestSchema.safeParse + formatZodErrors; the explicit id/version guard; packaging default and the Loaded line; entry resolution; the bundled-only esbuild call (only `@objectstack/*` external); findNativeAddons and the native-addon refusal ahead of the integrity map; computeIntegrity + the compiled manifest; the artifact write and its printed block", "packages/cli/src/utils/osplugin.ts#MANIFEST_FILENAME — MANIFEST_FILENAME = 'objectstack.plugin.json'; OSPLUGIN_EXT; computeIntegrity (manifest and SIGNATURE excluded, keys sorted); the ustar writer with mtime pinned to 0; readOspluginManifest", "packages/cli/src/commands/plugin/publish.ts#version — artifact discovery; readOspluginManifest before any network call; the id/version refusal; the unsigned-artifact notice; the auth refusal", + "packages/cli/src/commands/plugin/install.ts#PluginInstall — the install-time re-verification the integrity negative points at (verifyIntegrity over the unpacked entries, `integrity_mismatch` / `integrity_malformed`, nothing installed) and the install-time tier warning; not scored here", + "packages/core/src/security/plugin-artifact-integrity.ts#verifyIntegrity — the one integrity verifier, shared by the publish preflight and the install-time check; not scored here", + "packages/runtime/src/installed-plugin-loader.ts#dispatchPluginRuntime — the boot-time tier dispatch point the trust-tier negative points at (`node` only; `sandbox` / `worker` refused by name; an undeclared tier refused); not scored here", "packages/spec/src/kernel/manifest.zod.ts#ManifestSchema — ManifestSchema id and version (both required, version regex-pinned); PluginPackagingSchema ('bundled' only — 'manifest-deps' is a retired value refused with its migration prescription, ADR-0025 v2 §3.3); PluginRuntimeSchema (the trust tier, ADR-0025 §3.6)", "packages/cli/src/commands/lint.ts#manifest — the protocol/missing-engines-range rule, its manifest scoping and its fix string", "packages/rest/src/package-routes.ts#PACKAGE_MANIFEST_INVALID — the server mirror (400 PACKAGE_MANIFEST_INVALID), recorded in knownGaps rather than scored here", @@ -1829,6 +1832,12 @@ "date": "2026-10-29", "change": "A3 rewritten and a native-addon clause added: `packaging` no longer forks the artifact. PluginPackagingSchema is narrowed to `bundled` (manifest.zod.ts), so `manifest-deps` is refused at safeParse with its migration prescription and the build.ts externalize + package.json / pnpm-lock.yaml forks are gone — the old fork clause described a property the build no longer has. os plugin build now also refuses a native `.node` addon by name before writing the artifact (findNativeAddons, build.ts). Steps 2, 3, 4 and 8, the first fixture, the archive knownGap, the second negative and two source rows re-pointed to match; probes 6f-6h added for the addon refusal, and a knownGap records the addon an absolute or computed runtime path loads, which the build cannot see. Verified in the same PR through the real door (PluginBuild.run over on-disk projects, plugin-build-bundled-only.test.ts) — a verification re-run, no qa-run record. Untouched and not re-verified here: the integrity and trust-tier negatives", "ref": "#27123" + }, + { + "revision": 5, + "date": "2026-10-29", + "change": "the integrity and trust-tier negatives re-pointed, no clause changed: both said nothing in this repo verifies or dispatches, and both stopped being true with the install loop — os plugin install re-verifies the integrity map at unpack (install.ts#PluginInstall through verifyIntegrity, slice 1, 10f27b6508) and the boot-time loader dispatches on `runtime` before any import (installed-plugin-loader.ts#dispatchPluginRuntime, slice 2, d4ff1a025f). Each negative now names that code as the check this item does NOT score, in place of the tracker numbers it cited, which do not resolve in this repository; three source rows added for the code they point at. The scored clauses and steps are unchanged. Re-verified against the source at 86f2afeb23, no qa-run record", + "ref": "#27685" } ] }, diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 74682c7ab9c..c4796cd7692 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -1014,7 +1014,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | validation | seeded 2026-08-01 (#4488). The ADR-0020 carrier: the evaluator honors active/events/priority/severity/type/condition/message (the zod header's "only reads type/condition/…" prose is STALE — trust the ledger). Dead 3 = label/description/tags, declared governance metadata, kept unmarked. Union walk boundary recorded: only base + `script` keys walked; per-variant keys are governed by the evaluator's tests, not ledger rows. **No longer a registered metadata kind** — #4509 retired it under ADR-0088 (a standalone rule had no object-binding key and every variant is `.strict()`, so it bound to nothing and gated no write; a state machine authored that way saved cleanly and did nothing). The rule VOCABULARY is untouched and fully live via `object.validations[]`, so the ledger keeps governing it through the gate's spec-only override, alongside `webhook` and `query`. The contrast with the two bridges in the same batch is the point: enforce-or-remove picked ENFORCE where the feature existed and only the wiring was missing, and REMOVE where the shape itself could not carry the feature | | api | seeded 2026-08-04 (#5271, part of #5206; PR #5312) — **not a metadata type until that same change made it one**, which is the row's point: governance and registration landed together, the treatment `datasource` did not get (#4487) and paid for with six inert keys found by hand. What #5206 measured before the fix: `api` was in neither `DEFAULT_METADATA_TYPE_REGISTRY` nor `BUILTIN_METADATA_TYPE_SCHEMAS`, so `saveMetaItem`'s `resolveOverlaySchema('api', …)` → `getMetadataTypeSchema('api')` returned `undefined` and took its own documented branch — an unregistered type is stored **unvalidated** — while `getMetaTypes()` could not enumerate the type at all, so Studio rendered neither list nor form. That issue names the shape precisely and it is the inverse of this ledger's usual one: **enforced but undeclared** (the matcher was already indexing these entries, #5089), where `dead` is declared-but-unenforced. The seeding pass classified 27 keys — live 25 / planned 2 / dead 0 — each cited `file:line` at the consumer layer that reads it: the MATCHER (`packages/metadata/src/endpoint-matcher.ts`) indexes `name`/`path`/`method`; the EXECUTOR (`packages/runtime/src/endpoint-executor.ts`) dispatches on `type` and reads `target`/`objectParams`; the POLICY chain (`packages/runtime/src/endpoint-policy.ts` + `security/inbound-rate-limit.ts`) enforces `authRequired`/`rateLimit`/`cacheTtl`; the MAPPING layer (`packages/runtime/src/api-mapping.ts`) applies `inputMapping`/`outputMapping`; and OpenAPI enrichment (`packages/rest/src/openapi-endpoints.ts`) emits `summary`/`description`. Timing was the reason it was cheap: #5040's E-series had built every one of those consumers and all of it was on main, so each key had a real evidence path rather than a promise. **Planned 2 = `inputMapping.transform` + `outputMapping.transform`, and `planned` rather than `dead` is load-bearing**: `dead` here means parsed with no consumer — a silent no-op — and these are the opposite, parsed and then LOUDLY REFUSED at publish (`endpoint-publish-gate.ts` mappingGate) and again at runtime, because no transformation-function registry exists anywhere in the platform. An author who writes one is told so and told what to do instead, so there is nothing for enforce-or-remove to chase; they stay in the vocabulary because admitting them needs a function registry **and** a sandbox ruling (#5040 §3.4), which is a design decision, not a key to quietly delete. Zero dead | | capability | seeded 2026-08-08 (#5961; commit ae31a1912) — `CapabilityDeclarationSchema`, the DECLARATION side of ADR-0066 D1's three-way separation: packages DEFINE a capability, permission sets GRANT it via `systemPermissions`, resources REQUIRE it via `requiredPermissions`. **The gate's 12 and the seeding PR's 5 are the same measurement at two granularities** — commit ae31a1912 call-graph-closed **5 authorable properties**, every one to a real reader in `packages/plugins/plugin-security/src/bootstrap-declared-capabilities.ts` (the one consumer that turns a declaration into a `sys_capability` row), all `live`, with no `PENDING_GOVERNANCE` debt recorded; the other 7 are the ADR-0010 protection-envelope keys the gate auto-classifies `live` and which carry `null` verdicts in the file, exactly as on `permission`/`position`. The same worked example as `api` above and commit ae31a1912 says so in those words — **enforced but undeclared**, the mirror of the hole #5271 closed. What #5961 measured: absent from `DEFAULT_METADATA_TYPE_REGISTRY`, `BUILTIN_METADATA_TYPE_SCHEMAS` and `HAND_CRAFTED_SCHEMAS`, so `isRuntimeCreateAllowed()` took its no-static-entry fallback (permanently true) and `saveMetaItem` its no-schema branch — `PUT /api/v1/meta/capability/:name` accepted **arbitrary JSON** onto an authorization surface whose names `systemPermissions`/`requiredPermissions` resolve by string, while `/meta/types` synthesised a false `allowRuntimeCreate: true` descriptor Studio drew a raw-JSON create form from. #5870 did not open that path (the write gate reads the registry, not the item store); it only made the type visible in `getMetaTypes()`, and both the issue and this row say so to stop the next reader filing it as a regression. Landed as ruling A on ADR-0066 D1's own authority: `allowRuntimeCreate: false` **and** `allowEnvironmentOverlay: false`, the second self-judged inside the ruling's rationale and flagged for veto — a tenant overlay of a package declaration would lift `scope` from `org` to `platform`, which is the one field on this type that is an escalation rather than display. Its reverse verification is worth copying: deleting the registry entry gave 7 red / 3 green and measured something **sharper than predicted** — a garbage payload turned 422 rather than resolving, i.e. the schema binding is a real second line of defence behind the registry row, not a restatement of it; deleting the schema binding alone gave exactly 3 red. `packageId` is the one key that reads oddly: deliberately a FALLBACK, not the primary, since #5870 added `capabilities` to the ObjectQL stamped-collection list so `_packageId` now reaches a declaration and wins — it stays `live` because the fallback branch still decides materialization for any declaration arriving unstamped. Zero dead | -| manifest | seeded 2026-08-23 (commit 3fc606687) — **not a metadata type and not a stack collection either**, which is the row's whole point. `ManifestSchema` (`packages/spec/src/kernel/manifest.zod.ts:132`) is what an author writes as `objectstack.config.ts` or a packaged manifest; it is parsed at `packages/objectql/src/registry.ts:2950` and by `os plugin build`, and it sat outside the ratchet's universe entirely — `GOVERNED` listed no `plugin`/`manifest`/`package`, `SPEC_ONLY_SCHEMAS` covered only webhook/query/validation/qa, `PENDING_GOVERNANCE` was empty so the gate reported itself **complete**, and `liveness/` had no file for it. A ratchet extended only to unregistered KINDS would not have reached it either: the retired-key entry `17.kernel__Manifest__loading.ts` records that `PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry, so a manifest is never walked as a stack collection member. That blind spot was paid for twice, by hand and after the fact — `loading`'s ten inert keys (#4914, one of them `sandboxing`, which isolated nothing while looking like isolation) and the `contributes` census (recorded in commit be21955ba), which found exactly ONE reader of the 11-member block monorepo-wide. Dead 19 = the ten dead `contributes` members (`kinds` is the sole live one — `engine.ts:4504` → `registerKind`), the five `capabilities.*` and two `configuration.*` keys (all three containers have ZERO reads of the container itself, so no key beneath one can be read), plus `extensions` and the tombstoned `loading` whose row must stay because `retiredKey()` keeps the key in the walked shape. **`integrity`, the security-shaped key that stood `dead` beside them, is `live` since #27123**: the publisher computes the map at build and self-checks it at the `os plugin publish` preflight (#13464), and `os plugin install` re-verifies it at unpack (ADR-0025 §3.5 step 5), refusing the install by name before it writes anything. `runtime` — the ADR-0025 §3.6 trust tier, the other security-shaped key, read locally only by two CLI lines that ECHO the value with no `runtime === 'sandbox'` branch anywhere, while `loading`'s own tombstone used to redirect upgrading authors to it as something "which [is] enforced" — was the family's SPLIT verdict and the ledger's first `live-elsewhere` row (#13483): #12400 measured the cloud leg (cloud @15f55df, 2026-08-29) and found a real consumer — the marketplace publish gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier — so the maintainer ruling of 2026-08-30 (executed by commit a9ee98992) took "say it truthfully" over retirement, and retirement is ruled OUT because deleting the key would tear out that gate's input. The tombstone, both `runtime` describes and the D3 entry state the split (publish-gate enforced; load-side NOT implemented); the row's verdict now says it as data rather than as a qualifying note, under the elsewhere criteria (foreign pointer + cross-repo scope + attestation with a 180d expiry), and load-side enforcement is a v18 direction rather than a removal. **#27123 flipped it to `live`**: the boot-time loader for plugins `os plugin install` installed dispatches on the tier before it imports anything (`node` loads in process and is not isolation; `sandbox` / `worker` are refused by name until ADR-0025 M3; an undeclared tier is refused), both `runtime` describes say so, and the row cites that consumer. The account above is kept as the worked example of `live-elsewhere`, which no row holds today. The `contributes` dispositions have since MOVED: the cloud leg was measured CLEAN 2026-08-24 (recorded in commit be21955ba; cloud `origin/main` @ 5b5925a, zero `manifest.contributes` reads, controls held), and commit be21955ba then executed the retirement — the nine mechanically-dead members are `retiredKey()` tombstones (D3 `plugin-manifest-contributes-dead-members-retired`), their rows staying because a tombstone keeps the key in the walked shape. Commit bc56e1881 then executed the second retirement too (ruled B 2026-08-22; D3 `plugin-manifest-contributes-routes-retired`), tombstoning `routes` and leaving `kinds` the block's sole live member. The remaining non-`contributes` `dead` rows keep their three-repo census verdicts as an enforce-or-remove worklist, not a licence to delete. `permissions` stays `live` on corrected evidence after its legacy flat-list arm was retired (ADR-0049, conversion `manifest-permissions-string-list-removed`): the block is the structured ADR-0025 one alone, its measured consumer is objectui's install-consent disclosure (it refuses nothing at load), and the `collectDeclaredSuggestions` evidence it used to cite was the assembled-stage `PermissionSet[]` reader, which only reports an authoring-stage value as skipped | +| manifest | seeded 2026-08-23 (commit 3fc606687) — **not a metadata type and not a stack collection either**, which is the row's whole point. `ManifestSchema` (`packages/spec/src/kernel/manifest.zod.ts:132`) is what an author writes as `objectstack.config.ts` or a packaged manifest; it is parsed at `packages/objectql/src/registry.ts:2950` and by `os plugin build`, and it sat outside the ratchet's universe entirely — `GOVERNED` listed no `plugin`/`manifest`/`package`, `SPEC_ONLY_SCHEMAS` covered only webhook/query/validation/qa, `PENDING_GOVERNANCE` was empty so the gate reported itself **complete**, and `liveness/` had no file for it. A ratchet extended only to unregistered KINDS would not have reached it either: the retired-key entry `17.kernel__Manifest__loading.ts` records that `PLURAL_TO_SINGULAR` has no `packages`/`plugins` entry, so a manifest is never walked as a stack collection member. That blind spot was paid for twice, by hand and after the fact — `loading`'s ten inert keys (#4914, one of them `sandboxing`, which isolated nothing while looking like isolation) and the `contributes` census (recorded in commit be21955ba), which found exactly ONE reader of the 11-member block monorepo-wide. The dead set, partitioned so every row is counted exactly once: the ten dead `contributes` members (`kinds` is the sole live one — `engine.ts:4504` → `registerKind`), plus the three retired containers `capabilities`, `configuration` and `extensions`, ONE tombstone row each — the containers had ZERO reads of the container itself, so no key beneath one could be read, and the per-child rows (five under `capabilities`, two under `configuration`) left the ledger with the drilled shape when `retiredKey()` replaced it. That sums to 13, the dead count the generated `state-counts/manifest.md` shard carries. ⚠️ This cell used to say "Dead 19": that number counted the seven per-child rows instead of their two container rows, and `loading`, whose row LEFT the ledger when its retirement kit aged out (#26599: the key is no longer in the walked shape, so a surviving row would read as an ORPHAN) — a hand-typed count that outlived both moves, which is why the count is the shard's and never this cell's. **`packaging` is this type's only `planned` row (#27679, ADR-0025 v2 §3.3)**: `bundled` is the one strategy and the only value an author may write, the retired `manifest-deps` is refused BY NAME with its migration prescription at every `ManifestSchema` door, and `os plugin build` reads the key only to echo it on its `Loaded` line — the artifact is the same with the key or without it. Not `live`, because nothing dispatches on the value any more; not `dead`, because the refusal is real; and it flips back to `live` the day `PluginPackagingSchema` gains a second member the build branches on. **`integrity`, the security-shaped key that stood `dead` beside them, is `live` since #27123**: the publisher computes the map at build and self-checks it at the `os plugin publish` preflight (#13464), and `os plugin install` re-verifies it at unpack (ADR-0025 §3.5 step 5), refusing the install by name before it writes anything. `runtime` — the ADR-0025 §3.6 trust tier, the other security-shaped key, read locally only by two CLI lines that ECHO the value with no `runtime === 'sandbox'` branch anywhere, while `loading`'s own tombstone used to redirect upgrading authors to it as something "which [is] enforced" — was the family's SPLIT verdict and the ledger's first `live-elsewhere` row (#13483): #12400 measured the cloud leg (cloud @15f55df, 2026-08-29) and found a real consumer — the marketplace publish gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier — so the maintainer ruling of 2026-08-30 (executed by commit a9ee98992) took "say it truthfully" over retirement, and retirement is ruled OUT because deleting the key would tear out that gate's input. The tombstone, both `runtime` describes and the D3 entry state the split (publish-gate enforced; load-side NOT implemented); the row's verdict now says it as data rather than as a qualifying note, under the elsewhere criteria (foreign pointer + cross-repo scope + attestation with a 180d expiry), and load-side enforcement is a v18 direction rather than a removal. **#27123 flipped it to `live`**: the boot-time loader for plugins `os plugin install` installed dispatches on the tier before it imports anything (`node` loads in process and is not isolation; `sandbox` / `worker` are refused by name until ADR-0025 M3; an undeclared tier is refused), both `runtime` describes say so, and the row cites that consumer. The account above is kept as the worked example of `live-elsewhere`, which no row holds today. The `contributes` dispositions have since MOVED: the cloud leg was measured CLEAN 2026-08-24 (recorded in commit be21955ba; cloud `origin/main` @ 5b5925a, zero `manifest.contributes` reads, controls held), and commit be21955ba then executed the retirement — the nine mechanically-dead members are `retiredKey()` tombstones (D3 `plugin-manifest-contributes-dead-members-retired`), their rows staying because a tombstone keeps the key in the walked shape. Commit bc56e1881 then executed the second retirement too (ruled B 2026-08-22; D3 `plugin-manifest-contributes-routes-retired`), tombstoning `routes` and leaving `kinds` the block's sole live member. The remaining non-`contributes` `dead` rows keep their three-repo census verdicts as an enforce-or-remove worklist, not a licence to delete. `permissions` stays `live` on corrected evidence after its legacy flat-list arm was retired (ADR-0049, conversion `manifest-permissions-string-list-removed`): the block is the structured ADR-0025 one alone, its measured consumer is objectui's install-consent disclosure (it refuses nothing at load), and the `collectDeclaredSuggestions` evidence it used to cite was the assembled-stage `PermissionSet[]` reader, which only reports an authoring-stage value as skipped | | crud_endpoints | seeded 2026-09-02 (commit a3d5724c8) — one of the FOUR `RestServerConfig` sub-objects, and the family that made the `SPEC_ONLY_SCHEMAS` boundary explicit: SERVER CONFIGURATION. An author writes `RestServerConfigSchema` (`packages/spec/src/api/rest-server.zod.ts`) as the REST server's construction argument — not a metadata item, not a request body, not a manifest — so no registry has ever held it and no ratchet rooted in one could ask who reads it. Rooted on the four sub-schemas rather than on the whole config on purpose: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with `RestServerConfigSchema` as the root the sub-objects would BE the drilled level and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row of their own, their container's blanket `live` silently covering a dead key — #4956's shape, in the ledger written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is now enrolled SEPARATELY as `rest_api` (#14640, 2026-09-21): when these four landed its consumption seam was still validate-only, so a census of it would have recorded a half that was about to move — that half has moved (`normalizeConfig` builds the `api` block from its parsed output, released), so the fence expired and the fifth sub-object was measured on the settled seam. ⛔ Its ledger is `rest_api.json`, never `api.json`, which is a different surface entirely. **What #11984 settled and what it did not**: that PR made `RestServer.normalizeConfig` PARSE and CONSUME these four instead of casting them, so an out-of-enum or out-of-range value is now refused at construction — accept/reject. Executing a declared contract does not give a key a consumer, and this family is that distinction's worked example. Dead 5 = `objectParamStyle` (every CRUD route takes the object name as a PATH segment; `'query'` is validated against the enum and mounts exactly what `'path'` mounts) and the four members of `patterns` (custom URL patterns are normalized and never read — every route is mounted from hard-coded method/path pairs in `registerCrudEndpoints`). Live 6 = the five `operations.*` switches, each gating a route mount, and `dataPrefix`, which has five independent consumers and moves the mounted paths and the advertised discovery document together. A different question was open about `patterns` (its `z.record` input type demands all five operations) — a declaration defect, not a liveness one, not re-derived here **Commit b3a63d32c RETIRED the dead set (2026-09-03, ADR-0049 enforce-or-remove)**: `patterns` and `objectParamStyle` are `retiredKey()` tombstones (the schema is a non-strict `z.object`, so the rows STAY `dead` with a REMOVED note — the rls.priority precedent) and the four `patterns.*` child rows collapse into the one `patterns` row because the value def `CrudEndpointPattern` left the shape with its key (RETIRED_DEFS_BY_MAJOR[18]). REMOVE, not ENFORCE, because the mounted CRUD paths are the contract the client SDK, the discovery document and /openapi.json all describe — a per-operation pattern knob could only make them lie — and an endpoint on a custom path is a declarative `api` endpoint. `evidenceScope` widened to `cross-repo` on the cloud reading (#14796 @9b6abe0f2fd5: zero, structural). The `z.partialRecord` question closes with the record | | metadata_endpoints | seeded 2026-09-02 (commit a3d5724c8) — one of the FOUR `RestServerConfig` sub-objects, and the family that made the `SPEC_ONLY_SCHEMAS` boundary explicit: SERVER CONFIGURATION. An author writes `RestServerConfigSchema` (`packages/spec/src/api/rest-server.zod.ts`) as the REST server's construction argument — not a metadata item, not a request body, not a manifest — so no registry has ever held it and no ratchet rooted in one could ask who reads it. Rooted on the four sub-schemas rather than on the whole config on purpose: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with `RestServerConfigSchema` as the root the sub-objects would BE the drilled level and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row of their own, their container's blanket `live` silently covering a dead key — #4956's shape, in the ledger written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is now enrolled SEPARATELY as `rest_api` (#14640, 2026-09-21): when these four landed its consumption seam was still validate-only, so a census of it would have recorded a half that was about to move — that half has moved (`normalizeConfig` builds the `api` block from its parsed output, released), so the fence expired and the fifth sub-object was measured on the settled seam. ⛔ Its ledger is `rest_api.json`, never `api.json`, which is a different surface entirely. **What #11984 settled and what it did not**: that PR made `RestServer.normalizeConfig` PARSE and CONSUME these four instead of casting them, so an out-of-enum or out-of-range value is now refused at construction — accept/reject. Executing a declared contract does not give a key a consumer, and this family is that distinction's worked example. Dead 2 = `cacheTtl` and `endpoints.schema`. `enableCache` is live and `cacheTtl` is not, which is the pair worth reading together: the cached branch delegates to the protocol's `getMetaItemCached`, whose signature takes no TTL, and no cache header anywhere is built from this value. Its negative-bound observation travels in that row by triage ruling rather than as a separate defect — the schema declares `z.number().int()` with no lower bound, so `-1` is accepted, and #11984 pins it as accepted because that is what the contract says. `endpoints.schema` is the sharpest case in the family for per-key rows: its three siblings each gate a route mount and it gates nothing, because `GET /meta/:type/:name/schema` does not exist — `packages/rest/src` mounts no path ending in `/schema` at all **Commit b3a63d32c RETIRED both (2026-09-03, ADR-0049)**: `cacheTtl` and `endpoints.schema` are `retiredKey()` tombstones, rows kept `dead` with a REMOVED note. The negative-bound observation dies with `cacheTtl` (its #11984 acceptance pin is reversed to a refusal pin); `endpoints.schema` had no route to gate, so there was nothing to enforce. `evidenceScope` widened to `cross-repo` (#14796) | | batch_endpoints | seeded 2026-09-02 (commit a3d5724c8) — one of the FOUR `RestServerConfig` sub-objects, and the family that made the `SPEC_ONLY_SCHEMAS` boundary explicit: SERVER CONFIGURATION. An author writes `RestServerConfigSchema` (`packages/spec/src/api/rest-server.zod.ts`) as the REST server's construction argument — not a metadata item, not a request body, not a manifest — so no registry has ever held it and no ratchet rooted in one could ask who reads it. Rooted on the four sub-schemas rather than on the whole config on purpose: the walk drilled exactly ONE level when this was rooted (it recurses as of #17424; the rooting stands), so with `RestServerConfigSchema` as the root the sub-objects would BE the drilled level and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row of their own, their container's blanket `live` silently covering a dead key — #4956's shape, in the ledger written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is now enrolled SEPARATELY as `rest_api` (#14640, 2026-09-21): when these four landed its consumption seam was still validate-only, so a census of it would have recorded a half that was about to move — that half has moved (`normalizeConfig` builds the `api` block from its parsed output, released), so the fence expired and the fifth sub-object was measured on the settled seam. ⛔ Its ledger is `rest_api.json`, never `api.json`, which is a different surface entirely. **What #11984 settled and what it did not**: that PR made `RestServer.normalizeConfig` PARSE and CONSUME these four instead of casting them, so an out-of-enum or out-of-range value is now refused at construction — accept/reject. Executing a declared contract does not give a key a consumer, and this family is that distinction's worked example. Dead 2 = `operations.upsertMany` and `defaultAtomic`. `upsertMany` is `endpoints.schema`'s twin — a switch declared for a route that was never built (`this.protocol` carries `createManyData` / `updateManyData` / `deleteManyData` and no upsert counterpart), so `false` disables nothing. `defaultAtomic` promises a transaction default that no batch handler consults. Live 5 = `maxBatchSize` (load-bearing since #11984 gave it a real parse — before that a configured `0` was the live cap, because `0` is not nullish), `enableBatchEndpoint`, and the three `operations.*` switches that do gate a mount **Commit b3a63d32c RETIRED both (2026-09-03, ADR-0049)**: `operations.upsertMany` and `defaultAtomic` are `retiredKey()` tombstones, rows kept `dead` with a REMOVED note. `defaultAtomic` is the family's worked enforce-or-remove call: the per-request `options.atomic` (ADR-0119 D4, opt-in) IS the contract, and a server default that flipped it silently is the move that ADR refused, so the key was removed rather than wired; upsert lives on as an operation type of the generic batch endpoint. `evidenceScope` widened to `cross-repo` (#14796) |