@company-semantics/contracts 82.0.0 → 83.1.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.
@@ -0,0 +1,123 @@
1
+ /**
2
+ * NOTIFICATION_LIFECYCLE — every inbox kind's declared lifecycle
3
+ * (notification-lifecycle-is-declared-per-kind). See ./lifecycle for what each
4
+ * axis means.
5
+ */
6
+ import type { UserNotificationKind } from "./kinds";
7
+ import type { NotificationLifecycle } from "./lifecycle";
8
+
9
+ const INDEPENDENT = {
10
+ condition: "none",
11
+ resolution: "never",
12
+ retention: "standard",
13
+ } as const;
14
+ const PER_KIND = { scope: "kind" } as const;
15
+ const RESOLVED_FOR_ALL = {
16
+ mode: "source",
17
+ audience: "all-recipients",
18
+ } as const;
19
+ /** One active row per kind; each repeat is news again. */
20
+ const RENEWING = {
21
+ condition: PER_KIND,
22
+ supersedes: true,
23
+ attentionOnSupersede: "renew",
24
+ resolution: "never",
25
+ retention: "standard",
26
+ } as const;
27
+ /** A standing condition: repeats update it quietly; the domain resolves it. */
28
+ const STANDING = {
29
+ condition: PER_KIND,
30
+ supersedes: true,
31
+ attentionOnSupersede: "preserve",
32
+ resolution: RESOLVED_FOR_ALL,
33
+ retention: "standard",
34
+ } as const;
35
+
36
+ /**
37
+ * Every kind's lifecycle. Annotated (not `satisfies`) so a missing kind and an
38
+ * excess key are both compile errors. Values mirror
39
+ * company-semantics-control docs/notification-inbox.md § Per-kind lifecycle.
40
+ * (prettier-ignore keeps the annotation on one line, where it is greppable.)
41
+ */
42
+ // prettier-ignore
43
+ export const NOTIFICATION_LIFECYCLE: Readonly<Record<UserNotificationKind, NotificationLifecycle>> = {
44
+ "companyMd.access_requested": {
45
+ condition: { scope: "fields", fields: ["requestId"] },
46
+ supersedes: false,
47
+ resolution: RESOLVED_FOR_ALL,
48
+ retention: "standard",
49
+ },
50
+ "companyMd.access_request_approved": INDEPENDENT,
51
+ "companyMd.access_request_denied": INDEPENDENT,
52
+ "comment.mention": {
53
+ condition: { scope: "fields", fields: ["subjectId", "subjectType"] },
54
+ supersedes: true,
55
+ attentionOnSupersede: "renew",
56
+ resolution: "never",
57
+ retention: "standard",
58
+ },
59
+ "comment.reply": {
60
+ condition: { scope: "fields", fields: ["subjectId", "subjectType"] },
61
+ supersedes: true,
62
+ attentionOnSupersede: "renew",
63
+ resolution: "never",
64
+ retention: "standard",
65
+ },
66
+ "chat.mention": {
67
+ condition: { scope: "fields", fields: ["chatId"] },
68
+ supersedes: true,
69
+ attentionOnSupersede: "renew",
70
+ resolution: "never",
71
+ retention: "standard",
72
+ },
73
+ "proactive.org_became_shared": INDEPENDENT,
74
+ "proactive.brief_morning": RENEWING,
75
+ "proactive.billing_balance_below_half": STANDING,
76
+ "proactive.billing_balance_below_threshold": STANDING,
77
+ "proactive.billing_balance_exhausted": STANDING,
78
+ "proactive.billing_auto_recharge_failed": STANDING,
79
+ "proactive.billing_payment_past_due": STANDING,
80
+ "proactive.billing_suspended": STANDING,
81
+ "proactive.billing_byo_key_invalid": {
82
+ condition: { scope: "fields", fields: ["configId"] },
83
+ supersedes: true,
84
+ attentionOnSupersede: "preserve",
85
+ resolution: RESOLVED_FOR_ALL,
86
+ retention: "standard",
87
+ },
88
+ "proactive.billing_user_monthly_limit_reached": {
89
+ condition: { scope: "fields", fields: ["userId"] },
90
+ supersedes: true,
91
+ attentionOnSupersede: "renew",
92
+ resolution: "never",
93
+ retention: "standard",
94
+ },
95
+ "proactive.billing_auto_recharge_succeeded": RENEWING,
96
+ "proactive.billing_top_up_credited": RENEWING,
97
+ "proactive.billing_monthly_budget_80": RENEWING,
98
+ "proactive.billing_monthly_budget_100": RENEWING,
99
+ "proactive.billing_provider_price_changed": RENEWING,
100
+ "proactive.billing_payment_recovered": RENEWING,
101
+ "proactive.billing_subscription_canceled": RENEWING,
102
+ "proactive.source_sync_docs_updated": {
103
+ condition: PER_KIND,
104
+ supersedes: true,
105
+ attentionOnSupersede: "preserve",
106
+ resolution: "never",
107
+ retention: "standard",
108
+ },
109
+ "proactive.source_sync_review_ready": {
110
+ condition: { scope: "fields", fields: ["checkpoint"] },
111
+ supersedes: true,
112
+ attentionOnSupersede: "preserve",
113
+ resolution: RESOLVED_FOR_ALL,
114
+ retention: "standard",
115
+ },
116
+ "proactive.org_structure_review_ready": {
117
+ condition: PER_KIND,
118
+ supersedes: true,
119
+ attentionOnSupersede: "renew",
120
+ resolution: RESOLVED_FOR_ALL,
121
+ retention: "standard",
122
+ },
123
+ };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The lifecycle of an inbox row, declared per kind.
3
+ *
4
+ * A notification row is the CURRENT ATTENTION ITEM for one condition, not an
5
+ * event record (control ADR `notification-inbox-is-an-attention-surface`).
6
+ *
7
+ * condition — what the row is about; addresses RESOLUTION. `none` = an
8
+ * independent delivery with no identity.
9
+ * supersedes — a property of the condition: a repeat occurrence updates the
10
+ * active row instead of appending one.
11
+ * attentionOnSupersede — `renew` restores the kind's normal initial attention
12
+ * state (unread, or read when its class is born read);
13
+ * `preserve` keeps the row's current read state.
14
+ * resolution — `never`, or resolved by its producing DOMAIN (`source`) for
15
+ * `all-recipients` or only the `recipient`.
16
+ * retention — `standard`, or `persistent` (longer bounds; declared, never
17
+ * accidental).
18
+ *
19
+ * There is no Archive. See company-semantics-control docs/notification-inbox.md.
20
+ */
21
+
22
+ export const NOTIFICATION_ATTENTION_ON_SUPERSEDE = [
23
+ "renew",
24
+ "preserve",
25
+ ] as const;
26
+ export type NotificationAttentionOnSupersede =
27
+ (typeof NOTIFICATION_ATTENTION_ON_SUPERSEDE)[number];
28
+
29
+ export const NOTIFICATION_RESOLUTION_AUDIENCES = [
30
+ "all-recipients",
31
+ "recipient",
32
+ ] as const;
33
+ export type NotificationResolutionAudience =
34
+ (typeof NOTIFICATION_RESOLUTION_AUDIENCES)[number];
35
+
36
+ export const NOTIFICATION_RETENTION_CLASSES = [
37
+ "standard",
38
+ "persistent",
39
+ ] as const;
40
+ export type NotificationRetention =
41
+ (typeof NOTIFICATION_RETENTION_CLASSES)[number];
42
+
43
+ export type NotificationCondition =
44
+ | { readonly scope: "kind" }
45
+ | {
46
+ readonly scope: "fields";
47
+ readonly fields: readonly [string, ...string[]];
48
+ };
49
+
50
+ export type NotificationResolution =
51
+ | "never"
52
+ | {
53
+ readonly mode: "source";
54
+ readonly audience: NotificationResolutionAudience;
55
+ };
56
+
57
+ type Supersession =
58
+ | { readonly supersedes: false }
59
+ | {
60
+ readonly supersedes: true;
61
+ readonly attentionOnSupersede: NotificationAttentionOnSupersede;
62
+ };
63
+
64
+ export type NotificationLifecycle =
65
+ | {
66
+ /** An independent delivery: no identity, cannot resolve, cannot supersede. */
67
+ readonly condition: "none";
68
+ readonly resolution: "never";
69
+ readonly retention: NotificationRetention;
70
+ }
71
+ | ({
72
+ readonly condition: NotificationCondition;
73
+ readonly resolution: NotificationResolution;
74
+ readonly retention: NotificationRetention;
75
+ } & Supersession);
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { z } from "zod";
8
8
  import { ActionItemSchema, ActionItemTargetSchema } from "../action-items";
