@company-semantics/contracts 58.1.0 → 58.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/resource-keys.test.ts +30 -0
  3. package/src/api/generated-spec-hash.ts +2 -2
  4. package/src/api/generated.ts +21 -1
  5. package/src/chat/README.md +15 -4
  6. package/src/chat/__tests__/proactive-kind.test.ts +51 -0
  7. package/src/chat/index.ts +9 -0
  8. package/src/chat/proactive-kind.ts +51 -0
  9. package/src/chat/schemas.ts +92 -1
  10. package/src/chat/types.ts +19 -1
  11. package/src/index.ts +58 -0
  12. package/src/message-parts/README.md +5 -0
  13. package/src/message-parts/__tests__/suggested-replies.test.ts +52 -0
  14. package/src/message-parts/__tests__/wire.test.ts +48 -0
  15. package/src/message-parts/index.ts +8 -0
  16. package/src/message-parts/suggested-replies.ts +48 -0
  17. package/src/message-parts/types.ts +7 -1
  18. package/src/message-parts/wire.ts +26 -0
  19. package/src/proactive/README.md +125 -0
  20. package/src/proactive/__tests__/README.md +56 -0
  21. package/src/proactive/__tests__/chat-templates.test.ts +167 -0
  22. package/src/proactive/__tests__/compile-fixtures.ts +110 -0
  23. package/src/proactive/__tests__/vocabulary.test.ts +279 -0
  24. package/src/proactive/classes.ts +125 -0
  25. package/src/proactive/composer.ts +104 -0
  26. package/src/proactive/facts.ts +87 -0
  27. package/src/proactive/index.ts +52 -0
  28. package/src/proactive/kinds.ts +127 -0
  29. package/src/proactive/plan.ts +79 -0
  30. package/src/proactive/registry.ts +71 -0
  31. package/src/proactive/surfaces.ts +59 -0
  32. package/src/proactive/templates/README.md +58 -0
  33. package/src/proactive/templates/index.ts +32 -0
  34. package/src/proactive/templates/morning-brief.ts +77 -0
  35. package/src/proactive/templates/org-became-shared.ts +54 -0
  36. package/src/resource-key-types.ts +9 -0
  37. package/src/resource-keys.ts +2 -0
  38. package/src/user-notifications/README.md +10 -0
  39. package/src/user-notifications/kinds.ts +28 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "58.1.0",
3
+ "version": "58.2.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -80,6 +80,36 @@ describe("resource-keys: directGrants (org-scoped)", () => {
80
80
  });
81
81
  });
82
82
 
