@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "78.0.0",
3
+ "version": "79.0.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 = '7fba2cafe8fa' as const;
3
- export const SPEC_HASH_FULL = '7fba2cafe8fac0d1d504b68950dc7991091381a49522af2660bc8bcb3b2a865a' as const;
2
+ export const SPEC_HASH = '554ba5a13882' as const;
3
+ export const SPEC_HASH_FULL = '554ba5a138827be4f4959d0a9d0199be36e129c9caacce47370121ffede938bf' as const;
@@ -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;
@@ -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
@@ -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
  *