Skip to content

DeliveryPayload.severity is declared 'info' | 'warning' | 'critical' | string — the trailing | string collapses the union, so the closed set enforces nothing #7174

Description

@os-zhuang

Observation-class finding, noticed while closing the NotifyConfigSchema.severity gate for #7086 (PR pending). Recorded unassigned. Type-level only — no user-visible failure today, which is why it carries finding and no pm:queue.

Anchor

packages/services/service-messaging/src/outbox.ts, DeliveryPayload:

/** Rendered content snapshot carried on the delivery row for dispatch. */
export interface DeliveryPayload {
    title?: string;
    body?: string;
    severity?: 'info' | 'warning' | 'critical' | string;
    actionUrl?: string;
    [k: string]: unknown;
}

The defect

'info' | 'warning' | 'critical' | string is exactly string to TypeScript — a
literal union member is absorbed by the wider primitive it is unioned with. The three
names therefore contribute nothing but an editor hint; severity: 'urgent' type-checks
here with no error, and so does severity: ''.

The result is a type that reads as the closed vocabulary while enforcing the open one —
the same declared-vs-enforced shape as #7086, one layer down and in the type system
rather than in Zod. It sits in the package that also owns the genuinely closed
declarations, which is what makes it easy to misread:

  • messaging-service.ts:64readonly severity?: 'info' | 'warning' | 'critical';
  • channel.ts:40readonly severity?: 'info' | 'warning' | 'critical';
  • inbox-message.object.ts:88 — a Field.select offering exactly those three

so DeliveryPayload is the one member of that family that is silently open.

Why it matters

DeliveryPayload is the rendered snapshot carried on the delivery row. Anything that
builds or mutates one gets no compiler signal when it writes an out-of-vocabulary
severity, and the value then reaches inbox-channel.ts's severity: n.severity ?? 'info'
and the sys_inbox_message.severity select column, whose options are the three names.
Nothing today is measured to write a bad value — this is a missing guard-rail, not a
live bug — but it is the guard-rail that would have made #7086's 'urgent' visible to
tsc instead of only to a probe.

Suggested shape, if triage wants it fixed

Drop the | string so the union means what it says. If some caller genuinely needs to
carry an unvalidated severity through, that is a real decision worth stating explicitly
(e.g. a named string & {} escape or a comment recording the tolerance), not something
the current spelling communicates — as written it looks like a closed set and behaves
like an open one, which is the worst of both.

Worth checking in the same pass whether other 'a' | 'b' | string unions exist in the
services layer; this one was found by eye, not by a sweep.

Refs

#7086 (the Zod-side twin, being closed now), #6762 (the describe-claims-closed class).

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions