Skip to content

docs(protocol): put the realtime subscribe fences on the shape SubscribeMessageSchema declares - #17747

Merged
os-bill merged 1 commit into
mainfrom
claude/issue-17184-subscribe-fence-field-shapes
Sep 12, 2026
Merged

os-bill merged 1 commit into
mainfrom
claude/issue-17184-subscribe-fence-field-shapes

Conversation

@os-bill

@os-bill os-bill commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Part of #17184 — the second half. The first half (the four undeclared message types) landed as #17700; this PR does not revisit it.

  • Clause-②: no — this PR puts no new key on any published payload.

packages/spec/src/api/websocket.zod.ts is byte-unchanged: blob 3348dd76745b61e83801e2e8bae48c6756c5a69c on both sides (git rev-parse HEAD:<path> vs git hash-object <path>, and an empty git diff HEAD -- <path>). Prose only, one file.

The defect

Six subscribe/unsubscribe sites carried every subscription field at the top level, in snake_case, where the declared contract nests them and spells them camelCase. SubscribeMessageSchema (websocket.zod.ts:278:281) declares exactly type: 'subscribe' plus a subscription object, on top of the three BaseWebSocketMessage fields; EventSubscriptionSchema (:102:117) declares subscriptionId / events / objects / filters / channels. A fence copied verbatim parses against no schema in the file.

The mismatch is not one thing, and each axis was verified separately:

page (top level) declared axes
subscription_id subscription.subscriptionId nesting + snake/camel + z.string().uuid(), so "sub_1" does not parse
object, a string subscription.objects, an array nesting + singular/plural + scalar/array
filter subscription.filters nesting + singular/plural
events: ["created", …] z.array(EventPatternSchema) bare verbs vs patterns — DataEventType publishes data.record.created
absent messageId, timestamp both required by BaseWebSocketMessage — an axis not in the card

Two page keys map to nothing at all

record_id and query are declared nowhere in the protocol. The narrowest scope the contract can express is object + event pattern, so those two sections now say so and move the narrowing to the client rather than showing fields no schema carries.

Second, independent defect in the same bullet list

The page described filter as "Optional filter (same syntax as HTTP API filters)". The schema's docblock (:106:115) says the opposite in as many words — "⚠️ NOT YET ENFORCED — no runtime evaluates a payload filter. matchesSubscription matches on object name and event type only … a subscription carrying filters receives every event its patterns match." The page promised enforcement the contract explicitly disclaims; it now carries that disclaimer.

Fence census — the round's first deliverable

Enumerated on this tree, not inherited from the dispatch's page-wide counts.

# line (pre) section type top-level keys
1 256–266 Subscribe to Object Events subscribe type, subscription_id, object, events, filter
2 379–384 Unsubscribe unsubscribe type, subscription_id
3 404–412 Subscribe to Specific Record subscribe type, subscription_id, object, record_id, events
4 421–435 Subscribe to Query Results subscribe type, subscription_id, object, query, events
5 655–662 Permission Enforcement subscribe type, subscription_id, object, events
6 679–689 Client Reconnection (JS) subscribe type, subscription_id (+ sub_${Date.now()})

