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.

Activity

  1. os-warren commented on Aug 31, 2026

    @os-warren
    CollaboratorAuthor

    分诊定级 → domain:spec · needs-user-decision · p3 · tooling。摘 finding。

    锚定。 packages/spec/src/shared/identifiers.zod.ts 及两条路线的落点(kernel/events/core.zod.ts · api/websocket.zod.ts · api/events.zod.ts)全在 packages/spec ⇒ domain:spec。

    为什么交你裁。 ADR-0049 已经把原则定死(声明而不强制的必须强制或移除),所以要裁的是路线,而两条都动公共契约:路线 1(绑定 channel 等开放事件面)是 accept 集收窄;路线 2(退役)是已发布 export 的移除,须走 ADR-0087 conversion。⛔ 分诊不代裁。

    定级 p3:无行为缺陷、无错误答案上线。危害在 ③ 轴且是真的 —— 一个读 EventNameSchema 并据以生成事件名的 generator 满足了一个平台根本不检查的东西;而它若生成闭合枚举之外的任何名字,会被一条 identifier 文件从未提及的规则拒收。两个方向都误导,这是比单纯「死代码」更贵的形状。

    ⭐ 与 #13612 是同族,建议同一次裁决一并给方向 —— 但不要合卡

    三张卡出自同一次 #12245 普查,是同一种病的三个面:

    卡 形状 本轮处置
    本卡 EventNameSchema 有三个绑定 schema,但那三个零运行期消费者 needs-user-decision p3
    #13612 六个 branded identifier 零绑定(+ 评论补的三个「绑定了但被绑定方无读者」) needs-user-decision p3
    #13621 SystemIdentifierSchema 散文声称覆盖 11 个面,实测只绑 1 个 已入队 pm:queue p3(纯散文更正,零 accept 集变更)

    ⇒ 裁 #13612 与本卡时,同一句「bind 还是 retire」可以覆盖两张,省你一次上下文切换。

    ⛔ 但两张不合并,因为 bind 路线的爆炸半径完全不同:#13612 的绑定会改 object name / field name 的 accept 集(内联正则允许前导下划线,SnakeCaseIdentifierSchema 不允许 ⇒ 存量元数据可能被拒);本卡的绑定动的是 WebSocketEventSchema.channel(今天是裸 z.string())⇒ 收窄一个从未被约束过的自由文本面,风险与代价都是另一回事。一句方向、两张各自评估半径。

    ⚠️ 若裁 #13612 或本卡走 bind,#13621 的散文更正要跟着改(它现在的任务是「把散文写成今天的真相」;绑定会改变那个真相)。⇒ #13621 应排在两张裁决之后,或明确写成「按裁决后的绑定状态落笔」。本卡与 #13612 的评论里都记了这一条。

    派单/裁决时手上要有的三个读数(已复核口径,origin/main @ e2debee6)

    1. 三个绑定站点 —— EventTypeDefinitionSchema.name(kernel/events/core.zod.ts:94)· EventSchema.name(:122)· EventMessageSchema.eventName(api/websocket.zod.ts:299),三者均无运行期消费者;
    2. 平台实际校验事件名用的是闭合字面量枚举 —— DataEventType / BulkDataEventType(api/events.zod.ts),且 grep -c EventNameSchema packages/spec/src/api/events.zod.ts = 0;
    3. 唯一开放的事件类面 WebSocketEventSchema.channel(api/websocket.zod.ts:468)是裸 z.string(),其 describe() 自己举的合法拼法是 "record.account.123" / "user.456" —— ⚠️ 这两个例子是否满足 EventNameSchema 的文法,是 bind 路线的第一道判据,裁决前值得实测一次,否则「绑定」可能当场拒掉平台自己文档里的示例。

    Refs:#12245(普查)· #4673(同类,DataEventType 已退役成员 data.field.changed)· ADR-0049。


    Generated by Claude Code

  2. self-assigned this
    on Sep 1, 2026
  3. os-support-ai commented on Sep 1, 2026

    @os-support-ai
    Collaborator

    Claim: session_01Mciyv38maJ6HYVMiaM26T1 (domain:spec execution seat, dispatch batch 3) · branch claude/issue-13612-identifier-retirements · family-folded with #13612 per both cards' 2026-09-01 RULED comments (one dispatch, one PR, each member independently verifiable). Route: retire per the ruling — EventNameSchema and its three bindings out under ADR-0049 enforce-or-remove + ADR-0087 conversion; DataEventType/BulkDataEventType stand as the only event-name contract; ⛔ WebSocketEventSchema.channel stays a deliberate z.string() — the ruling adds no constraint there. Premise re-verified on origin/main @ c85a2657: exactly three binding sites (core.zod.ts:94, :122, websocket.zod.ts:299), no runtime consumer. Clause-② ⇒ contract-review tier; in-seat review before enqueue. ⛔ #13621 (the prose-correction sibling) is NOT part of this dispatch and proceeds after this lands.


    Generated by Claude Code

  4. added a commit that references this issue on Sep 1, 2026
  5. os-support-ai commented on Sep 1, 2026

    @os-support-ai
    Collaborator

    os-dev-report

    {
      "issue": 13613,
      "status": "done",
      "branch": "claude/issue-13612-identifier-retirements",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14158",
      "premise_still_valid": true,
      "summary": "Retired EventNameSchema and its EventName type per the 2026-09-01 RULED comment (director batch C): removed from shared/identifiers.zod.ts with a retirement note; the three binding fields (EventTypeDefinitionSchema.name and EventSchema.name in kernel/events/core.zod.ts, EventMessageSchema.eventName in api/websocket.zod.ts) stay and widen to plain z.string() with describes pointing at the closed enums; retired-defs row shared/EventName registered under protocol major 18 plus D3 semantic entry event-name-schema-retired; manifest key deleted behind the #4725 gate; Iso499 isomorphism pin left with the schema (836 to 835, ledgered in the pin file per house style). DataEventType / BulkDataEventType byte-for-byte untouched and stand as the only event-name contract; WebSocketEventSchema.channel untouched as a deliberate bare z.string() per the ruling. Premise re-verified on origin/main at c85a2657 and again on the merged tree: exactly three binding sites, zero runtime consumers. #13621 untouched (the Event-keys line in SystemIdentifierSchema's docblock is that card's surface). Family-folded with #13612 in one PR; this card's increment is commit 928a373a plus its share of the changeset.",
      "files": [
        "packages/spec/src/shared/identifiers.zod.ts (EventNameSchema + EventName removed, retirement note added)",
        "packages/spec/src/kernel/events/core.zod.ts (two name fields to z.string(), retirement note)",
        "packages/spec/src/api/websocket.zod.ts (eventName to z.string(), retirement note, import dropped)",
        "packages/spec/src/shared/identifiers.test.ts (EventNameSchema block removed)",
        "packages/spec/src/kernel/events.test.ts (reject-side grammar fixture repinned to the widening)",
        "packages/spec/src/type-alias-convention.pin.test.ts (Iso499 pin removed, count 836 to 835, ledger comment added)",
        "packages/spec/src/migrations/entries/retired-defs/18.shared__EventName.ts (new)",
        "packages/spec/src/migrations/entries/semantic/18.event-name-schema-retired.ts (new)",
        "packages/spec/src/migrations/registry.ts + json-schema.manifest/shared.json + api-surface/declaration-map/export-origins + content/docs/references/** + skills/*/references/_index.md (regenerated)",
        "packages/spec/ZOD_SCHEMA_AUDIT_REPORT.md (hand-maintained line corrected)",
        ".changeset/identifier-schemas-declared-unenforced-retired.md (shared with #13612)"
      ],
      "gates_and_tests": "Full readings on PR #14158 (shared with #13612), all at merged head a46ef310: spec suite 448 files / 12033 tests all passed (the pre-merge lap caught the events.test.ts fixture that pinned the deleted grammar branch - triaged and repinned as an acceptance pin holding the widening); spec typecheck green; dispatch gate list plus derived union all PASS. NOT MEASURED locally, CI-owed by their own printed verdicts: dual-build-cjs-loads, type-check-debt, test-completeness, dev-prereqs. Skills diff is generated refs only: 8 _index.md files, per-file line counts unchanged, SKILL.md bundle sum 10537 to 10537.",
      "derived_judgments": [
        "Replacement shape for the three binding fields: plain z.string() with a describe naming the closed enums as the platform-checked vocabulary - a widening, deliberately NOT a binding to the enums (EventTypeDefinitionSchema declares custom event types; binding would be a new constraint the ruling did not order). Declared per-field in the PR body for contract review",
        "events.test.ts reject fixture pinned exactly the deleted grammar branch; repinned as an acceptance pin over the previously refused spellings so a silent grammar regression goes red",
        "EventName had no EventNameParsed sibling and no external type consumers - type removal is TS2305-only",
        "ZOD_SCHEMA_AUDIT_REPORT.md treated as hand-maintained (last touched by #13895) and its EventNameSchema mention corrected"
      ],
      "semver": "@objectstack/spec minor + BREAKING banner (launch-window grade, the ruling's own words); shared changeset carries the adr-0087 marker registering event-name-schema-retired",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  6. os-support-ai commented on Sep 1, 2026

    @os-support-ai
    Collaborator

    Collection review: ACCEPT · Clause-② contract review: PASS

    Reviewed PR #14158 at head 881b685f (this card's increment is commit 928a373a, independently verifiable per the family rule). Tier fuse read before review: last_served_model = claude-fable-5 (= CONTRACT_REVIEW_TIER); in-seat review per the 2026-08-31 ruling.

    Contract judgment list (this card's increment):

    1. Derived judgments — verified from hunks. EventNameSchema + EventName removed from shared/identifiers.zod.ts with a tombstone note; SystemIdentifierSchema/SnakeCaseIdentifierSchema/MetadataItemNameSchema byte-untouched. The three binding fields (EventTypeDefinitionSchema.name, EventSchema.name, EventMessageSchema.eventName) stay and widen to plain z.string() with describes naming the closed enums — exactly the ruled shape (DataEventType/BulkDataEventType stand as the only event-name contract; both byte-untouched in the diff). WebSocketEventSchema.channel untouched (the ruling adds no constraint there). Every previously-valid document stays valid (pure widening, no stored break). The widening is pinned in events.test.ts: spellings the retired grammar refused now must parse, so a silent regression to a schema-level grammar becomes a deliberate ruling rather than drift — a judgment I endorse. retired-defs/18.shared__EventName.ts + D3 semantic entry event-name-schema-retired; the Iso pin ledgered 836→835.
    2. Semver — covered by the family changeset (minor + BREAKING, launch-window grade), with this card's own section, FROM→TO mapping (including the no-action path for stored documents), and its semantic id in the ADR-0087 marker. Consistent.
    3. Boundary flags — the one declared judgment call (replacement shape at the three fields) confirmed against the ruling's text; no open flags.

    Micro-patch audit and the NOT-governed (0/44, #11705-certified twice) landing reading are recorded on #13612's ACCEPT comment — same PR, same head.

    PASS ⇒ stripping needs:contract-review (both carriers) in this same stroke; ordinary queue landing proceeds (ready → auto-merge).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions