@company-semantics/contracts 78.0.0 → 79.0.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/action-items/__tests__/vocabulary.test.ts +10 -0
- package/src/action-items/schemas.ts +10 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +4 -1
- package/src/proactive/README.md +6 -0
- package/src/proactive/__tests__/vocabulary.test.ts +18 -0
- package/src/proactive/kinds.ts +20 -0
- package/src/proactive/registry.ts +9 -0
- package/src/user-notifications/README.md +3 -0
- package/src/user-notifications/__tests__/vocabulary.test.ts +35 -0
- package/src/user-notifications/kinds.ts +8 -0
- package/src/user-notifications/schemas.ts +10 -0
package/package.json
CHANGED
|
@@ -31,6 +31,7 @@ function makeItem(over: Partial<Record<string, unknown>> = {}) {
|
|
|
31
31
|
title: "Maya Chen wants access",
|
|
32
32
|
detail: "Reviewing the cross-team roadmap",
|
|
33
33
|
contextLabel: "Sales.md",
|
|
34
|
+
provider: null,
|
|
34
35
|
href: "/@acme/md/doc-1?share=1&request=req-1",
|
|
35
36
|
createdAt: "2026-07-27T00:00:00.000Z",
|
|
36
37
|
...over,
|
|
@@ -139,6 +140,15 @@ describe("ActionItemSchema", () => {
|
|
|
139
140
|
expect(parsed.unitPath).toBeNull();
|
|
140
141
|
});
|
|
141
142
|
|
|
143
|
+
it("requires a provider — null when the item is about the product", () => {
|
|
144
|
+
const { provider: _omitted, ...withoutProvider } = makeItem();
|
|
145
|
+
expect(ActionItemSchema.safeParse(withoutProvider).success).toBe(false);
|
|
146
|
+
expect(
|
|
147
|
+
ActionItemSchema.safeParse(makeItem({ provider: "google_drive" }))
|
|
148
|
+
.success,
|
|
149
|
+
).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
142
152
|
it("rejects a kind outside the vocabulary", () => {
|
|
143
153
|
expect(() =>
|
|
144
154
|
ActionItemSchema.parse(makeItem({ kind: "org.invite" })),
|
|
@@ -69,6 +69,16 @@ export const ActionItemSchema = z.object({
|
|
|
69
69
|
detail: z.string().nullable(),
|
|
70
70
|
/** The thing it is about, as the user names it, e.g. "Sales.md". */
|
|
71
71
|
contextLabel: z.string(),
|
|
72
|
+
/**
|
|
73
|
+
* The external system this row is ABOUT, when it is about one — the open
|
|
74
|
+
* provider id the identity-link vocabulary uses system-wide ('bamboohr',
|
|
75
|
+
* 'google_drive', 'slack'). A surface shows that system's own mark.
|
|
76
|
+
*
|
|
77
|
+
* Set by the producer, because only it knows: an org's structure findings
|
|
78
|
+
* come from whichever HRIS it connected, which the kind cannot say.
|
|
79
|
+
* `null` when the row is about the product itself (a doc, a chat, billing).
|
|
80
|
+
*/
|
|
81
|
+
provider: z.string().min(1).nullable(),
|
|
72
82
|
/**
|
|
73
83
|
* Where to go to RESOLVE it — a RELATIVE app path, e.g.
|
|
74
84
|
* `/@acme/md/{docId}?share=1&request={id}`.
|
|
@@ -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 = '554ba5a13882' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '554ba5a138827be4f4959d0a9d0199be36e129c9caacce47370121ffede938bf' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -7470,6 +7470,7 @@ export interface components {
|
|
|
7470
7470
|
title: string;
|
|
7471
7471
|
detail: string | null;
|
|
7472
7472
|
contextLabel: string;
|
|
7473
|
+
provider: string | null;
|
|
7473
7474
|
href: string;
|
|
7474
7475
|
createdAt: string;
|
|
7475
7476
|
}[];
|
|
@@ -7491,6 +7492,7 @@ export interface components {
|
|
|
7491
7492
|
title: string;
|
|
7492
7493
|
detail: string | null;
|
|
7493
7494
|
contextLabel: string;
|
|
7495
|
+
provider: string | null;
|
|
7494
7496
|
href: string;
|
|
7495
7497
|
createdAt: string;
|
|
7496
7498
|
};
|
|
@@ -7500,7 +7502,7 @@ export interface components {
|
|
|
7500
7502
|
notification: {
|
|
7501
7503
|
id: string;
|
|
7502
7504
|
/** @enum {string} */
|
|
7503
|
-
kind: "companyMd.access_requested" | "companyMd.access_request_approved" | "companyMd.access_request_denied" | "comment.mention" | "comment.reply" | "chat.mention" | "proactive.org_became_shared" | "proactive.brief_morning" | "proactive.billing_balance_below_half" | "proactive.billing_balance_below_threshold" | "proactive.billing_auto_recharge_succeeded" | "proactive.billing_top_up_credited" | "proactive.billing_monthly_budget_80" | "proactive.billing_monthly_budget_100" | "proactive.billing_user_monthly_limit_reached" | "proactive.billing_byo_key_invalid" | "proactive.billing_provider_price_changed" | "proactive.billing_payment_recovered" | "proactive.billing_balance_exhausted" | "proactive.billing_auto_recharge_failed" | "proactive.billing_payment_past_due" | "proactive.billing_suspended" | "proactive.billing_subscription_canceled" | "proactive.source_sync_docs_updated" | "proactive.source_sync_review_ready";
|
|
7505
|
+
kind: "companyMd.access_requested" | "companyMd.access_request_approved" | "companyMd.access_request_denied" | "comment.mention" | "comment.reply" | "chat.mention" | "proactive.org_became_shared" | "proactive.brief_morning" | "proactive.billing_balance_below_half" | "proactive.billing_balance_below_threshold" | "proactive.billing_auto_recharge_succeeded" | "proactive.billing_top_up_credited" | "proactive.billing_monthly_budget_80" | "proactive.billing_monthly_budget_100" | "proactive.billing_user_monthly_limit_reached" | "proactive.billing_byo_key_invalid" | "proactive.billing_provider_price_changed" | "proactive.billing_payment_recovered" | "proactive.billing_balance_exhausted" | "proactive.billing_auto_recharge_failed" | "proactive.billing_payment_past_due" | "proactive.billing_suspended" | "proactive.billing_subscription_canceled" | "proactive.source_sync_docs_updated" | "proactive.source_sync_review_ready" | "proactive.org_structure_review_ready";
|
|
7504
7506
|
target: {
|
|
7505
7507
|
/** @enum {string} */
|
|
7506
7508
|
type: "company_md" | "org_unit" | "org" | "execution";
|
|
@@ -7510,6 +7512,7 @@ export interface components {
|
|
|
7510
7512
|
title: string;
|
|
7511
7513
|
detail: string | null;
|
|
7512
7514
|
contextLabel: string;
|
|
7515
|
+
provider: string | null;
|
|
7513
7516
|
href: string | null;
|
|
7514
7517
|
createdAt: string;
|
|
7515
7518
|
readAt: string | null;
|
package/src/proactive/README.md
CHANGED
|
@@ -128,6 +128,12 @@ the three from another.
|
|
|
128
128
|
candidate set id or the synthesis batch id for `review_ready`. Their inbox
|
|
129
129
|
rows are `proactive.source_sync_*`, distinct from the kind strings. Neither
|
|
130
130
|
sets `emailKind`.
|
|
131
|
+
- **`orgStructure.review_ready` is a `notice`** (the ADR with slug
|
|
132
|
+
`org-structure-review-ready-is-a-notice`). The first-sync analysis finished
|
|
133
|
+
and its proposal waits for review. Its counts are scope-gated and nothing is
|
|
134
|
+
broken, so it gets an unread inbox row (`proactive.org_structure_review_ready`)
|
|
135
|
+
and no chat or banner. It is triggered by the committed insert of a
|
|
136
|
+
`validated` proposal, and its occurrence key is the proposal id.
|
|
131
137
|
- **The occurrence vocabulary is closed.** `ProactiveEventId` is branded so a
|
|
132
138
|
raw string cannot stand in for an occurrence; `ProactiveRecipientReason` is a
|
|
133
139
|
closed enum because the recipients table is the frozen audit record of who
|
|
@@ -257,6 +257,8 @@ describe("the kind registry", () => {
|
|
|
257
257
|
// renderer is what resolves the scope-gated count and names at read time.
|
|
258
258
|
"sourceSync.docs_updated": "proactive",
|
|
259
259
|
"sourceSync.review_ready": "proactive",
|
|
260
|
+
// The org structure row is rendered by `src/proactive/` as well.
|
|
261
|
+
"orgStructure.review_ready": "proactive",
|
|
260
262
|
};
|
|
261
263
|
|
|
262
264
|
expect(Object.keys(EXPECTED_INBOX_DOMAIN).sort()).toEqual(
|
|
@@ -310,6 +312,22 @@ describe("the kind registry", () => {
|
|
|
310
312
|
expect(plan.inboxKind).toBe("proactive.source_sync_docs_updated");
|
|
311
313
|
});
|
|
312
314
|
|
|
315
|
+
it("orgStructure.review_ready is a notice: an unread inbox row, no banner and no chat", () => {
|
|
316
|
+
// The analysis counts are scope-gated, so a frozen chat has nothing it may
|
|
317
|
+
// say, and nothing is broken, so there is no banner (control
|
|
318
|
+
// docs/proactive-delivery.md). The row carries the attention.
|
|
319
|
+
const plan = PROACTIVE_EVENT_KINDS["orgStructure.review_ready"];
|
|
320
|
+
|
|
321
|
+
expect(plan.class).toBe("notice");
|
|
322
|
+
expect(CLASS_SURFACES[plan.class].chat).toBe(false);
|
|
323
|
+
expect(CLASS_SURFACES[plan.class].banner).toBe(false);
|
|
324
|
+
expect("chatTemplate" in plan).toBe(false);
|
|
325
|
+
expect("bannerType" in plan).toBe(false);
|
|
326
|
+
expect("emailKind" in plan).toBe(false);
|
|
327
|
+
expect(bornRead(CLASS_SURFACES[plan.class], "inbox")).toBe(false);
|
|
328
|
+
expect(plan.inboxKind).toBe("proactive.org_structure_review_ready");
|
|
329
|
+
});
|
|
330
|
+
|
|
313
331
|
it("sourceSync.review_ready is explained: a chat and an inbox row, never a banner", () => {
|
|
314
332
|
// A review is waiting, so the chat explains what it is and what happens if
|
|
315
333
|
// it is left alone — and carries the badge. Nothing is broken and nothing
|
package/src/proactive/kinds.ts
CHANGED
|
@@ -282,6 +282,26 @@ export const PROACTIVE_EVENT_KIND_IDS = [
|
|
|
282
282
|
* silent.
|
|
283
283
|
*/
|
|
284
284
|
"sourceSync.review_ready",
|
|
285
|
+
|
|
286
|
+
// ===========================================================================
|
|
287
|
+
// Org structure (ADR-BE-680 deferred it; the first-sync notice)
|
|
288
|
+
// ===========================================================================
|
|
289
|
+
/**
|
|
290
|
+
* The analysis a first BambooHR connect started on its own has finished, and
|
|
291
|
+
* its proposal is waiting for review. A `notice`, which means an inbox row
|
|
292
|
+
* and nothing else (control docs/proactive-delivery.md): the analysis counts
|
|
293
|
+
* are scope-gated, so there is nothing a frozen chat may say, and nothing is
|
|
294
|
+
* broken or stopped, so there is no banner. A person who launched a run by
|
|
295
|
+
* hand is already watching it dock in their chat (ADR-APP-298), so only the
|
|
296
|
+
* connect chain's run announces.
|
|
297
|
+
*
|
|
298
|
+
* TRIGGER: the committed insert of a `validated` proposal by that run. A
|
|
299
|
+
* `blocked` proposal is not a review and announces nothing.
|
|
300
|
+
*
|
|
301
|
+
* OCCURRENCE KEY: the proposal id. One proposal announces once, however many
|
|
302
|
+
* times its job is redelivered.
|
|
303
|
+
*/
|
|
304
|
+
"orgStructure.review_ready",
|
|
285
305
|
] as const;
|
|
286
306
|
export type ProactiveEventKind = (typeof PROACTIVE_EVENT_KIND_IDS)[number];
|
|
287
307
|
|
|
@@ -253,4 +253,13 @@ export const PROACTIVE_EVENT_KINDS = {
|
|
|
253
253
|
// later (ADR slug `action-items-are-standing-state-not-notifications`).
|
|
254
254
|
actionItemKind: "sourceSync.proposal_pending",
|
|
255
255
|
},
|
|
256
|
+
/**
|
|
257
|
+
* A first-sync org structure review is waiting. A NOTICE: the inbox row is
|
|
258
|
+
* the whole announcement and arrives unread, and the plan union makes a chat
|
|
259
|
+
* for it a compile error. Keyed on the proposal id.
|
|
260
|
+
*/
|
|
261
|
+
"orgStructure.review_ready": {
|
|
262
|
+
class: "notice",
|
|
263
|
+
inboxKind: "proactive.org_structure_review_ready",
|
|
264
|
+
},
|
|
256
265
|
} as const satisfies Record<ProactiveEventKind, ProactiveEventDefinition>;
|
|
@@ -75,6 +75,9 @@ that renders them alongside action items at the top of `/me/work`.
|
|
|
75
75
|
candidate-set id; the COUNT of updated documents, and which ones, is resolved
|
|
76
76
|
by the renderer at read time under the reader's authority — which is why it
|
|
77
77
|
belongs in this row and never in the frozen pushed chat.
|
|
78
|
+
- **`proactive.org_structure_review_ready` is the org structure notice's row.**
|
|
79
|
+
It arrives UNREAD, because a `notice` has no other surface to hold the badge.
|
|
80
|
+
The renderer links it to the place the review is opened.
|
|
78
81
|
- **This is a mention row, not a chat-unread model.** Being named is a discrete
|
|
79
82
|
fact you come back to; "this chat has new messages" is not modeled here or
|
|
80
83
|
anywhere, and the proactive `unread` flag on a chat summary stays a one-time
|
|
@@ -22,6 +22,7 @@ function makeNotification(over: Record<string, unknown> = {}) {
|
|
|
22
22
|
title: "Dev Patel approved your request",
|
|
23
23
|
detail: null,
|
|
24
24
|
contextLabel: "Q3 OKRs",
|
|
25
|
+
provider: null,
|
|
25
26
|
href: "/@acme/md/doc-1",
|
|
26
27
|
createdAt: "2026-07-30T12:00:00.000Z",
|
|
27
28
|
readAt: null,
|
|
@@ -138,6 +139,30 @@ describe("UserNotificationSchema", () => {
|
|
|
138
139
|
});
|
|
139
140
|
});
|
|
140
141
|
|
|
142
|
+
describe("provider — the external system a row is about", () => {
|
|
143
|
+
it("carries a provider id when the row is about one", () => {
|
|
144
|
+
expect(
|
|
145
|
+
UserNotificationSchema.safeParse(
|
|
146
|
+
makeNotification({ provider: "bamboohr" }),
|
|
147
|
+
).success,
|
|
148
|
+
).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("is REQUIRED — null says 'about the product', absence says nothing", () => {
|
|
152
|
+
const { provider: _omitted, ...withoutProvider } = makeNotification();
|
|
153
|
+
expect(UserNotificationSchema.safeParse(withoutProvider).success).toBe(
|
|
154
|
+
false,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("rejects an empty id — a mark nobody can look up", () => {
|
|
159
|
+
expect(
|
|
160
|
+
UserNotificationSchema.safeParse(makeNotification({ provider: "" }))
|
|
161
|
+
.success,
|
|
162
|
+
).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
141
166
|
describe("FeedItem discriminates on `row`", () => {
|
|
142
167
|
it("accepts an informational row", () => {
|
|
143
168
|
const parsed = FeedItemSchema.safeParse({
|
|
@@ -158,6 +183,7 @@ describe("FeedItem discriminates on `row`", () => {
|
|
|
158
183
|
title: "Maya Chen wants access",
|
|
159
184
|
detail: null,
|
|
160
185
|
contextLabel: "Q3 OKRs",
|
|
186
|
+
provider: null,
|
|
161
187
|
href: "/@acme/md/doc-1?share=1&request=req-1",
|
|
162
188
|
createdAt: "2026-07-30T12:00:00.000Z",
|
|
163
189
|
},
|
|
@@ -206,3 +232,12 @@ describe("FeedListResponse", () => {
|
|
|
206
232
|
).toBe(false);
|
|
207
233
|
});
|
|
208
234
|
});
|
|
235
|
+
|
|
236
|
+
describe("the org structure inbox row", () => {
|
|
237
|
+
it("is registered under the proactive renderer, never under its kind's string", () => {
|
|
238
|
+
expect(USER_NOTIFICATION_KINDS).toContain(
|
|
239
|
+
"proactive.org_structure_review_ready",
|
|
240
|
+
);
|
|
241
|
+
expect(USER_NOTIFICATION_KINDS).not.toContain("orgStructure.review_ready");
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -221,6 +221,14 @@ export const USER_NOTIFICATION_KINDS = [
|
|
|
221
221
|
* `sourceSync.proposal_pending` ACTION ITEM, not this row.
|
|
222
222
|
*/
|
|
223
223
|
"proactive.source_sync_review_ready",
|
|
224
|
+
/**
|
|
225
|
+
* The org structure analysis a first BambooHR connect started has finished,
|
|
226
|
+
* and its review is waiting. The durable row of the
|
|
227
|
+
* `orgStructure.review_ready` proactive kind, a `notice`: no banner and no
|
|
228
|
+
* pushed chat, so this row IS the attention and arrives UNREAD. It links to
|
|
229
|
+
* where the review is opened.
|
|
230
|
+
*/
|
|
231
|
+
"proactive.org_structure_review_ready",
|
|
224
232
|
] as const;
|
|
225
233
|
|
|
226
234
|
export type UserNotificationKind = (typeof USER_NOTIFICATION_KINDS)[number];
|
|
@@ -49,6 +49,16 @@ export const UserNotificationSchema = z.object({
|
|
|
49
49
|
detail: z.string().nullable(),
|
|
50
50
|
/** The thing it is about, as the user names it, e.g. "Sales.md". */
|
|
51
51
|
contextLabel: z.string(),
|
|
52
|
+
/**
|
|
53
|
+
* The external system this row is ABOUT, when it is about one — the open
|
|
54
|
+
* provider id the identity-link vocabulary uses system-wide ('bamboohr',
|
|
55
|
+
* 'google_drive', 'slack'). A surface shows that system's own mark.
|
|
56
|
+
*
|
|
57
|
+
* Set by the producer, because only it knows: an org's structure findings
|
|
58
|
+
* come from whichever HRIS it connected, which the kind cannot say.
|
|
59
|
+
* `null` when the row is about the product itself (a doc, a chat, billing).
|
|
60
|
+
*/
|
|
61
|
+
provider: z.string().min(1).nullable(),
|
|
52
62
|
/**
|
|
53
63
|
* Where to go to SEE it — a RELATIVE app path.
|
|
54
64
|
*
|