Skip to content

fix(spec): close notify.severity to its declared info | warning | critical vocabulary (#7086) - #7192

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7086-severity-enum
Aug 10, 2026
Merged

fix(spec): close notify.severity to its declared info | warning | critical vocabulary (#7086)#7192
os-zhuang merged 1 commit into
mainfrom
claude/issue-7086-severity-enum

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #7086

NotifyConfigSchema.severity was a bare z.string() whose .describe() read
'info | warning | critical'. The enumeration existed only in the sentence, so
'urgent' parsed green, was forwarded raw by the notify executor, and was
blind-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:

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

(b) Probed acceptance face (safeParse on the unmodified source, worktree clean at
3e8e669c0, controls both sides):

severity "info"     -> ACCEPTED     severity "urgent"   -> ACCEPTED
severity "warning"  -> ACCEPTED     severity "INFO"     -> ACCEPTED
severity "critical" -> ACCEPTED     severity "Critical" -> ACCEPTED
severity "p1"       -> ACCEPTED     severity ""         -> ACCEPTED
--- controls ---
severity omitted        -> ACCEPTED
unknown key control     -> REJECTED (control ok)
title:42 control        -> REJECTED (control ok)

(c) No real producer or consumer relies on an out-of-vocabulary severity. Swept the
monorepo: every notify config in examples/ (app-showcase 14 sites, app-todo 1),
every test fixture, and every seed spells only info / warning / critical. The only
tolerance on the path is the dispatcher's blind cast ?? 'info' (which fires on
undefined, never on a non-empty unknown value) and notify-node.ts's raw forward, both
named in the issue. Premise valid — proceeded.

One measurement the issue did not have, and the reason this tightening is safe:
the executor reads severity RAW. It is one of three keys (channels, topic,
severity) that never pass through interpolate(), so a {record.x} template there was
forwarded verbatim and never resolved — closing the gate removes no working authoring
shape. The schema's module JSDoc claimed "every string-ish value except channels" is
interpolated; that was stale for topic and severity, and since it is the statement
this change's safety rests on, it is corrected here.

Changes

File What
packages/spec/src/automation/io-node-config.zod.ts severity becomes z.enum(['info', 'warning', 'critical']).optional(); describe becomes a sentence about the field (the vocabulary is now carried by the type, matching BpmnDiagnostic.severity / screen.mode house style); module JSDoc's interpolation claim corrected
packages/spec/src/automation/io-node-config.test.ts accept pins for the three values; reject pins for 'urgent' / 'INFO' / '' asserting code and path; a pin that the vocabulary lives in .options and is not smuggled back into prose
packages/services/service-automation/src/builtin/notify-node.ts the Studio form descriptor declares the same closed set
packages/services/service-automation/src/builtin/io-node-form-zod-ledger.test.ts the form-vs-Zod reconciliation now also compares closed value vocabularies, not only key sets
.changeset/notify-severity-closed-vocabulary.md minor / minor

The refusal is self-prescribing, which is the ADR-0033 half:

Invalid option: expected one of "info"|"warning"|"critical"

Blast radius: an execute-time refusal, not a load failure

FlowNodeSchema.config is z.record(z.string(), z.unknown()).optional(), so
NotifyConfigSchema runs only at execute time via parseNodeConfig. A stored flow
carrying severity: 'urgent' still loads and rehydrates exactly as before; the notify
step refuses when it runs, naming the three legal values. '' previously degraded to
info two 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 automatic
rewrite is correct — mapping a stored 'urgent' to 'info' would silently pick a
severity on the author's behalf, which is the blind-cast defect this change removes.

Reverse verification — direction predicted BEFORE running

Pin Predicted Measured
reject 'urgent' / 'INFO' / '' RED pre-fix, GREEN post-fix as predicted
accept 'info' / 'warning' / 'critical' GREEN on both sides as predicted
.options + describe pins RED pre-fix, GREEN post-fix as predicted

Stated 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/main before the edit.

The reject pins assert the issue code and path, never a bare success === false:
a strictObject refuses for several reasons, so a throw-only assertion would stay green
if 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 verification
lock. packages/spec was built before any dist-derived regeneration (#7122).

Gate Result
pnpm --filter @objectstack/spec build pass
check:generated (11 artifacts) pass — 1 proved stale (content/docs/references/**), regenerated with gen:docs exactly as the gate prescribed; re-run green
spec full suite 359 files / 9388 tests passed
service-automation full suite 72 files / 887 tests passed
targeted io-node-config.test.ts 21/21 passed
targeted io-node-form-zod-ledger + notify-node 19/19 passed
pnpm --filter @objectstack/spec typecheck pass
node scripts/check-nul-bytes.mjs pass (6593 files, no raw control bytes)

Snapshot note: check:api-surface and check:export-origins both stayed green and needed
no 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
severity row now carries the enum in the Type column instead of a bare string:

| **severity** | `Enum< 'info' \| 'warning' \| 'critical' >` | optional | Severity forwarded to the messaging service |

Reverse verification, run for real (schema restored from origin/main with
git checkout origin/main -- packages/spec/src/automation/io-node-config.zod.ts, never
git stash):

160:  severity: z.string().optional().describe('info | warning | critical'),
 FAIL  src/automation/io-node-config.test.ts > … > severity (#7086) >
       declares the vocabulary in the TYPE, not only in the sentence
 AssertionError: expected undefined to deeply equal [ 'info', 'warning', 'critical' ]
 Test Files  1 failed (1)
      Tests  4 failed | 17 passed (21)

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

  1. Scope call — the two service-automation files are mine to defend. The ruling
    named 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.mode is the in-repo precedent for
    enum-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.
  2. The module JSDoc correction is a behaviour claim I re-measured against
    notify-node.ts:200-215, not a wording preference. If you would rather it landed
    separately, 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.
  3. '' now hard-refuses. Today it degrades to info. I read that as correct (an
    empty 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.
  4. Not fixed here, filed instead: DeliveryPayload.severity is declared 'info' | 'warning' | 'critical' | string — the trailing | string collapses the union, so the closed set enforces nothing #7174DeliveryPayload.severity is declared
    'info' | 'warning' | 'critical' | string, whose trailing | string collapses the
    union to string. Type-level twin of this issue, finding-labelled, unassigned.

Generated by Claude Code

…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
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 2:27am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/service-automation, @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 10, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 03:36
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 42da73d Aug 10, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7086-severity-enum branch August 10, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants