Skip to content

chore(spec): govern the realtime SubscriptionSchema surface in the liveness ledger - #15242

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-14446-realtime-liveness-ledger
Sep 4, 2026
Merged

chore(spec): govern the realtime SubscriptionSchema surface in the liveness ledger#15242
os-justin merged 2 commits into
mainfrom
claude/issue-14446-realtime-liveness-ledger

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #14446

Governs the realtime SubscriptionSchema surface in the spec liveness ledger. Ledger rows and their evidence only: no key is enforced, removed, deprecated, re-described or repointed, and packages/spec/src/api/realtime.zod.ts and packages/spec/src/api/events.zod.ts are not edited.

What the triage ruled, and what this PR is

Triage 5506262367 (2026-09-02), verbatim where operative:

The deliverable is both keys landed in packages/spec/liveness/api.json with the evidence this card already measured — and nothing else in that PR: no key enforced, removed, re-described or repointed.

⇒ 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.

That direction narrowing is quoted verbatim inside the events[].type row's note, which is where the next reader will look.

Premise re-verified at the base ref, and one correction

Everything the card measured holds at 5f5511f0, with one attribution error in the card and its triage. Evidence, with refs:

Card's claim Measured at 5f5511f0 Verdict
RealtimeEventType spells record.* realtime.zod.ts:33-38record.created / record.updated / record.deleted / field.changed holds; the card's enumeration omits the fourth member
runtime emits data.record.* events.zod.ts#DataEventTypedata.record.created/updated/deleted; emitter branches on those strings in packages/services/service-knowledge/src/knowledge-service-plugin.ts holds
filters is z.unknown().optional() realtime.zod.ts:49filters: z.unknown().optional().describe('Filter conditions') holds
no ledger row exists for either packages/spec/liveness/*.json — zero hits for Subscription / Realtime outside webhook.json prose holds
the two keys are on SubscriptionSchema they are on SubscriptionEventSchema (realtime.zod.ts:46-50). SubscriptionSchema (:57-62) declares id / events / transport / channel corrected

The cited lines (:47, :49) and the quoted shapes are exact; only the owning symbol was misattributed, and this package's own authorable-surface census already spells the two apart — packages/spec/authorable-surface/api.json lists api/SubscriptionEvent:type and api/SubscriptionEvent:filters beside api/Subscription:id / :events / :transport / :channel. Reached from the enrolled root the two keys are the events container's children, so they land as events[].type and events[].filters.

Enrolment decision, and the authority for it

The triage names api.json by family, not by measurement: packages/spec/liveness/api.json governs ApiEndpointSchema (metadata type api, thirteen endpoint properties) and has no relationship to SubscriptionSchema. Writing subscription rows into it would have produced rows the gate reports as ORPHAN.

The authority on the correct landing is the gate's own header (packages/spec/scripts/liveness/check-liveness.mts): the walk is rooted in the metadata-type registry, and a schema no registry holds is enrolled through the SPEC_ONLY_SCHEMAS override — the route webhook, query, validation, qa, manifest and the four RestServerConfig sub-objects (PR #14638) already take. This PR follows that shape end to end: import, GOVERNED entry, override entry with its rationale, a new ledger file, the README table row and heading count, and the regenerated counts artifact.

Rooted on SubscriptionSchema, not RealtimeConfigSchema — measurement, not taste, and the same reasoning PR #14638 recorded for rooting on four sub-objects instead of the whole config. The walk drills exactly one level. With RealtimeConfigSchema as the root, subscriptions would be the drilled level and events[].type / events[].filters would sit one deeper with no row of their own, inheriting a container verdict — #4956's shape, in the ledger written to end it. --dump realtime_subscription confirms the walk auto-expands events into exactly the three keys, so drilling events puts both measured keys at a falsifiable depth and adds no new undrilled-container baseline entry.

RealtimeConfigSchema's own three keys are deliberately not enrolled, the way RestApiConfigSchema was left out of the sibling family: whether enabling realtime does anything is a different question with no census behind it.

The enrolment's own rule: every authorable property classified

The RestServerConfig precedent classified every authorable property of each enrolled schema, and the gate's ratchet requires it. All six are classified, and all six are dead.

Property Status Why
id dead The adapter mints and keys its own id; an author-supplied UUID reaches nothing.
events[].type dead Accept set RealtimeEventType, whose four members are disjoint from the emitted data.record.* vocabulary. Carries the settled direction verbatim.
events[].object dead The narrowing that IS executed is RealtimeSubscriptionOptions.object on a different type.
events[].filters dead z.unknown().optional() — no shape, no consumer; fails in the permissive direction.
transport dead Validated as an enum, then read by nothing on this schema.
channel dead Never read from a subscription; the channel index is keyed elsewhere.

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. Census at 5f5511f0: git grep -nE 'SubscriptionEventSchema|\bSubscriptionSchema\b|RealtimeConfigSchema' over the whole repo excluding CHANGELOGs hits exactly eleven files, every one of them spec's own tree, a generated artifact or published prose. The Subscription / SubscriptionEvent / RealtimeConfig type names were swept separately across *.ts / *.tsx outside packages/spec and produced only file-local interfaces of the same name. objectui @24e027e: one import of RealtimeConfigSchema, in a divergence pin asserting that objectui's own RealtimeSubscriptionConfig is not a dialect of the spec's; zero hits for the other symbols. The closed cloud runtime was not reachable from this container, so evidenceScope stays in-repo rather than claiming a sweep that was not run.

Two traps the file note records so the next census does not repeat them: three spellings of a realtime subscription exist and only the third is executed (this schema, websocket.zod.ts#EventSubscriptionSchema, and the plain interface contracts/realtime-service.ts#RealtimeSubscriptionOptions that in-memory-realtime-adapter.ts#matchesSubscription actually reads); and the Subscription identifiers in in-memory-realtime-adapter.ts and service-cluster/src/memory/pubsub.ts are file-local interfaces with the same name and a different shape.

field.changed is worth naming separately. The identical spelling data.field.changed was removed from the sibling DataEventType in 17.0.0 under ADR-0049 (#4673, PR #4685 — route 3 of the retirement playbook), for having no producer; 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 PR changes. No removal is taken here.

No entry carries authorWarn: the CLI lint walks stack collections and a realtime subscription is not part of a stack at all (stack.zod.ts has no realtime key, TYPE_COLLECTIONS no subscription entry), so a warning here could never be emitted. The file note records that as _authorWarnSkipped, the convention qa.json and the RestServerConfig files use.

Gates

Derived after the last commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no path args — it reads the change set itself), on a tree at origin/main with no staleness warning. All runs below are pinned to head 779b0736; exits were captured before any pipe.

  • 43/43 derived families exit 0, except one that measured nothing (below).
  • pnpm --filter @objectstack/spec check:liveness — exit 0, realtime_subscription 6 classified (dead 6), and ✓ … the README state table carries a row for each of the 36 governed type(s) it claims to index / ✓ packages/spec/liveness/state-counts.md is current — the same 36 row(s).
  • pnpm --filter @objectstack/spec check:generated — exit 0, ✓ All 15 generated artifacts are up to date. First run said api-surface/ was stale; that was a phantom of an unbuilt dist/ — the gate reads built output and says so. After pnpm --filter @objectstack/spec build it is green, and nothing was regenerated.
  • pnpm check:published-files, pnpm check:doc-authoring, pnpm check:nul-bytes, node scripts/check-system-context-census.mjs (+ --self-test), pnpm check:changeset-gate-self-tests, node scripts/check-empty-changeset.mjs --self-test — all exit 0.
  • pnpm check:dual-build-cjs-loads — exit 3, PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/, with its own ⛔ This is NOT a pass: nothing was measured. It needs a whole-monorepo pnpm build; recorded as not measured, not as green, and left to CI.
  • Because this PR edits a gate script, that script's own suites were run beyond the derived families: vitest run scripts/liveness/{check-liveness,elsewhere,evidence,readme-table}.test.ts scripts/zod-graph.test.ts src/system/metadata-form-zod-reconciliation.test.tsTest Files 6 passed (6), Tests 210 passed (210) — plus check:scripts-typecheck exit 0.
  • The six artifact-roster families whose silence the derivation flags as non-evidence (roster under a directory one of these paths is in) were run rather than assumed: check-changeset-fixed, check:spec-changes, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:swallow-census-controls — all exit 0.
  • pnpm lint (repo-wide eslint . --no-inline-config) — exit 0 in 76s. No narrowing claimed.

Changeset

patch for @objectstack/spec: liveness/** is named in the package's files, so this is a published-data change even though no runtime behaviour moves. No BREAKING banner and no ADR-0087 marker — nothing authorable is removed, renamed or re-described, and the marker gate applies to changesets a PR newly declares breaking.


Generated by Claude Code

…veness ledger (#14446)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
@github-actions github-actions Bot added the size/m label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 3 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/realtime_subscription.json, packages/spec/liveness/state-counts.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/realtime_subscription.json, packages/spec/liveness/state-counts.md) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1bc3c092a118885671d85bff5259d82fe40d9ad6packageMentionDocs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

2 participants