Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .changeset/inbox-list-result-unread-count-jsdoc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
"@objectstack/spec": patch
---

fix(spec): `InboxListResult.unreadCount` no longer documents the window count it stopped being (#6438)

`INotificationService` is a published contract — its JSDoc ships in the `.d.ts` and is
the sentence a TS SDK consumer reads in their editor. The `unreadCount` member said:

> Unread count over the returned window.

That recorded the implementation as it was *before* #6363. After #6363 (Option A,
maintainer ruling 2026-08-07; PR #6439, merged as `17d095413`) `service-messaging`
counts the **total** unread across the user's whole matching inbox, and the window
bounds `notifications[]` only. The wire declaration one directory over had already said
the same thing all along —
`ListNotificationsResponseSchema.unreadCount.describe('Total number of unread
notifications')` (`api/protocol.zod.ts`) — so one package carried two opposite sentences
about one field, with the implementation standing on the `.describe()` side and this
JSDoc the last statement of the retired semantics.

Left alone, it is the sentence that teaches the bug back. A consumer told the number is
"over the returned window" writes exactly the adaptation #6363 exists to delete: counting
`notifications` themselves, or clamping the badge to the page size. That holds double for
AI-written consumers, which are generated from this JSDoc and nothing else.

Both members are now documented, because after #6363 their bounds differ **on purpose**
and the interface had never written that difference down anywhere:

* `notifications` — the `limit`-bounded window, one page, implementations may clamp.
* `unreadCount` — the total across the whole matching inbox, explicitly NOT the window,
with the "do not re-derive, do not clamp" consequence spelled out for consumers.

Text only. No schema, no value, no behavior: every input that validated before validates
byte-for-byte after, and the generated artifacts (`check:docs`, `check:authorable-surface`,
`check:skill-refs`, `check:api-surface`) are unchanged — the reference docs render from
Zod `.describe()` strings, none of which this touches.
28 changes: 26 additions & 2 deletions packages/spec/src/contracts/notification-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,34 @@ export interface InboxNotification {
createdAt: string;
}

/** Result of {@link INotificationService.listInbox}. */
/**
* Result of {@link INotificationService.listInbox}.
*
* Its two members carry deliberately DIFFERENT bounds (#6363): `notifications`
* is the requested page, `unreadCount` is the whole matching inbox.
*/
export interface InboxListResult {
/**
* The `limit`-bounded window — at most {@link InboxQuery.limit} rows
* (implementations may clamp), newest first. One page of the inbox, not
* the whole of it.
*/
notifications: InboxNotification[];
/** Unread count over the returned window. */
/**
* Total unread across the user's whole matching inbox — NOT the window
* above (#6363). The same quantity the wire contract publishes as
* `ListNotificationsResponseSchema.unreadCount` ("Total number of unread
* notifications", `api/protocol.zod.ts`).
*
* This is the number a bell badge shows, so it must not saturate at the
* page size: do not re-derive it by counting `notifications`, and do not
* clamp it to `notifications.length`. Counting over the window is exactly
* the defect #6363 fixed — a user with 60 unread was told 50, and
* `?limit=10` told them 10.
*
* {@link InboxQuery.read} does not zero it either: asking for the READ half
* of an inbox is not a claim that nothing is unread.
*/
unreadCount: number;
}

Expand Down
Loading