⭐ The card named four sections. The census found six sites: Unsubscribe (#2) and the JS client snippet (#6) were not on the card's list. #6 was also missed by a first pass that matched only JSON "type": "subscribe" — it uses the single-quoted JS form, and the census was widened to catch it.

After the edit all six carry exactly messageId / type / timestamp / subscription (or request for unsubscribe).

Fork verdict: (a) the page is wrong — every fence

Producer evidence on this tree, all six fences landing on the same verdict:

  • SubscribeMessageSchema, UnsubscribeMessageSchema and EventSubscriptionSchema have zero consumers outside packages/spec's own declaration, its unit test and the type-alias pin.
  • IRealtimeService.handleUpgrade is deliberately unimplemented platform-wide — service-realtime/src/no-channel-route.pin.test.ts:49 asserts it is not a function.
  • subscription_id0 across 6120 tracked packages/** source files (no dist/), with four lit controls on the same corpus and the same call: subscriptionId 40, eventTypes 27, handleUpgrade 18, EventSubscriptionSchema 13.
  • The 660 record_id hits are the generic DB-column convention: zero files carry both record_id and WebSocket.

Nothing produces or consumes a WebSocket subscribe message, so no top-level field on one can be real. Reading (b) — the schema is incomplete — would need a producer, and there is none.

objectui: reported, not counted. All WS-subscription probes return 0 there, but the schema-side control is also dark (subscriptionId 0, EventSubscription 0) — only the generic useState (2262) lights, which proves the corpus is real source and nothing more. A zero whose control does not light is not a reading. cloud is not checked out here: NOT MEASURED.

Verification

  • 40/40 derived gates green, on a clean full re-run after the builds. Gate family derived mechanically via node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (it reads the change set from the merge base itself), not hand-listed.
  • Five gates first returned PREREQUISITE NOT MET / "build first" (exit 3 and exit 1) — NOT MEASURED, not red. After building @objectstack/lint..., @objectstack/spec and @objectstack/client-react... all five are green. Exit codes captured before any pipe.
  • MDX compiles: pnpm exec fumadocs-mdx exit 0.
  • No generated or tracked artifact moved — authorable-surface.base.json included. git status --porcelain shows exactly one file.
  • No control characters: check:nul-bytes exit 0, plus a direct grep -naP sweep of the edited file.

skip-changeset, and a conflict with the dispatch to flag

⚠️ The dispatch asked for a patch changeset; this PR carries none, and the label instead. Reporting rather than silently choosing:

  • content/docs/** ships in no published package's files[] — 0 of 70 publishable packages mention content/ or docs/ there, with all 70 shipping dist as the positive control.
  • @objectstack/docs, which owns the page, is private: true — there is no publishable package to bump. Naming any other package would ship a release note for a package this PR does not touch.
  • Precedent on the identical surface: docs(protocol): drop the five WebSocket message types WebSocketMessageType does not declare #17700, the first half of this very card, merged with no changeset.

By the standing criterion — nothing published moves — this is the textbook skip-changeset case, which lint.yml states in those words. Happy to add a changeset if the maintainer reads it the other way.

Scope

Confined to the subscribe/unsubscribe fences and their Parameters bullet lists. ⛔ Not websocket.zod.ts, ⛔ not the message-type sections #17700 corrected, ⛔ not content/docs/releases/, ⛔ no other page.

One finding deliberately left out and filed separately: the type: "event" fences on this same page carry top-level subscription_id / event / data / changes / reason where EventMessageSchema declares subscriptionId / eventName / payload / userId and no changes or reason. Same defect class, different message family — it is not this round's declared face, and it is not silently widened into.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH


Generated by Claude Code

…ibeMessageSchema declares

Six subscribe/unsubscribe sites on realtime-protocol.mdx carried every
subscription field at the TOP LEVEL, in snake_case, where the declared contract
nests them and spells them camelCase. SubscribeMessageSchema declares exactly
`type` plus a `subscription` object on top of the three BaseWebSocketMessage
fields; EventSubscriptionSchema declares subscriptionId / events / objects /
filters / channels. The fences copied verbatim parse against no schema.

The mismatch was not one thing, and each axis is corrected separately:

  subscription_id  -> subscription.subscriptionId  nesting + snake/camel, and
                                                   the schema declares uuid(),
                                                   so "sub_1" does not parse
  object (string)  -> subscription.objects (array) nesting + singular/plural
                                                   + scalar/array
  filter           -> subscription.filters         nesting + singular/plural
  events verbs     -> event patterns               "created" -> data.record.created
  (absent)         -> messageId, timestamp         both required by
                                                   BaseWebSocketMessage

Two page keys map to nothing at all: `record_id` and `query` are declared
nowhere in the protocol, so the narrowest scope the contract can express is
object + event pattern. Those two sections now say so and move the narrowing to
the client rather than showing fields no schema carries.

Second, independent defect in the same bullet list: the page described `filter`
as "Optional filter (same syntax as HTTP API filters)" while the schema's own
docblock says the opposite in as many words -- NOT YET ENFORCED, no runtime
evaluates a payload filter, matchesSubscription matches on object name and event
type only, so a subscription carrying filters receives every event its patterns
match. The page promised enforcement the contract explicitly disclaims; it now
carries that disclaimer.

Measured on this tree, fork (a): the page is wrong, not the schema.
SubscribeMessageSchema, UnsubscribeMessageSchema and EventSubscriptionSchema
have zero consumers outside packages/spec's own declaration, unit test and type
pin; handleUpgrade is deliberately unimplemented platform-wide and
no-channel-route.pin.test.ts asserts it is not a function. `subscription_id`
returns 0 across 6120 tracked package source files with four lit controls on the
same corpus (subscriptionId 40, eventTypes 27, handleUpgrade 18,
EventSubscriptionSchema 13). Nothing produces these messages, so no top-level
field can be real.

Prose only. packages/spec/src/api/websocket.zod.ts is byte-unchanged -- blob
3348dd7 on both sides.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
@os-bill os-bill added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 12, 2026 — with Claude
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Sep 12, 2026
@os-bill
os-bill marked this pull request as ready for review September 12, 2026 02:17
@os-bill
os-bill added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit ec739e6 Sep 12, 2026
37 checks passed
@os-bill
os-bill deleted the claude/issue-17184-subscribe-fence-field-shapes branch September 12, 2026 02:44
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 skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants