From b74c57649a9d66dcb58ae14e9ba0dc18f29b692d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 08:20:19 +0000 Subject: [PATCH] fix(service-messaging): close DeliveryPayload.severity to its declared vocabulary (#7174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `'info' | 'warning' | 'critical' | string` collapses to exactly `string` in TypeScript's type system — the trailing `| string` absorbs the three literal members, so the closed vocabulary enforced nothing. Drop it, aligning DeliveryPayload with its already-closed siblings (messaging-service.ts EmitInput.severity, channel.ts Notification.severity, the inbox-message select field). Adds a compile-time pin (outbox-delivery-payload-severity.test.ts) proving severity: 'urgent' is now a type error, reverse-verified against the pre-fix declaration (@ts-expect-error goes unused / TS2578 pre-fix, clean post-fix). No runtime behaviour change: every real construction site already writes input.severity ?? 'info' where input.severity is itself the already-closed EmitInput.severity. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015fkdTyGmMD5s8ZtEifvuGy --- .../delivery-payload-severity-closed-union.md | 31 ++++++++++++++ .../outbox-delivery-payload-severity.test.ts | 40 +++++++++++++++++++ .../services/service-messaging/src/outbox.ts | 2 +- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 .changeset/delivery-payload-severity-closed-union.md create mode 100644 packages/services/service-messaging/src/outbox-delivery-payload-severity.test.ts diff --git a/.changeset/delivery-payload-severity-closed-union.md b/.changeset/delivery-payload-severity-closed-union.md new file mode 100644 index 0000000000..0b8a7fb2d1 --- /dev/null +++ b/.changeset/delivery-payload-severity-closed-union.md @@ -0,0 +1,31 @@ +--- +"@objectstack/service-messaging": patch +--- + +fix(service-messaging): close `DeliveryPayload.severity` to `'info' | 'warning' | 'critical'` (#7174) + +`DeliveryPayload.severity` was declared `'info' | 'warning' | 'critical' | string`. +TypeScript absorbs a literal union member into the wider primitive it is unioned +with, so this was exactly `string` — the three names read as a closed vocabulary +but enforced nothing. `severity: 'urgent'` (or `''`) type-checked with no error, +even though the value flows into `inbox-channel.ts`'s `n.severity ?? 'info'` and +the `sys_inbox_message.severity` select column, whose options are the three +names, and even though this package's three sibling declarations of the same +concept — `MessagingService['emit']`'s `EmitInput.severity` +(`messaging-service.ts`), `MessagingChannel`'s `Notification['severity']` +(`channel.ts`), and the `inbox-message` object's `severity` select field — are +already closed to exactly this set. + +Dropping the trailing `| string` makes the type mean what it says. No runtime +behaviour changes — every real producer already writes `input.severity ?? 'info'` +where `input.severity` is itself the already-closed `EmitInput.severity`, so no +in-repo construction site changes shape. This is the type-level twin of #7086 +(`NotifyConfigSchema.severity`, closed in PR #7192): a construction site that +would previously narrow silently through the collapsed union now gets a +compiler refusal instead, named as a `@ts-expect-error` pin. + +This is a narrowing of a publicly exported type +(`packages/services/service-messaging/src/outbox.ts`), so a consumer assigning +an out-of-vocabulary literal directly to `DeliveryPayload.severity` would newly +fail to compile — hence `patch`, following the #7140 precedent for a type-side +enforcement tightening with no runtime behaviour change. diff --git a/packages/services/service-messaging/src/outbox-delivery-payload-severity.test.ts b/packages/services/service-messaging/src/outbox-delivery-payload-severity.test.ts new file mode 100644 index 0000000000..3c851b990a --- /dev/null +++ b/packages/services/service-messaging/src/outbox-delivery-payload-severity.test.ts @@ -0,0 +1,40 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #7174 — compile-time pin for `DeliveryPayload.severity`. + * + * The field used to be declared `'info' | 'warning' | 'critical' | string`, + * which TypeScript collapses to exactly `string` (a literal union member is + * absorbed by the wider primitive it is unioned with). The three names read + * as a closed vocabulary but enforced nothing — `severity: 'urgent'` + * type-checked with no error. This file is the guard-rail: every line below + * is a type-level assertion evaluated by `tsc --noEmit`, run as part of this + * package's ordinary `typecheck` script (this package's tsconfig does not + * exclude `*.test.ts`, so no separate `.pin.ts` is needed — see AGENTS.md's + * `PINS_CHECKED` invariant). + */ + +import { describe, it, expect } from 'vitest'; +import type { DeliveryPayload } from './outbox.js'; + +describe('DeliveryPayload.severity (#7174)', () => { + it('accepts the closed info | warning | critical vocabulary', () => { + const payloads: DeliveryPayload[] = [ + { severity: 'info' }, + { severity: 'warning' }, + { severity: 'critical' }, + {}, // severity is optional + ]; + expect(payloads.map((p) => p.severity)).toEqual(['info', 'warning', 'critical', undefined]); + }); + + it('rejects an out-of-vocabulary literal at compile time', () => { + // @ts-expect-error 'urgent' is not a member of the closed severity vocabulary + const bad: DeliveryPayload = { severity: 'urgent' }; + // Runtime side is unreachable in practice (a real construction site + // would fail to compile); assert only that the pin above is real — + // if the `| string` collapse ever comes back, this line stops being + // an error and `tsc --noEmit` goes red on the missing `@ts-expect-error`. + expect(bad.severity).toBe('urgent'); + }); +}); diff --git a/packages/services/service-messaging/src/outbox.ts b/packages/services/service-messaging/src/outbox.ts index 8d8ed67d3d..c95f866ae1 100644 --- a/packages/services/service-messaging/src/outbox.ts +++ b/packages/services/service-messaging/src/outbox.ts @@ -22,7 +22,7 @@ export type DeliveryStatus = export interface DeliveryPayload { title?: string; body?: string; - severity?: 'info' | 'warning' | 'critical' | string; + severity?: 'info' | 'warning' | 'critical'; actionUrl?: string; [k: string]: unknown; }