9
+ import { FeedSelectorSchema } from "./feed-lifecycle";
9
10
  import { USER_NOTIFICATION_KINDS } from "./kinds";
10
11
 
11
12
  /** Zod mirror of the vocabulary. `./kinds` owns the list; this never restates it. */
@@ -68,9 +69,22 @@ export const UserNotificationSchema = z.object({
68
69
  * longer see" is itself true and useful; it simply does not link anywhere.
69
70
  */
70
71
  href: z.string().nullable(),
72
+ /** When the row was first written — its FIRST occurrence. */
71
73
  createdAt: z.string(),
72
- /** `null` while unread. The only mutable field on the row. */
74
+ /** `null` while unread. Changed by the read/unread verbs and by supersession. */
73
75
  readAt: z.string().nullable(),
76
+ /**
77
+ * The time of the LATEST occurrence this row represents — the ordering and
78
+ * retention clock. Equals `createdAt` until a repeat supersedes into the row.
79
+ */
80
+ activityAt: z.string(),
81
+ /** How many occurrences this row has absorbed by supersession. 1 for a fresh row. */
82
+ occurrenceCount: z.number().int().positive(),
83
+ /**
84
+ * Set when the producing domain resolved the row's condition; `null` while
85
+ * it stands. A resolved row still renders (muted) until retention removes it.
86
+ */
87
+ resolvedAt: z.string().nullable(),
74
88
  });