83
+ describe("resource-keys: orgSystemEvents (org-scoped)", () => {
84
+ // The banner strip's invalidation key (PRD-00958). Registered in contracts
85
+ // rather than the app's AppOnlyKey union because the SERVER publishes it over
86
+ // the user-event stream — and a key the registry does not know cannot be
87
+ // routed through the SSE coalescer at all.
88
+ const key: ResourceKey = { type: "orgSystemEvents", orgId: ORG_ID };
89
+
90
+ it("toQueryKey produces [type, orgId]", () => {
91
+ const qk = toQueryKey(key);
92
+ expect(qk).toEqual(["orgSystemEvents", ORG_ID]);
93
+ expect(qk).toHaveLength(2);
94
+ });
95
+
96
+ it("roundtrips through fromQueryKey", () => {
97
+ expect(fromQueryKey(toQueryKey(key))).toEqual(key);
98
+ });
99
+
100
+ it("is recognised by isResourceKeyShape", () => {
101
+ // The wire-boundary predicate that gates `resource.invalidated` frames; a
102
+ // key it rejects never reaches the coalescer, so the banner never refreshes.
103
+ expect(isResourceKeyShape(key)).toBe(true);
104
+ });
105
+
106
+ it("matchesResourceKey is org-scoped: rejects a different orgId", () => {
107
+ const other = "22222222-2222-4222-8222-222222222222";
108
+ expect(matchesResourceKey(toQueryKey(key), key)).toBe(true);
109
+ expect(matchesResourceKey(["orgSystemEvents", other], key)).toBe(false);
110
+ });
111
+ });
112
+
83
113
  describe("resource-keys: orgUnitMyAuthority (per-unit identity)", () => {
84
114
  const UNIT_ID = "33333333-3333-4333-8333-333333333333";
85
115
  const key: ResourceKey = {
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = '49b741e4372d' as const;
3
- export const SPEC_HASH_FULL = '49b741e4372d56fb1a87312fe62f6e86ffd4987de590d03414ee833a4b6586b0' as const;
2
+ export const SPEC_HASH = 'a69faedfed8f' as const;
3
+ export const SPEC_HASH_FULL = 'a69faedfed8f36af0b36dc657e4f5b2c4e90c137e0f48823debd5578c433c77a' as const;
@@ -3994,7 +3994,12 @@ export interface components {
3994
3994
  pinnedAt: string | null;
3995
3995
  titleSource: ("auto" | "manual") | null;
3996
3996
  titleGeneratedAt: string | null;
3997
+ /** @enum {string} */
3998
+ origin?: "user" | "proactive";
3999
+ unread?: boolean;
4000
+ proactiveKind?: string | null;
3997
4001
  }[];
4002
+ unreadCount?: number;
3998
4003
  };
3999
4004
  CreateChatResponse: {
4000
4005
  chat: {
@@ -4009,6 +4014,10 @@ export interface components {
4009
4014
  pinnedAt: string | null;
4010
4015
  titleSource: ("auto" | "manual") | null;
4011
4016
  titleGeneratedAt: string | null;
4017
+ /** @enum {string} */
4018
+ origin?: "user" | "proactive";
4019
+ unread?: boolean;
4020
+ proactiveKind?: string | null;
4012
4021
  };
4013
4022
  };
4014
4023
  CreateChatRequest: {
@@ -4036,6 +4045,9 @@ export interface components {
4036
4045
  createdAt: string;
4037
4046
  /** Format: date-time */
4038
4047
  updatedAt: string;
4048
+ /** @enum {string} */
4049
+ origin?: "user" | "proactive";
4050
+ unread?: boolean;
4039
4051
  };
4040
4052
  messages: {
4041
4053
  id: string;
@@ -4166,6 +4178,10 @@ export interface components {
4166
4178
  /** Format: date-time */
4167
4179
  updatedAt: string;
4168
4180
  isShared: boolean;
4181
+ /** @enum {string} */
4182
+ origin?: "user" | "proactive";
4183
+ unread?: boolean;
4184
+ proactiveKind?: string | null;
4169
4185
  };
4170
4186
  } | {
4171
4187
  /** @constant */
@@ -4187,6 +4203,10 @@ export interface components {
4187
4203
  /** Format: date-time */
4188
4204
  updatedAt: string;
4189
4205
  isShared: boolean;
4206
+ /** @enum {string} */
4207
+ origin?: "user" | "proactive";
4208
+ unread?: boolean;
4209
+ proactiveKind?: string | null;
4190
4210
  };
4191
4211
  changed: ("title" | "titleSource" | "pinnedAt")[];
4192
4212
  } | {
@@ -6232,7 +6252,7 @@ export interface components {
6232
6252
  notification: {
6233
6253
  id: string;
6234
6254
  /** @enum {string} */
6235
- kind: "companyMd.access_requested" | "companyMd.access_request_approved" | "companyMd.access_request_denied" | "comment.mention" | "comment.reply";
6255
+ kind: "companyMd.access_requested" | "companyMd.access_request_approved" | "companyMd.access_request_denied" | "comment.mention" | "comment.reply" | "proactive.org_became_shared" | "proactive.brief_morning";
6236
6256
  target: {
6237
6257
  /** @enum {string} */
6238
6258
  type: "company_md" | "org_unit" | "org" | "execution";
@@ -12,6 +12,11 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
12
12
  - Invalidation events ensure convergence after SSE disconnects
13
13
  - Runtime profile labels are vendor-agnostic (no model names in UI)
14
14
  - Default runtime profile is `agentic`
15
+ - The proactive wire fields (`origin`, `unread`, `proactiveKind`) ride the extended summary AND both chat SSE event payloads — the app builds a sidebar row from event data alone, so a field missing from an event schema means every pushed chat arrives silently read (PRD-00958, ADR-CONTRACTS-142)
16
+ - `unread` is a ONE-TIME proactive badge, cleared permanently on open; ordinary conversation never sets it again
17
+ - `proactiveKind` is a bare string on the wire so a newly shipped kind cannot make an older client reject the whole chat list; it becomes a `ProactiveEventKind` exactly once, at the edge, via `recognizeProactiveKind`
18
+ - `unreadCount` on the GET /api/chats envelope is the total across all the reader's chats, never derived from the returned page (app ADR slug `feed-mark-on-open-and-paging`)
19
+ - `ProactiveChatResolution` has four bounded states (`ready` / `pending` / `unavailable` / `ineligible`) — never an open-ended "preparing"
15
20
 
16
21
  <!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
17
22
 
@@ -25,7 +30,7 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
25
30
  - `ChatChangedField` _(type)_ — Fields that can change on a chat, used in changed array.
26
31
  - `ChatChangedFieldSchema`
27
32
  - `ChatCreatedEvent` _(type)_ — Emitted exactly once after a chat is successfully persisted.
28
- - `ChatCreatedEventSchema`
33
+ - `ChatCreatedEventSchema` — Emitted exactly once after a chat is persisted; a pushed proactive chat is announced over this event too.
29
34
  - `ChatDeletedEvent` _(type)_ — Emitted when a chat is deleted.
30
35
  - `ChatDeletedEventSchema`
31
36
  - `ChatDetail` _(type)_ — Chat detail object in GET /api/chats/:id — includes interactionId for append-on-edit flows.
@@ -36,9 +41,11 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
36
41
  - `ChatInvalidationEventSchema`
37
42
  - `ChatListFilters` _(type)_ — Filters for listing chats.
38
43
  - `ChatListResponse` _(type)_ — Response for GET /api/chats
39
- - `ChatListResponseSchema` — Response for GET /api/chats
44
+ - `ChatListResponseSchema` — Response for GET /api/chats.
40
45
  - `ChatMessage` _(type)_ — Individual message in a chat response (GET /api/chats/:id).
41
46
  - `ChatMessageSchema` — Individual message in a chat, as returned by GET /api/chats/:id.
47
+ - `ChatOrigin` _(type)_ — How a chat was born: `user` or `proactive` (the system spoke first).
48
+ - `ChatOriginSchema` — How a chat was born: by the user, or by the system speaking first (a proactive occurrence projected onto the…
42
49
  - `ChatRuntimeProfile` _(type)_ — Chat runtime profile — user-selectable orchestration strategy.
43
50
  - `ChatRuntimeProfileInfo` _(type)_ — Runtime profile metadata for UI rendering.
44
51
  - `ChatShareInfo` _(type)_ — Chat share record with snapshot metadata.
@@ -48,7 +55,7 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
48
55
  - `ChatSuccessResponse` _(type)_ — Generic mutation success confirmation.
49
56
  - `ChatSuccessResponseSchema` — Generic success confirmation for mutations that return no entity data.
50
57
  - `ChatSummary` _(type)_ — Lightweight chat summary for list views.
51
- - `ChatSummaryExtended` _(type)_ — Extended chat summary with pin/title status.
58
+ - `ChatSummaryExtended` _(type)_ — Extended chat summary with pin/title status, plus the optional proactive wire fields (`origin`, `unread`…
52
59
  - `ChatSummaryExtendedSchema`
53
60
  - `ChatSummarySchema`
54
61
  - `ChatUpdatedEvent` _(type)_ — Emitted when chat metadata changes (title, pin, etc.) Carries full current state - clients should replace…
@@ -72,6 +79,9 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
72
79
  - `IsoDateString` — ISO 8601 datetime string — the runtime safety net for date serialization.
73
80
  - `ListSharesResponse` _(type)_ — Response for GET /api/chats/:chatId/shares
74
81
  - `ListSharesResponseSchema` — Response for GET /api/chats/:chatId/shares
82
+ - `ProactiveChatResolution` _(type)_ — Resolving an occurrence id to the CURRENT reader's chat — one of the four bounded states `ready` (with…
83
+ - `ProactiveChatResolutionSchema` — Resolving an occurrence id to THIS reader's chat (INV-PROACTIVE-LINK).
84
+ - `RecognizedProactiveKind` _(type)_ — A wire `proactiveKind` after recognition: a kind this contracts version knows, or the raw string it did not —…
75
85
  - `SharedChatMessage` _(type)_ — Individual message in a shared chat view.
76
86
  - `SharedChatMessageSchema`
77
87
  - `SharedChatView` _(type)_ — Shared chat view returned to viewers.
@@ -84,6 +94,7 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
84
94
  - `UpdateShareRequest` _(type)_ — Request to update an existing share.
85
95
  - `UpdateShareResponse` _(type)_ — Response for PATCH /api/shares/:shareId
86
96
  - `UpdateShareResponseSchema` — Response for PATCH /api/shares/:shareId
97
+ - `recognizeProactiveKind` — Recognize a wire `proactiveKind`.
87
98
 
88
99
  <!-- END GENERATED: readme-public-api -->
89
100
 
@@ -93,7 +104,7 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
93
104
 
94
105
  **Internal domains:**
95
106
 
96
- _None._
107
+ - `proactive`
97
108
 
98
109
  **External packages:**
99
110
 
@@ -0,0 +1,51 @@
1
+ /**
2
+ * recognizeProactiveKind — the ONE conversion from the wire-lenient
3
+ * `proactiveKind` string to a ProactiveEventKind (PRD-00958).
4
+ *
5
+ * The wire schema accepts any string so a new kind cannot make an older
6
+ * client reject the whole chat list; these tests pin that the looseness stops
7
+ * here, and that "not proactive" and "proactive but unknown" never collapse.
8
+ */
9
+ import { describe, expect, it } from "vitest";
10
+ import { PROACTIVE_EVENT_KIND_IDS } from "../../proactive/kinds.js";
11
+ import { recognizeProactiveKind } from "../proactive-kind.js";
12
+
13
+ describe("recognizeProactiveKind", () => {
14
+ it("returns null for an absent kind (a user-born chat)", () => {
15
+ expect(recognizeProactiveKind(null)).toBeNull();
16
+ expect(recognizeProactiveKind(undefined)).toBeNull();
17
+ expect(recognizeProactiveKind("")).toBeNull();
18
+ });
19
+
20
+ it("recognizes every member of PROACTIVE_EVENT_KIND_IDS as itself", () => {
21
+ // Total over the vocabulary: a kind added to ../proactive/kinds is
22
+ // recognized here with no edit, because there is no second list to drift.
23
+ for (const kind of PROACTIVE_EVENT_KIND_IDS) {
24
+ expect(recognizeProactiveKind(kind)).toBe(kind);
25
+ }
26
+ });
27
+
28
+ it("carries an unknown kind as { unknown } rather than dropping it", () => {
29
+ // Distinct from null: a pushed chat of a kind this version has not learned
30
+ // must still render as a (generic) proactive chat, never as a user chat.
31
+ expect(recognizeProactiveKind("future.kind_not_in_this_version")).toEqual({
32
+ unknown: "future.kind_not_in_this_version",
33
+ });
34
+ });
35
+
36
+ it("does not recognize an inbox kind string as an event kind", () => {
37
+ // The inbox kind (`proactive.org_became_shared`) is DELIBERATELY distinct
38
+ // from the event kind (`org.became_shared`); recognition must not blur
39
+ // the two unions.
40
+ expect(recognizeProactiveKind("proactive.org_became_shared")).toEqual({
41
+ unknown: "proactive.org_became_shared",
42
+ });
43
+ });
44
+
45
+ it("is pure: the same input yields the same result across calls", () => {
46
+ expect(recognizeProactiveKind("org.became_shared")).toBe(
47
+ recognizeProactiveKind("org.became_shared"),
48
+ );
49
+ expect(recognizeProactiveKind("x")).toEqual(recognizeProactiveKind("x"));
50
+ });
51
+ });
package/src/chat/index.ts CHANGED
@@ -20,9 +20,12 @@ export type {
20
20
  // Chat lifecycle types
21
21
  TitleSource,
22
22
  ChatListFilters,
23
+ ChatOrigin,
23
24
  ChatSummaryExtended,
24
25
  TitleGenerationRequest,
25
26
  TitleGenerationResponse,
27
+ // Proactive chat resolution (occurrence id → this reader's chat)
28
+ ProactiveChatResolution,
26
29
  // Event system types
27
30
  BaseEvent,
28
31
  InvalidationReason,
@@ -61,8 +64,10 @@ export {
61
64
  TitleSourceSchema,
62
65
  InvalidationReasonSchema,
63
66
  ChatChangedFieldSchema,
67
+ ChatOriginSchema,
64
68
  ChatSummarySchema,
65
69
  ChatSummaryExtendedSchema,
70
+ ProactiveChatResolutionSchema,
66
71
  TitleGenerationResponseSchema,
67
72
  SharedChatMessageSchema,
68
73
  ChatShareInfoSchema,
@@ -89,6 +94,10 @@ export {
89
94
  GetChatResponseSchema,
90
95
  } from "./schemas";
91
96
 
97
+ // Recognizing a wire `proactiveKind` — once, at the edge
98
+ export type { RecognizedProactiveKind } from "./proactive-kind";
99
+ export { recognizeProactiveKind } from "./proactive-kind";
100
+
92
101
  // Runtime profile types and constants
93
102
  export type {
94
103
  ChatRuntimeProfile,
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Recognizing a chat's `proactiveKind` — ONCE, at the edge.
3
+ *
4
+ * On the wire `proactiveKind` is a bare `z.string()` (`./schemas`), so a kind
5
+ * shipped by a newer backend cannot make an older client reject the WHOLE
6
+ * chat list. That looseness must stop here: the domain model speaks
7
+ * `ProactiveEventKind`, and this is the only place a raw string is allowed to
8
+ * become one.
9
+ *
10
+ * INVARIANTS:
11
+ * - Recognition happens ONCE, at the boundary where the parsed wire shape is
12
+ * turned into domain state. Downstream code takes the recognized value and
13
+ * must never re-derive it from the raw string, and must never treat an
14
+ * unrecognised string as a valid current-domain kind — an older client
15
+ * renders a generic proactive chat for `{ unknown }` instead.
16
+ * - `null` means "not a proactive chat" (origin `user`, or an absent field).
17
+ * It is distinct from `{ unknown }`, which means "a proactive chat of a kind
18
+ * this contracts version has not learned" — the two must not collapse, or a
19
+ * pushed chat from a newer kind silently renders as a user chat.
20
+ * - The vocabulary is `PROACTIVE_EVENT_KIND_IDS` and nothing else; there is no
21
+ * second list here to drift from `../proactive`.
22
+ *
23
+ * @see ../proactive/kinds.ts
24
+ */
25
+
26
+ import { PROACTIVE_EVENT_KIND_IDS } from "../proactive/kinds";
27
+ import type { ProactiveEventKind } from "../proactive/kinds";
28
+
29
+ /**
30
+ * A wire `proactiveKind` after recognition: a kind this contracts version
31
+ * knows, or the raw string it did not — carried, not discarded, so the
32
+ * unknown can still be logged and rendered generically.
33
+ */
34
+ export type RecognizedProactiveKind = ProactiveEventKind | { unknown: string };
35
+
36
+ /**
37
+ * Recognize a wire `proactiveKind`. Pure, total, and the ONLY sanctioned
38
+ * conversion from the wire string to `ProactiveEventKind`.
39
+ *
40
+ * - `null` / `undefined` / `""` → `null`: not a proactive chat.
41
+ * - a member of `PROACTIVE_EVENT_KIND_IDS` → that `ProactiveEventKind`.
42
+ * - anything else → `{ unknown: raw }`: proactive, of a kind not yet known.
43
+ */
44
+ export function recognizeProactiveKind(
45
+ raw: string | null | undefined,
46
+ ): RecognizedProactiveKind | null {
47
+ if (!raw) return null;
48
+ return (PROACTIVE_EVENT_KIND_IDS as readonly string[]).includes(raw)
49
+ ? (raw as ProactiveEventKind)
50
+ : { unknown: raw };
51
+ }
@@ -24,6 +24,52 @@ export const ChatChangedFieldSchema = z.enum([
24
24
  "pinnedAt",
25
25
  ]);
26
26
 
27
+ /**
28
+ * How a chat was born: by the user, or by the system speaking first (a
29
+ * proactive occurrence projected onto the chat surface — `../proactive`).
30
+ * A CLOSED set on the wire, unlike `proactiveKind`: the two origins are the
31
+ * whole space, and an older client must know which of the two it holds.
32
+ */
33
+ export const ChatOriginSchema = z.enum(["user", "proactive"]);
34
+
35
+ /**
36
+ * The three proactive wire fields, stated ONCE and spread into every shape
37
+ * that carries a chat: the extended summary, BOTH chat SSE event payloads and
38
+ * (origin + unread) the detail. The app's `eventDataToSummary()` builds a full
39
+ * `ChatSummaryExtended` out of event data — a field on the summary but not on
40
+ * the event schema means every pushed chat arrives silently READ, with no type
41
+ * error to catch it. Spreading one definition is what makes that drift a
42
+ * compile error instead of a bug report.
43
+ *
44
+ * All three are OPTIONAL in this wave so consumer object literals (the app's
45
+ * `insertPendingChat` and `eventDataToSummary`) keep compiling. Dropping
46
+ * `.optional()` is a later major release.
47
+ */
48
+ const proactiveChatFields = {
49
+ /** How the chat was born. Absent means `user`. */
50
+ origin: ChatOriginSchema.optional(),
51
+ /**
52
+ * A ONE-TIME proactive badge, not a general chat-read model.
53
+ *
54
+ * unread <=> origin = 'proactive' AND unread_at IS NOT NULL
55
+ *
56
+ * Opening the chat clears it permanently; ordinary conversation NEVER makes
57
+ * the thread unread again. Deliberately a boolean rather than a timestamp:
58
+ * there is no "new since" semantics in a one-time flag.
59
+ */
60
+ unread: z.boolean().optional(),
61
+ /**
62
+ * Which ProactiveEventKind produced it; null for `origin: "user"`.
63
+ *
64
+ * DELIBERATELY `z.string()`, not `z.enum(PROACTIVE_EVENT_KIND_IDS)` — a newly
65
+ * shipped kind must not make an older client reject the WHOLE chat list. The
66
+ * looseness stops at the wire: call `recognizeProactiveKind` (in
67
+ * `./proactive-kind`) ONCE at the edge, and never treat the raw string as a
68
+ * current-domain kind downstream.
69
+ */
70
+ proactiveKind: z.string().nullable().optional(),
71
+ };
72
+
27
73
  // =============================================================================
28
74
  // Core Entities
29
75
  // =============================================================================
@@ -41,6 +87,7 @@ export const ChatSummaryExtendedSchema = ChatSummarySchema.extend({
41
87
  pinnedAt: IsoDateString.nullable(),
42
88
  titleSource: TitleSourceSchema.nullable(),
43
89
  titleGeneratedAt: IsoDateString.nullable(),
90
+ ...proactiveChatFields,
44
91
  });
45
92
 
46
93
  export const TitleGenerationResponseSchema = z.object({
@@ -105,6 +152,14 @@ export const BaseEventSchema = z.object({
105
152
  eventId: z.string().optional(),
106
153
  });
107
154
 
155
+ /**
156
+ * Emitted exactly once after a chat is persisted; a pushed proactive chat is
157
+ * announced over this event too.
158
+ *
159
+ * The SAME three proactive fields ride both chat event payloads (see
160
+ * `proactiveChatFields`) because the app builds its sidebar row from `data`
161
+ * alone.
162
+ */
108
163
  export const ChatCreatedEventSchema = BaseEventSchema.extend({
109
164
  type: z.literal("chat.created"),
110
165
  data: z.object({
@@ -117,6 +172,7 @@ export const ChatCreatedEventSchema = BaseEventSchema.extend({
117
172
  createdAt: IsoDateString,
118
173
  updatedAt: IsoDateString,
119
174
  isShared: z.boolean(),
175
+ ...proactiveChatFields,
120
176
  }),
121
177
  });
122
178
 
@@ -132,6 +188,7 @@ export const ChatUpdatedEventSchema = BaseEventSchema.extend({
132
188
  createdAt: IsoDateString,
133
189
  updatedAt: IsoDateString,
134
190
  isShared: z.boolean(),
191
+ ...proactiveChatFields,
135
192
  }),
136
193
  changed: z.array(ChatChangedFieldSchema),
137
194
  });
@@ -205,11 +262,40 @@ export const ChatByInteractionResponseSchema = z.object({
205
262
  chatId: z.string(),
206
263
  });
207
264
 
208
- /** Response for GET /api/chats */
265
+ /**
266
+ * Response for GET /api/chats.
267
+ *
268
+ * `unreadCount` rides the ENVELOPE, never derived from the returned page. The
269
+ * sidebar list is a 50-row page; deriving a badge from it is the windowed-count
270
+ * failure the app ADR slug `feed-mark-on-open-and-paging` forbids. It is the
271
+ * total across ALL of this reader's chats, and optional only because an older
272
+ * backend does not send it (a missing count is "no badge", never "zero").
273
+ */
209
274
  export const ChatListResponseSchema = z.object({
210
275
  chats: z.array(ChatSummaryExtendedSchema),
276
+ unreadCount: z.number().int().nonnegative().optional(),
211
277
  });
212
278
 
279
+ /**
280
+ * Resolving an occurrence id to THIS reader's chat (INV-PROACTIVE-LINK). A
281
+ * banner or inbox row links to the occurrence, not to a chat id, because the
282
+ * chat is per recipient; the server resolves it for the CURRENT reader.
283
+ *
284
+ * Bounded states, never an open-ended "preparing": the first time a chat
285
+ * projection exhausts its retries while the banner already exists, an
286
+ * unbounded pending state becomes a dead link.
287
+ * - `ready`: the reader's chat exists; open it.
288
+ * - `pending`: the projection is still being written; try again shortly.
289
+ * - `unavailable`: the projection gave up; the durable inbox row stands alone.
290
+ * - `ineligible`: the reader is not in the occurrence's frozen audience.
291
+ */
292
+ export const ProactiveChatResolutionSchema = z.discriminatedUnion("state", [
293
+ z.object({ state: z.literal("ready"), chatId: z.string() }),
294
+ z.object({ state: z.literal("pending") }),
295
+ z.object({ state: z.literal("unavailable") }),
296
+ z.object({ state: z.literal("ineligible") }),
297
+ ]);
298
+
213
299
  /** Response for POST /api/chats */
214
300
  export const CreateChatResponseSchema = z.object({
215
301
  chat: ChatSummaryExtendedSchema,
@@ -244,6 +330,11 @@ export const ChatDetailSchema = z.object({
244
330
  titleGeneratedAt: IsoDateString.nullable().optional(),
245
331
  createdAt: IsoDateString,
246
332
  updatedAt: IsoDateString,
333
+ // Opening the detail is what clears `unread`; the detail carries the flag so
334
+ // the reader can tell a first open from a return visit. `proactiveKind` is
335
+ // a list concern (the sidebar row) and does not ride the detail.
336
+ origin: proactiveChatFields.origin,
337
+ unread: proactiveChatFields.unread,
247
338
  });
248
339
 
249
340
  /** Response for GET /api/chats/:id */
package/src/chat/types.ts CHANGED
@@ -12,8 +12,10 @@ import {
12
12
  TitleSourceSchema,
13
13
  InvalidationReasonSchema,
14
14
  ChatChangedFieldSchema,
15
+ ChatOriginSchema,
15
16
  ChatSummarySchema,
16
17
  ChatSummaryExtendedSchema,
18
+ ProactiveChatResolutionSchema,
17
19
  TitleGenerationResponseSchema,
18
20
  ChatShareInfoSchema,
19
21
  SharedChatViewSchema,
@@ -132,10 +134,26 @@ export interface ChatListFilters {
132
134
  }
133
135
 
134
136
  /**
135
- * Extended chat summary with pin/title status.
137
+ * How a chat was born: `user` or `proactive` (the system spoke first).
138
+ */
139
+ export type ChatOrigin = z.infer<typeof ChatOriginSchema>;
140
+
141
+ /**
142
+ * Extended chat summary with pin/title status, plus the optional proactive
143
+ * wire fields (`origin`, `unread`, `proactiveKind`). Recognize `proactiveKind`
144
+ * once at the edge with `recognizeProactiveKind` — it is a bare string here.
136
145
  */
137
146
  export type ChatSummaryExtended = z.infer<typeof ChatSummaryExtendedSchema>;
138
147
 
148
+ /**
149
+ * Resolving an occurrence id to the CURRENT reader's chat — one of the four
150
+ * bounded states `ready` (with `chatId`), `pending`, `unavailable`,
151
+ * `ineligible`. Never an open-ended "preparing".
152
+ */
153
+ export type ProactiveChatResolution = z.infer<
154
+ typeof ProactiveChatResolutionSchema
155
+ >;
156
+
139
157
  /**
140
158
  * Request to generate a title for a chat.
141
159
  */
package/src/index.ts CHANGED
@@ -324,6 +324,49 @@ export type {
324
324
  UserNotificationKind,
325
325
  } from "./user-notifications/index";
326
326
 
327
+ // Proactive delivery — how the system speaks first (ADR-CONTRACTS-142). A
328
+ // COMPOSER above the three notification vocabularies, not a fourth peer: the
329
+ // presentation class is a TOTAL FUNCTION from class to surfaces
330
+ // (CLASS_SURFACES, checked in both directions by the compiler), and a kind
331
+ // carries its class plus the IDS each surface needs — which banner, which chat
332
+ // template, which inbox kind — never the surface booleans. The born-read rule
333
+ // is stated once (bornRead) so the backend imports it rather than re-deriving
334
+ // it per call site. Pushed-chat prose lives here too (PROACTIVE_CHAT_COMPOSERS,
335
+ // total over the templates): the backend hands a composer facts and writes what
336
+ // comes back, FROZEN into the transcript — so a composer may only interpolate
337
+ // facts the recipient was entitled to at compose time. See src/proactive/README.md.
338
+ export {
339
+ PROACTIVE_PRESENTATION_CLASSES,
340
+ CLASS_SURFACES,
341
+ bornRead,
342
+ ORG_SYSTEM_EVENT_TYPES,
343
+ PROACTIVE_CHAT_TEMPLATES,
344
+ PROACTIVE_EVENT_KIND_IDS,
345
+ PROACTIVE_RECIPIENT_REASONS,
346
+ PROACTIVE_PROJECTIONS,
347
+ PROACTIVE_EVENT_KINDS,
348
+ PROACTIVE_CHAT_MAX_REPLIES,
349
+ PROACTIVE_CHAT_COMPOSERS,
350
+ } from "./proactive/index";
351
+
352
+ export type {
353
+ ProactivePresentationClass,
354
+ BadgeCarrier,
355
+ ClassSurfacePlan,
356
+ OrgSystemEventType,
357
+ ProactiveChatTemplate,
358
+ ProactiveEventKind,
359
+ ProactiveEventId,
360
+ ProactiveRecipientReason,
361
+ ProactiveProjection,
362
+ ProactiveEventDefinition,
363
+ OrgBecameSharedFacts,
364
+ MorningBriefFacts,
365
+ ProactiveChatFacts,
366
+ ProactiveChatComposer,
367
+ ProactiveChatMessage,
368
+ } from "./proactive/index";
369
+
327
370
  // Comment wire contract — where a thread hangs (the anchor union), what a
328
371
  // thread and its comments look like on the wire, and who a comment may name.
329
372
  // The anchor is the single most load-bearing shape in this package: the backend
@@ -481,11 +524,21 @@ export type {
481
524
  // Runtime profile types (PRD-00229)
482
525
  ChatRuntimeProfile,
483
526
  ChatRuntimeProfileInfo,
527
+ // Proactive chat wire vocabulary (ADR-CONTRACTS-142): how a chat was born,
528
+ // the occurrence-to-chat resolution states, and the recognized form of the
529
+ // wire-lenient `proactiveKind` string
530
+ ChatOrigin,
531
+ ProactiveChatResolution,
532
+ RecognizedProactiveKind,
484
533
  } from "./chat/index";
485
534
 
486
535
  export {
487
536
  CHAT_RUNTIME_PROFILES,
488
537
  DEFAULT_CHAT_RUNTIME_PROFILE,
538
+ // `proactiveKind` is a bare string on the wire so a new kind cannot make an
539
+ // older client reject the whole chat list; this is the ONE place it becomes
540
+ // a ProactiveEventKind. See src/chat/proactive-kind.ts.
541
+ recognizeProactiveKind,
489
542
  } from "./chat/index";
490
543
 
491
544
  // Organization domain types
@@ -961,6 +1014,11 @@ export type {
961
1014
  InteractiveTaskData,
962
1015
  InteractiveTaskPart,
963
1016
  InteractiveTaskDataPart,
1017
+ // Suggested replies surface types (non-governed chips, PRD-00958)
1018
+ SuggestedReply,
1019
+ SuggestedRepliesData,
1020
+ SuggestedRepliesPart,
1021
+ SuggestedRepliesDataPart,
964
1022
  // Execution result types (Phase 5)
965
1023
  ExecutionArtifactStatus,
966
1024
  ExecutionResultSummary,
@@ -14,6 +14,7 @@ Canonical vocabulary for structured assistant message output. Defines the type s
14
14
  - Confirmation is deterministic — no natural language inference ("yes" is invalid)
15
15
  - Execution results carry `state` resolved from ExecutionState (single authority, no redundant status)
16
16
  - Undo creates append-only audit rows — original execution is never mutated
17
+ - `SuggestedRepliesPart` is a surface but NOT a governed one: a chip fires an ordinary user turn (no `executionId`, no `submitEndpoint`), is stateless (clicking never consumes it) and advisory (the composer stays enabled) — so it is outside the at-most-one-governed-surface-per-turn rule
17
18
 
18
19
  <!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
19
20
 
@@ -65,6 +66,10 @@ Canonical vocabulary for structured assistant message output. Defines the type s
65
66
  - `StatusPanelEntry` _(type)_ — Single entry in a status panel.
66
67
  - `StatusPanelPart` _(type)_ — Status panel surface part.
67
68
  - `StreamPhase` _(type)_ — Derived stream phase for UI state management.
69
+ - `SuggestedRepliesData` _(type)_ — Suggested replies surface data payload.
70
+ - `SuggestedRepliesDataPart` _(type)_ — Wire form, as persisted in `chat_messages.parts`.
71
+ - `SuggestedRepliesPart` _(type)_ — Normalized form, as rendered.
72
+ - `SuggestedReply` _(type)_ — Suggested follow-up chips beneath an assistant turn.
68
73
  - `SurfacePart` _(type)_ — Surface parts are rendered atomically (never streamed).
69
74
  - `TablePart` _(type)_ — Table surface part.
70
75
  - `TextPart` _(type)_ — Text content part.
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { isSurfacePart, isTextPart } from "../types.js";
3
+ import type { AssistantMessagePart, SurfacePart } from "../types.js";
4
+ import { createPartBuilder, addSurface, addText } from "../builder.js";
5
+ import type { SuggestedRepliesPart } from "../suggested-replies.js";
6
+
7
+ // The semantic part is what the app renders after normalizing the wire part.
8
+ // Typing the fixture as SuggestedRepliesPart and then assigning it to
9
+ // SurfacePart is the compile-time proof that the union gained the member.
10
+ const chips: SuggestedRepliesPart = {
11
+ type: "suggested-replies",
12
+ data: {
13
+ replies: [
14
+ { id: "r1", label: "What moved?" },
15
+ { id: "r2", label: "Undo", prompt: "Undo the last change" },
16
+ ],
17
+ },
18
+ };
19
+
20
+ describe("SuggestedRepliesPart in the SurfacePart union", () => {
21
+ it("is assignable to SurfacePart and AssistantMessagePart", () => {
22
+ const asSurface: SurfacePart = chips;
23
+ const asPart: AssistantMessagePart = chips;
24
+ expect(asSurface.type).toBe("suggested-replies");
25
+ expect(asPart.type).toBe("suggested-replies");
26
+ });
27
+
28
+ it("is classified as a surface part, not a text part", () => {
29
+ expect(isSurfacePart(chips)).toBe(true);
30
+ expect(isTextPart(chips)).toBe(false);
31
+ });
32
+
33
+ // The builder's narrative-before-surface state machine applies to chips like
34
+ // any other surface: they are emitted after the prose, never streamed.
35
+ it("is accepted by addSurface after narrative text", () => {
36
+ let state = createPartBuilder();
37
+ state = addText(state, "Five settings sections moved.").state;
38
+ const result = addSurface(state, chips);
39
+ expect(result.accepted).toBe(true);
40
+ expect(result.state.parts).toEqual([
41
+ { type: "text", text: "Five settings sections moved." },
42
+ chips,
43
+ ]);
44
+ });
45
+
46
+ it("does not consume: the same part can be added to two builders unchanged", () => {
47
+ const a = addSurface(createPartBuilder(), chips).state.parts[0];
48
+ const b = addSurface(createPartBuilder(), chips).state.parts[0];
49
+ expect(a).toEqual(b);
50
+ expect(a).toEqual(chips);
51
+ });
52
+ });