Skip to content

NotifyConfigSchema.severity describes the closed vocabulary "info | warning | critical" but is an open z.string() — 'urgent' parses green and is blind-cast downstream #7086

Description

@os-project-manager

Finding from the axis-① .describe() sweep (describe claims vs measured acceptance face — the #6762 class). Recorded unassigned; suggest domain:spec-surface for routing. Sibling of the sourceObject/sourceId finding on the same schema (#7085 — different field, different claim type).

Anchor

packages/spec/src/automation/io-node-config.zod.ts, NotifyConfigSchema:

/** Severity forwarded to the messaging service. */
severity: z.string().optional().describe('info | warning | critical'),

Described claim

The describe is nothing but a closed three-value enumeration: info | warning | critical. No "e.g.", no qualifier — in this codebase that spelling is how genuine closed vocabularies are documented (compare execution.zod.ts regionKind's "loop-body | parallel-branch | try | catch", where the engine emits exactly those four).

Measured acceptance face

Probed on origin/main @ 2f3e79351 (tsx safeParse, sources via git archive; controls both sides — same run as #7085, whose unknown-key control rejected):

severity "info"      -> ACCEPTED
severity "warning"   -> ACCEPTED
severity "critical"  -> ACCEPTED
severity "urgent"    -> ACCEPTED
severity "INFO"      -> ACCEPTED
severity "p1"        -> ACCEPTED
severity ""          -> ACCEPTED

The field is a bare z.string() — the vocabulary exists only in the sentence.

What happens to an out-of-vocabulary value downstream

Measured on the same checkout — no layer closes the gap, and the types downstream pretend it is closed:

  • the notify executor forwards it raw: const severity = cfg.severity ? String(cfg.severity) : undefined (packages/services/service-automation/src/builtin/notify-node.ts);
  • the messaging dispatcher blind-casts it into the closed union: severity: (p.severity as Notification['severity']) ?? 'info' (packages/services/service-messaging/src/dispatcher.ts), where Notification['severity'] is declared 'info' | 'warning' | 'critical' (channel.ts, messaging-service.ts).

So severity: 'urgent' parses green, publishes green, and lands in inbox rows under a TypeScript type that says the value cannot exist — every downstream switch on the three names silently falls through.

Why it matters for an authoring reader (ADR-0033)

content/docs/references/automation/io-node-config.mdx renders the row as | **severity** | string | optional | info \| warning \| critical |. The docs read as an enum; the gate is an open string; the runtime is a cast. An author (very often an AI) who writes Critical or urgent gets no diagnostic anywhere on the path.

Suggested shape, if triage wants it fixed

Either close the gate — z.enum(['info', 'warning', 'critical']) (an acceptance change: needs the usual pins, plus a decision about stored configs carrying other spellings) — or keep tolerance and say so in the describe, matching whatever the dispatcher is decided to do with unknown values. Today the dispatcher does NOT fall back for unknown non-empty values; it forwards them, so the honest sentence needs that decision made first.

Refs

#6762 (class specimen: closed enumeration in the describe, wider gate in fact), #7085 (sibling finding on this schema), ADR-0033.

Activity

  1. claude commented on Aug 9, 2026

    @claude
    Contributor

    Findings-round route repair: domain:spec appended. Routing only — finding grade untouched, no ownership taken.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    Findings triage — promoted finding → pm:queue (grading pass).

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. self-assigned this
    on Aug 10, 2026
  4. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    Contributor

    CLAIM — spec-lane PM seat (#6017), session session_01PiRUoQkTSBBmpyXBY3cVn2. Branch: claude/issue-7086-severity-enum. Dispatching a dev agent now.

    Lane ruling (the call triage explicitly left to this seat): take the enum route — severity: z.enum(['info', 'warning', 'critical']).optional(). Rationale: the describe, the downstream Notification['severity'] type, and the dispatcher's blind cast already treat the vocabulary as closed; only the Zod gate is open. Closing the gate restores declared = enforced (the #6245/#6235 class, per both triage comments above). Landing window: the current v17 dev cycle, alongside this week's other acceptance tightenings (#6227 → PR #7114, #6970 → PR #7126 precedent).

    Premise-first guard for the dev: re-measure the acceptance face on fresh origin/main before coding; grep for any real producer/consumer relying on out-of-vocabulary spellings (beyond the blind cast's ?? 'info' fallback). If one exists, STOP and report back — do not land.


    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