Skip to content

feat(spec)!: RealtimeEventType names the emitted data.record.* / data.records.* vocabulary (#20288) - #20567

Merged
os-justin merged 4 commits into
mainfrom
claude/issue-20288-realtime-event-type-emitted-vocabulary
Sep 29, 2026
Merged

os-justin merged 4 commits into
mainfrom
claude/issue-20288-realtime-event-type-emitted-vocabulary

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Closes #20288

Clause-②: yes (narrowing)

RealtimeEventType (@objectstack/spec/api, the type of SubscriptionEvent.type) now names exactly what the runtime emits: DataEventType + BulkDataEventType, i.e. data.record.created / data.record.updated / data.record.deleted and data.records.updated / data.records.deleted. Its four old values (record.created, record.updated, record.deleted, field.changed), which nothing ever emitted, are refused with a per-value prescription. The runtime's published event names do not change.

No transport, no route and no client subscribe primitive (#14646 ruling A). The verification evidence is in the dev report on the card.


Generated by Claude Code

….records.* vocabulary

The enum's four values (record.created / record.updated / record.deleted /
field.changed) were never emitted by anything; it now lists exactly
DataEventType + BulkDataEventType, and each retired value is refused with a
per-value prescription. ADR-0087 D3 entry
realtime-event-type-unemitted-values-retired; service-realtime examples name
the emitted events; ledger _note points the six rows at the rulings.

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
…w records (it has no reader)

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
… names the emitted events)

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 29, 2026
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-realtime, @objectstack/spec, touching 15 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/services/service-realtime/README.md, packages/spec/liveness/README.md, packages/spec/liveness/realtime_subscription.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/webhooks.mdx (via data.record.updated (literal, a string literal in RealtimeEventType; a string literal in replacement; a string literal in semantic), data.records.updated (literal, a string literal in RealtimeEventType; a string literal in a comment in RealtimeEventPayload; a string literal in replacement; a string literal in semantic))
  • content/docs/protocol/kernel/realtime-protocol.mdx (via data.record.created (literal, a string literal in RealtimeEventType; a string literal in a comment in RealtimeEventPayload; a string literal in a comment on a changed line; a string literal in replacement; a string literal in semantic), data.record.deleted (literal, a string literal in RealtimeEventType; a string literal in replacement; a string literal in semantic), data.record.updated (literal, a string literal in RealtimeEventType; a string literal in replacement; a string literal in semantic))
  • content/docs/protocol/knowledge.mdx (via data.record.created (literal, a string literal in RealtimeEventType; a string literal in a comment in RealtimeEventPayload; a string literal in a comment on a changed line; a string literal in replacement; a string literal in semantic), data.record.deleted (literal, a string literal in RealtimeEventType; a string literal in replacement; a string literal in semantic), data.record.updated (literal, a string literal in RealtimeEventType; a string literal in replacement; a string literal in semantic), data.records.deleted (literal, a string literal in RealtimeEventType; a string literal in replacement; a string literal in semantic), data.records.updated (literal, a string literal in RealtimeEventType; a string literal in a comment in RealtimeEventPayload; a string literal in replacement; a string literal in semantic))
What this run could not see
  • 3 changed file(s) yielded no anchor (packages/services/service-realtime/README.md, packages/spec/liveness/README.md, packages/spec/liveness/realtime_subscription.json) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 139 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 f11b5f20a2ee22698c6647c2fb76aa67e99a7a53 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5187981860e61c1a229a97589428eaba236d25e4 — the merge of head 17612ad5eaf0c3cac6e67f6a03daf04eb952051e into base f11b5f20a2ee22698c6647c2fb76aa67e99a7a53, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5187981860e61c1a229a97589428eaba236d25e4 && git checkout 5187981860e61c1a229a97589428eaba236d25e4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f11b5f20a2ee22698c6647c2fb76aa67e99a7a53 17612ad5eaf0c3cac6e67f6a03daf04eb952051e && git checkout -B drift-repro f11b5f20a2ee22698c6647c2fb76aa67e99a7a53 && git merge --no-ff 17612ad5eaf0c3cac6e67f6a03daf04eb952051e

node scripts/docs-audit/affected-docs.mjs --json f11b5f20a2ee22698c6647c2fb76aa67e99a7a53

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f11b5f20a2ee22698c6647c2fb76aa67e99a7a53 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0b2a76726c6503d31ca13962d7260c2fe0438ac9
Local-runs: none

Inputs read: card #20288 (body + 5 comments: first grade 5859582726, retriage request 5881693927, re-grade 5882152948, claim 5882309567, dev report 5883321020); rulings by id — #14646 A (5542632689), #8347 甲 (5791499872), #14446 direction (5506262367); PR #20567 body, file list (11 files, +399 −55, matches git diff origin/main...0b2a767 byte-for-byte on the stat) and the net diff against merge-base ba5927f714; the 39 check-runs on the head. Git objects only — git show origin/main:path and git grep sha -- path; no checkout, no build, no test.

Checks: 39 runs, 0 failures. All seven required contexts success (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard). Every other run success or skipped (path-filtered Console Pin Gate, opt-in tarball smoke, the second-trigger duplicates of Auto Label / Check PR Size). No path is a governed surface; head repo = base repo; 454 changed lines. PR is draft, unarmed, unqueued — correct for a needs:contract-review head.

