diff --git a/.changeset/liveness-rest-server-config-sub-objects.md b/.changeset/liveness-rest-server-config-sub-objects.md new file mode 100644 index 0000000000..1251c1027b --- /dev/null +++ b/.changeset/liveness-rest-server-config-sub-objects.md @@ -0,0 +1,53 @@ +--- +"@objectstack/spec": patch +--- + +chore(spec): govern the four `RestServerConfig` sub-objects in the liveness ledger (#14369) + +The `liveness/` ledgers ship inside this package's npm tarball (they are named in +`files`), so this is a published-data change even though no runtime behaviour +moves, no schema key changes spelling, and `packages/spec/src/api/rest-server.zod.ts` +is not edited at all. + +Four new ledger files — `crud_endpoints.json`, `metadata_endpoints.json`, +`batch_endpoints.json`, `route_generation.json` — classify all 32 authorable +properties of `CrudEndpointsConfigSchema`, `MetadataEndpointsConfigSchema`, +`BatchEndpointsConfigSchema` and `RouteGenerationConfigSchema`, the four +`RestServerConfig` sub-objects a host writes when it constructs the REST server. +They are enrolled through the gate's `SPEC_ONLY_SCHEMAS` override, the route +`query` / `qa` / `manifest` already take: server configuration is neither a +metadata item nor a request body nor a manifest, so no registry has ever held it +and no ratchet rooted in one could ask who reads it. + +Seventeen properties are `live` with a symbol-anchored consumer and a producer +pointer at the normalizer that threads the authored value into `this.config`. +Fifteen are `dead` — the ten keys the census filed with this card measured, with +the two container keys (`crud.patterns`, `routes.overrides`) expanded into a row +per member. `routes` is dead entire: `excludeObjects: ['sys_log']` excludes +nothing and `nameTransform: 'plural'` still mounts every route under the raw +object name. `metadata.endpoints.schema` and `batch.operations.upsertMany` are +switches for routes that were never built — no path ending in `/schema` is +mounted anywhere in `packages/rest/src`, and the protocol has no `upsertManyData` +counterpart to its three sibling batch methods. + +What this records, and what it deliberately does not. #11984 made +`RestServer.normalizeConfig` PARSE and CONSUME these four sub-objects instead of +casting them, so an out-of-enum or out-of-range value is now refused at +construction. That settles accept/reject and nothing else: executing a declared +contract does not give a key a consumer. No key is removed, enforced, deprecated +or re-described here. The enforce-or-remove call per dead key (ADR-0049) is a +follow-up on the human floor — the enforce route is a feature per key, and +`routes.excludeObjects` is advertised in `RestServerConfigSchema`'s own +`@example`, which makes its removal a capability retirement rather than a cleanup. + +Rooted on the four sub-schemas rather than on `RestServerConfigSchema` itself, +which is measurement rather than taste: the ledger walk drills exactly ONE level, +so with the whole config 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` (three of four members gate a real +route mount) silently covering a dead key, which is the #4956 shape in the file +written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is not +enrolled: its consumption seam is still validate-only and is the subject of its +own card, so a census of it would record a half that is about to move. + + diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 3e97d86541..0fce0ebcf6 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -780,7 +780,7 @@ The governed set is `GOVERNED` at the top of `check-liveness.mts`. To add a type RecordDetailView had been gating the History tab on it the whole time (#2707). 4. Add the type to `GOVERNED`; confirm the gate is green. -## Current state — 31 governed types (complete registry coverage) +## Current state — 35 governed types (complete registry coverage) > **This heading is now checked** (#7257). `check:liveness` reconciles the table > against `GOVERNED` in both directions — a governed type with no row fails, a row @@ -893,6 +893,10 @@ marker where the Notes cell goes, never a guess at what belongs there. | 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; PR #6540) — `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** — PR #6540 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 PR #6540 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** `allowOrgOverride: 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 (#10728) — **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 (#10627), which found exactly ONE reader of the 11-member block monorepo-wide. Dead 20 = 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`, `integrity`, and the tombstoned `loading` whose row must stay because `retiredKey()` keeps the key in the walked shape. **`integrity` is security-shaped and not retired here**: it declares per-file digests the runtime is documented to re-verify at unpack while nothing computes or checks them (#11331 — cloud persists the map, nothing verifies it). `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" — is 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 (#11330) 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. The `contributes` dispositions have since MOVED: the cloud leg was measured CLEAN 2026-08-24 (#10812; cloud `origin/main` @ 5b5925a, zero `manifest.contributes` reads, controls held), and #10724 then executed — 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. #10726 then executed 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 | +| crud_endpoints | seeded 2026-09-02 (#14369) — 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 drills exactly ONE level, 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 deliberately not enrolled: its consumption seam is still validate-only and is the subject of its own card, so a census of it would record a half that is about to move. **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. #14365 asks a different question about `patterns` (its `z.record` input type demands all five operations) — a declaration defect, not a liveness one, not re-derived here | +| metadata_endpoints | seeded 2026-09-02 (#14369) — 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 drills exactly ONE level, 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 deliberately not enrolled: its consumption seam is still validate-only and is the subject of its own card, so a census of it would record a half that is about to move. **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 | +| batch_endpoints | seeded 2026-09-02 (#14369) — 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 drills exactly ONE level, 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 deliberately not enrolled: its consumption seam is still validate-only and is the subject of its own card, so a census of it would record a half that is about to move. **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 | +| route_generation | seeded 2026-09-02 (#14369) — 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 drills exactly ONE level, 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 deliberately not enrolled: its consumption seam is still validate-only and is the subject of its own card, so a census of it would record a half that is about to move. **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 6 = every key it has, and that is the finding: `routes` is parsed, defaulted and normalized into `this.config.routes`, and nothing ever reads it back. `excludeObjects: ['sys_log']` excludes nothing, `nameTransform: 'plural'` still mounts every route under the raw object name, and the per-object `overrides` record (drilled to `enabled` / `basePath` / `operations`) turns nothing on or off. ⚠️ The `overrides` hits in `packages/rest/src` are a REQUEST BODY and a test builder — different keys with the same name. This is the one member of the family with a customer-visible limb: `RestServerConfigSchema`'s own `@example` advertises `routes: { excludeObjects: ['system_log'] }`, so the published prose promises a capability the runtime does not deliver (Prime Directive #10). Fixing that example belongs to whichever enforce-or-remove limb the key lands on — `routes.*` reads as designed-but-never-wired, so enforcing it is real work in route generation that changes the mounted surface, and no dev agent decides that | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every misleading entry carries `authorWarn` so authors hear about it at compile time diff --git a/packages/spec/liveness/batch_endpoints.json b/packages/spec/liveness/batch_endpoints.json new file mode 100644 index 0000000000..7cda6f0dc7 --- /dev/null +++ b/packages/spec/liveness/batch_endpoints.json @@ -0,0 +1,62 @@ +{ + "type": "batch_endpoints", + "_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.", + "props": { + "maxBatchSize": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`const maxBatch = batch.maxBatchSize ?? 200` — the cap every batch request is measured against)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish." + }, + "enableBatchEndpoint": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (batch.enableBatchEndpoint && this.protocol.batchData)` gates the generic POST /data/:object/batch mount)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed." + }, + "operations": { + "children": { + "createMany": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.createMany && this.protocol.createManyData)` gates the POST /data/:object/createMany mount)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Gates a mount." + }, + "updateMany": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.updateMany && this.protocol.updateManyData)` gates the POST /data/:object/updateMany mount)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Gates a mount." + }, + "deleteMany": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.deleteMany && this.protocol.deleteManyData)` gates the POST /data/:object/deleteMany mount)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Gates a mount." + }, + "upsertMany": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. Its three siblings each gate a route mount; this one gates nothing, because there is no upsertMany route to gate — the switch was declared for a batch operation that was never built (`this.protocol` has createManyData / updateManyData / deleteManyData and no upsert counterpart). `operations.upsertMany: false` therefore disables nothing and `true` enables nothing." + } + } + }, + "defaultAtomic": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. The key is normalized into `this.config.batch.defaultAtomic` and no batch handler consults it, so `batch.defaultAtomic: false` changes no batch's transaction mode. A switch whose describe() promises a transaction default while the transaction default is decided elsewhere is the false-compliance shape this ledger exists to surface, which is why it is recorded rather than left to a reader's grep." + } + } +} diff --git a/packages/spec/liveness/crud_endpoints.json b/packages/spec/liveness/crud_endpoints.json new file mode 100644 index 0000000000..32bb370d82 --- /dev/null +++ b/packages/spec/liveness/crud_endpoints.json @@ -0,0 +1,92 @@ +{ + "type": "crud_endpoints", + "_note": "CrudEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#CrudEndpointsConfigSchema, the `crud` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `operations.*` and `dataPrefix` gate and shape the mounted CRUD surface, while `patterns` and `objectParamStyle` are normalized and never read. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.", + "props": { + "operations": { + "children": { + "create": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`if (operations.create)` gates the POST data route mount — an author who sets it false gets no create route at all)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Per key, not per object: since ADR-0122 `crud.operations` is the author state, so the parse applies each `.default(true)` whenever the object is present and normalizeConfig covers the one case the schema leaves open (an absent `operations` object)." + }, + "read": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`if (operations.read)` gates the GET-by-id route mount)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Same shape as `create`." + }, + "update": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`if (operations.update)` gates the PATCH route mount)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Same shape as `create`." + }, + "delete": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`if (operations.delete)` gates the DELETE route mount)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Same shape as `create`." + }, + "list": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`if (operations.list)` gates the list route mounts)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Same shape as `create`." + } + } + }, + "patterns": { + "children": { + "method": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. The `patterns` record is normalized into `this.config.crud.patterns` and never read: every CRUD route is mounted from the hard-coded method/path pairs in rest-server.ts#registerCrudEndpoints, so a custom pattern changes nothing. The four rows here are that one container's members — `patterns` is dead as a whole, and each member is recorded so the verdict is falsifiable per key rather than inherited. Related: #14365 asks a different question about this same key (its z.record input type demands all five operations); that is a declaration defect, not a liveness one, and is not re-derived here." + }, + "path": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. See `patterns.method`: the container is never read, so a custom URL path pattern is accepted, validated and ignored." + }, + "summary": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. See `patterns.method`. Additionally documentation-shaped: nothing in the served OpenAPI document reads a configured per-operation summary — that document is the pre-generated @objectstack/spec contract enriched at request time." + }, + "description": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. See `patterns.summary`." + } + } + }, + "dataPrefix": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`const dataPath = \\`${basePath}${crud.dataPrefix}\\`` — the base every data route is mounted under); also packages/rest/src/rest-server.ts#registerDataActionEndpoints, packages/rest/src/rest-server.ts#registerSharingEndpoints and packages/rest/src/rest-server.ts#registerBatchEndpoints (each builds its own dataPath the same way), and packages/rest/src/rest-server.ts#registerDiscoveryEndpoints (advertises it as `routes.data`)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Five independent consumers, so this key is live in the strong sense: changing it moves the mounted paths AND the advertised discovery document together." + }, + "objectParamStyle": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. Every CRUD route takes the object name as a PATH segment; `'query'` is accepted, validated against the enum since #11984, and mounts exactly what `'path'` mounts. Enforcing it would be a second routing shape for every data route, which is why the call is a follow-up and not a ledger decision." + } + } +} diff --git a/packages/spec/liveness/metadata_endpoints.json b/packages/spec/liveness/metadata_endpoints.json new file mode 100644 index 0000000000..ec9aa34185 --- /dev/null +++ b/packages/spec/liveness/metadata_endpoints.json @@ -0,0 +1,70 @@ +{ + "type": "metadata_endpoints", + "_note": "MetadataEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema, the `metadata` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `prefix`, `enableCache`, `maskObjectFields` and three of the four `endpoints.*` switches are read; `cacheTtl` and `endpoints.schema` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.", + "props": { + "prefix": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (`const metaPath = \\`${basePath}${metadata.prefix}\\`` — the base every metadata route is mounted under, and the string the _migrate-stored audit line reports); also packages/rest/src/rest-server.ts#registerDiscoveryEndpoints (advertises it as `routes.metadata`)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Live in the same strong sense as `crud.dataPrefix`: it moves the mounted paths and the advertised discovery document together." + }, + "enableCache": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (`if (metadata.enableCache && p.getMetaItemCached ...)` selects the cached read path; the uncached branch is the default when it is false)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "The key selects between two real read paths, so authoring it changes behaviour rather than only a header." + }, + "cacheTtl": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. `enableCache` is live and `cacheTtl` is not: the cached branch delegates to the protocol's own `getMetaItemCached`, which does not take a TTL from this config, and no ETag / Cache-Control / Last-Modified header is built from this value anywhere. `metadata.cacheTtl: 60` changes no header and no cache lifetime. Travelling with this row rather than as a separate defect (triage ruling, 2026-09-02): the schema declares `z.number().int()` with no lower bound, so a NEGATIVE TTL is accepted — #11984 pins `-1` as accepted precisely because that is what the contract says. If this key survives its enforce-or-remove call, that bound is part of enforcing it; if it goes, the pin goes with it. Either way it is one key's story, not two." + }, + "maskObjectFields": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#resolveObjectMasker (`if (metaType !== 'object' || !this.config.metadata.maskObjectFields)` — false serves the full object schema to every authenticated caller, ADR-0106 D8)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Security-shaped and genuinely enforced: the opt-out reaches the masker, and `isObjectSchemaMaskingEnabled` also honours the OS_ALLOW_UNMASKED_OBJECT_METADATA deployment escape hatch that the runtime /metadata dispatcher shares." + }, + "endpoints": { + "children": { + "types": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (`if (metadata.endpoints.types !== false)` gates the GET /meta route mount)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Read as `!== false`, so an author who omits the key gets the route; setting it false removes the mount." + }, + "items": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (`if (metadata.endpoints.items !== false)` gates four separate list-of-type route mounts)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Same `!== false` shape as `types`." + }, + "item": { + "status": "live", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (`if (metadata.endpoints.item !== false)` gates the GET /meta/:type/:name route mount)", + "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", + "note": "Same `!== false` shape as `types`." + }, + "schema": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. The odd one out of the four: its three siblings each gate a route mount and this one gates nothing, because the route its describe() names — `GET /meta/:type/:name/schema` — does not exist. packages/rest/src mounts no path ending in `/schema` at all, so `endpoints.schema: false` removes nothing and `true` adds nothing; the switch was declared for a route that was never built, the same shape as `batch.operations.upsertMany`. This is also the sharpest case in the family for why this container may not carry one blanket verdict: three live members and one dead one under a single `endpoints` key would have hidden exactly this." + } + } + } + } +} diff --git a/packages/spec/liveness/route_generation.json b/packages/spec/liveness/route_generation.json new file mode 100644 index 0000000000..5290eb1161 --- /dev/null +++ b/packages/spec/liveness/route_generation.json @@ -0,0 +1,46 @@ +{ + "type": "route_generation", + "_note": "RouteGenerationConfigSchema — packages/spec/src/api/rest-server.zod.ts#RouteGenerationConfigSchema, the `routes` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Every key in this file is `dead`, and that is the finding: the whole sub-object is parsed, defaulted and normalized into `this.config.routes`, and nothing ever reads it back. `routes.excludeObjects: ['sys_log']` excludes nothing; `routes.nameTransform: 'plural'` still mounts every route under the object's raw name. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. `routes.*` in particular reads as designed-but-never-wired: enforcing it is real work in route generation and changes the mounted surface. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.", + "props": { + "includeObjects": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. Normalized into `this.config.routes.includeObjects` and never consulted: route registration iterates the registered objects with no include filter, so an author who names three objects still gets routes for all of them. Dead at the CONSUMER — the authored value arrives intact and is simply never read." + }, + "excludeObjects": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. Same shape as `includeObjects`: normalized, never consulted, so an excluded object is still mounted. This key is the one customer-visible member of the ten: RestServerConfigSchema's own `@example` advertises `routes: { excludeObjects: ['system_log'] }`, and the generated reference page (content/docs/references/api/rest-server.mdx) repeats the declaration — published prose promising a capability the runtime does not deliver. Fixing that example belongs to whichever limb this key lands on, not here." + }, + "nameTransform": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. The enum is enforced at the door since #11984 — `'snake_case'` is refused at construction — but no route builder transforms a name, so `'plural'`, `'kebab-case'` and `'camelCase'` all mount exactly what `'none'` mounts. An enum that is validated and then ignored is the clearest case in this file of accept/reject and liveness being different questions." + }, + "overrides": { + "children": { + "enabled": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. The per-object `overrides` record is normalized and never read, so no object's routes can be turned off through it. ⚠️ Do not read the `overrides` hits in packages/rest/src as consumers of this key: rest-server.ts#registerDataActionEndpoints reads `body.overrides` off a REQUEST BODY, and http-request-test-builder.ts takes an `overrides` argument — different keys with the same name." + }, + "basePath": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. Inherits the container's fate: the record is never read, so a per-object custom base path changes no mounted path. The live base path is `crud.dataPrefix` (see the crud_endpoints ledger), which is deployment-wide rather than per-object." + }, + "operations": { + "status": "dead", + "verifiedAt": "2026-09-02", + "evidenceScope": "in-repo", + "note": "0 read sites at 2514d49f3. The per-object operation switches are never read. The operation switches that ARE read are the deployment-wide `crud.operations.*` (crud_endpoints ledger) — this key is their per-object counterpart and it does nothing." + } + } + } + } +} diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index f12dc96624..48f1c005ce 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -58,4 +58,8 @@ for both corollaries. | `capability` | 12 | 0 | 0 | 0 | 0 | 12 | | `qa` | 4 | 0 | 0 | 5 | 0 | 9 | | `manifest` | 22 | 0 | 1 | 15 | 0 | 38 | -| **total** | **825** | **5** | **1** | **74** | **10** | **915** | +| `crud_endpoints` | 6 | 0 | 0 | 5 | 0 | 11 | +| `metadata_endpoints` | 6 | 0 | 0 | 2 | 0 | 8 | +| `batch_endpoints` | 5 | 0 | 0 | 2 | 0 | 7 | +| `route_generation` | 0 | 0 | 0 | 6 | 0 | 6 | +| **total** | **842** | **5** | **1** | **89** | **10** | **947** | diff --git a/packages/spec/scripts/liveness/check-liveness.mts b/packages/spec/scripts/liveness/check-liveness.mts index da6e5acad9..2433a81273 100644 --- a/packages/spec/scripts/liveness/check-liveness.mts +++ b/packages/spec/scripts/liveness/check-liveness.mts @@ -151,6 +151,12 @@ import { QuerySchema } from '../../src/data/query.zod'; import { ValidationRuleSchema } from '../../src/data/validation.zod'; import { TestSuiteSchema } from '../../src/qa/testing.zod'; import { ManifestSchema } from '../../src/kernel/manifest.zod'; +import { + BatchEndpointsConfigSchema, + CrudEndpointsConfigSchema, + MetadataEndpointsConfigSchema, + RouteGenerationConfigSchema, +} from '../../src/api/rest-server.zod'; import { BOUND_PROOF_PATHS, HIGH_RISK_CLASSES, @@ -228,7 +234,7 @@ const ledgerRoot = ledgerRootArg // Governed metadata types, rolled out highest-frequency / highest-risk first. // (`query` is not a metadata type — see SPEC_ONLY_SCHEMAS below.) -const GOVERNED = ['object', 'field', 'flow', 'action', 'hook', 'permission', 'position', 'agent', 'tool', 'skill', 'dataset', 'page', 'view', 'report', 'dashboard', 'webhook', 'query', 'datasource', 'app', 'book', 'doc', 'email_template', 'job', 'mapping', 'seed', 'translation', 'validation', 'api', 'capability', 'qa', 'manifest']; +const GOVERNED = ['object', 'field', 'flow', 'action', 'hook', 'permission', 'position', 'agent', 'tool', 'skill', 'dataset', 'page', 'view', 'report', 'dashboard', 'webhook', 'query', 'datasource', 'app', 'book', 'doc', 'email_template', 'job', 'mapping', 'seed', 'translation', 'validation', 'api', 'capability', 'qa', 'manifest', 'crud_endpoints', 'metadata_endpoints', 'batch_endpoints', 'route_generation']; // Registered metadata types that are NOT yet governed — the coverage ratchet. // @@ -325,12 +331,45 @@ const PENDING_GOVERNANCE: Record = { // asked either question, because the manifest was not in the denominator. // There is no registry to fold this one back onto — like `query` and `qa`, the // override IS its governance. +// The FOUR `RestServerConfig` sub-objects are the fourth category, and the one +// that made the override's boundary explicit: SERVER configuration. An author +// writes `RestServerConfigSchema` (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 reach it. #11984 made `RestServer.normalizeConfig` PARSE and CONSUME +// these four instead of casting them, which settles accept/reject; executing a +// declared contract is not the same as having a consumer, and the census that +// rode along with it found TEN normalized keys nothing reads (`routes` entire, +// `crud.patterns` / `crud.objectParamStyle`, `metadata.cacheTtl` / +// `metadata.endpoints.schema`, `batch.defaultAtomic` / +// `batch.operations.upsertMany`). One of them is customer-visible today: the +// schema's own `@example` advertises `routes: { excludeObjects: [...] }`, which +// is accepted, validated, normalized — and excludes nothing. +// +// WHY FOUR ROOTS AND NOT ONE. `RestServerConfigSchema` is the obvious root and +// it does not work: the walk drills exactly ONE level, so with the whole config +// as the root the four sub-objects are the drilled level and their own keys sit +// one level deeper — `metadata.endpoints.schema` and +// `batch.operations.upsertMany` would then have no row of their own, and their +// container's blanket verdict (`live` — three of four members gate a real route +// mount) would silently cover a dead key. That is #4956's shape, in the file +// written to end it. Rooting on the four sub-schemas instead puts every one of +// the ten keys at a drillable depth, so each carries its own falsifiable +// verdict. `RestApiConfigSchema` (the fifth sub-object, `api`) is deliberately +// NOT enrolled here: its consumption seam is still validate-only and is the +// subject of its own card, so a census of it would be recording a half that is +// about to move. Like `query`, `qa` and `manifest`, there is no registry to +// fold any of these back onto — the override IS their governance. const SPEC_ONLY_SCHEMAS: Record = { webhook: WebhookSchema, query: QuerySchema, validation: ValidationRuleSchema, qa: TestSuiteSchema, manifest: ManifestSchema, + crud_endpoints: CrudEndpointsConfigSchema, + metadata_endpoints: MetadataEndpointsConfigSchema, + batch_endpoints: BatchEndpointsConfigSchema, + route_generation: RouteGenerationConfigSchema, }; // ADR-0010 provenance/lock overlay fields — system-stamped, on every type; auto-live.