Skip to content

Commit 368a82e

Browse files
os-justinclaude
andauthored
chore(spec): govern the realtime SubscriptionSchema surface in the liveness ledger (#14446) (#15242)
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. Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6530f75 commit 368a82e

5 files changed

Lines changed: 148 additions & 3 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
chore(spec): govern the realtime `SubscriptionSchema` surface in the liveness ledger (#14446)
6+
7+
The `liveness/` ledgers ship inside this package's npm tarball (they are named in
8+
`files`), so this is a published-data change even though no runtime behaviour moves,
9+
no schema key changes spelling, and `packages/spec/src/api/realtime.zod.ts` is not
10+
edited at all.
11+
12+
A new ledger file — `realtime_subscription.json` — classifies all six authorable
13+
properties of `SubscriptionSchema`, what a client declares to open a realtime
14+
subscription: the item type of `RealtimeConfigSchema.subscriptions` and the
15+
`Subscription` the generated API reference publishes. It is enrolled through the
16+
gate's `SPEC_ONLY_SCHEMAS` override, the route `query` / `qa` / `manifest` and the
17+
four `RestServerConfig` sub-objects already take. A transport-protocol surface is
18+
neither a metadata item nor stored metadata nor a manifest, so no registry has ever
19+
held it and no ratchet rooted in one could ask who reads it — and
20+
`RealtimeConfigSchema` is `.passthrough()`, so nothing downstream even refuses an
21+
unknown key.
22+
23+
All six are `dead`, and the container is the finding: nothing outside
24+
`packages/spec` imports `SubscriptionSchema`, `SubscriptionEventSchema` or
25+
`RealtimeConfigSchema` at all, so no key beneath them can be read. The two the
26+
census filed with this card measured are the sharp ones. `events[].type` accepts
27+
`RealtimeEventType`, whose four members (`record.created` / `record.updated` /
28+
`record.deleted` / `field.changed`) are disjoint from what the engine publishes —
29+
`DataEventType`'s `data.record.*`, with a live emitter in `service-knowledge` — so
30+
an author who writes the enum's own `record.created` gets a subscription that
31+
silently never fires, and the enum is what the API reference shows them.
32+
`events[].filters` is `z.unknown().optional()`: an authorable key with no shape and
33+
no consumer, failing in the permissive direction, since the only payload matching
34+
the platform performs compares object name and event type.
35+
36+
What this records, and what it deliberately does not. The enum's direction is
37+
settled and the row carries the 2026-09-02 triage ruling verbatim so the next reader
38+
does not re-open it: if the verdict is enforce it means repointing the enum, never
39+
changing what the runtime publishes, which would break a live event contract to
40+
satisfy a member nothing has ever used. `field.changed` is the same spelling the
41+
sibling `DataEventType` removed in 17.0.0 under ADR-0049 (#4673, PR #4685) for
42+
having no producer; it survives here only because this enum was never in a ratchet's
43+
denominator. No key is removed, enforced, deprecated or re-described here — the
44+
enforce-or-remove call per dead key is a follow-up on the human floor.
45+
46+
Rooted on `SubscriptionSchema` rather than on `RealtimeConfigSchema`, which is
47+
measurement rather than taste: the ledger walk drills exactly one level, so with the
48+
config as the root `subscriptions` would be the drilled level and `events[].type` /
49+
`events[].filters` would have no row of their own, inheriting a container verdict —
50+
the same reasoning that rooted the four `RestServerConfig` sub-objects separately.
51+
`RealtimeConfigSchema`'s own three keys are not enrolled: whether enabling realtime
52+
does anything is a different question with no census behind it yet.
53+
54+
One correction the ledger records because the next reader will hit it too. The card
55+
and its triage both name the two keys on `SubscriptionSchema`; they are declared on
56+
`SubscriptionEventSchema` (`realtime.zod.ts:46-50`), reached from this root as
57+
`events[].type` and `events[].filters`. The cited lines and quoted shapes are exact;
58+
only the owning symbol was misattributed, and this package's own authorable-surface
59+
census (`authorable-surface/api.json`) already spells the two schemas apart.

packages/spec/liveness/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,7 @@ The governed set is `GOVERNED` at the top of `check-liveness.mts`. To add a type
780780
RecordDetailView had been gating the History tab on it the whole time (#2707).
781781
4. Add the type to `GOVERNED`; confirm the gate is green.
782782

783-
## Current state — 35 governed types (complete registry coverage)
783+
## Current state — 36 governed types (complete registry coverage)
784784

785785
> **This heading is now checked** (#7257). `check:liveness` reconciles the table
786786
> 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.
897897
| 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) |
898898
| 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) |
899899
| 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) |
900+
| 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 |
900901

901902
The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every
902903
misleading entry carries `authorWarn` so authors hear about it at compile time

0 commit comments

Comments
 (0)