From 18c51e9b0205c5013bf8f51a4ec6d91d8d2bce3c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 06:32:31 +0000 Subject: [PATCH] chore(spec): govern the realtime SubscriptionSchema surface in the liveness ledger (#14446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new ledger file classifies all six authorable properties of SubscriptionSchema — the item type of RealtimeConfigSchema.subscriptions and the `Subscription` the generated API reference publishes — enrolled through the gate's SPEC_ONLY_SCHEMAS override, the route query / qa / manifest and the four RestServerConfig sub-objects already take. All six are dead, and the container is the finding: nothing outside packages/spec imports SubscriptionSchema, SubscriptionEventSchema or RealtimeConfigSchema, so no key beneath them can be read. events[].type accepts RealtimeEventType, whose four members are disjoint from the emitted DataEventType vocabulary; events[].filters is z.unknown().optional() with no shape and no consumer. No key is enforced, removed, deprecated, re-described or repointed; api/realtime.zod.ts and api/events.zod.ts are not edited. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk --- .../liveness-realtime-subscription-surface.md | 59 +++++++++++++++++++ packages/spec/liveness/README.md | 3 +- .../spec/liveness/realtime_subscription.json | 46 +++++++++++++++ packages/spec/liveness/state-counts.md | 3 +- .../spec/scripts/liveness/check-liveness.mts | 40 ++++++++++++- 5 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 .changeset/liveness-realtime-subscription-surface.md create mode 100644 packages/spec/liveness/realtime_subscription.json diff --git a/.changeset/liveness-realtime-subscription-surface.md b/.changeset/liveness-realtime-subscription-surface.md new file mode 100644 index 0000000000..186e4045cb --- /dev/null +++ b/.changeset/liveness-realtime-subscription-surface.md @@ -0,0 +1,59 @@ +--- +"@objectstack/spec": patch +--- + +chore(spec): govern the realtime `SubscriptionSchema` surface in the liveness ledger (#14446) + +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/realtime.zod.ts` is not +edited at all. + +A new ledger file — `realtime_subscription.json` — classifies all six authorable +properties of `SubscriptionSchema`, what a client declares to open a realtime +subscription: the item type of `RealtimeConfigSchema.subscriptions` and the +`Subscription` the generated API reference publishes. It is enrolled through the +gate's `SPEC_ONLY_SCHEMAS` override, the route `query` / `qa` / `manifest` and the +four `RestServerConfig` sub-objects already take. A transport-protocol surface is +neither a metadata item nor stored metadata nor a manifest, so no registry has ever +held it and no ratchet rooted in one could ask who reads it — and +`RealtimeConfigSchema` is `.passthrough()`, so nothing downstream even refuses an +unknown key. + +All six are `dead`, and the container is the finding: nothing outside +`packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or +`RealtimeConfigSchema` at all, so no key beneath them can be read. The two the +census filed with this card measured are the sharp ones. `events[].type` accepts +`RealtimeEventType`, whose four members (`record.created` / `record.updated` / +`record.deleted` / `field.changed`) are disjoint from what the engine publishes — +`DataEventType`'s `data.record.*`, with a live emitter in `service-knowledge` — so +an author who writes the enum's own `record.created` gets a subscription that +silently never fires, and the enum is what the API reference shows them. +`events[].filters` is `z.unknown().optional()`: an authorable key with no shape and +no consumer, failing in the permissive direction, since the only payload matching +the platform performs compares object name and event type. + +What this records, and what it deliberately does not. The enum's direction is +settled and the row carries the 2026-09-02 triage ruling verbatim so the next reader +does not re-open it: if the verdict is enforce it means repointing the enum, never +changing what the runtime publishes, which would break a live event contract to +satisfy a member nothing has ever used. `field.changed` is the same spelling the +sibling `DataEventType` removed in 17.0.0 under ADR-0049 (#4673, PR #4685) for +having no producer; it survives here only because this enum was never in a ratchet's +denominator. No key is removed, enforced, deprecated or re-described here — the +enforce-or-remove call per dead key is a follow-up on the human floor. + +Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema`, which is +measurement rather than taste: the ledger walk drills exactly one level, so with the +config as the root `subscriptions` would be the drilled level and `events[].type` / +`events[].filters` would have no row of their own, inheriting a container verdict — +the same reasoning that rooted the four `RestServerConfig` sub-objects separately. +`RealtimeConfigSchema`'s own three keys are not enrolled: whether enabling realtime +does anything is a different question with no census behind it yet. + +One correction the ledger records because the next reader will hit it too. The card +and its triage both name the two keys on `SubscriptionSchema`; they are declared on +`SubscriptionEventSchema` (`realtime.zod.ts:46-50`), reached from this root as +`events[].type` and `events[].filters`. The cited lines and quoted shapes are exact; +only the owning symbol was misattributed, and this package's own authorable-surface +census (`authorable-surface/api.json`) already spells the two schemas apart. diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index ab66b16b4c..f2b24a5181 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 — 35 governed types (complete registry coverage) +## Current state — 36 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 @@ -897,6 +897,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | 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 **#14691 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 (#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 **#14691 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) | | 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 **#14691 RETIRED all six (2026-09-03, ADR-0049)**: every key is now a `retiredKey()` tombstone and the sub-object is tombstones-only; the rows stay `dead` with a REMOVED note (non-strict schema) and the three `overrides.*` child rows collapse into the one `overrides` row. Triage held `overrides` open as an ENFORCE candidate; the measurement closed it as REMOVE because the capability already exists at its proper seat — per-object exposure is the object's own `enable.apiEnabled` / `enable.apiMethods`, enforced by rest-server.ts#enforceApiAccess (404 / 405) — and `basePath` / `nameTransform` would contradict the one deployment-wide data base and Prime Directive #6 (the object name IS the REST path segment). The `@example` limb is corrected in the same change. `evidenceScope` widened to `cross-repo` (#14796) | +| realtime_subscription | seeded 2026-09-04 (#14446) — a TRANSPORT-PROTOCOL surface, the fifth category the `SPEC_ONLY_SCHEMAS` override has had to reach. `SubscriptionSchema` (`packages/spec/src/api/realtime.zod.ts`) is what a client declares to open a realtime subscription: the item type of `RealtimeConfigSchema.subscriptions` and the `Subscription` the generated API reference publishes. Like `query` it is a request surface rather than stored metadata, and like `query` that is exactly why it went unasked — no registry holds it, `RealtimeConfigSchema` is `.passthrough()` so nothing downstream even refuses an unknown key, and the whole vocabulary sat outside the denominator while the reference kept publishing it. Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema` for the reason the four `RestServerConfig` sub-objects document one row up: the walk drills exactly ONE level, so with the config as the root `events[].type` and `events[].filters` would inherit a container verdict instead of carrying rows of their own — #4956's shape. **Dead 6 = every key it has, and the CONTAINER is the finding**: nothing outside `packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or `RealtimeConfigSchema` at all, so no key beneath them can be read (the `manifest.contributes` reasoning). The two keys the card measured are the sharp ones. `events[].type` accepts `RealtimeEventType`, whose four members (`record.created` / `record.updated` / `record.deleted` / `field.changed`) are DISJOINT from what the engine publishes (`DataEventType`'s `data.record.*`, live emitter in `service-knowledge`), so an author who writes the enum's own `record.created` gets a subscription that silently never fires — and the enum is what the API reference shows them. Its direction is settled by the 2026-09-02 triage and quoted verbatim in the row: enforce means REPOINTING THE ENUM, never changing what the runtime publishes. `field.changed` is the same spelling the sibling `DataEventType` REMOVED in 17.0.0 (#4673, PR #4685) for having no producer; it survives here only because this enum was never in a ratchet's denominator. `events[].filters` is `z.unknown().optional()` — the textbook ADR-0049 fourth state, no shape and no reader, failing in the permissive direction (a subscriber who filters receives every event). ⚠️ Three spellings of a realtime subscription exist and only the third is executed: this one, `websocket.zod.ts#EventSubscriptionSchema`, and the plain interface `contracts/realtime-service.ts#RealtimeSubscriptionOptions` that `in-memory-realtime-adapter.ts#matchesSubscription` actually reads. The file note names the same-name-different-shape traps so the next census does not mistake one for a consumer. Zero live | 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/realtime_subscription.json b/packages/spec/liveness/realtime_subscription.json new file mode 100644 index 0000000000..64803aaac8 --- /dev/null +++ b/packages/spec/liveness/realtime_subscription.json @@ -0,0 +1,46 @@ +{ + "type": "realtime_subscription", + "_note": "SubscriptionSchema — packages/spec/src/api/realtime.zod.ts#SubscriptionSchema, the item type of RealtimeConfigSchema.subscriptions and the `Subscription` the generated API reference publishes (content/docs/references/api/realtime.mdx). It is not a metadata type, not stored metadata, not a request body and not a manifest: it is a TRANSPORT-PROTOCOL surface — what a client declares to open a realtime subscription — 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` and the four RestServerConfig sub-objects take; check-liveness.mts carries the rationale, including why SubscriptionSchema is the root rather than RealtimeConfigSchema (the walk drills one level, so rooting on the config would leave `events[].type` and `events[].filters` with no row of their own — #4956's shape). Seeded 2026-09-04 from the census filed with #14446. WHERE THE CARD'S TWO KEYS ACTUALLY LIVE, because the card and its triage both name them on the wrong schema: `type` and `filters` are declared on SubscriptionEventSchema (realtime.zod.ts:46-50), not on SubscriptionSchema (:57-62, whose own keys are id / events / transport / channel). The lines the card cites (:47, :49) are exact and the shapes it quotes are exact; only the owning symbol is misattributed. The platform's own authorable-surface census already spells them apart — packages/spec/authorable-surface/api.json lists `api/SubscriptionEvent:type` and `api/SubscriptionEvent:filters` beside `api/Subscription:id` / `:events` / `:transport` / `:channel`. Reached from this root they are the `events` container's children, which is why this file roots here and drills `events`. EVERY KEY IN THIS FILE IS `dead`, and the container is why: nothing outside packages/spec imports SubscriptionSchema, SubscriptionEventSchema or RealtimeConfigSchema at all, so no key beneath them can be read (the `manifest.contributes` reasoning — a container with zero reads of the container itself). THREE SPELLINGS OF A REALTIME SUBSCRIPTION EXIST AND ONLY THE THIRD IS EXECUTED, which is the trap for the next reader: (1) this schema, published in the API reference and parsed by nothing; (2) EventSubscriptionSchema (packages/spec/src/api/websocket.zod.ts#EventSubscriptionSchema), the WebSocket protocol's own subscription, whose `filters` carries a NOT-YET-ENFORCED describe() for the same reason as this one's; (3) RealtimeSubscriptionOptions (packages/spec/src/contracts/realtime-service.ts#RealtimeSubscriptionOptions), a plain TS interface with `object` + `eventTypes: string[]`, which is what the shipped adapter actually reads (packages/services/service-realtime/src/in-memory-realtime-adapter.ts#matchesSubscription). ⚠️ Do not read the `Subscription` identifiers in packages/services/service-realtime/src/in-memory-realtime-adapter.ts or packages/services/service-cluster/src/memory/pubsub.ts as consumers of this schema: both are file-local interfaces with the same name and a different shape. ⚠️ Nor the prose in websocket.zod.ts:62-63 saying \"the subscription shape the transports actually carry is … SubscriptionEventSchema\" — measured at 5f5511f0 the transports carry (3), not this schema; that sentence is a comment, not a consumer. This file RECORDS status; it decides nothing. No key is enforced, removed, deprecated, re-described or repointed by the change that seeds it, and realtime.zod.ts is not edited. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor. CENSUS METHOD AND SCOPE, run at 5f5511f0 (2026-09-04): `git grep -nE 'SubscriptionEventSchema|\\bSubscriptionSchema\\b|RealtimeConfigSchema'` over the whole repo excluding CHANGELOGs returns hits in exactly eleven files, and every one is spec's own tree, a generated artifact or published prose — realtime.zod.ts (the declaration), realtime.test.ts (its own unit test), type-alias-convention.pin.test.ts (the alias pin), websocket.zod.ts (the comment above), check-liveness.mts (this enrolment), api-surface/ + declaration-map/ + export-origins/ (generated), content/docs/references/api/realtime.mdx (generated reference) and skills/objectstack-api (published prose). Zero runtime readers. The `Subscription` / `SubscriptionEvent` / `RealtimeConfig` TYPE names were swept separately across *.ts / *.tsx outside packages/spec and produced only the file-local interfaces named above. objectui @24e027e: one import of RealtimeConfigSchema, in packages/collaboration/src/__tests__/spec-symbol-batch7.test.ts, which is a DIVERGENCE PIN — it asserts that objectui's own RealtimeSubscriptionConfig is not a dialect of the spec's RealtimeConfig and reads the schema's key NAMES to do it; zero hits for SubscriptionSchema, SubscriptionEventSchema or RealtimeEventType. 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 realtime subscription is not part of a stack at all: `stack.zod.ts` has no `realtime` key and TYPE_COLLECTIONS has no subscription entry. 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`.", + "props": { + "id": { + "status": "dead", + "verifiedAt": "2026-09-04", + "evidenceScope": "in-repo", + "note": "0 read sites at 5f5511f0. A subscription's identifier is never read because a subscription is never parsed: the shipped adapter mints and keys its own id (in-memory-realtime-adapter.ts#subscribe returns the id it generates, and #unsubscribe looks it up in a Map), so an author-supplied UUID reaches nothing. Dead at the CONTAINER, not at this key: no consumer of SubscriptionSchema exists, so no key beneath it can be read." + }, + "events": { + "children": { + "type": { + "status": "dead", + "verifiedAt": "2026-09-04", + "evidenceScope": "in-repo", + "note": "0 read sites at 5f5511f0, and the sharpest row in this file: the accept set is PUBLISHED and every member of it names an event nothing emits. `type` is `RealtimeEventType` (realtime.zod.ts:33-38), whose members are `record.created` / `record.updated` / `record.deleted` / `field.changed`. What the engine publishes is `DataEventType` (packages/spec/src/api/events.zod.ts#DataEventType) — `data.record.created` / `data.record.updated` / `data.record.deleted` — with a live emitter at packages/services/service-knowledge/src/knowledge-service-plugin.ts (it branches on the `data.record.*` strings by hand). The two vocabularies are DISJOINT: every member here is a prefix-short spelling of a name no producer sends, so an author who writes the enum's own `record.created` gets a subscription that silently never fires — and this enum is what the generated API reference shows them (content/docs/references/api/realtime.mdx). DIRECTION, SETTLED — do not re-open it. Triage ruling of 2026-09-02 on #14446, verbatim: \"⇒ The emitted vocabulary is the established one. If the verdict is **enforce**, it means **repointing the enum**; ⛔ it does not mean changing what the runtime publishes, which would break a live event contract to satisfy a member nothing has ever used. Write that into the ledger entry so the next reader does not re-open a settled direction.\" The asymmetry it rests on is re-measured here: `data.record.*` is declared AND emitted, while `record.*` has no runtime reader at all — only this enum's own unit test (realtime.test.ts) and the alias pin (type-alias-convention.pin.test.ts:541). `field.changed` is the fourth member and the one with a precedent already spent on it: the identical spelling `data.field.changed` was REMOVED from the sibling DataEventType in 17.0.0 under ADR-0049 enforce-or-remove (#4673, PR #4685 — route 3 of the retirement playbook), for having no producer and no slot to carry per-field semantics. events.zod.ts records that removal in its own header. The same spelling survives here only because this enum has never been in a ratchet's denominator, which is what this file changes. Enforce-or-remove worklist: the enforce limb is repointing the enum at DataEventType's vocabulary (a published-vocabulary change with its own ADR-0087 conversion question), the remove limb retires the subscription surface; either is a decision on the human floor, not a cleanup, and neither is taken here." + }, + "object": { + "status": "dead", + "verifiedAt": "2026-09-04", + "evidenceScope": "in-repo", + "note": "0 read sites at 5f5511f0. The per-object narrowing an author writes on a subscription event is never read. ⚠️ The narrowing that IS executed is a different key on a different type: RealtimeSubscriptionOptions.object (packages/spec/src/contracts/realtime-service.ts#RealtimeSubscriptionOptions), which in-memory-realtime-adapter.ts#matchesSubscription compares against the event's own `object` before delivering. That interface is not this schema and nothing converts one into the other, so this key's capability exists at another seat and this spelling of it does nothing. Dead at the container." + }, + "filters": { + "status": "dead", + "verifiedAt": "2026-09-04", + "evidenceScope": "in-repo", + "note": "0 read sites at 5f5511f0 — the textbook ADR-0049 fourth state, and the easier half of the pair this file seeds: `filters: z.unknown().optional().describe('Filter conditions')` (realtime.zod.ts:49) is an authorable key with NO SHAPE and NO CONSUMER. Whatever an author writes parses, because `z.unknown()` accepts everything, and nothing ever evaluates it: the only payload matching the platform performs is in-memory-realtime-adapter.ts#matchesSubscription, which compares object name and event type and nothing else. So a subscriber who filters receives every event the subscription matches — the failure is silent and in the permissive direction. The sibling spelling is already annotated and worth reading beside this row: packages/spec/src/api/websocket.zod.ts#EventSubscriptionSchema carries its own `filters: z.unknown()` with a NOT-YET-ENFORCED describe() and a header recording that a whole FilterOperator / EventFilterSchema vocabulary was deleted from that file for describing a capability no code provided (objectui#2945). This key is the surface that deletion pointed AT, still undescribed. Enforce-or-remove worklist, and removal is the limb this row expects to be chosen: the enforce limb means designing real subscription filtering and lowering it onto AST_OPERATOR_MAP (packages/spec/src/data/filter.zod.ts) rather than inventing a third vocabulary — a feature, not a fix — while the remove limb is the #4673 / PR #4685 route the sibling enum's dead member already took. Neither is decided here; this file records the state." + } + } + }, + "transport": { + "status": "dead", + "verifiedAt": "2026-09-04", + "evidenceScope": "in-repo", + "note": "0 read sites at 5f5511f0. `TransportProtocol` (`websocket` | `sse` | `polling`) is validated as an enum and then read by nothing on this schema: no transport is selected, negotiated or refused from a subscription's `transport`, and the shipped adapter is in-memory with no transport dimension at all. ⚠️ The enum itself is NOT dead — packages/spec/src/api/protocol.zod.ts#RealtimeConnectRequestSchema and #RealtimeConnectResponseSchema both carry a `transport` of the same type — but those are different keys on different schemas, and a shared accept set is not a shared consumer. An enum that is validated and then ignored is accept/reject, which is a different question from liveness. Dead at the container." + }, + "channel": { + "status": "dead", + "verifiedAt": "2026-09-04", + "evidenceScope": "in-repo", + "note": "0 read sites at 5f5511f0. The optional grouping channel is never read from a subscription. ⚠️ Channels are a real runtime concept elsewhere — in-memory-realtime-adapter.ts keys a channel index and contracts/realtime-service.ts declares channel-scoped subscribe — and packages/spec/src/api/websocket.zod.ts#EventSubscriptionSchema has its own `channels` key, so a grep for the word finds plenty. None of those hits reads THIS key: nothing parses a SubscriptionSchema, so an authored `channel` reaches no index. Dead at the container." + } + } +} diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index 1c05900b29..1c86358a7d 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -62,4 +62,5 @@ for both corollaries. | `metadata_endpoints` | 6 | 0 | 0 | 2 | 0 | 8 | | `batch_endpoints` | 5 | 0 | 0 | 2 | 0 | 7 | | `route_generation` | 0 | 0 | 0 | 4 | 0 | 4 | -| **total** | **844** | **5** | **1** | **84** | **13** | **947** | +| `realtime_subscription` | 0 | 0 | 0 | 6 | 0 | 6 | +| **total** | **844** | **5** | **1** | **90** | **13** | **953** | diff --git a/packages/spec/scripts/liveness/check-liveness.mts b/packages/spec/scripts/liveness/check-liveness.mts index 2433a81273..a1bb4f7d19 100644 --- a/packages/spec/scripts/liveness/check-liveness.mts +++ b/packages/spec/scripts/liveness/check-liveness.mts @@ -157,6 +157,7 @@ import { MetadataEndpointsConfigSchema, RouteGenerationConfigSchema, } from '../../src/api/rest-server.zod'; +import { SubscriptionSchema } from '../../src/api/realtime.zod'; import { BOUND_PROOF_PATHS, HIGH_RISK_CLASSES, @@ -234,7 +235,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', 'crud_endpoints', 'metadata_endpoints', 'batch_endpoints', 'route_generation']; +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', 'realtime_subscription']; // Registered metadata types that are NOT yet governed — the coverage ratchet. // @@ -360,6 +361,42 @@ const PENDING_GOVERNANCE: Record = { // 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. +// `realtime_subscription` is the FIFTH category and the narrowest one yet: a +// TRANSPORT-PROTOCOL surface. `SubscriptionSchema` (src/api/realtime.zod.ts) is +// what a client sends to open a realtime subscription — the `subscriptions` +// array of `RealtimeConfigSchema`, and the `Subscription` type the generated API +// reference publishes. Like `query` it is a request surface rather than stored +// metadata, and like `query` that is exactly why it went unasked: no registry +// holds it, `RealtimeConfigSchema` is `.passthrough()` so nothing downstream +// even refuses an unknown key, and the whole vocabulary sat outside the +// denominator while the generated reference kept publishing it. +// +// The census filed with #14446 found the two members that make governing it +// worth the row: `events[].type` accepts `RealtimeEventType`, whose four members +// are spelled `record.created` / `record.updated` / `record.deleted` / +// `field.changed` while the engine publishes `data.record.*` (DataEventType, +// src/api/events.zod.ts) — DISJOINT vocabularies, so every member of the +// published enum names an event nothing emits; and `events[].filters` is +// `z.unknown().optional()`, a key with no shape and no reader. `field.changed` +// is the sharper half: its sibling `DataEventType.data.field.changed` was +// REMOVED in 17.0.0 (#4673, ADR-0049 enforce-or-remove) for having no producer, +// and the same spelling survives here because this enum was never in a +// ratchet's denominator. +// +// WHY THE SUBSCRIPTION AND NOT THE CONFIG. `RealtimeConfigSchema` is the +// obvious root and it does not work, for the reason the four `RestServerConfig` +// sub-objects above document: the walk drills exactly ONE level. With the config +// as the root, `subscriptions` is the drilled level and a subscription's own +// keys sit one deeper — `events[].type` and `events[].filters` would then have +// no row of their own and would inherit a container verdict, which is #4956's +// shape in the file written to end it. Rooting on `SubscriptionSchema` puts +// `events` at the drillable level and both measured keys in rows of their own. +// `RealtimeConfigSchema`'s own three keys (`enabled`, `transport`, +// `subscriptions`) are deliberately NOT enrolled here: they are a different +// question (does enabling realtime do anything?) with no census behind it yet, +// and this override records what has been measured. There is no registry to +// fold this one back onto either — like `query`, `qa` and `manifest`, the +// override IS its governance. const SPEC_ONLY_SCHEMAS: Record = { webhook: WebhookSchema, query: QuerySchema, @@ -370,6 +407,7 @@ const SPEC_ONLY_SCHEMAS: Record = { metadata_endpoints: MetadataEndpointsConfigSchema, batch_endpoints: BatchEndpointsConfigSchema, route_generation: RouteGenerationConfigSchema, + realtime_subscription: SubscriptionSchema, }; // ADR-0010 provenance/lock overlay fields — system-stamped, on every type; auto-live.