@company-semantics/contracts 58.1.0 → 58.3.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.
- package/package.json +1 -1
- package/src/__tests__/resource-keys.test.ts +30 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +77 -1
- package/src/chat/README.md +15 -4
- package/src/chat/__tests__/proactive-kind.test.ts +51 -0
- package/src/chat/index.ts +9 -0
- package/src/chat/proactive-kind.ts +51 -0
- package/src/chat/schemas.ts +92 -1
- package/src/chat/types.ts +19 -1
- package/src/generated/openapi-routes.ts +1 -0
- package/src/index.ts +58 -0
- package/src/message-parts/README.md +5 -0
- package/src/message-parts/__tests__/suggested-replies.test.ts +52 -0
- package/src/message-parts/__tests__/wire.test.ts +48 -0
- package/src/message-parts/index.ts +8 -0
- package/src/message-parts/suggested-replies.ts +48 -0
- package/src/message-parts/types.ts +7 -1
- package/src/message-parts/wire.ts +26 -0
- package/src/proactive/README.md +125 -0
- package/src/proactive/__tests__/README.md +56 -0
- package/src/proactive/__tests__/chat-templates.test.ts +167 -0
- package/src/proactive/__tests__/compile-fixtures.ts +110 -0
- package/src/proactive/__tests__/vocabulary.test.ts +279 -0
- package/src/proactive/classes.ts +125 -0
- package/src/proactive/composer.ts +104 -0
- package/src/proactive/facts.ts +87 -0
- package/src/proactive/index.ts +52 -0
- package/src/proactive/kinds.ts +127 -0
- package/src/proactive/plan.ts +79 -0
- package/src/proactive/registry.ts +71 -0
- package/src/proactive/surfaces.ts +59 -0
- package/src/proactive/templates/README.md +58 -0
- package/src/proactive/templates/index.ts +32 -0
- package/src/proactive/templates/morning-brief.ts +77 -0
- package/src/proactive/templates/org-became-shared.ts +54 -0
- package/src/resource-key-types.ts +9 -0
- package/src/resource-keys.ts +2 -0
- package/src/user-notifications/README.md +10 -0
- package/src/user-notifications/kinds.ts +28 -0
package/package.json
CHANGED
|
@@ -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 = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = '433bb009d9f1' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '433bb009d9f1037871adde049808503dc040cc4cb44122a8f2d603da02b4dd2b' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -465,6 +465,26 @@ export interface paths {
|
|
|
465
465
|
patch?: never;
|
|
466
466
|
trace?: never;
|
|
467
467
|
};
|
|
468
|
+
"/api/chats/proactive/{eventId}": {
|
|
469
|
+
parameters: {
|
|
470
|
+
query?: never;
|
|
471
|
+
header?: never;
|
|
472
|
+
path?: never;
|
|
473
|
+
cookie?: never;
|
|
474
|
+
};
|
|
475
|
+
/**
|
|
476
|
+
* Resolve a proactive occurrence to the current reader's chat
|
|
477
|
+
* @description A banner or inbox row links to an OCCURRENCE (/chat/proactive/{eventId}), never to a chat id, because the pushed chat is per recipient. This resolves the occurrence for the CURRENT reader: `ready` with their chat id, `pending` while the projection job is still running or retrying, `unavailable` when it gave up (the durable inbox row stands alone), or `ineligible` when the reader is not in the occurrence’s frozen audience.
|
|
478
|
+
*/
|
|
479
|
+
get: operations["resolveProactiveChat"];
|
|
480
|
+
put?: never;
|
|
481
|
+
post?: never;
|
|
482
|
+
delete?: never;
|
|
483
|
+
options?: never;
|
|
484
|
+
head?: never;
|
|
485
|
+
patch?: never;
|
|
486
|
+
trace?: never;
|
|
487
|
+
};
|
|
468
488
|
"/api/workspace/access": {
|
|
469
489
|
parameters: {
|
|
470
490
|
query?: never;
|
|
@@ -3994,7 +4014,12 @@ export interface components {
|
|
|
3994
4014
|
pinnedAt: string | null;
|
|
3995
4015
|
titleSource: ("auto" | "manual") | null;
|
|
3996
4016
|
titleGeneratedAt: string | null;
|
|
4017
|
+
/** @enum {string} */
|
|
4018
|
+
origin?: "user" | "proactive";
|
|
4019
|
+
unread?: boolean;
|
|
4020
|
+
proactiveKind?: string | null;
|
|
3997
4021
|
}[];
|
|
4022
|
+
unreadCount?: number;
|
|
3998
4023
|
};
|
|
3999
4024
|
CreateChatResponse: {
|
|
4000
4025
|
chat: {
|
|
@@ -4009,6 +4034,10 @@ export interface components {
|
|
|
4009
4034
|
pinnedAt: string | null;
|
|
4010
4035
|
titleSource: ("auto" | "manual") | null;
|
|
4011
4036
|
titleGeneratedAt: string | null;
|
|
4037
|
+
/** @enum {string} */
|
|
4038
|
+
origin?: "user" | "proactive";
|
|
4039
|
+
unread?: boolean;
|
|
4040
|
+
proactiveKind?: string | null;
|
|
4012
4041
|
};
|
|
4013
4042
|
};
|
|
4014
4043
|
CreateChatRequest: {
|
|
@@ -4036,6 +4065,9 @@ export interface components {
|
|
|
4036
4065
|
createdAt: string;
|
|
4037
4066
|
/** Format: date-time */
|
|
4038
4067
|
updatedAt: string;
|
|
4068
|
+
/** @enum {string} */
|
|
4069
|
+
origin?: "user" | "proactive";
|
|
4070
|
+
unread?: boolean;
|
|
4039
4071
|
};
|
|
4040
4072
|
messages: {
|
|
4041
4073
|
id: string;
|
|
@@ -4166,6 +4198,10 @@ export interface components {
|
|
|
4166
4198
|
/** Format: date-time */
|
|
4167
4199
|
updatedAt: string;
|
|
4168
4200
|
isShared: boolean;
|
|
4201
|
+
/** @enum {string} */
|
|
4202
|
+
origin?: "user" | "proactive";
|
|
4203
|
+
unread?: boolean;
|
|
4204
|
+
proactiveKind?: string | null;
|
|
4169
4205
|
};
|
|
4170
4206
|
} | {
|
|
4171
4207
|
/** @constant */
|
|
@@ -4187,6 +4223,10 @@ export interface components {
|
|
|
4187
4223
|
/** Format: date-time */
|
|
4188
4224
|
updatedAt: string;
|
|
4189
4225
|
isShared: boolean;
|
|
4226
|
+
/** @enum {string} */
|
|
4227
|
+
origin?: "user" | "proactive";
|
|
4228
|
+
unread?: boolean;
|
|
4229
|
+
proactiveKind?: string | null;
|
|
4190
4230
|
};
|
|
4191
4231
|
changed: ("title" | "titleSource" | "pinnedAt")[];
|
|
4192
4232
|
} | {
|
|
@@ -4227,6 +4267,20 @@ export interface components {
|
|
|
4227
4267
|
id?: string;
|
|
4228
4268
|
runtimeProfile?: string;
|
|
4229
4269
|
};
|
|
4270
|
+
ProactiveChatResolution: {
|
|
4271
|
+
/** @constant */
|
|
4272
|
+
state: "ready";
|
|
4273
|
+
chatId: string;
|
|
4274
|
+
} | {
|
|
4275
|
+
/** @constant */
|
|
4276
|
+
state: "pending";
|
|
4277
|
+
} | {
|
|
4278
|
+
/** @constant */
|
|
4279
|
+
state: "unavailable";
|
|
4280
|
+
} | {
|
|
4281
|
+
/** @constant */
|
|
4282
|
+
state: "ineligible";
|
|
4283
|
+
};
|
|
4230
4284
|
WorkspaceAccessResponse: {
|
|
4231
4285
|
hasAccess: boolean;
|
|
4232
4286
|
};
|
|
@@ -6232,7 +6286,7 @@ export interface components {
|
|
|
6232
6286
|
notification: {
|
|
6233
6287
|
id: string;
|
|
6234
6288
|
/** @enum {string} */
|
|
6235
|
-
kind: "companyMd.access_requested" | "companyMd.access_request_approved" | "companyMd.access_request_denied" | "comment.mention" | "comment.reply";
|
|
6289
|
+
kind: "companyMd.access_requested" | "companyMd.access_request_approved" | "companyMd.access_request_denied" | "comment.mention" | "comment.reply" | "proactive.org_became_shared" | "proactive.brief_morning";
|
|
6236
6290
|
target: {
|
|
6237
6291
|
/** @enum {string} */
|
|
6238
6292
|
type: "company_md" | "org_unit" | "org" | "execution";
|
|
@@ -7816,6 +7870,28 @@ export interface operations {
|
|
|
7816
7870
|
};
|
|
7817
7871
|
};
|
|
7818
7872
|
};
|
|
7873
|
+
resolveProactiveChat: {
|
|
7874
|
+
parameters: {
|
|
7875
|
+
query?: never;
|
|
7876
|
+
header?: never;
|
|
7877
|
+
path: {
|
|
7878
|
+
eventId: string;
|
|
7879
|
+
};
|
|
7880
|
+
cookie?: never;
|
|
7881
|
+
};
|
|
7882
|
+
requestBody?: never;
|
|
7883
|
+
responses: {
|
|
7884
|
+
/** @description The resolution for the current reader (one of four bounded states) */
|
|
7885
|
+
200: {
|
|
7886
|
+
headers: {
|
|
7887
|
+
[name: string]: unknown;
|
|
7888
|
+
};
|
|
7889
|
+
content: {
|
|
7890
|
+
"application/json": components["schemas"]["ProactiveChatResolution"];
|
|
7891
|
+
};
|
|
7892
|
+
};
|
|
7893
|
+
};
|
|
7894
|
+
};
|
|
7819
7895
|
getWorkspaceAccess: {
|
|
7820
7896
|
parameters: {
|
|
7821
7897
|
query?: never;
|
package/src/chat/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/chat/schemas.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
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
|
*/
|
|
@@ -15,6 +15,7 @@ export const openApiRoutes = {
|
|
|
15
15
|
'/api/chats': ['GET', 'POST'],
|
|
16
16
|
'/api/chats/by-interaction/{interactionId}': ['GET'],
|
|
17
17
|
'/api/chats/events': ['GET'],
|
|
18
|
+
'/api/chats/proactive/{eventId}': ['GET'],
|
|
18
19
|
'/api/chats/{chatId}/shares': ['GET', 'POST'],
|
|
19
20
|
'/api/chats/{id}': ['DELETE', 'GET', 'PATCH'],
|
|
20
21
|
'/api/chats/{id}/generate-title': ['POST'],
|
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,
|