From 6852007603f8dbc4345318d1beb0aecf916a3a59 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 06:48:19 +0000 Subject: [PATCH 1/2] docs(service-messaging): re-anchor the dead tracker citations to the commits that decided them Every comment and docblock site in packages/services/service-messaging/src that cited a tracker number answering 404 now cites the commit in this repository's history that decided what the line describes, in ruling C+D's form C, and says in its own words what that commit decided. 127 comment sites on 109 lines in 28 files, 13 numbers, 13 distinct commits. Comments only: every touched file keeps its line count, so no line citation into these files moves. No citation number is added. The three generated *.source-hashes.generated.ts headers (their producer is the CLI's i18n extract template) and the twelve string-literal sites are left as they were. Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude --- ...ery-update-tenant-audit.integration.test.ts | 10 +++++----- .../service-messaging/src/dispatcher.ts | 6 +++--- .../src/email-channel.test.ts | 4 ++-- .../service-messaging/src/email-channel.ts | 18 +++++++++--------- .../service-messaging/src/http-dispatcher.ts | 2 +- ...box-ack-claim-ownership.integration.test.ts | 2 +- .../service-messaging/src/http-outbox.ts | 4 ++-- .../service-messaging/src/inbox-caller.ts | 10 +++++----- .../services/service-messaging/src/index.ts | 4 ++-- .../service-messaging/src/memory-outbox.ts | 10 +++++----- .../src/messaging-service.test.ts | 16 ++++++++-------- .../service-messaging/src/messaging-service.ts | 18 +++++++++--------- .../objects/notification-delivery.object.ts | 10 +++++----- .../notification-keyed-text-bounds.test.ts | 12 ++++++------ .../objects/notification-preference.object.ts | 6 +++--- .../src/objects/notification-receipt.object.ts | 6 +++--- .../notification-subscription.object.ts | 16 ++++++++-------- .../objects/notification-template.object.ts | 6 +++--- ...box-ack-claim-ownership.integration.test.ts | 8 ++++---- ...outbox-ack-precondition.integration.test.ts | 4 ++-- .../src/outbox-dispatcher-scope.ts | 8 ++++---- .../services/service-messaging/src/outbox.ts | 16 ++++++++-------- .../service-messaging/src/sms-channel.test.ts | 2 +- .../service-messaging/src/sms-channel.ts | 10 +++++----- .../service-messaging/src/sql-http-outbox.ts | 2 +- .../src/sql-outbox-audit-columns.test.ts | 6 +++--- .../service-messaging/src/sql-outbox.ts | 14 +++++++------- .../src/translations/index.ts | 2 +- 28 files changed, 116 insertions(+), 116 deletions(-) diff --git a/packages/services/service-messaging/src/delivery-update-tenant-audit.integration.test.ts b/packages/services/service-messaging/src/delivery-update-tenant-audit.integration.test.ts index 5815f5c73b8..94feb0323eb 100644 --- a/packages/services/service-messaging/src/delivery-update-tenant-audit.integration.test.ts +++ b/packages/services/service-messaging/src/delivery-update-tenant-audit.integration.test.ts @@ -18,7 +18,7 @@ * tenant-classification contract this file pins (#10740) is unchanged — * threaded `tenantId`, never `bypassTenantAudit`. * - * [#11453] `SqlNotificationOutbox.ack` has since made the SAME move for the + * [commit 1a47a5368] `SqlNotificationOutbox.ack` has since made the SAME move for the * same reason: its new status precondition ("this row must still be * `in_flight`") is a compare-and-set, and a predicate on the by-id path is * silently discarded, so it rides `multi: true` too. Its audit op is @@ -89,10 +89,10 @@ let driver: SqlDriver; let warns: Array<{ msg: string; meta: any }>; /** Every `options` bag that reached `SqlDriver.update` — the `update` op only. */ let driverUpdates: Array<{ object: string; id: unknown; options: any }>; -/** Every `options` bag that reached `SqlDriver.updateMany` — `redeliver`'s op since #11009, the notification `ack`'s since #11453, and the HTTP `ack`'s since #17634. */ +/** Every `options` bag that reached `SqlDriver.updateMany` — `redeliver`'s op since #11009, the notification `ack`'s since commit 1a47a5368, and the HTTP `ack`'s since #17634. */ let driverUpdateManys: Array<{ object: string; where: unknown; options: any }>; -/** The audit line for the PREDICATE op — `redeliver`'s write since #11009, the notification `ack`'s since #11453. */ +/** The audit line for the PREDICATE op — `redeliver`'s write since #11009, the notification `ack`'s since commit 1a47a5368. */ const auditedUpdateMany = (object: string): boolean => warns.some((w) => w.msg.includes(`[tenant-audit] updateMany on tenant-scoped object "${object}"`)); @@ -226,7 +226,7 @@ describe('ack — the two dispatcher sites are a classified global sweep (update // // [#17634] The dispatcher's ack hands the claim credential, so it is a // compare-and-set on the predicate path and the reading moves to the - // `updateMany` spy — the move the notification ack made in #11453. The + // `updateMany` spy — the move the notification ack made in commit 1a47a5368. The // claim path writes there too (its reap and its atomic claim), so the // filter names what an ACK write looks like: a scalar id bound to // `in_flight` AND to the claiming node. That predicate IS the @@ -283,7 +283,7 @@ describe('ack — the two dispatcher sites are a classified global sweep (update ]); // ② Declared global, for both organizations' rows. // - // [#11453] The ack's op is `updateMany` now, so the reading moves to + // [commit 1a47a5368] The ack's op is `updateMany` now, so the reading moves to // that spy. The claim path writes there too (its reap and its atomic // claim), so the filter names what an ACK write looks like — and that // predicate is not incidental: `{ id: , status: 'in_flight' }` diff --git a/packages/services/service-messaging/src/dispatcher.ts b/packages/services/service-messaging/src/dispatcher.ts index 68051b59f70..99e92e79612 100644 --- a/packages/services/service-messaging/src/dispatcher.ts +++ b/packages/services/service-messaging/src/dispatcher.ts @@ -194,7 +194,7 @@ export class NotificationDispatcher { // // No partition lock is needed, and none was ever in force: the reap only // moves rows already past their timeout, a claim only takes `pending` - // rows, and an ack whose claim was reaped matches nothing (#11859) — while + // rows, and an ack whose claim was reaped matches nothing (commit d9cf78eaa) — while // the per-claim reap, run under partition p's lock, was already rewriting // rows in every other partition. // @@ -368,7 +368,7 @@ export class NotificationDispatcher { } /** - * [#11453] Record one attempt's outcome, tolerating the ONE refusal a + * [commit 1a47a5368] Record one attempt's outcome, tolerating the ONE refusal a * correct dispatcher can legitimately provoke. * * `ack()` now refuses a row that is not `in_flight`, and this loop can meet @@ -389,7 +389,7 @@ export class NotificationDispatcher { */ private async ackAttempt(row: ClaimedDeliveryRecord, result: AckResult): Promise { try { - // [#11859] The record is handed back WHOLE: its (claimedBy, + // [commit d9cf78eaa] The record is handed back WHOLE: its (claimedBy, // claimedAt) pair is the claim credential the store stamped, and // the ack's compare-and-set binds it — this loop never needs to // know or repeat its own nodeId. diff --git a/packages/services/service-messaging/src/email-channel.test.ts b/packages/services/service-messaging/src/email-channel.test.ts index f6a0dd6b7e8..c72653b8c24 100644 --- a/packages/services/service-messaging/src/email-channel.test.ts +++ b/packages/services/service-messaging/src/email-channel.test.ts @@ -144,7 +144,7 @@ describe('email channel', () => { it('END TO END: one emit, one outbox, one dispatcher tick — the row lands `dead`, ⛔ not `success`', async () => { // The unit assertions above are about a return value; THIS is the // reading the card is written against — what an operator sees on - // `sys_notification_delivery`. Before #18424 this row read + // `sys_notification_delivery`. Before commit 879b51270 this row read // `status: 'success'`, which is the silent half of the defect. const data = fakeData(); const outbox = new MemoryNotificationOutbox(1); @@ -580,7 +580,7 @@ describe('email channel', () => { }); }); - // ── #11741 — organization threading. This channel is the producer the + // ── Commit b706af987 — organization threading. This channel is the producer the // ruling names as HOLDING an organization (`delivery.notification // .organizationId`, the tenant stamp the outbox snapshots per delivery), // so it threads that value into the email service's input on BOTH of its diff --git a/packages/services/service-messaging/src/email-channel.ts b/packages/services/service-messaging/src/email-channel.ts index 7fc96c9658e..a35f24537f6 100644 --- a/packages/services/service-messaging/src/email-channel.ts +++ b/packages/services/service-messaging/src/email-channel.ts @@ -35,7 +35,7 @@ export interface EmailSenderSurface { html?: string; text?: string; /** - * Structural mirror of `SendEmailInput.organizationId` (#11741) — + * Structural mirror of `SendEmailInput.organizationId` (commit b706af987) — * the tenant stamp for `sys_email.organization_id`, threaded from * `delivery.notification.organizationId` when the delivery holds * one. Optional: an org-less delivery sends without it. @@ -56,7 +56,7 @@ export interface EmailSenderSurface { to: string | string[]; data?: Record; locale?: string; - /** Same tenant stamp as on `send` (#11741) — forwarded by the email service into the send it performs. */ + /** Same tenant stamp as on `send` (commit b706af987) — forwarded by the email service into the send it performs. */ organizationId?: string; }): Promise<{ id?: string; status?: string; error?: string } | unknown>; /** @@ -81,7 +81,7 @@ export interface EmailChannelOptions { * Resolve the email service; `undefined` ⇒ there is no transport, which * {@link MessagingChannel.isAvailable} reports as * `transport_not_configured` and {@link MessagingChannel.send} REFUSES with - * the same reason (#18424). ⛔ Not a no-op success: a delivery nothing was + * the same reason (commit 879b51270). ⛔ Not a no-op success: a delivery nothing was * sent for is never reported as delivered. */ getEmail(): EmailSenderSurface | undefined; @@ -112,7 +112,7 @@ export interface EmailChannelOptions { /** * The ONE token both members of this channel use for "there is no transport" - * (#18424) — the reason `isAvailable()` already returns, reused verbatim so the + * (commit 879b51270) — the reason `isAvailable()` already returns, reused verbatim so the * refusal `send()` writes onto the delivery row and the suppression fan-out * records on `sys_notification.suppressed_channels` name the same condition. * @@ -160,7 +160,7 @@ const EMAIL_SHAPE = (s: string): boolean => { * path) use that one resolution. A producer-set `payload.locale` — the * pre-ruling single value for the whole notification — is no longer consulted. * - * Failure is always REPORTED, never absorbed (#18424): no email service ⇒ a + * Failure is always REPORTED, never absorbed (commit 879b51270): no email service ⇒ a * refusal carrying the declared `transport_not_configured` reason, graded * `permanent` so the row dead-letters on attempt one; a recipient with no * resolvable address ⇒ a reported failure. Either way the delivery row shows @@ -273,7 +273,7 @@ export function createEmailChannel(opts: EmailChannelOptions): MessagingChannel async send(ctx: MessagingChannelContext, delivery: Delivery): Promise { const email = opts.getEmail(); if (!email) { - // [#18424] The SAME condition `isAvailable()` answers above, so + // [commit 879b51270] The SAME condition `isAvailable()` answers above, so // it gets the same answer — a refusal naming // `transport_not_configured`, ⛔ never `{ ok: true }`. // @@ -340,7 +340,7 @@ export function createEmailChannel(opts: EmailChannelOptions): MessagingChannel to: address, ...(data !== undefined ? { data } : {}), ...(templateLocale ? { locale: templateLocale } : {}), - // #11741 — this channel HOLDS the organization (the + // Commit b706af987 — this channel HOLDS the organization (the // tenant stamp the outbox snapshots per delivery), so // it threads it for the sys_email.organization_id // stamp. Absent stays absent — never fabricated. @@ -380,7 +380,7 @@ export function createEmailChannel(opts: EmailChannelOptions): MessagingChannel subject: rendered.subject, ...(rendered.html !== undefined ? { html: rendered.html } : {}), ...(rendered.text !== undefined ? { text: rendered.text } : {}), - // #11741 — same threading as the template arm above. + // Commit b706af987 — same threading as the template arm above. ...(n.organizationId ? { organizationId: n.organizationId } : {}), }); const id = result?.id; @@ -399,7 +399,7 @@ export function createEmailChannel(opts: EmailChannelOptions): MessagingChannel // are `IEmailService.sendTemplate`'s own error codes plus this // channel's missing-capability refusal above. // - // [#18424] `transport_not_configured` joins them, and the grade is + // [commit 879b51270] `transport_not_configured` joins them, and the grade is // driven rather than assumed: in the shipping composition the same // condition already terminates a claimed row at once — the mount // gate unmounts the channel and the dispatcher acks diff --git a/packages/services/service-messaging/src/http-dispatcher.ts b/packages/services/service-messaging/src/http-dispatcher.ts index a9dfab46473..99ecada0cdf 100644 --- a/packages/services/service-messaging/src/http-dispatcher.ts +++ b/packages/services/service-messaging/src/http-dispatcher.ts @@ -269,7 +269,7 @@ export class HttpDispatcher { * [#17634] Record one attempt's outcome with the claim credential this * node's `claim()` stamped on the row, tolerating the ONE refusal a correct * dispatcher can legitimately provoke — `NotificationDispatcher.ackAttempt`'s - * shape (#11453, #11859). + * shape (commits 1a47a5368, d9cf78eaa). * * A send slower than `claimTtlMs` lets the visibility-timeout reap return the * row to `pending`, and another node — or this one, on a later tick — diff --git a/packages/services/service-messaging/src/http-outbox-ack-claim-ownership.integration.test.ts b/packages/services/service-messaging/src/http-outbox-ack-claim-ownership.integration.test.ts index 810c0c718e3..f122a5009c6 100644 --- a/packages/services/service-messaging/src/http-outbox-ack-claim-ownership.integration.test.ts +++ b/packages/services/service-messaging/src/http-outbox-ack-claim-ownership.integration.test.ts @@ -4,7 +4,7 @@ * #17634 — an HTTP ack proves OWNERSHIP of the claim it completes, not just * that a row id exists: a late ack from a claim the visibility-timeout reap * took back must not overwrite the live re-claim on `sys_http_delivery`. The - * notification outbox closed the same shape in #11859; this file pins the HTTP + * notification outbox closed the same shape in commit d9cf78eaa; this file pins the HTTP * outbox against the same sequence. * * ## The reachable sequence this file replays — for real diff --git a/packages/services/service-messaging/src/http-outbox.ts b/packages/services/service-messaging/src/http-outbox.ts index 31184c15d84..c236cd4dcb0 100644 --- a/packages/services/service-messaging/src/http-outbox.ts +++ b/packages/services/service-messaging/src/http-outbox.ts @@ -363,7 +363,7 @@ export type HttpAckResult = HttpAckSuccess | HttpAckFailure; * stamps on a row when {@link IHttpOutbox.claim} takes it — handed back to * {@link IHttpOutbox.ack} so the outcome is written only while that claim still * holds the row. It is exactly the pair the notification outbox's - * `ClaimedDeliveryRecord` guarantees for `INotificationOutbox.ack` (#11859), with + * `ClaimedDeliveryRecord` guarantees for `INotificationOutbox.ack` (commit d9cf78eaa), with * the same meaning: * * - ownership is proven by ROUND-TRIPPING what `claim()` returned, never by the @@ -649,7 +649,7 @@ export interface IHttpOutbox { * * [#17634] **Pass `claimed`** — the claim credential on the row {@link claim} * returned (the row itself will do). With it, `ack` is the ownership-checked - * completion `INotificationOutbox.ack` performs (#11453, #11859): + * completion `INotificationOutbox.ack` performs (commits 1a47a5368, d9cf78eaa): * * ⛔ **Precondition: the row MUST still be held by that claim.** Two tests, * both re-stated IN the conditional write: the row is `in_flight`, AND its diff --git a/packages/services/service-messaging/src/inbox-caller.ts b/packages/services/service-messaging/src/inbox-caller.ts index eae5b0b8f3d..7479d7b652b 100644 --- a/packages/services/service-messaging/src/inbox-caller.ts +++ b/packages/services/service-messaging/src/inbox-caller.ts @@ -2,7 +2,7 @@ /** * Authenticated-caller scoping for the plugin-facing inbox surface - * (ADR-0030 Layer 5) — the write door (#10753) and the read door (#11452). + * (ADR-0030 Layer 5) — the write door (#10753) and the read door (commit 3b5f0360c). * * ## The shape this closes * @@ -22,7 +22,7 @@ * permission check sees it either. Unconstrained and undeclared, in both * directions. * - * The READ side has the same shape (#11452): `listInbox(userId, opts)` keys + * The READ side has the same shape (closed by commit 3b5f0360c): `listInbox(userId, opts)` keys * its whole read — inbox rows joined with read-state — on the same free * parameter, so an in-process caller could read ANY user's inbox titles, * bodies and read-state. Both doors resolve their recipient here. @@ -83,9 +83,9 @@ import type { ExecutionContext } from '@objectstack/spec/kernel'; * `ExecutionContext` the caller was handed, passed through whole. * * Passed WHOLE, deliberately: the measured defect family behind - * `assembleExecutionContext` (#6071, #6206, #6551) is "a field exists on - * `ExecutionContext`, one copy carries it, another silently does not". A - * hand-picked `{ userId }` slice here would be one more such copy. + * `assembleExecutionContext` (#6071, #6551, and the share-link envelope trim + * commit 8e13ca876 undid) is "a field exists on `ExecutionContext`, one copy + * carries it, another silently does not". A hand-picked `{ userId }` slice here would be one more such copy. */ export type InboxCaller = ExecutionContext; diff --git a/packages/services/service-messaging/src/index.ts b/packages/services/service-messaging/src/index.ts index 6f0c6476278..5506104ec13 100644 --- a/packages/services/service-messaging/src/index.ts +++ b/packages/services/service-messaging/src/index.ts @@ -105,7 +105,7 @@ export { CHANNEL_UNAVAILABLE_REASONS } from './channel.js'; export type { INotificationOutbox, NotificationDeliveryRecord, - // [#11859] What claim()/claimDigest() hand out and ack() takes back — the + // [commit d9cf78eaa] What claim()/claimDigest() hand out and ack() takes back — the // record carrying the claim credential the compare-and-set binds. ClaimedDeliveryRecord, DeliveryStatus, @@ -116,7 +116,7 @@ export type { ReapOptions, AckResult, } from './outbox.js'; -// [#11453] `ack()`'s status precondition refuses with this, so a caller that +// [commit 1a47a5368] `ack()`'s status precondition refuses with this, so a caller that // wants to distinguish "I lost the claim" from a transport fault can catch it. export { NotificationAckError } from './outbox.js'; export { SqlNotificationOutbox, DELIVERY_OBJECT } from './sql-outbox.js'; diff --git a/packages/services/service-messaging/src/memory-outbox.ts b/packages/services/service-messaging/src/memory-outbox.ts index 3bbd19f17fe..6973787618d 100644 --- a/packages/services/service-messaging/src/memory-outbox.ts +++ b/packages/services/service-messaging/src/memory-outbox.ts @@ -87,7 +87,7 @@ export class MemoryNotificationOutbox implements INotificationOutbox { r.claimedBy = opts.nodeId; r.claimedAt = now; r.updatedAt = now; - // [#11859] The copy handed out carries the claim credential the + // [commit d9cf78eaa] The copy handed out carries the claim credential the // two lines above just stamped — the record IS the credential. out.push({ ...r, claimedBy: opts.nodeId, claimedAt: now }); } @@ -117,7 +117,7 @@ export class MemoryNotificationOutbox implements INotificationOutbox { async ack(claimed: ClaimedDeliveryRecord, result: AckResult): Promise { const id = claimed.id; - // [#11859] The runtime half of the ClaimedDeliveryRecord contract, for + // [commit d9cf78eaa] The runtime half of the ClaimedDeliveryRecord contract, for // JS callers and casts: a record with no claim credential was not // handed out by claim()/claimDigest() and is refused before any read. if (typeof claimed.claimedBy !== 'string' || typeof claimed.claimedAt !== 'number') { @@ -128,7 +128,7 @@ export class MemoryNotificationOutbox implements INotificationOutbox { // to corrupt and no claim to lose. Unchanged, and declared on the // interface so the two backends agree about it. if (!r) return; - // [#11453] The status precondition. `ack` completes a delivery this + // [commit 1a47a5368] The status precondition. `ack` completes a delivery this // caller CLAIMED; an unclaimed `pending` row (the ack-as-cancel trap) // or an already-terminal one is refused, and nothing below runs — so a // refused ack leaves status, attempts and error exactly as they were. @@ -141,7 +141,7 @@ export class MemoryNotificationOutbox implements INotificationOutbox { 'DELIVERY_NOT_ELIGIBLE', ); } - // [#11859] Ownership: the row is claimed, but not by the claim this + // [commit d9cf78eaa] Ownership: the row is claimed, but not by the claim this // record came from — it was reaped and re-claimed while the send ran // (possibly by this same store handing it to this same node again: the // credential is the PAIR, so a later claim's `claimedAt` refuses the @@ -155,7 +155,7 @@ export class MemoryNotificationOutbox implements INotificationOutbox { } const now = this.clock(); // Reached only for a genuinely claimed row, so this counts a real - // dispatch attempt and nothing else (#11453). + // dispatch attempt and nothing else (commit 1a47a5368). r.attempts += 1; r.lastAttemptedAt = now; r.claimedBy = undefined; diff --git a/packages/services/service-messaging/src/messaging-service.test.ts b/packages/services/service-messaging/src/messaging-service.test.ts index b8ae0d66593..d957d31dc4b 100644 --- a/packages/services/service-messaging/src/messaging-service.test.ts +++ b/packages/services/service-messaging/src/messaging-service.test.ts @@ -796,7 +796,7 @@ function recordFinds(engine: any): Array<{ object: string; query: any }> { } /** - * [#6363] `ListNotificationsResponseSchema.unreadCount` is published into the + * [commit 17d095413] `ListNotificationsResponseSchema.unreadCount` is published into the * API reference as "Total number of unread notifications". It was counted * inside `rows.map(...)`, i.e. over the `limit`-truncated window, so the badge * saturated at the window size forever: measured on a real stack with 60 @@ -960,7 +960,7 @@ describe('[#6363] listInbox — unreadCount is the TOTAL unread, not the fetched * documented as "mark **every** currently-unread inbox message as read" * cleared at most 200 receipts per call. * - * #6363 did not introduce this; it removed the cover. While `unreadCount` was + * Commit 17d095413 did not introduce this; it removed the cover. While `unreadCount` was * counted over the window the truncation was self-consistent and invisible * (clear 200, poll, see a window with nothing unread in it, badge 0). Now that * the badge is the true total, one response pair states the contradiction on @@ -968,8 +968,8 @@ describe('[#6363] listInbox — unreadCount is the TOTAL unread, not the fetched * `GET /notifications → { unreadCount: 150 }`. * * Route C — redefine "all" as "the current window" — was excluded by the - * maintainer's #6363 Option A ruling (make the declaration true). The sweep now - * reads the unread SET directly instead of a page of the list. + * maintainer's Option A ruling that commit 17d095413 landed (make the declaration + * true). The sweep now reads the unread SET directly instead of a page of the list. */ describe('[#6436] markAllRead — sweeps the whole inbox, not one 200-row window', () => { const logger = silentLogger(); @@ -980,7 +980,7 @@ describe('[#6436] markAllRead — sweeps the whole inbox, not one 200-row window const res = await svc.markAllRead('u1'); // Before: `readCount: 200`, and 150 messages still unread behind a - // badge that — since #6363 — reported them correctly. + // badge that — since commit 17d095413 — reported them correctly. expect(res).toEqual({ success: true, readCount: 350 }); expect((await svc.listInbox('u1')).unreadCount).toBe(0); @@ -1042,7 +1042,7 @@ describe('[#6436] markAllRead — sweeps the whole inbox, not one 200-row window expect(calls, `inbox of ${n}`).toHaveLength(2); const inboxRead = calls.find((c) => c.object === 'sys_inbox_message')!; // Unwindowed, unordered and one column wide — the same projection - // #6363's `countUnreadTotal` already reads to answer the badge, so + // commit 17d095413's `countUnreadTotal` already reads to answer the badge, so // the sweep asks the data layer for nothing the bell poll does not // ask it on every saturated page. expect(inboxRead.query.where).toEqual({ user_id: 'u1' }); @@ -1119,7 +1119,7 @@ describe('[#6436] markAllRead — sweeps the whole inbox, not one 200-row window // fed `markRead` the inbox ROW id (`listInbox` views it as `nid ?? // String(m.id)`), which inserted a receipt the join never reads back — // it could not make the row read and still counted itself into - // `readCount`. Skipping it keeps `readCount` honest; #6363's count goes + // `readCount`. Skipping it keeps `readCount` honest; commit 17d095413's count goes // on reporting the row as unread, which is the true state. Whether such // a row should be readable at all is #6448 — a gap in the receipt KEY, // not in this sweep, and dormant: the single `emit()` ingress always @@ -1268,7 +1268,7 @@ describe('MessagingService — plugin-facing inbox writes scoped to the authenti }); /** - * [#11452] The plugin-facing inbox READ door. + * [commit 3b5f0360c] The plugin-facing inbox READ door. * * The measured BEFORE, the read-side sibling of #10753: the messaging service * is registered as a kernel service and the kernel hands every plugin ONE diff --git a/packages/services/service-messaging/src/messaging-service.ts b/packages/services/service-messaging/src/messaging-service.ts index 428fc9e935d..e786cf421df 100644 --- a/packages/services/service-messaging/src/messaging-service.ts +++ b/packages/services/service-messaging/src/messaging-service.ts @@ -497,7 +497,7 @@ export class MessagingService { * receipt; the `read` filter (when given) is applied in-memory after the * join. * - * Two different bounds, deliberately (#6363): + * Two different bounds, deliberately (commit 17d095413): * * * `notifications[]` is the fetched WINDOW — `limit` rows, defaulting to * 50 and hard-capped at 200, newest first. Unchanged: the Console @@ -508,7 +508,7 @@ export class MessagingService { * notifications"). Counting it over `rows` — the window — made the * badge saturate at the window size forever: a user with 60 unread was * told 50, and `?limit=10` told them 10. The declaration was right and - * the implementation was wrong (maintainer ruling, #6363 Option A). + * the implementation was wrong (maintainer ruling Option A, commit 17d095413). * * The `read` filter never moves `unreadCount`: asking for the read half of * the inbox does not mean the badge is zero. A `type` filter does — the @@ -522,7 +522,7 @@ export class MessagingService { * * An IN-PROCESS caller has no such door in front of it, and for that * caller the parameter is simply a free string — the "any plugin can read - * any user's inbox" shape (#11452), the read-side sibling of the one + * any user's inbox" shape (closed by commit 3b5f0360c), the read-side sibling of the one * {@link markReadAsCaller} closed for writes. Plugins use * {@link listInboxAsCaller}, which derives the recipient from the * caller's execution context and has no target-user parameter to get @@ -568,7 +568,7 @@ export class MessagingService { // truncated, so the window count already IS the total and the second // read would be a duplicate of the first. Only a saturated window // (`rows.length === limit`) can be hiding rows, and that is exactly the - // case #6363 is about. So the common inbox — fewer messages than the + // case commit 17d095413 fixed. So the common inbox — fewer messages than the // page size — costs precisely what it cost before this change. const unreadCount = rows.length < limit ? windowUnread @@ -581,7 +581,7 @@ export class MessagingService { /** * List **the calling user's own inbox** — the plugin-facing counterpart to * {@link listInbox}, on the same authenticated-caller axis as - * {@link markReadAsCaller} / {@link markAllReadAsCaller} (#11452; the + * {@link markReadAsCaller} / {@link markAllReadAsCaller} (commit 3b5f0360c; the * write door is #10753). * * Takes no target user at all: the recipient is derived from the caller's @@ -613,7 +613,7 @@ export class MessagingService { /** * Total unread across the user's whole matching inbox — the reverse join - * `unreadCount` is declared to answer (#6363). + * `unreadCount` is declared to answer (commit 17d095413). * * Read-state lives on `sys_notification_receipt`, not on the inbox row * (ADR-0030), so no single `count()` answers this: the predicate spans two @@ -736,7 +736,7 @@ export class MessagingService { * cleared at most 200 receipts per call. Two ways that showed: * * * 350 unread → `{ readCount: 200 }`, 150 still unread. Invisible while - * `unreadCount` was itself window-scoped; since #6363 made the badge a + * `unreadCount` was itself window-scoped; since commit 17d095413 made the badge a * true total, one response pair states the contradiction on its own. * * Worse, and the reason a paging loop is not the fix: that window is * `created_at desc` over ALL rows, with the `read` filter applied in @@ -747,7 +747,7 @@ export class MessagingService { * * So the sweep reads the unread SET instead of a page of the list, in a * FIXED two reads whatever the inbox size: the same one-column, unwindowed - * projection of `sys_inbox_message` that #6363's `countUnreadTotal` already + * projection of `sys_inbox_message` that commit 17d095413's `countUnreadTotal` already * issues to answer the badge, joined against the receipt spine that * `listInbox` already reads unbounded. No loop, no page count to bound, and * nothing asked of the data layer that the bell's poll does not ask on @@ -760,7 +760,7 @@ export class MessagingService { * * No cap: a numeric safety valve is route C wearing a larger number — above * it, "all" would be a lie again, which is the reading the maintainer ruled - * against on #6363 (make the declaration true rather than document the + * against (commit 17d095413: make the declaration true rather than document the * shortfall). What bounds a pathological inbox instead is that the work is * idempotent and resumable — a failed receipt write is logged, skipped, and * picked up by the next sweep. diff --git a/packages/services/service-messaging/src/objects/notification-delivery.object.ts b/packages/services/service-messaging/src/objects/notification-delivery.object.ts index 0d903749211..49299c2b88e 100644 --- a/packages/services/service-messaging/src/objects/notification-delivery.object.ts +++ b/packages/services/service-messaging/src/objects/notification-delivery.object.ts @@ -99,7 +99,7 @@ export const NotificationDelivery = ObjectSchema.create({ label: 'Notification Event', required: true, searchable: true, - // [#12978] Referenced-column bound (#11374 route A): FK to + // [commit e4902d2b9] Referenced-column bound (route A, ruling 2026-08-24): FK to // `sys_notification.id`, whose physical column is the id column // driver-sql creates — `table.string('id').primary()`, knex's // varchar(255), spelled `DEFAULT_STRING_VARCHAR_CHARS`. 255 by @@ -112,10 +112,10 @@ export const NotificationDelivery = ObjectSchema.create({ label: 'Recipient User', required: true, searchable: true, - // [#12978] Referenced-column bound (#11374 route A): a resolved + // [commit e4902d2b9] Referenced-column bound (route A, ruling 2026-08-24): a resolved // recipient is a `sys_user.id` (physical varchar(255), as above) // or an email-shaped value `RecipientResolver.resolveOne()` keeps - // verbatim (#9807) — RFC 5321 caps an address at 254 octets and + // verbatim (commit 44738f7af) — RFC 5321 caps an address at 254 octets and // `sys_user.email` stores one in a string-family varchar(255) // column. 255 admits both producers. maxLength: 255, @@ -123,7 +123,7 @@ export const NotificationDelivery = ObjectSchema.create({ channel: Field.text({ label: 'Channel', required: true, - // [#12978] Machine channel-id vocabulary (#11374 route A): values + // [commit e4902d2b9] Machine channel-id vocabulary (route A, ruling 2026-08-24): values // are the `MessagingChannel.id`s the service fans out to — // `registerChannel` registers `inbox` / `email` / `sms` today, and // the spec's `NotificationChannelSchema` widest member is @@ -141,7 +141,7 @@ export const NotificationDelivery = ObjectSchema.create({ // digest pass collapses all same-key rows into ONE rendered message at // window time. Null ⇒ an ordinary (immediate / quiet-hours) delivery. digest_key: Field.text({ label: 'Digest Key', searchable: true, - // [#12978] Derived bound (#11374 route A): the one producer is + // [commit e4902d2b9] Derived bound (route A, ruling 2026-08-24): the one producer is // `enqueueDeliveries`' `${recipient}|${channel}|${digest.window}` // — recipient ≤ 255 (recipient_id above) + '|' + channel ≤ 64 // (channel above) + '|' + window ≤ 10 (`digestDeferral` emits a diff --git a/packages/services/service-messaging/src/objects/notification-keyed-text-bounds.test.ts b/packages/services/service-messaging/src/objects/notification-keyed-text-bounds.test.ts index 04588be2eec..9d04b2deec6 100644 --- a/packages/services/service-messaging/src/objects/notification-keyed-text-bounds.test.ts +++ b/packages/services/service-messaging/src/objects/notification-keyed-text-bounds.test.ts @@ -1,8 +1,8 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. // -// [#12978] The VALUE half of the keyed-text-bounds contract for this package's -// five `sys_notification_*` objects (#11374 route A). The class-level gate -// (`scripts/check-keyed-text-bounds.mjs`, #12147) asks whether a bound EXISTS; +// [commit e4902d2b9] The VALUE half of the keyed-text-bounds contract for this package's +// five `sys_notification_*` objects (route A, ruling 2026-08-24). The class-level gate +// (`scripts/check-keyed-text-bounds.mjs`, commit 945e91a13) asks whether a bound EXISTS; // it cannot ask whether the bound is the RIGHT one, because "right" here is a // RELATION to another declaration -- exactly what a later edit breaks without // noticing. Same division of labour the plugin-audit pin states for its @@ -78,9 +78,9 @@ describe('sys_notification_* keyed-text bounds carry their producers’ widths ( it('principal covers the widest declared selector arm: owner_of::', () => { // 'owner_of:' (9) + object API name (<= 255, storage-owned by - // `sys_metadata.name`, #12144) + ':' (1) + record id (<= 255, the physical - // id width above). #9807: every other arm is narrower (an email is <= 254; - // 'user:' + id is 260). + // `sys_metadata.name`, commit 3a04b0125) + ':' (1) + record id (<= 255, the physical + // id width above). Every other arm of the grammar commit 44738f7af documented + // is narrower (an email is <= 254; 'user:' + id is 260). expect(bound(NotificationSubscription, 'principal')).toBe(9 + 255 + 1 + PHYSICAL_ID_WIDTH); }); }); diff --git a/packages/services/service-messaging/src/objects/notification-preference.object.ts b/packages/services/service-messaging/src/objects/notification-preference.object.ts index 2e3381cba6b..8f0349a566f 100644 --- a/packages/services/service-messaging/src/objects/notification-preference.object.ts +++ b/packages/services/service-messaging/src/objects/notification-preference.object.ts @@ -61,7 +61,7 @@ export const NotificationPreference = ObjectSchema.create({ label: 'User', required: true, searchable: true, - // [#12978] Referenced-column bound (#11374 route A): a + // [commit e4902d2b9] Referenced-column bound (route A, ruling 2026-08-24): a // `sys_user.id` — physical varchar(255), the id column driver-sql // creates (`table.string('id').primary()`) — or the 1-char // literal '*'. @@ -74,7 +74,7 @@ export const NotificationPreference = ObjectSchema.create({ required: true, searchable: true, defaultValue: '*', - // [#12978] Sibling-declaration bound (#11374 route A): rows are + // [commit e4902d2b9] Sibling-declaration bound (route A, ruling 2026-08-24): rows are // matched against the event's `sys_notification.topic` // (maxLength: 200 there) — `preference-resolver` keys // `${user}|${topic}|${channel}` against `ctx.topic` — so a longer @@ -88,7 +88,7 @@ export const NotificationPreference = ObjectSchema.create({ label: 'Channel', required: true, defaultValue: '*', - // [#12978] Machine channel-id vocabulary (#11374 route A), same + // [commit e4902d2b9] Machine channel-id vocabulary (route A, ruling 2026-08-24), same // sourcing as `sys_notification_delivery.channel`: registered // `MessagingChannel.id`s (inbox/email/sms today; spec's widest // enum member is 7 chars), 64 per the landed machine-vocabulary diff --git a/packages/services/service-messaging/src/objects/notification-receipt.object.ts b/packages/services/service-messaging/src/objects/notification-receipt.object.ts index 85f87356c06..ad01bbca68d 100644 --- a/packages/services/service-messaging/src/objects/notification-receipt.object.ts +++ b/packages/services/service-messaging/src/objects/notification-receipt.object.ts @@ -59,7 +59,7 @@ export const NotificationReceipt = ObjectSchema.create({ label: 'Notification Event', required: true, searchable: true, - // [#12978] Referenced-column bound (#11374 route A): FK to + // [commit e4902d2b9] Referenced-column bound (route A, ruling 2026-08-24): FK to // `sys_notification.id` — physical varchar(255), the id column // driver-sql creates (`table.string('id').primary()`). maxLength: 255, @@ -76,7 +76,7 @@ export const NotificationReceipt = ObjectSchema.create({ label: 'Recipient User', required: true, searchable: true, - // [#12978] Referenced-column bound (#11374 route A): a + // [commit e4902d2b9] Referenced-column bound (route A, ruling 2026-08-24): a // `sys_user.id` — physical varchar(255), as above. maxLength: 255, }), @@ -84,7 +84,7 @@ export const NotificationReceipt = ObjectSchema.create({ channel: Field.text({ label: 'Channel', required: true, - // [#12978] Machine channel-id vocabulary (#11374 route A), same + // [commit e4902d2b9] Machine channel-id vocabulary (route A, ruling 2026-08-24), same // sourcing as `sys_notification_delivery.channel`: registered // `MessagingChannel.id`s, 64 per the landed machine-vocabulary // precedent (sys_session.revoke_reason, maxLength: 64). diff --git a/packages/services/service-messaging/src/objects/notification-subscription.object.ts b/packages/services/service-messaging/src/objects/notification-subscription.object.ts index 12a6a9041a7..5f214f27d6c 100644 --- a/packages/services/service-messaging/src/objects/notification-subscription.object.ts +++ b/packages/services/service-messaging/src/objects/notification-subscription.object.ts @@ -10,7 +10,7 @@ import { F } from '@objectstack/spec'; * Declares standing interest in a `topic` by a `principal` (see that field for * the accepted selector forms). * - * ⚠️ [#9807] The subscription→recipient expansion is **NOT WIRED in this + * ⚠️ [commit 44738f7af] The subscription→recipient expansion is **NOT WIRED in this * repo**: `AudienceSpec` (`messaging-service.ts`) has no `'subscribers'` * member, `EmitInput.audience` is REQUIRED, and no `RecipientResolver` branch * expands a topic's subscriptions — so nothing here reads these rows at @@ -65,7 +65,7 @@ export const NotificationSubscription = ObjectSchema.create({ label: 'Topic', required: true, searchable: true, - // [#12978] Sibling-declaration bound (#11374 route A): subscribed + // [commit e4902d2b9] Sibling-declaration bound (route A, ruling 2026-08-24): subscribed // topics are matched against the event's `sys_notification.topic` // (maxLength: 200 there), so a longer stored topic could never // match an event the platform can store. @@ -77,15 +77,15 @@ export const NotificationSubscription = ObjectSchema.create({ label: 'Principal', required: true, searchable: true, - // [#9807] Kept in step with what `RecipientResolver.resolveOne()` really - // accepts for a string spec, so this does not under-describe the day the - // expansion above is wired: an email-shaped value is matched against + // [commit 44738f7af] Kept in step with what `RecipientResolver.resolveOne()` + // really accepts for a string spec, so this does not under-describe the day + // the expansion above is wired: an email-shaped value is matched against // `sys_user` (kept verbatim when no user matches), and anything otherwise // unrecognized falls through as a bare user id. - // [#12978] Derived bound (#11374 route A) over the declared + // [commit e4902d2b9] Derived bound (route A, ruling 2026-08-24) over the declared // selector grammar: the widest arm is `owner_of:object:id` = // 'owner_of:' (9) + object API name (≤ 255 — storage-owned by - // `sys_metadata.name`, maxLength: 255, #12144) + ':' (1) + record + // `sys_metadata.name`, maxLength: 255, commit 3a04b0125) + ':' (1) + record // id (≤ 255 — the physical id column, varchar(255)) = 520. Every // other arm is narrower: an email ≤ 254 (RFC 5321) and // `sys_user.email` is a string-family varchar(255); 'user:' + id @@ -119,7 +119,7 @@ export const NotificationSubscription = ObjectSchema.create({ // org_yi (billing.invoice, user:u1) 201 / org_yi's own GET on the // colliding pair 0 rows. // - // ⚠️ [#9722, correcting this note] `principal` names are per-organization: + // ⚠️ [commit 2074b2651, correcting this note] `principal` names are per-organization: // `role:x` resolves against `sys_member` (tenant-scoped org-membership // rows — the org-administration tier that is the sole ADR-0090 D3 // "role" exception) and `team:x` against `sys_team_member` (tenant-scoped diff --git a/packages/services/service-messaging/src/objects/notification-template.object.ts b/packages/services/service-messaging/src/objects/notification-template.object.ts index b770df5e507..4431b2f9cf9 100644 --- a/packages/services/service-messaging/src/objects/notification-template.object.ts +++ b/packages/services/service-messaging/src/objects/notification-template.object.ts @@ -38,7 +38,7 @@ export const NotificationTemplate = ObjectSchema.create({ label: 'Topic', required: true, searchable: true, - // [#12978] Sibling-declaration bound (#11374 route A): template + // [commit e4902d2b9] Sibling-declaration bound (route A, ruling 2026-08-24): template // topics are matched against the event's `sys_notification.topic` // (maxLength: 200 there). maxLength: 200, @@ -48,7 +48,7 @@ export const NotificationTemplate = ObjectSchema.create({ label: 'Channel', required: true, defaultValue: 'email', - // [#12978] Machine channel-id vocabulary (#11374 route A), same + // [commit e4902d2b9] Machine channel-id vocabulary (route A, ruling 2026-08-24), same // sourcing as `sys_notification_delivery.channel`: registered // `MessagingChannel.id`s, 64 per the landed machine-vocabulary // precedent (sys_session.revoke_reason, maxLength: 64). @@ -60,7 +60,7 @@ export const NotificationTemplate = ObjectSchema.create({ label: 'Locale', required: true, defaultValue: 'en', - // [#12978] Sibling-declaration bound (#11374 route A): the same + // [commit e4902d2b9] Sibling-declaration bound (route A, ruling 2026-08-24): the same // BCP-47 tag family `sys_email_template.locale` stores, bounded 16 // there. The BOUND is shared; the RESOLUTION is not, and neither // side picks a "best-matching" locale. This object is loaded by diff --git a/packages/services/service-messaging/src/outbox-ack-claim-ownership.integration.test.ts b/packages/services/service-messaging/src/outbox-ack-claim-ownership.integration.test.ts index f8e7598c1f2..f9183516d65 100644 --- a/packages/services/service-messaging/src/outbox-ack-claim-ownership.integration.test.ts +++ b/packages/services/service-messaging/src/outbox-ack-claim-ownership.integration.test.ts @@ -1,7 +1,7 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * #11859 — `ack()` proves OWNERSHIP, not just "a claim exists": the claim + * Commit d9cf78eaa — `ack()` proves OWNERSHIP, not just "a claim exists": the claim * credential rides the record `claim()` returns, and the compare-and-set * binds it (ruling C on the card; option A's caller-supplied identity and * option B's required `nodeId` parameter were both refused). @@ -12,7 +12,7 @@ * 2. the send outruns `claimTtlMs`; * 3. another node's `claim()` reaps R back to `pending` and re-claims it — * R is `in_flight` again, claimed by B; - * 4. node A finishes and acks. Before #11859, `status = 'in_flight'` MATCHED + * 4. node A finishes and acks. Before commit d9cf78eaa, `status = 'in_flight'` MATCHED * and A's outcome was written over B's live attempt. * * Every step is driven through the public contract (`claim` with an explicit @@ -37,7 +37,7 @@ * * ## Why both backends, one table * - * Same warrant as the #11453 file beside this one: the guarantee is a + * Same warrant as `outbox-ack-precondition.integration.test.ts` beside this one: the guarantee is a * property of {@link INotificationOutbox}, the SQL leg runs on a REAL engine * (`ObjectQL` + `SqlDriver`, better-sqlite3 `:memory:` — the #5704 ruled test * backend) because the fix IS an atomic conditional UPDATE and a fake engine @@ -142,7 +142,7 @@ describe.each([memoryBackend(), sqlBackend()])('$name — ack() claim ownership // 2.–3. The send outruns claimTtlMs; node B's claim() reaps R back to // pending and re-claims it in the same call. R is in_flight AGAIN — - // the state #11453's status-only predicate cannot tell from step 1. + // the state commit 1a47a5368's status-only predicate cannot tell from step 1. const claimedByB = await outbox.claim(claimOpts('node-b', T_AFTER_TTL)); expect(claimedByB.map((r) => `${r.id}:${r.claimedBy}:${r.claimedAt}`)).toEqual([`${id}:node-b:${T_AFTER_TTL}`]); diff --git a/packages/services/service-messaging/src/outbox-ack-precondition.integration.test.ts b/packages/services/service-messaging/src/outbox-ack-precondition.integration.test.ts index 43988d41e21..6a224c01b63 100644 --- a/packages/services/service-messaging/src/outbox-ack-precondition.integration.test.ts +++ b/packages/services/service-messaging/src/outbox-ack-precondition.integration.test.ts @@ -1,7 +1,7 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * #11453 — `ack()` is the dispatcher's completion callback for a row IT + * Commit 1a47a5368 — `ack()` is the dispatcher's completion callback for a row IT * CLAIMED, and both implementations must enforce that. * * ## The measured defect this file pins the fix for @@ -134,7 +134,7 @@ describe.each([memoryBackend(), sqlBackend()])('$name — ack() status precondit expect(`${(await readRow(id)).status}:${(await readRow(id)).attempts}`).toBe('pending:0'); // The card's trap, verbatim: ack-as-cancel on a row no dispatcher - // holds. [#11859] `ack` now takes the claimed record back, so the + // holds. [commit d9cf78eaa] `ack` now takes the claimed record back, so the // literal spelling of the trap is handing it a `list()` row — which // carries NO claim credential; the cast is the JS caller/miscast this // pin keeps refused at runtime, not just at compile time. diff --git a/packages/services/service-messaging/src/outbox-dispatcher-scope.ts b/packages/services/service-messaging/src/outbox-dispatcher-scope.ts index 2a8646124a6..472a222076b 100644 --- a/packages/services/service-messaging/src/outbox-dispatcher-scope.ts +++ b/packages/services/service-messaging/src/outbox-dispatcher-scope.ts @@ -53,7 +53,7 @@ import type { EngineUpdateOptions } from '@objectstack/spec/data'; * {@link dispatcherAckOptions} carries the sweep warrant to the deprecated * credential-less arity of `SqlHttpOutbox.ack`, and * {@link dispatcherAckCasOptions} carries it to `SqlNotificationOutbox.ack` (a - * `multi: true` compare-and-set since #11453) and to `SqlHttpOutbox.ack` handed + * `multi: true` compare-and-set since commit 1a47a5368) and to `SqlHttpOutbox.ack` handed * a claim credential (since #17634), while `SqlHttpOutbox.redeliver` * — request-reachable — carries a threaded tenant and no bypass at all. * @@ -79,7 +79,7 @@ export function dispatcherSweepOptions( * (`multi: false`) write that records one delivery attempt's outcome on * `SqlHttpOutbox.ack`. * - * [#11453] `SqlNotificationOutbox.ack` no longer uses this helper: its ack + * [commit 1a47a5368] `SqlNotificationOutbox.ack` no longer uses this helper: its ack * grew a status precondition, and a precondition on the by-id path is silently * discarded (#11009), so it rides {@link dispatcherAckCasOptions} instead. * @@ -139,7 +139,7 @@ export function dispatcherAckOptions( /** - * [#11453] The write options for **`SqlNotificationOutbox.ack`** — and, since + * [commit 1a47a5368] The write options for **`SqlNotificationOutbox.ack`** — and, since * #17634, for **`SqlHttpOutbox.ack`** handed a claim credential — the same * warrant as {@link dispatcherAckOptions} above, spelled as a PREDICATE write * because each of those acks is a compare-and-set. @@ -177,7 +177,7 @@ export function dispatcherAckOptions( * `redeliver` stays the one request-reachable write on `sys_http_delivery`, and * it threads the caller's tenant. * - * ## [#11859] Ownership joined the predicate + * ## [commit d9cf78eaa] Ownership joined the predicate * * `status = 'in_flight'` can prove a claim EXISTS but not WHOSE: after a * visibility-timeout reap plus a re-claim, the row is `in_flight` again under diff --git a/packages/services/service-messaging/src/outbox.ts b/packages/services/service-messaging/src/outbox.ts index 51114e29417..80618f8711e 100644 --- a/packages/services/service-messaging/src/outbox.ts +++ b/packages/services/service-messaging/src/outbox.ts @@ -63,13 +63,13 @@ export interface NotificationDeliveryRecord { } /** - * [#11859] A delivery row as handed out by {@link INotificationOutbox.claim} / + * [commit d9cf78eaa] A delivery row as handed out by {@link INotificationOutbox.claim} / * {@link INotificationOutbox.claimDigest}: the **claim credential** — the * (`claimedBy`, `claimedAt`) pair the store stamped when it took the row — is * guaranteed present. {@link INotificationOutbox.ack} takes this record back, * and the credential joins the compare-and-set predicate, so ownership is * proven by ROUND-TRIPPING what `claim()` returned rather than by the caller - * supplying an identity it had to know (the option-A shape the #11859 ruling + * supplying an identity it had to know (the option-A shape commit d9cf78eaa's ruling * refused). The pair identifies one CLAIM, not one node: `claimedAt` is what * refuses a late ack even when the SAME node re-claimed its own reaped row — * the outcome belongs to the attempt, and a re-claim is a new attempt. @@ -153,7 +153,7 @@ export interface AckFailure { export type AckResult = AckSuccess | AckFailure; /** - * [#11453] Error raised by {@link INotificationOutbox.ack} when the delivery + * [commit 1a47a5368] Error raised by {@link INotificationOutbox.ack} when the delivery * row's status does not permit the completion it was handed. * * `DELIVERY_NOT_ELIGIBLE` is this package's already-registered ADR-0112 code @@ -191,7 +191,7 @@ export function notificationAckNotClaimedMessage(id: string, status: DeliverySta /** * The refusal message for the OTHER half of the precondition: the row WAS * claimed by this caller and had stopped being so by the time the outcome was - * recorded — a claim lost to the visibility-timeout reap. [#11859] Covers BOTH + * recorded — a claim lost to the visibility-timeout reap. [commit d9cf78eaa] Covers BOTH * post-reap states: the row moved out of `in_flight`, and the row re-claimed * (still `in_flight`, but under a different claim credential — possibly the * same node's LATER claim, which is still not the claim this ack completes). @@ -212,7 +212,7 @@ export function notificationAckLostClaimMessage(id: string, status: DeliveryStat } /** - * [#11859] The refusal message for a record that carries no claim credential + * [commit d9cf78eaa] The refusal message for a record that carries no claim credential * at all. `ack()` takes back the exact record {@link INotificationOutbox.claim} * / {@link INotificationOutbox.claimDigest} returned; a row read via `list()` * while unclaimed, or a hand-built record, has no (`claimedBy`, `claimedAt`) @@ -248,7 +248,7 @@ export interface INotificationOutbox { * * Safe at any moment and from any number of nodes: it moves only rows already * past their timeout, {@link claim} takes only `pending` rows, and an - * {@link ack} whose claim was reaped matches nothing and is refused (#11859). + * {@link ack} whose claim was reaped matches nothing and is refused (commit d9cf78eaa). * * Optional, so an outbox written before it keeps working unchanged: the * dispatcher probes for it and, when it is absent, lets every claim reap as @@ -267,12 +267,12 @@ export interface INotificationOutbox { * status — an unclaimed `pending` row, or one already terminal — throws * {@link NotificationAckError}, writes nothing, and leaves `attempts` * untouched; so does a late ack whose claim was reaped and re-claimed - * (#11859): `status = 'in_flight'` alone could not tell "claimed" from + * (commit d9cf78eaa): `status = 'in_flight'` alone could not tell "claimed" from * "claimed by the caller", so a node whose send outran `claimTtlMs` wrote * its outcome over the re-claiming node's live attempt. `ack` is the * dispatcher's completion callback, NOT a cancellation primitive: using it * to flip a `pending` row to `suppressed` raced the dispatcher and - * recorded an attempt that never happened (#11453). A record whose id + * recorded an attempt that never happened (commit 1a47a5368). A record whose id * matches no row is not a contract violation and stays a silent no-op — * an absent row has no state to corrupt and no claim to lose. * diff --git a/packages/services/service-messaging/src/sms-channel.test.ts b/packages/services/service-messaging/src/sms-channel.test.ts index cd9568f83d7..33d646f89cf 100644 --- a/packages/services/service-messaging/src/sms-channel.test.ts +++ b/packages/services/service-messaging/src/sms-channel.test.ts @@ -139,7 +139,7 @@ describe('sms channel', () => { it('END TO END: one emit, one outbox, one dispatcher tick — the row lands `dead`, ⛔ not `success`', async () => { // What an operator reads off `sys_notification_delivery`. Before - // #18424 this row read `status: 'success'` — the silent half. + // commit 879b51270 this row read `status: 'success'` — the silent half. // // [#18567] The resolver is now present at emit and gone by dispatch. // Since this channel declares `isAvailable()`, a resolver that is diff --git a/packages/services/service-messaging/src/sms-channel.ts b/packages/services/service-messaging/src/sms-channel.ts index be10dc96181..c55cc69f2f9 100644 --- a/packages/services/service-messaging/src/sms-channel.ts +++ b/packages/services/service-messaging/src/sms-channel.ts @@ -38,7 +38,7 @@ export interface SmsChannelOptions { /** * Resolve the SMS service; `undefined` ⇒ there is no transport, which * {@link MessagingChannel.send} REFUSES with the declared - * `transport_not_configured` reason (#18424). ⛔ Not a no-op success: a + * `transport_not_configured` reason (commit 879b51270). ⛔ Not a no-op success: a * delivery nothing was sent for is never reported as delivered. */ getSms(): SmsSenderSurface | undefined; @@ -85,7 +85,7 @@ const SMS_QUOTA_EXCEEDED_CODE = 'TOO_MANY_REQUESTS'; /** * The ONE token both members of this channel use for "there is no transport" - * (#18424, #18567) — the reason `isAvailable()` returns, reused verbatim so the + * (commit 879b51270, #18567) — the reason `isAvailable()` returns, reused verbatim so the * refusal `send()` writes onto a delivery row and the suppression fan-out * records on `sys_notification.suppressed_channels` name one condition. The * email channel names the same condition with the same token. @@ -110,7 +110,7 @@ const TRANSPORT_NOT_CONFIGURED: ChannelUnavailableReason = 'transport_not_config * `payload.title`/`body`), and hand the text to the `sms` service. * Retry/backoff/dead-letter come for free from the P1 outbox dispatcher. * - * Failure is always REPORTED, never absorbed (#18424): no sms service ⇒ a + * Failure is always REPORTED, never absorbed (commit 879b51270): no sms service ⇒ a * refusal carrying the declared `transport_not_configured` reason, graded * `permanent` so the row dead-letters on attempt one; a recipient with no * resolvable phone number ⇒ a reported failure. Either way the delivery row @@ -238,7 +238,7 @@ export function createSmsChannel(opts: SmsChannelOptions): MessagingChannel { async send(ctx: MessagingChannelContext, delivery: Delivery): Promise { const sms = opts.getSms(); if (!sms) { - // [#18424] A refusal, ⛔ never `{ ok: true }`. This used to + // [commit 879b51270] A refusal, ⛔ never `{ ok: true }`. This used to // return success ("capability not installed — no-op"), which // recorded a delivery nobody performed: the row reached // `status: 'success'`, nothing went red, and a deployment with @@ -312,7 +312,7 @@ export function createSmsChannel(opts: SmsChannelOptions): MessagingChannel { // `sms send failed: TOO_MANY_REQUESTS: …`. const text = err instanceof Error ? err.message : String(err ?? ''); if (text.includes(SMS_QUOTA_EXCEEDED_CODE)) return 'rate_limited'; - // [#18424] The grade is driven rather than assumed: in the shipping + // [commit 879b51270] The grade is driven rather than assumed: in the shipping // composition the same condition already terminates a claimed row // at once — the mount gate unmounts the channel and the dispatcher // acks `dead: true, attempts: 1` without consulting this method at diff --git a/packages/services/service-messaging/src/sql-http-outbox.ts b/packages/services/service-messaging/src/sql-http-outbox.ts index ba8f3c188c7..1e24dac34dc 100644 --- a/packages/services/service-messaging/src/sql-http-outbox.ts +++ b/packages/services/service-messaging/src/sql-http-outbox.ts @@ -334,7 +334,7 @@ export class SqlHttpOutbox implements IHttpOutbox { * Record one attempt's outcome — see {@link IHttpOutbox.ack}. * * [#17634] Handed `claimed` — as `HttpDispatcher` always hands it — this is - * the compare-and-set `SqlNotificationOutbox.ack` performs (#11453, #11859): + * the compare-and-set `SqlNotificationOutbox.ack` performs (commits 1a47a5368, d9cf78eaa): * two deterministic refusals read before any write, the same two tests * re-stated IN a conditional UPDATE (the half that holds under the race), * and a read-back that reports a write which matched nothing instead of a diff --git a/packages/services/service-messaging/src/sql-outbox-audit-columns.test.ts b/packages/services/service-messaging/src/sql-outbox-audit-columns.test.ts index 12072720d2d..d7b7ad69444 100644 --- a/packages/services/service-messaging/src/sql-outbox-audit-columns.test.ts +++ b/packages/services/service-messaging/src/sql-outbox-audit-columns.test.ts @@ -32,7 +32,7 @@ interface RecordedUpdate { * sets so a claim can be driven all the way through candidate-select → * atomic-claim → read-back; everything else is inert. * - * [#11453] `SqlNotificationOutbox.ack` is a compare-and-set now — it reads the + * [commit 1a47a5368] `SqlNotificationOutbox.ack` is a compare-and-set now — it reads the * row's STATUS as well as its attempts, writes conditionally, then reads back * to confirm the write landed. So the fake keeps one row's state and applies * writes to it, rather than answering a bare `{ attempts }` forever: @@ -48,7 +48,7 @@ function makeEngine(findResults: Array>> = []) { const updates: RecordedUpdate[] = []; const inserts: Array<{ object: string; data: Record }> = []; let findCall = 0; - // [#11859] The row carries the claim credential ack()'s ownership check + // [commit d9cf78eaa] The row carries the claim credential ack()'s ownership check // reads back; the record handed to ack() below round-trips the same pair. const row: Record = { status: 'in_flight', attempts: 2, claimed_by: 'n1', claimed_at: 111 }; @@ -134,7 +134,7 @@ describe('SqlNotificationOutbox — audit columns on UPDATE (#4765)', () => { const { engine, updates } = makeEngine(); const outbox = new SqlNotificationOutbox(engine, { partitionCount: 8 }); - // [#11859] ack() takes the claimed record back; the credential here + // [commit d9cf78eaa] ack() takes the claimed record back; the credential here // matches what the fake row carries, as a real claim's would. await outbox.ack({ id: 'd1', notificationId: 'n1', recipientId: 'u1', channel: 'inbox', diff --git a/packages/services/service-messaging/src/sql-outbox.ts b/packages/services/service-messaging/src/sql-outbox.ts index 5ea002b8f28..1b351dfcb60 100644 --- a/packages/services/service-messaging/src/sql-outbox.ts +++ b/packages/services/service-messaging/src/sql-outbox.ts @@ -166,7 +166,7 @@ export class SqlNotificationOutbox implements INotificationOutbox { dispatcherSweepOptions({ id: { $in: ids }, status: 'pending' }), ); - // 4. Read back only the rows we own. [#11859] The read-back WHERE just + // 4. Read back only the rows we own. [commit d9cf78eaa] The read-back WHERE just // proved (claimed_by, claimed_at) — the claim credential — so the // explicit stamp below narrows to ClaimedDeliveryRecord without a // cast, and states nothing the query did not already establish. @@ -216,7 +216,7 @@ export class SqlNotificationOutbox implements INotificationOutbox { async ack(claimed: ClaimedDeliveryRecord, result: AckResult): Promise { const id = claimed.id; - // [#11859] The runtime half of the ClaimedDeliveryRecord contract, for + // [commit d9cf78eaa] The runtime half of the ClaimedDeliveryRecord contract, for // JS callers and casts: a record with no claim credential was not // handed out by claim()/claimDigest() and is refused before any IO. if (typeof claimed.claimedBy !== 'string' || typeof claimed.claimedAt !== 'number') { @@ -234,7 +234,7 @@ export class SqlNotificationOutbox implements INotificationOutbox { // An id matching no row is not a contract violation: no state to // corrupt, no claim to lose. Declared on the interface, unchanged. if (!current) return; - // [#11453] Precondition, half one: the loud, deterministic refusal for + // [commit 1a47a5368] Precondition, half one: the loud, deterministic refusal for // a row that is not claimed at all — the ack-as-cancel trap. Refused // BEFORE any write, so a refused ack leaves the row byte-identical. if (current.status !== 'in_flight') { @@ -243,7 +243,7 @@ export class SqlNotificationOutbox implements INotificationOutbox { 'DELIVERY_NOT_ELIGIBLE', ); } - // [#11859] Ownership, read half: the row is claimed, but not by the + // [commit d9cf78eaa] Ownership, read half: the row is claimed, but not by the // claim this record came from — reaped and re-claimed while the send // ran (`status = 'in_flight'` alone matches B's live attempt, which is // exactly the overwrite the card measured). Deterministic refusal @@ -275,12 +275,12 @@ export class SqlNotificationOutbox implements INotificationOutbox { error = result.error ?? null; } - // [#11453] Precondition, half two: the ATOMIC one. The tests above are + // [commit 1a47a5368] Precondition, half two: the ATOMIC one. The tests above are // reads, and a read cannot hold a row still — `claim()` is atomic by // contract and this call was never part of that atom, which is the - // race the card describes. So the requirement is re-stated IN the + // race that commit describes. So the requirement is re-stated IN the // write: the row is transitioned only if it is STILL `in_flight` AND - // [#11859] still held by THIS claim — the (`claimed_by`, `claimed_at`) + // [commit d9cf78eaa] still held by THIS claim — the (`claimed_by`, `claimed_at`) // credential round-tripped from the record `claim()` returned. A row // reaped by the visibility timeout and re-claimed between the read and // here matches nothing and is left entirely alone, whoever re-claimed diff --git a/packages/services/service-messaging/src/translations/index.ts b/packages/services/service-messaging/src/translations/index.ts index 2356ad01c83..7f49ef86d0a 100644 --- a/packages/services/service-messaging/src/translations/index.ts +++ b/packages/services/service-messaging/src/translations/index.ts @@ -26,7 +26,7 @@ import { esESGeneratedSourceHashes } from './es-ES.source-hashes.generated.js'; * ## The provenance companions are READ here, not merely recorded * * `os i18n extract --source-hashes` writes `.source-hashes.generated.ts` - * beside these bundles (maintainer ruling #12069 Option A, #11671). A record + * beside these bundles (maintainer ruling #12069 Option A, commit 09b4f4e4e). A record * says: "this locale's leaf at that path is still a byte copy of THAT source * revision". Recording alone changes nothing a user sees — the substitution is * what {@link withSourceFallback} does, and until it was wired here this set From 267c1156211c9351d9963594808cf6db1f675d14 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 06:55:36 +0000 Subject: [PATCH 2/2] chore(changeset): patch for the service-messaging provenance re-anchor The rewritten docblocks reach the published dist: the built index.d.ts and index.js carry the new commit anchors, so the change ships bytes. Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude --- .changeset/service-messaging-provenance-anchors.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/service-messaging-provenance-anchors.md diff --git a/.changeset/service-messaging-provenance-anchors.md b/.changeset/service-messaging-provenance-anchors.md new file mode 100644 index 00000000000..1d72c6ad969 --- /dev/null +++ b/.changeset/service-messaging-provenance-anchors.md @@ -0,0 +1,10 @@ +--- +'@objectstack/service-messaging': patch +--- + +Provenance comments in `service-messaging` were re-anchored + +Comment and docblock lines under `src/` that cited tracker numbers which no +longer resolve on GitHub now cite the commit in this repository's history that +decided the matter, and say in their own words what was decided. Comments +only: no type, schema, export, refusal text or runtime behaviour changes.