Repository navigation
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
Activity
Findings-round route repair:
domain:specappended. Routing only —findinggrade untouched, no ownership taken.- Acceptance-face call (why this sibling routes differently from GroupingConfigSchema.fields describes "(supports up to 3 levels)" but the gate is .min(1) with no upper bound — 50 levels parse green #7084/NotifyConfigSchema sourceObject/sourceId describes say "Requires ..." while the schema deliberately accepts the half pair (executor drops it silently) #7085): here the prose, the downstream TypeScript types, and the dispatcher's blind cast all already treat the vocabulary as closed (
info | warning | critical); only the Zod gate is open. The fix that reconciles the surfaces is closing the gate (enum or documented open-set semantics with a widened downstream type) — and any change that makesseverity: "urgent"stop parsing green is an accept/reject change, which the standing red line routes todomain:specno matter how small (meta: bind Zod schemas for webhook / connector / sharing_rule WITHOUT registering the kinds — close the unvalidatedPUT /metawrite hole (#2657 audit, option A) #6245 / feat(spec): action param 的 options[] 声明逐选项 visibleWhen (#5016) #6235 precedents). Even the fallback fix (softening the describe to "e.g." prose) requires that seat to first decide the vocabulary's intended openness, so the decision sits withdomain:speceither way. - Version-window note for that seat: tightening to an enum is breaking for any stored config carrying an out-of-vocabulary value — if the enum route is taken it wants the v17 window and the usual migration treatment, which is a reason this card should not linger ungraded past the RC cut.
- Premise verified on
origin/main@ac244ad:io-node-config.zod.ts:160still readsseverity: z.string().optional().describe('info | warning | critical').
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Acceptance-face call (why this sibling routes differently from GroupingConfigSchema.fields describes "(supports up to 3 levels)" but the gate is .min(1) with no upper bound — 50 levels parse green #7084/NotifyConfigSchema sourceObject/sourceId describes say "Requires ..." while the schema deliberately accepts the half pair (executor drops it silently) #7085): here the prose, the downstream TypeScript types, and the dispatcher's blind cast all already treat the vocabulary as closed (
Findings triage — promoted
finding→pm:queue(grading pass).- Why queue, not hold: restore-invariant class. As recorded at routing time, the prose, the downstream types, and the blind cast all treat
severityas the closed vocabulary "info | warning | critical" — only the gate is open. The fix tightens the gate to the already-declared contract; that is an accept/reject change, which is why this card staysdomain:spec(red line, meta: bind Zod schemas for webhook / connector / sharing_rule WITHOUT registering the kinds — close the unvalidatedPUT /metawrite hole (#2657 audit, option A) #6245/feat(spec): action param 的 options[] 声明逐选项 visibleWhen (#5016) #6235 precedent). - Version window: tightening acceptance is breaking for anyone relying on the open string today, so the landing window (inside v17 vs deferred) is the spec seat's call, made against its strictness ledger. No
target:v17from triage — the open gate does not itself fail the four blocking classes (values flow through; no user-visible failure today); if the seat judges the tightening must ride v17's window, escalate via the referral path for the label. - Premise — unchanged since the 15:19Z routing round; re-checked describes on
origin/main@ 2d1ddf0 in the same sweep as GroupingConfigSchema.fields describes "(supports up to 3 levels)" but the gate is .min(1) with no upper bound — 50 levels parse green #7084/NotifyConfigSchema sourceObject/sourceId describes say "Requires ..." while the schema deliberately accepts the half pair (executor drops it silently) #7085.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Why queue, not hold: restore-invariant class. As recorded at routing time, the prose, the downstream types, and the blind cast all treat
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 downstreamNotification['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/mainbefore 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
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Oct 7, 2026
Finding from the axis-①
.describe()sweep (describe claims vs measured acceptance face — the #6762 class). Recorded unassigned; suggestdomain:spec-surfacefor routing. Sibling of thesourceObject/sourceIdfinding on the same schema (#7085 — different field, different claim type).Anchor
packages/spec/src/automation/io-node-config.zod.ts,NotifyConfigSchema: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 (compareexecution.zod.tsregionKind's "loop-body | parallel-branch | try | catch", where the engine emits exactly those four).Measured acceptance face
Probed on
origin/main@2f3e79351(tsxsafeParse, sources viagit archive; controls both sides — same run as #7085, whose unknown-key control rejected):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:
const severity = cfg.severity ? String(cfg.severity) : undefined(packages/services/service-automation/src/builtin/notify-node.ts);severity: (p.severity as Notification['severity']) ?? 'info'(packages/services/service-messaging/src/dispatcher.ts), whereNotification['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 downstreamswitchon the three names silently falls through.Why it matters for an authoring reader (ADR-0033)
content/docs/references/automation/io-node-config.mdxrenders 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 writesCriticalorurgentgets 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.