fix(spec): close notify.severity to its declared info | warning | critical vocabulary (#7086) - #7192
Conversation
…l vocabulary (#7086) NotifyConfigSchema.severity was a bare z.string() whose .describe() read 'info | warning | critical', so the enumeration existed only in the sentence: 'urgent', 'INFO' and '' all parsed green, were forwarded raw by the notify executor, and were blind-cast by the messaging dispatcher into a union that declares those values impossible. Every other surface already declared the set closed (the describe, the Notification['severity'] type, the sys_inbox_message.severity select field), so this closes the last open one. Safe because the executor reads severity RAW -- it is one of three keys (channels, topic, severity) that never pass through interpolate() -- so a {token} template there never resolved. The module JSDoc claimed "every string-ish value except channels" is interpolated; that was stale for topic and severity and is corrected here, since the tightening's safety rests on it. Blast radius is an execute-time refusal, not a load failure: FlowNodeSchema .config is an untyped record, so stored flows still load and rehydrate. Also closes the Studio form descriptor to the same set, and extends the IO-node form/Zod ledger test to reconcile closed value vocabularies rather than key sets alone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Closes #7086
NotifyConfigSchema.severitywas a barez.string()whose.describe()read'info | warning | critical'. The enumeration existed only in the sentence, so'urgent'parsed green, was forwarded raw by thenotifyexecutor, and wasblind-cast by the messaging dispatcher into a union that declares the value
impossible. This closes the gate to the vocabulary every other surface already
declared, per the spec-lane ruling on the issue.
Premise re-measurement (before writing any code)
Re-measured on fresh
origin/main@3e8e669c0, not taken from the issue:(a) The field is still an open
z.string()—io-node-config.zod.ts:160:(b) Probed acceptance face (
safeParseon the unmodified source, worktree clean at3e8e669c0, controls both sides):(c) No real producer or consumer relies on an out-of-vocabulary severity. Swept the
monorepo: every
notifyconfig inexamples/(app-showcase14 sites,app-todo1),every test fixture, and every seed spells only
info/warning/critical. The onlytolerance on the path is the dispatcher's blind cast
?? 'info'(which fires onundefined, never on a non-empty unknown value) andnotify-node.ts's raw forward, bothnamed in the issue. Premise valid — proceeded.
One measurement the issue did not have, and the reason this tightening is safe:
the executor reads
severityRAW. It is one of three keys (channels,topic,severity) that never pass throughinterpolate(), so a{record.x}template there wasforwarded verbatim and never resolved — closing the gate removes no working authoring
shape. The schema's module JSDoc claimed "every string-ish value except
channels" isinterpolated; that was stale for
topicandseverity, and since it is the statementthis change's safety rests on, it is corrected here.
Changes
packages/spec/src/automation/io-node-config.zod.tsseveritybecomesz.enum(['info', 'warning', 'critical']).optional(); describe becomes a sentence about the field (the vocabulary is now carried by the type, matchingBpmnDiagnostic.severity/screen.modehouse style); module JSDoc's interpolation claim correctedpackages/spec/src/automation/io-node-config.test.ts'urgent'/'INFO'/''assertingcodeandpath; a pin that the vocabulary lives in.optionsand is not smuggled back into prosepackages/services/service-automation/src/builtin/notify-node.tspackages/services/service-automation/src/builtin/io-node-form-zod-ledger.test.ts.changeset/notify-severity-closed-vocabulary.mdThe refusal is self-prescribing, which is the ADR-0033 half:
Blast radius: an execute-time refusal, not a load failure
FlowNodeSchema.configisz.record(z.string(), z.unknown()).optional(), soNotifyConfigSchemaruns only at execute time viaparseNodeConfig. A stored flowcarrying
severity: 'urgent'still loads and rehydrates exactly as before; thenotifystep refuses when it runs, naming the three legal values.
''previously degraded toinfotwo layers down and is now refused at the gate.No ADR-0087 entry is owed, and the changeset records that disposition explicitly
(
not-required (no-migration-prescription)): nothing fails to load, and no automaticrewrite is correct — mapping a stored
'urgent'to'info'would silently pick aseverity on the author's behalf, which is the blind-cast defect this change removes.
Reverse verification — direction predicted BEFORE running
'urgent'/'INFO'/'''info'/'warning'/'critical'.options+ describe pinsStated plainly because the usual template presumes before-green/after-red for every pin:
the accept pins prove nothing about this fix — pre-fix everything parsed, so they
were always green. Their job is the opposite direction, that the tightening did not
overshoot and take a legal spelling with it. The reject pins are the only ones that
carry the change, and they were measured red on
origin/mainbefore the edit.The reject pins assert the issue
codeandpath, never a baresuccess === false:a
strictObjectrefuses for several reasons, so a throw-only assertion would stay greenif the refusal ever came from an unknown key instead of the vocabulary — the two defects
this file has to keep apart.
Gates
All run locally in a fresh worktree off
3e8e669c0, serialized on the shared verificationlock.
packages/specwas built before any dist-derived regeneration (#7122).pnpm --filter @objectstack/spec buildcheck:generated(11 artifacts)content/docs/references/**), regenerated withgen:docsexactly as the gate prescribed; re-run greenservice-automationfull suiteio-node-config.test.tsio-node-form-zod-ledger+notify-nodepnpm --filter @objectstack/spec typechecknode scripts/check-nul-bytes.mjsSnapshot note:
check:api-surfaceandcheck:export-originsboth stayed green and neededno regeneration — this change adds and removes no public export, it only narrows an
existing field's type. The single regenerated artifact is the docs reference, whose
severityrow now carries the enum in the Type column instead of a barestring:Reverse verification, run for real (schema restored from
origin/mainwithgit checkout origin/main -- packages/spec/src/automation/io-node-config.zod.ts, nevergit stash):4 failed = the three reject pins + the vocabulary pin. 17 passed = the 14 pre-existing
tests + the three accept pins, which stayed green on both sides exactly as predicted.
Special inspection items for the PM
service-automationfiles are mine to defend. The rulingnamed only the spec line. I included the Studio form descriptor because closing only
the Zod creates the mirror-image drift: a free-text form field inviting a value the
gate now refuses at execute time.
screen.modeis the in-repo precedent forenum-on-both-sides, and the IO-node ledger test exists to keep those two descriptions
in agreement — it compared key SETS only, which is exactly the gap this field sat in,
so I extended it to closed value vocabularies. Both are one revert away if you read
the scope line more strictly.
notify-node.ts:200-215, not a wording preference. If you would rather it landedseparately, it is a self-contained hunk — but the enum's safety argument depends on it
being true, so I did not want to ship the enum next to a docblock contradicting it.
''now hard-refuses. Today it degrades toinfo. I read that as correct (anempty severity is an authoring mistake, not an intent to say "info"), but it is the one
behaviour change a user could conceivably be leaning on, and it is the one I would
re-examine first if anything downstream complains.
| stringcollapses the union, so the closed set enforces nothing #7174 —DeliveryPayload.severityis declared'info' | 'warning' | 'critical' | string, whose trailing| stringcollapses theunion to
string. Type-level twin of this issue,finding-labelled, unassigned.Generated by Claude Code