75
89
  export type UserNotification = z.infer<typeof UserNotificationSchema>;
76
90
 
@@ -103,7 +117,8 @@ export type FeedItem = z.infer<typeof FeedItemSchema>;
103
117
  * `GET /api/me/feed`.
104
118
  *
105
119
  * ORDERING IS INTENTIONALLY GROUPED, NOT CHRONOLOGICALLY INTERLEAVED: every
106
- * `actionable` row first, then `informational` rows newest-first. "Merged feed"
120
+ * `actionable` row first, then `informational` rows — unread-and-unresolved
121
+ * first, then `activityAt` DESC, `id` DESC. "Merged feed"
107
122
  * names one visual surface, not one timeline. Grouping is what keeps "what do I
108
123
  * owe?" answerable at a glance instead of scattered between announcements — and
109
124
  * it is what makes the pagination rule below coherent.
@@ -119,10 +134,16 @@ export const FeedListResponseSchema = z.object({
119
134
  /**
120
135
  * Opaque cursor for the NEXT page of informational rows; `null` at the end.
121
136
  *
122
- * Encodes `(createdAt, id)` — `id` breaks ties so two rows written in the same
123
- * transaction cannot straddle a page boundary and be served twice or skipped.
137
+ * Encodes `(unread, activityAt, id)`, mirroring the ordering above — `id`
138
+ * breaks ties so two rows with the same activity cannot straddle a page
139
+ * boundary and be served twice or skipped. Never parse it.
124
140
  */
125
141
  nextCursor: z.string().nullable(),
142
+ /**
143
+ * The number of notification rows matching the requested view — never action
144
+ * items, never a window. What "Select all in this view" would touch.
145
+ */
146
+ viewCount: z.number().int().nonnegative(),
126
147
  /**
127
148
  * Total unread across ALL retained notifications — NOT within the returned
128
149
  * window.
@@ -136,8 +157,8 @@ export const FeedListResponseSchema = z.object({
136
157
  });
137
158
  export type FeedListResponse = z.infer<typeof FeedListResponseSchema>;
138
159
 
139
- /** `POST /api/me/feed/read` — mark specific informational rows read. */
160
+ /** `POST /api/me/feed/read` — mark the selected informational rows read. */
140
161
  export const FeedMarkReadRequestSchema = z.object({
141
- notificationIds: z.array(z.string()).min(1).max(200),
162
+ selector: FeedSelectorSchema,
142
163
  });
143
164
  export type FeedMarkReadRequest = z.infer<typeof FeedMarkReadRequestSchema>;