@company-semantics/contracts 58.0.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.
- package/package.json +4 -4
- package/src/__tests__/resource-keys.test.ts +30 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +33 -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/index.ts +110 -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/notifications/__tests__/__snapshots__/monospace-budget.test.ts.snap +1 -0
- package/src/notifications/__tests__/__snapshots__/registry.test.ts.snap +1 -0
- package/src/notifications/__tests__/__snapshots__/render-snapshot.test.ts.snap +207 -0
- package/src/notifications/__tests__/fixtures.ts +9 -0
- package/src/notifications/__tests__/org-invite.test.ts +75 -0
- package/src/notifications/__tests__/render-snapshot.test.ts +8 -0
- package/src/notifications/kinds/org-invite.ts +27 -12
- package/src/notifications/payloads.ts +7 -0
- package/src/org/README.md +38 -0
- package/src/org/__tests__/canonical-facts.test.ts +118 -0
- package/src/org/__tests__/structure-inference.test.ts +392 -0
- package/src/org/__tests__/structure-provenance.test.ts +187 -0
- package/src/org/canonical-facts.ts +94 -1
- package/src/org/index.ts +54 -0
- package/src/org/schemas.ts +23 -0
- package/src/org/structure-inference.ts +521 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@company-semantics/contracts",
|
|
3
|
-
"version": "58.
|
|
3
|
+
"version": "58.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"@types/node": "^22.20.1",
|
|
143
143
|
"amphtml-validator": "^1.0.38",
|
|
144
144
|
"culori": "^4.0.2",
|
|
145
|
-
"eslint": "^10.
|
|
145
|
+
"eslint": "^10.9.0",
|
|
146
146
|
"husky": "^9.1.7",
|
|
147
147
|
"lint-staged": "^17.3.0",
|
|
148
148
|
"markdownlint-cli2": "^0.23.2",
|
|
@@ -151,8 +151,8 @@
|
|
|
151
151
|
"tsx": "^4.23.12",
|
|
152
152
|
"typescript": "^5.8.3",
|
|
153
153
|
"typescript-eslint": "^8.67.0",
|
|
154
|
-
"vite": "^8.2.
|
|
155
|
-
"vitest": "^4.1.
|
|
154
|
+
"vite": "^8.2.2",
|
|
155
|
+
"vitest": "^4.1.11",
|
|
156
156
|
"yaml": "^2.9.0"
|
|
157
157
|
},
|
|
158
158
|
"pnpm": {
|
|
@@ -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 = 'a69faedfed8f' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = 'a69faedfed8f36af0b36dc657e4f5b2c4e90c137e0f48823debd5578c433c77a' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -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
|
} | {
|
|
@@ -4834,6 +4854,10 @@ export interface components {
|
|
|
4834
4854
|
};
|
|
4835
4855
|
SendInviteBatchRequest: {
|
|
4836
4856
|
expectedRevision: number;
|
|
4857
|
+
message?: string;
|
|
4858
|
+
roleByPersonId?: {
|
|
4859
|
+
[key: string]: "admin" | "member";
|
|
4860
|
+
};
|
|
4837
4861
|
};
|
|
4838
4862
|
InviteValidationResult: {
|
|
4839
4863
|
valid: boolean;
|
|
@@ -5989,6 +6013,14 @@ export interface components {
|
|
|
5989
6013
|
/** @enum {string} */
|
|
5990
6014
|
relationshipType: "solid" | "dotted";
|
|
5991
6015
|
}[];
|
|
6016
|
+
unplaced: {
|
|
6017
|
+
personId: string;
|
|
6018
|
+
fullName: string;
|
|
6019
|
+
/** @enum {string} */
|
|
6020
|
+
state: "needs_review" | "excluded";
|
|
6021
|
+
/** @enum {string} */
|
|
6022
|
+
reason: "insufficient_evidence" | "conflicting_evidence" | "ambiguous_boundary" | "service_account" | "not_workforce" | "inactive";
|
|
6023
|
+
}[];
|
|
5992
6024
|
};
|
|
5993
6025
|
/** @description Polling snapshot of a generic ingestion operation. */
|
|
5994
6026
|
IngestionOperationPollResponse: {
|
|
@@ -6220,7 +6252,7 @@ export interface components {
|
|
|
6220
6252
|
notification: {
|
|
6221
6253
|
id: string;
|
|
6222
6254
|
/** @enum {string} */
|
|
6223
|
-
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";
|
|
6224
6256
|
target: {
|
|
6225
6257
|
/** @enum {string} */
|
|
6226
6258
|
type: "company_md" | "org_unit" | "org" | "execution";
|
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
|
*/
|