① Derived judgments

  1. RealtimeEventType accept set (@objectstack/spec/api, a real exports entry): FROM {record.created, record.updated, record.deleted, field.changed} TO {data.record.created, data.record.updated, data.record.deleted, data.records.updated, data.records.deleted} — exactly DataEventType ∪ BulkDataEventType (events.zod.ts:138-142, :172-175), pinned equal to that union in realtime.test.ts. Widens by five, narrows by four. Right.
  2. Doors that parse it: SubscriptionEventSchema.type (realtime.zod.ts:115, the only schema reader) and therefore SubscriptionSchema.events[] and RealtimeConfigSchema.subscriptions[].events[] (.passthrough() on other keys, strict on this enum). All three now refuse each retired value with its own prescription (invalid_value at ['events', 0, 'type'], pinned through SubscriptionSchema); the error map returns undefined for any other input so a typo keeps zod's own message — the HookBodyCapability / RuntimeMode precedent, verified same shape (hook-body.zod.ts, kernel/context.zod.ts). No other spec schema embeds these doors (repo grep: comments and the migration entry only); stack.zod.ts has no realtime key; nothing outside packages/spec imports them. Right, and the changeset names exactly these three doors.
  3. Doors whose accept set does NOT move, correctly left: protocol.zod.ts#RealtimeSubscribeRequestSchema.events is z.array(z.string()); websocket.zod.ts#EventSubscriptionSchema.events is EventPatternSchema; contracts/realtime-service.ts#RealtimeSubscriptionOptions.eventTypes is string[]; RealtimeEventPayload.type / RealtimeEventSchema.type are string. Only their example prose moved, where it was stale. Right.
  4. TS narrowing: RealtimeEventType, SubscriptionEvent, Subscription, RealtimeConfig are z.input of the enum, so tsc refuses the old literals — stated in the changeset. Right.
  5. Producers untouched — the card's ⛔ holds. objectql/src/engine.ts publishDataEvent (:7099-7118, data.record.${action}, parsed through DataEventSchema, envelope payload: { ...event }), publishBulkDataEvent (:7198-7215, data.records.${action}), metadata/src/metadata-manager.ts (:695-723, metadata.${type}.${action} gated by MetadataEventType, 39 members). None is in the file list. The runtime's published names do not change. Right (declared ≠ enforced (ADR-0049): RealtimeEventType spells record.* while the runtime emits data.record.*, and SubscriptionSchema.filters is z.unknown() nothing reads #14446 direction: repoint the enum, never the runtime).
  6. No transport, no route, no client subscribe primitive. The 11 paths touch no packages/rest, packages/runtime, packages/client, no route table, no ws/sse dependency; the in-memory-realtime-adapter.ts change is a docblock @example only. realtime: the bell cannot prefer a live channel because discovery advertises realtime as enabled:true with no HTTP/WS surface mounted and no route to connect to #14646 ruling A and realtime: no WS/SSE transport — service-realtime is in-process only and clients long-poll; big-screen/command-center scenarios need real push #8347 ruling 甲 hold. Right.
  7. metadata.* deliberately not members. A SubscriptionEvent is record-shaped (object names a data object, filters narrows records); metadata events have their own closed MetadataEventType and their own client primitive subscribeMetadata (client/src/realtime-api.ts:73); adding 39 names would widen past the re-grade. The new RealtimeEventPayload.type example 'metadata.object.updated' is a real member (events.zod.ts:19) on a string field the envelope genuinely carries. Right.
  8. Every prescription is true of the code: each retired value names an emitted twin that exists; "data.records.* carries a count and no record" — BulkDataEventSchema has no recordId; "field.changed → data.record.updated, read the field from changes" — DataEventSchema.changes exists; "no event is published per field" — no producer does. True.
  9. The version each prescription names — @objectstack/spec 17.5.0: spec is 17.4.0 on origin/main; pending changesets include many minor and no major, and check-changeset-no-major refuses a major (launch-window guard) — so the release that carries this is 17.5.0. ADR-0087's amended level ruling says a tombstone names the npm release it ships in, never the protocol major; the same 17.5.0 form is already on 21 sites in 14 files at main. True of the release that will carry it (see ③ for the one caveat shared with those 21 sites).
  10. Regenerated reference content/docs/references/api/realtime.mdx: description equals the new .describe(), Allowed Values are the five, the Subscription / SubscriptionEvent tables show the new enum, RealtimeEvent.type example moved; produced by the repo's generator (check:docs inside the green TypeScript Type Check). True.
  11. README / adapter @example: the published object { id: uuid, type, object, recordId, after, timestamp } is a valid DataEvent (required: id, type, object, recordId, timestamp) and is what engine.ts wraps as payload; subscribe('records', cb, { object, eventTypes }) matches RealtimeSubscriptionOptions; "the engine publishes data.record.created / data.record.updated with the full record body (after)" matches engine.ts. True.
  12. Enum docblock: "Nothing in the open framework parses a subscription" — true (ledger census, no importer outside spec); "realtime.test.ts pins this enum equal to their union" — true; the old NOT YET ENFORCED block is replaced by a sentence that stays true (the vocabulary now fires; the container is still unparsed, and the docblock says so). True.
  13. D3 entry text: "field.changed … DataEventType already dropped in protocol 17 (data-field-changed-event-retired)" — that entry sits in step17 (registry.ts:2138, toMajor: 17); "stack.zod.ts has no realtime key" — 0 hits; "maintainer ruling of 2026-09-04: realtime stays out of open core" — realtime: the bell cannot prefer a live channel because discovery advertises realtime as enabled:true with no HTTP/WS surface mounted and no route to connect to #14646 A. surface / replacement carry no backticks or pipes as build-upgrade-guide.ts needs. entries/semantic/18.*.ts and the registry.ts copy are the house pattern (check:migration-registry green). True.
  14. Ledger: one-line _note addition only; all six rows stay dead (they land live-elsewhere only when a channel-serving occupant parses the contract — re-grade 5882152948); Spec property liveness green; no state-counts.md change owed. Right.
  15. PR body: every sentence checked against the diff — "Closes realtime: RealtimeEventType names the emitted data.record.* / data.records.* vocabulary (open-core contract half; the transport stays out, #14646 A and #8347 甲) #20288" (the card's claim names this branch; check green), the enum description, the five bullets, "No transport, no route and no client subscribe primitive", the session-URL footer. All true. RealtimeEventPayload.type and RealtimeEvent.type examples are both in the diff as stated.
  16. objectui at the pin dd3f7e1b: 0 hits for RealtimeEventType or any old spelling (grep at the pinned commit) — the Console Pin Gate is unaffected, as the dev measured.

② Semver level

  • @objectstack/spec: minor with the **BREAKING** banner, FROM → TO mapping, the one-line fix, and the marker adr-0087: registered realtime-event-type-unemitted-values-retired (an HTML-comment marker in the changeset; the gate resolved it — Check Changeset and Lint & Repo Gates green). Matches ADR-0087's amended level ruling (pre-GA, a metadata-facing retirement ships minor; the banner and the disposition carry breaking-ness) and AGENTS.md (yes ⇒ at least minor; (narrowing) ⇒ BREAKING). Right.
  • Clause-②: yes (narrowing) in the PR body and the changeset. Right. The criterion is "does this widen the accept set or the public surface" — five values now parse that were refused before, so yes; four are refused that parsed before, so (narrowing). This is the exact shape the gate's own fixture names ("a diff that widens AND narrows"). The pure-removal precedents that declared no (17707, 19116, 20295) removed without adding and are not this case.
  • ADR-0087 disposition: D3 semantic entry, no D2 conversion — fits. ADR-0087 D2 does list "enum re-spellings" as lossless, but D2 is applied by the loader to metadata sources, and a Subscription is reachable from no such seam: not a stack collection member (stack.zod.ts has no realtime key), not a metadata type, not embedded in any other spec schema, not imported outside spec. A conversion entry would be a declared transform nothing ever runs — the declared≠enforced shape ADR-0049 forbids. Established: 47 step18 semantic entries are D3-only for the same reason (error codes, contract-map entries, REST config keys, route params). RETIRED_KEYS_BY_MAJOR correctly untouched — an enum value is not a key. The (narrowing) arm and a D3-without-D2 entry are consistent: the prescription lives on the enum's error map, which IS the door.
  • @objectstack/service-realtime: patch. README ships in the tarball (files), the @example ships in dist .d.ts; a stale example that could never fire is a documentation defect in a released package (precedent 17152). skip-changeset would have been wrong. Right.

③ Boundary flags

open_questions: none declared. Dev deviations (5) and out-of-scope findings (5), each answered or escalated:

  • Assignee unset on the PR (label-write --assign refused locally) — seat sets os-justin (the card's) when it adopts this record. Answered.
  • "Closes" vs "Fixes" — both close; the card is pm:dispatched, not in the decision box. Answered.
  • PR body carries no test evidence — the evidence of record is the tests field of dev report 5883321020 at 0b2a76726c (deletion leg, final run, two ablations A/B with restore proofs, 117-gate reconciliation); the seat may cite it in the body. Answered.
  • _note carries a repoint clause beside the two ruling pointers — still one line; it qualifies the seed-time member list ("as measured at 5f5511f") rather than contradicting it. Accepted.
  • origin/main moved after branch-off — mergeable state clean; the queue rebuilds the merged generation. Answered.
  • service-knowledge record.* branch left unmoved (knowledge-service-plugin.ts:218-231, TSDoc :24, knowledge-service.ts:232, :252) — reads event.type as a plain string off its own IRealtimeService subscription, never the enum or SubscriptionSchema; dead before and after; leaving it in this PR is right. But it is a consumer-side legacy-shape fallback (payload.record ?? payload, Prime Directive Add comprehensive test suite for Zod schema validation #12 class) plus stale TSDoc, and "承接者:无" is not a carrier — under Prime Directive chore: version packages #10 the reviewer files a note that is a contract violation. Escalated: the seat files a domain:services card (dedupe words: knowledge-event-sync, record.created legacy branch, handleRecordUpsert payload.record).
  • packages/spec/liveness/README.md:945 left unmoved — the row reads, present tense, that RealtimeEventType's "four members (record.created / record.updated / record.deleted / field.changed) are DISJOINT from what the engine publishes". False at this head, and liveness/ ships in the npm tarball (files). Hand-owned enrolment prose (no generator writes it; readme-table.mts only reads it). The dev stayed inside the claim's surface and reported it, so nothing is buried — but leaving it leaves one shipped sentence false. Escalated with a carrier: a one-clause qualifier mirroring the JSON _note ("repointed by realtime: RealtimeEventType names the emitted data.record.* / data.records.* vocabulary (open-core contract half; the transport stays out, #14646 A and #8347 甲) #20288; member list as measured at 5f5511f"), either folded into this PR before enqueue or a docs-only follow-up the seat files; check-liveness.mts:452-458 (same sentence; scripts/ is outside files, so not shipped) rides that same fix. Not a blocker: the accept set, the prescriptions, the reference page and the changeset are all true.
  • websocket.zod.ts:104 describe example "record.*" — an EventPatternSchema wildcard on spelling (2), an open door whose accept set did not move; correctly fenced to the five-spelling convergence stage the re-grade reserved. Answered: stays with that stage's card.
  • client/src/realtime-api.ts:153-159 comment says data.field.changed is "declared in DataEventType" — false since protocol 17, unrelated to this enum, a code comment. Noted; rides the same follow-up as the README line or the next client touch.
  • check:platform-checklist red — identical on the clean base, not CI-wired. Answered: pre-existing, unrelated.
  • Version caveat, shared with 21 existing sites: "17.5.0" is true only while 17.5.0 is uncut. If the maintainer cuts 17.5.0 before this lands, this PR's four prescriptions go stale together with every pending tombstone at main — a fleet convention, not this PR's defect; the seat re-dates before enqueue if that happens.

Implemented-by: claude/issue-20288-realtime-event-type-emitted-vocabulary
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1

VERDICT: PASS

Adopted and posted by domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-09-29T04:16Z · rendered by the seat's at-tier review subagent; its served tier family was read from the subagent transcript before posting. The seat folds the ③ liveness/README.md:945 fix into this PR (a new head, reviewed again before enqueue), so needs:contract-review stays on.


Generated by Claude Code

…ent date RealtimeEventType's old member list (as measured at 5f5511f, before the repoint)

Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 17612ad5eaf0c3cac6e67f6a03daf04eb952051e
Local-runs: none

Inputs read: card #20288 (body + all 7 comments — first grade 5859582726, retriage request 5881693927, re-grade 5882152948, claim 5882309567, dev report 5883321020, surface extension 5883551279, dev follow-up report 5884206324); the PR thread (Docs Drift Check 5882899479, the prior record 5883544890 on head 0b2a76726c); rulings by id — #14646 A (5542632689), #8347 甲 (5791499872), #14446 direction (5506262367); PR #20567 body (byte-identical to the body reviewed at 0b2a76726c), file list (13 files, +402 −56, matches git diff origin/main...17612ad5ea on the stat) and the whole net diff against merge-base ba5927f714; the 35 check-runs on this head. Git objects only — no checkout, no build, no test.

Whole PR, not the delta. 17612ad5ea is one commit on top of 0b2a76726c (parent verified); the delta is exactly packages/spec/liveness/README.md (1 line changed) and packages/spec/scripts/liveness/check-liveness.mts (+2 lines). The other 11 files are byte-identical to the head reviewed in 5883544890, so judgments ①1–16 below are re-stated as they stood and re-checked against this head's checks; ①17–20 are new.

Checks: 35 runs, 33 success, 2 skipped (path-filtered Console Pin Gate, opt-in tarball smoke), 0 failures. All seven required contexts success (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard). Spec property liveness green on the edited README row. No path is a governed surface or a single-writer path (both guards green); head repo = base repo; 458 changed lines; mergeable clean. PR is draft, unarmed, unqueued — correct for a needs:contract-review head.

① Derived judgments

  1. RealtimeEventType accept set (@objectstack/spec/api, a real exports entry): FROM {record.created, record.updated, record.deleted, field.changed} TO {data.record.created, data.record.updated, data.record.deleted, data.records.updated, data.records.deleted} — exactly DataEventType ∪ BulkDataEventType (events.zod.ts:138-142, :172-175), pinned equal to that union in realtime.test.ts. Widens by five, narrows by four. Right.
  2. Doors that parse it: SubscriptionEventSchema.type (realtime.zod.ts:115, the only schema reader), hence SubscriptionSchema.events[] and RealtimeConfigSchema.subscriptions[].events[]. All three refuse each retired value with its own prescription (invalid_value at ['events', 0, 'type'], pinned through SubscriptionSchema); the error map returns undefined for any other input so a typo keeps zod's own message — the HookBodyCapability / RuntimeMode precedent, same shape. No other spec schema embeds these doors; stack.zod.ts has no realtime key; nothing outside packages/spec imports them. Right; the changeset names exactly these three doors.
  3. Doors whose accept set does NOT move, correctly left: protocol.zod.ts#RealtimeSubscribeRequestSchema.events (z.array(z.string())), websocket.zod.ts#EventSubscriptionSchema.events (EventPatternSchema), contracts/realtime-service.ts#RealtimeSubscriptionOptions.eventTypes (string[]), RealtimeEventPayload.type / RealtimeEventSchema.type (string). Only stale example prose on them moved. Right.
  4. TS narrowing: RealtimeEventType, SubscriptionEvent, Subscription, RealtimeConfig are z.input of the enum, so tsc refuses the old literals — stated in the changeset. Right.
  5. Producers untouched — the card's ⛔ holds. objectql/src/engine.ts publishDataEvent (:7099-7118, data.record.${action} parsed through DataEventSchema, payload: { ...event }), publishBulkDataEvent (:7198-7215, data.records.${action}), metadata/src/metadata-manager.ts (:695-723, metadata.${type}.${action} gated by MetadataEventType). None is in the file list. Right (declared ≠ enforced (ADR-0049): RealtimeEventType spells record.* while the runtime emits data.record.*, and SubscriptionSchema.filters is z.unknown() nothing reads #14446: repoint the enum, never the runtime).
  6. No transport, no route, no client subscribe primitive. No packages/rest, packages/runtime, packages/client, route table or ws/sse dependency in the 13 paths; the adapter change is a docblock @example. realtime: the bell cannot prefer a live channel because discovery advertises realtime as enabled:true with no HTTP/WS surface mounted and no route to connect to #14646 A and realtime: no WS/SSE transport — service-realtime is in-process only and clients long-poll; big-screen/command-center scenarios need real push #8347 甲 hold. Right.
  7. metadata.* deliberately not members — a SubscriptionEvent is record-shaped; metadata events keep MetadataEventType and subscribeMetadata (client/src/realtime-api.ts:73). The RealtimeEventPayload.type example 'metadata.object.updated' is a real member (events.zod.ts:19) on a string field the envelope carries. Right.
  8. Every prescription is true of the code: each retired value names an emitted twin that exists; data.records.* "carries a count and no record" (BulkDataEventSchema has no recordId); field.changed → data.record.updated with changes (DataEventSchema.changes exists); "no event is published per field" (no producer does). True.
  9. The version each prescription names — @objectstack/spec 17.5.0: spec is 17.4.0 on origin/main, pending changesets are minor/patch only, check-changeset-no-major refuses a major, so the carrier is 17.5.0; ADR-0087's amended level ruling names the npm release; the same form is already on 21 sites in 14 files at main. True of the release that will carry it (caveat in ③).
  10. Regenerated reference content/docs/references/api/realtime.mdx: description = the new .describe(), Allowed Values = the five, the Subscription / SubscriptionEvent tables and the RealtimeEvent.type example follow; generator-owned, check:docs green. True.
  11. README / adapter @example: the published { id: uuid, type, object, recordId, after, timestamp } is a valid DataEvent and is what engine.ts wraps as payload; the subscribe call matches RealtimeSubscriptionOptions; the security-posture sentence matches engine.ts. True.
  12. Enum docblock: "Nothing in the open framework parses a subscription" — true; "realtime.test.ts pins this enum equal to their union" — true; the old NOT YET ENFORCED block replaced by a sentence that stays true. True.
  13. D3 entry text: data-field-changed-event-retired sits in step17 (registry.ts:2138, toMajor: 17); stack.zod.ts has no realtime key; the 2026-09-04 ruling is realtime: the bell cannot prefer a live channel because discovery advertises realtime as enabled:true with no HTTP/WS surface mounted and no route to connect to #14646 A; surface / replacement carry no backticks or pipes; the entries/semantic/18.*.ts + registry.ts copy is the house pattern (check:migration-registry green). True.
  14. Ledger JSON: one-line _note addition only, unchanged since 0b2a76726c; all six rows stay dead per re-grade 5882152948; Spec property liveness green. Right.
  15. PR body: byte-identical to the body reviewed at 0b2a76726c; every sentence re-read against the 13-file diff — all still true. It does not name the two prose texts this head adds (see ③: silence, not falsity). True.
  16. objectui at the pin dd3f7e1b: 0 hits for RealtimeEventType or any old spelling. Console Pin Gate unaffected.
  17. NEW — packages/spec/liveness/README.md, the realtime_subscription row (shipped: liveness/ is in the tarball's files). The inserted clause reads ", as measured at 5f5511f0 before realtime: RealtimeEventType names the emitted data.record.* / data.records.* vocabulary (open-core contract half; the transport stays out, #14646 A and #8347 甲) #20288 repointed the enum at the emitted DataEventType + BulkDataEventType names," placed after the member list and before "are DISJOINT from what the engine publishes". Checked: 5f5511f0 is a real commit (2026-09-04, the census day the JSON _note records — "CENSUS METHOD AND SCOPE, run at 5f5511f (2026-09-04)"); the enum at this head is exactly DataEventType + BulkDataEventType; the qualifier is the same one the JSON _note carries ("the events.type row's member list is as measured at 5f5511f"). The sentence is now true as a dated record; the row otherwise reads as it did (delta: this one table line, nothing else in the file). Right. Inherited, not introduced: the row's pre-existing phrase "live emitter in service-knowledge" (present at origin/main) misnames a subscriber as an emitter — the engine emits, knowledge-service-plugin.ts branches on the strings. Outside the extension's surface ("nothing else in either file moves"), pre-dates this PR, no action owed here; noted for a later ledger tidy.
  18. NEW — packages/spec/scripts/liveness/check-liveness.mts:452-453 (not shipped: scripts/ is outside files). Two comment lines inserted after "whose four members": "(as measured at 5f5511f0, before realtime: RealtimeEventType names the emitted data.record.* / data.records.* vocabulary (open-core contract half; the transport stays out, #14646 A and #8347 甲) #20288 repointed the enum at the emitted / DataEventType + BulkDataEventType names)". Same qualifier, same truth; the delta shows +2 lines and nothing else in the file. Right.
  19. Gate derivation for a script edit: editing check-liveness.mts derives the pm-dispatch-gates / governed-merges / bare-root-worklist self-test families; all ride Lint & Repo Gates, green on this head. Neither edited file is a governed surface or single-writer path (both guards green). Right.
  20. Docs Drift Check (advisory, 5882899479) names three hand-written pages (automation/webhooks.mdx, protocol/kernel/realtime-protocol.mdx, protocol/knowledge.mdx) because they contain the NEW names data.record.* / data.records.*. Grep at this head: none of them names an old spelling or any RealtimeEventType member; the repoint makes them consistent, not false. No page falsified.

② Semver level

Unchanged from the reviewed head, and still right:

  • @objectstack/spec: minor with the **BREAKING** banner, FROM → TO mapping, the one-line fix, and the adr-0087: registered realtime-event-type-unemitted-values-retired marker (Check Changeset and Lint & Repo Gates green). Matches ADR-0087's amended level ruling and AGENTS.md. Right. The two prose texts this head adds are in the same package and ship (README) or do not ship (scripts/) under that same changeset; no further changeset is owed, and the dev rightly added none.
  • Clause-②: yes (narrowing) in the PR body and the changeset. Right — five values now parse that were refused (yes), four are refused that parsed ((narrowing)); the gate's own fixture names this shape ("a diff that widens AND narrows"). The prose delta moves no accept set and does not change the arm.
  • ADR-0087 disposition: D3 semantic entry, no D2 conversion — fits. D2 applies at the loader seam to metadata sources; a Subscription is reachable from no such seam (no stack key, no metadata type, no embedding schema, no importer outside spec), so a conversion entry would be a declared transform nothing runs. 47 step18 semantic entries are D3-only for the same reason. RETIRED_KEYS_BY_MAJOR correctly untouched.
  • @objectstack/service-realtime: patch — README ships in the tarball, the @example ships in dist .d.ts; a non-firing example in a released package is a documentation defect (precedent 17152). Right.

③ Boundary flags

Dev follow-up report 5884206324: open_questions none, out_of_scope_findings none, no deviations listed. Status of every flag from record 5883544890, plus what this head adds:

  • README.md:945 and check-liveness.mts:452-458 shipped-false / stale sentences (escalated in 5883544890 ③) — RESOLVED on this head by surface extension 5883551279 and commit 17612ad5ea: each carries the qualifier the JSON _note already carried; nothing else in either file moved (verified on the delta). The extension corrects the claim's composition error (a fence that forbade touching a text the change falsified) and says so on the card. Closed.
  • service-knowledge record.* legacy branch (knowledge-service-plugin.ts:218-231, TSDoc :24, knowledge-service.ts:232, :252) — unchanged and rightly outside this PR (reads event.type as a plain string, never the enum). Still a Prime Directive Add comprehensive test suite for Zod schema validation #12-class consumer fallback plus stale TSDoc with no carrier; still escalated: the seat files a domain:services card (dedupe words: knowledge-event-sync, record.created legacy branch, handleRecordUpsert payload.record). Not a blocker.
  • PR body does not list the two new prose texts — every sentence in it is true; the omission is silence, not falsity. The seat may add one bullet ("Ledger prose: the liveness README row and the check-liveness enrolment comment date the old member list to 5f5511f0") when it rewrites the body to cite the test evidence (5883321020 and 5884206324). Answered.
  • PR assignee unset — the seat sets os-justin (the card's) when it adopts this record. Answered.
  • "Closes" vs "Fixes" — both close; the card is pm:dispatched, not in the decision box. Answered.
  • No merge of origin/main on the branch — mergeable clean; the queue rebuilds the merged generation and re-runs the required set. Answered.
  • Inherited phrase "live emitter in service-knowledge" in the README row (①17) — pre-existing at origin/main, outside the extension's surface, not made false by this change. Noted; rides the next ledger tidy, no action owed by this PR.
  • websocket.zod.ts:104 describe example "record.*" — an EventPatternSchema wildcard on spelling (2); correctly fenced to the five-spelling convergence stage the re-grade reserved. Answered: stays with that stage's card.
  • client/src/realtime-api.ts:153-159 comment on data.field.changed "declared in DataEventType" — false since protocol 17, unrelated to this enum. Noted; rides the next client touch.
  • check:platform-checklist red — identical on the clean base, not CI-wired. Answered: pre-existing.
  • Version caveat, shared with 21 existing sites: "17.5.0" holds while 17.5.0 is uncut; if the maintainer cuts 17.5.0 first, this PR's four prescriptions go stale together with every pending tombstone at main — a fleet convention, not this PR's defect; the seat re-dates before enqueue if that happens.

Implemented-by: claude/issue-20288-realtime-event-type-emitted-vocabulary
Reviewed-by: session_01Sfe5YjBLwB9J3y8fvm2xq1

VERDICT: PASS

Adopted and posted by domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-09-29T05:31Z · rendered by the seat's at-tier review subagent on the new head; its served tier family was read from the subagent transcript before posting. The ③ service-knowledge escalation is already filed as #20573.


Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 29, 2026 05:33
@os-justin
os-justin added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit e73ee2d Sep 29, 2026
40 checks passed
@os-justin
os-justin deleted the claude/issue-20288-realtime-event-type-emitted-vocabulary branch September 29, 2026 06:02
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
… notes (objectstack-ai#20667)

Fixes objectstack-ai#20622

Clause-②: no

One new `patch` changeset (`@objectstack/cli`, in the fixed release
group) and nothing else. No code, no docs pages, no edit to any existing
changeset.

## What it carries

1. An upgrade line: the raised dependency floors cover what objectstack
loads; a lockfile-preserving upgrade can keep an older `hono` under
`@modelcontextprotocol/sdk` (via `@objectstack/cli` to
`@objectstack/mcp`), which objectstack never loads. `pnpm update hono`
clears a scanner. It states no version number.
2. A section naming, by PR number and title only, the 16 changesets (15
PRs) that shipped inside 17.5.0 without being consumed. Breaking entries
first: objectstack-ai#20458, objectstack-ai#20504, objectstack-ai#20567 (two changesets).

## Measurement

The brief's range command (`git log --diff-filter=A --name-only
8c87d26..0f6dcac -- .changeset/`) yields only 8 files. The other 8
were added BEFORE the version commit and were already left unconsumed by
it (the tree at `8c87d26a5d` still holds them). The set that shipped in
17.5.0 and is still pending is the changeset directory at `0f6dcac5e9`
intersected with `origin/main`: 16 files, all still pending, matching
the triage's 16 and its breaking set (3 PRs, 4 files). Breaking was
decided by each file's text (`BREAKING` banner / narrowing arm).

Code anchors for the upgrade line: `packages/mcp/package.json` depends
on `@modelcontextprotocol/sdk ^1.30.0`; `packages/cli/package.json`
depends on `@objectstack/mcp`;
`packages/plugins/plugin-hono-server/package.json` carries `hono
^4.13.5`; `packages/mcp/src` imports only `server/mcp`, `server/stdio`,
`server/webStandardStreamableHttp` and `types` from the SDK (no
`server/streamableHttp`).

## Gates

19 derived by `dispatch-gates.mjs --commands`, all 19 run and exit 0
(adr-0087-registration, changeset-no-major, closing-keyword-parity,
comment-mask-corpus, empty-changeset, gate self-tests, nul-bytes,
published-files and the rest); `--ran` reconciliation: 19 derived, 19
run, 0 NOT-MEASURED, 0 UNRUN. `check-changeset-fixed` green. Ordering:
must land before objectstack-ai#20639 (Version Packages, open at the time of writing).

---

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…bjectstack-ai#21290)

## What this is

A pre-cut draft of the curated release page for **17.6.0**,
`content/docs/releases/v17/17-6.mdx`, plus its entry in
`content/docs/releases/v17/meta.json` and in
`scripts/docs-audit/handwritten-docs.json`. It also appends one dated
correction to `content/docs/releases/v17/17-5.mdx` (details below).

Docs-only. Everything is under `content/docs/releases/` (release-owned)
or the docs-audit list, so this is the dedicated docs-only PR
`AGENTS.md` sanctions for that tree. It publishes nothing from any
package, hence `skip-changeset`.

Clause-②: no

It follows the 17.5.0 page's structure:

1. Highlights
2. What's new, including the "read this before treating the version
number as a safety guarantee" list of silent runtime changes
3. Breaking changes & migration (triaged, not exhaustive)
4. New capabilities
5. Notable fixes
6. New in Console (Studio)
7. Shipped in 17.5.0, listed again in 17.6.0's CHANGELOG
8. Upgrade checklist (every line marked *not exercised*)

## How it was compiled

- **Source:** all 338 changesets pending on `main` at `748b2407`. Every
changeset was read and triaged into breaking, feature, fix or internal.
PR numbers and short SHAs are taken from the commit that added each
changeset.
- **Already shipped in 17.5.0:** 16 of the 338 changesets (from 15
commits) describe code that 17.5.0 already published. I checked each
commit with `git merge-base --is-ancestor` against the version commit
`8c87d26a` and the publish commit `0f6dcac5`.
- Eight follow the version commit in first-parent order. The 17.5.0 page
already covers them.
- Seven are ancestors of the version commit but were not in the version
PR when it merged. The 17.5.0 page does not mention them. Two of them
are breaking (`e73ee2d` objectstack-ai#20567, `c876a74` objectstack-ai#20504), and `7a1faf1`
(objectstack-ai#20579) makes `os validate --strict` fail on a live conversion.
- The new section matches the list in
`.changeset/20622-release-aftercare-upgrade-and-unannounced-notes.md`.
- **Cross-check:** each of the nine ADR-0087 conversions added since
17.5.0 is covered by a migration entry on the page:
  - `action-block-endpoint-to-target`
  - `connector-sync-keys-removed`
  - `connector-triggers-removed`
  - `cube-refresh-key-removed`
  - `dataset-count-measure-empty-field-removed`
  - `form-field-public-picker-removed`
  - `form-view-subform-columns-canonicalized`
  - `page-header-breadcrumb-removed`
  - `time-default-utc-suffix-dropped`
- **Console section:** built from the three pin-bump changesets
(`dd3f7e1be356 → db11afd4967c → e420df310f5b → 31971ff1e28f`), which
carry 232 releasing objectui changesets, 23 of them declared breaking.
The range was also read with `scripts/objectui-range.mjs`.
- **Fact-check pass:** a second pass checked every cited SHA's changeset
against the page, in three slices. It found 31 claims that were wrong or
overstated, and the three follow-up commits correct them. Some examples:
- An RLS `contains` verdict that `d2bc644` (objectstack-ai#21253) later reversed in
this same release.
- The `manage_platform_settings` scope: reads of both types, but writes
of `datasource` only.
  - The dataset door's status after `434c6c7` (objectstack-ai#21240).
  - Filter positions that differ per change.
  - `7a1faf1`, which is not a breaking change.
- **Other corrections made while compiling:**
- The connector migration table does not claim that a `job` schedules a
`connectorSource` pull. `0efbdc3` says nothing schedules a pull until
the `job` stage lands, and none landed in this release.
- The public-form picker search-key change (`bafb8c9`, objectstack-ai#21136) is noted
as moot, because the route it changed is retired later in the same
release (`3dc33b2`, objectstack-ai#21222).

## Open items for the cut

These are recorded in the page's RELEASE-TIME TODO comment.

- **Anonymous endpoints.** Under the deny baseline (objectstack-ai#21217), an
app-declared anonymous endpoint (`authRequired: false`) can no longer
read or write objects. objectstack-ai#21158 is still open, and until it lands no
supported channel grants anonymous callers a permission set. The page
says so instead of prescribing a grant.
- **Console pin.** Three 17.6.0 server changes refuse a body that the
pinned Studio (`31971ff1e28f`) still sends. objectui fixed each one
after the pin:

| Studio action | Server change that refuses it | objectui fix (not in
the pin) |
|:--|:--|:--|
| A dataset count measure saved with the Field box blank (`field: ''`) |
objectstack-ai#21240 | `0858267e` |
| An External / Validate-only datasource saved without a credential |
objectstack-ai#21133 | `8001068b` |
| Republishing an html page that gained a plugin component | objectstack-ai#20852 |
`3ae91930` |

The page lists these under "Known console issues". If the pin moves
before the cut, the lines it fixes come out. Otherwise the
accepted-for-GA waiver is recorded.

## The correction to 17.5.0

One dated correction line is appended in place to the "Also shipped in
17.5.0" paragraph of `17-5.mdx`, in the form objectstack-ai#20985 used. It names the
seven commits that shipped in 17.5.0 with no CHANGELOG entry and links
to the 17.6.0 section. The original text is unchanged.

## Deliberately not in this PR

- `content/docs/releases/v17/index.mdx` is **untouched**. Its status
blockquote and per-release list may only claim 17.6.0 after the release
ships.
- An MDX comment at the top of `17-6.mdx` lists the release-time edits:
  1. the publish date;
  2. changesets landed after `748b2407`;
  3. the per-package CHANGELOG entry count;
  4. the console-pin outcome;
  5. objectstack-ai#21158 if it lands first;
  6. the `v17/index.mdx` update.

## Verification

Run locally on the head commit, with the workspace installed. All of
these pass:

- `check-issue-citations --base origin/main`: every added citation
resolves.
- `check:doc-anchors`, `check:role-word`, `check:docs-audit-scope`,
`check:nul-bytes`, `check:doc-authoring`, `check:docs-single-h1`,
`check:docs-redirects`, `check:docs-locale-catch-all`
- `check:corpus-claim-drift`, `check:docs-spec-enumerations`,
`check:published-readme-links`
- `check:release-notes`, `check:release-page-status`,
`check:release-index-currency-sync`, `check:release-body`
- `check-release-section-coverage` (plain and `--strict`),
`check-doc-frontmatter`, `check-docs-nav-label`,
`check-docs-section-name`, `check-section-landing-index`,
`check-doc-route-spelling --advisory`

Both pages compile as MDX with `@mdx-js/mdx` 3 + `remark-gfm`, and the
three tables parse with no ragged rows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_014VGCS11YUtYAiinRcdqQwL

---------

Co-authored-by: Claude <noreply@anthropic.com>
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 tests tooling

Projects

None yet

2 participants