Skip to content

finding(spec): EventNameSchema's only three binding schemas have no runtime consumer — the live event vocabulary is a closed enum that never touches it #13613

Description

@os-warren

Observed while running the per-surface identifier census for #12245. Filed unassigned as a finding, not a defect claim.

What was observed

EventNameSchema (packages/spec/src/shared/identifiers.zod.ts) is bound by exactly
three schemas, and by nothing else in either repository:

Binding Site Runtime consumer?
EventTypeDefinitionSchema.name kernel/events/core.zod.ts:94 none
EventSchema.name kernel/events/core.zod.ts:122 none
EventMessageSchema.eventName api/websocket.zod.ts:299 none

Reproduced on origin/main at e2debee6: outside kernel/events/core.zod.ts and
api/websocket.zod.ts themselves, every reference to those three names is a generated
baseline (api-surface/*.json), the file's own test, type-alias-convention.pin.test.ts,
CHANGELOG.md, or V3_MIGRATION_GUIDE.md. No plugin, service, driver, or app parses
through any of them.

What the platform actually validates event names with is a closed literal enum that
does not reference EventNameSchema at all — packages/spec/src/api/events.zod.ts:

export const DataEventType = z.enum([
  'data.record.created',
  'data.record.updated',
  'data.record.deleted',
]);

export const BulkDataEventType = z.enum([
  'data.records.updated',
  'data.records.deleted',
]);

grep -c EventNameSchema packages/spec/src/api/events.zod.ts returns 0.

The sibling free-text surface is unconstrained rather than schema-guarded:
WebSocketEventSchema.channel is a bare z.string() (api/websocket.zod.ts:468) whose
own describe() documents "record.account.123" and "user.456" as legitimate channel
spellings.

Why it may matter

EventNameSchema is presented as the platform's event-name grammar — the docblock on
SystemIdentifierSchema lists "Event keys" among the surfaces it governs, and
EventNameSchema carries its own worked examples. In practice it governs nothing that
runs: the real vocabulary is closed and enumerated, and the one open event-ish surface
(channel) opted out into z.string().

That makes it the ADR-0049 declared-but-unenforced shape, with the usual consequence for
AI-authored metadata: a generator that reads EventNameSchema and emits an event name
satisfying it has satisfied nothing the platform will check, while a generator that emits
anything outside the closed enum is refused by a rule the identifier file never mentions.

Two routes, neither obvious and neither recommended here:

  1. Bind it — have the open event surfaces (starting with channel, and any future
    custom-event registration) run through EventNameSchema, making the declared grammar
    the enforced one.
  2. Retire it under ADR-0049 enforce-or-remove, and let DataEventType /
    BulkDataEventType stand as the only event-name contract. This is a published-export
    removal, so it takes the ADR-0087 conversion.

Recording the measurement only; sizing and the route are a maintainer call.

Related: #12245 (the census that surfaced this) · #4673 (the same class, on
DataEventType's retired data.field.changed member) · ADR-0049.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions