@company-semantics/contracts 89.2.0 → 91.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.
Files changed (53) hide show
  1. package/package.json +4 -3
  2. package/src/affordances/README.md +65 -0
  3. package/src/affordances/__tests__/README.md +30 -0
  4. package/src/affordances/__tests__/affordances.test.ts +251 -0
  5. package/src/affordances/affordance.ts +124 -0
  6. package/src/affordances/args.ts +59 -0
  7. package/src/affordances/availability.ts +78 -0
  8. package/src/affordances/ids.ts +45 -0
  9. package/src/affordances/index.ts +39 -0
  10. package/src/api/generated-spec-hash.ts +2 -2
  11. package/src/api/generated.ts +142 -2
  12. package/src/generated/openapi-routes.ts +1 -0
  13. package/src/proactive/README.md +26 -4
  14. package/src/proactive/__tests__/README.md +10 -0
  15. package/src/proactive/__tests__/chat-templates.test.ts +8 -0
  16. package/src/proactive/__tests__/vocabulary.test.ts +11 -0
  17. package/src/proactive/__tests__/workflow-attention-fixtures.ts +49 -0
  18. package/src/proactive/__tests__/workflow-attention.test.ts +146 -0
  19. package/src/proactive/composer.ts +8 -0
  20. package/src/proactive/facts.ts +36 -0
  21. package/src/proactive/index.ts +2 -0
  22. package/src/proactive/kinds.ts +29 -0
  23. package/src/proactive/registry.ts +22 -0
  24. package/src/proactive/surfaces.ts +2 -0
  25. package/src/proactive/templates/README.md +9 -1
  26. package/src/proactive/templates/index.ts +2 -0
  27. package/src/proactive/templates/workflow-needs-attention.ts +35 -0
  28. package/src/root-exports/README.md +1 -0
  29. package/src/root-exports/delivery.ts +69 -1
  30. package/src/source-sync/README.md +95 -5
  31. package/src/source-sync/__tests__/attention-evidence.test.ts +96 -0
  32. package/src/source-sync/__tests__/barrel.test.ts +39 -0
  33. package/src/source-sync/__tests__/enumeration.test.ts +160 -0
  34. package/src/source-sync/__tests__/schemas.test.ts +223 -0
  35. package/src/source-sync/__tests__/vocabulary.test.ts +104 -5
  36. package/src/source-sync/attention-evidence.ts +93 -0
  37. package/src/source-sync/candidate-item.ts +25 -0
  38. package/src/source-sync/enumeration.ts +152 -0
  39. package/src/source-sync/index.ts +35 -1
  40. package/src/source-sync/relevance.ts +85 -11
  41. package/src/source-sync/vocabulary.ts +37 -1
  42. package/src/user-notifications/README.md +8 -0
  43. package/src/user-notifications/kinds.ts +13 -0
  44. package/src/user-notifications/lifecycle-registry.ts +10 -0
  45. package/src/workflows/README.md +38 -2
  46. package/src/workflows/__tests__/README.md +21 -2
  47. package/src/workflows/__tests__/affordances-of-outcome.test.ts +99 -0
  48. package/src/workflows/__tests__/deferred-route.test.ts +168 -0
  49. package/src/workflows/__tests__/outcome.test.ts +66 -0
  50. package/src/workflows/affordances-of-outcome.ts +65 -0
  51. package/src/workflows/deferred-route.ts +240 -0
  52. package/src/workflows/index.ts +34 -0
  53. package/src/workflows/outcome.ts +114 -1
@@ -18,6 +18,7 @@ import { IsoDateTime } from "../api/primitives";
18
18
  import {
19
19
  AUDIENCE_FITS,
20
20
  DOCUMENT_TYPES,
21
+ OPERATING_KNOWLEDGE_FAMILIES,
21
22
  SHARING_BREADTHS,
22
23
  SOURCE_CLASSES,
23
24
  SOURCE_SENSITIVITY_CLASSES,
@@ -34,6 +35,7 @@ import {
34
35
  SOURCE_RELEVANCE_TIERS,
35
36
  TARGET_AUDIENCES,
36
37
  } from "./relevance";
38
+ import { AttentionEvidenceSchema } from "./attention-evidence";
37
39
 
38
40
  /** ONE source proposed for ONE target document. */
39
41
  export const SourceSyncCandidateItemSchema = z
@@ -75,6 +77,22 @@ export const SourceSyncCandidateItemSchema = z
75
77
  documentType: z.enum(DOCUMENT_TYPES).nullable(),
76
78
  /** Confidence of the resolved type, 0..1. NULL exactly when documentType is null or `unknown`. */
77
79
  documentTypeConfidence: z.number().min(0).max(1).nullable(),
80
+ /**
81
+ * WHAT KIND of operating knowledge the line is — a NAME the backend RESOLVES from the
82
+ * resolved document type through its versioned policy table (control ADR
83
+ * index-first-operating-knowledge-discovery), exactly as it resolves sourceClass. The
84
+ * backend's internal relevance-policy families never travel. NULL when the type is
85
+ * null or unknown, or names no operating knowledge (a legal agreement, a transaction,
86
+ * personal material), and on every line of a run that predates index-first discovery.
87
+ */
88
+ operatingKnowledgeFamily: z.enum(OPERATING_KNOWLEDGE_FAMILIES).nullable(),
89
+ /**
90
+ * WHERE the source lives in the provider ("Product/Planning/2026") — the Tier-1
91
+ * canonical path the backend resolves from its location graph once the file reached
92
+ * T1 attention (Tier-0 never retains a name or a path). NULL for a file that never
93
+ * reached attention, on a withheld line, and on a run that predates index-first discovery.
94
+ */
95
+ canonicalPath: z.string().min(1).max(1024).nullable(),
78
96
  periodLabel: z.string().max(64).nullable(),
79
97
  seriesRole: z.enum(["current", "history"]).nullable(),
80
98
  /** The FUSED relevance score (control ADR source-discovery-relevance-funnel), 0..1. */
@@ -106,6 +124,13 @@ export const SourceSyncCandidateItemSchema = z
106
124
  targetAudience: z.enum(TARGET_AUDIENCES).nullable(),
107
125
  /** Which lane earned each model tier's attention (INV-SOURCESYNC-EXPLORATION-RESERVED). */
108
126
  attention: CandidateAttentionSchema,
127
+ /**
128
+ * WHY the line received T1 attention: each signal with its producer and version
129
+ * (INV-SOURCESYNC-ATTENTION-EVIDENCE-HAS-PROVENANCE). NULL for a file that never
130
+ * reached attention, on a withheld line (terms and folder ids identify a source), and
131
+ * on every line of a run that predates index-first discovery.
132
+ */
133
+ attentionEvidence: AttentionEvidenceSchema.nullable(),
109
134
  /**
110
135
  * Whether this line is in the set that approval would land — the SERVER's
111
136
  * answer, never derived from `use`. A decoy, a series' history or a line a
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Index-first discovery's ENUMERATION vocabulary (control ADR
3
+ * index-first-operating-knowledge-discovery; INV-SOURCESYNC-PARTIAL-ENUMERATION-IS-UNKNOWN).
4
+ *
5
+ * A discovery run first enumerates every source reachable through the connection,
6
+ * root by root (the person's own corpus, each shared drive, the domain corpus).
7
+ * How completely it did so is a closed answer that separates OUR ceiling from the
8
+ * PROVIDER's incompleteness, so a partial enumeration can never read as an index of
9
+ * the corpus: only `complete` lets a surface say "full index". Anything else means
10
+ * absence is UNKNOWN, never negative evidence.
11
+ *
12
+ * Stage counters name what each count did: enumerated / metadata-eligible / T0-scored
13
+ * are metadata only; attended / peeked / classified involve a model; shown / retained
14
+ * are outcomes. `filesEnumerated = 100000` with `local_budget_truncated` never reads as
15
+ * "indexed every file".
16
+ */
17
+ import { z } from "zod";
18
+ import { IsoDateTime } from "../api/primitives";
19
+
20
+ /** How completely a run (or one root) was enumerated. */
21
+ export const ENUMERATION_COMPLETENESS = [
22
+ "complete",
23
+ "local_budget_truncated",
24
+ "provider_incomplete",
25
+ "both",
26
+ ] as const;
27
+ export type EnumerationCompleteness = (typeof ENUMERATION_COMPLETENESS)[number];
28
+
29
+ /** Which corpus a root is: the connected person's, one shared drive, or the domain. */
30
+ export const ENUMERATION_ROOT_KINDS = [
31
+ "user",
32
+ "shared_drive",
33
+ "domain",
34
+ ] as const;
35
+ export type EnumerationRootKind = (typeof ENUMERATION_ROOT_KINDS)[number];
36
+
37
+ /**
38
+ * PURE: one completeness for many roots. Our ceiling anywhere and the provider's
39
+ * incompleteness anywhere combine to `both`; only every root complete is `complete`.
40
+ */
41
+ export function combineEnumerationCompleteness(
42
+ statuses: readonly EnumerationCompleteness[],
43
+ ): EnumerationCompleteness {
44
+ const local = statuses.some(
45
+ (s) => s === "local_budget_truncated" || s === "both",
46
+ );
47
+ const provider = statuses.some(
48
+ (s) => s === "provider_incomplete" || s === "both",
49
+ );
50
+ if (local && provider) return "both";
51
+ if (local) return "local_budget_truncated";
52
+ if (provider) return "provider_incomplete";
53
+ return "complete";
54
+ }
55
+
56
+ const MS = (iso: string): number => Date.parse(iso);
57
+
58
+ /** A modified-time range the sampler did not enumerate. */
59
+ export const EnumerationSkippedRangeSchema = z
60
+ .object({ from: IsoDateTime, to: IsoDateTime })
61
+ .refine((r) => MS(r.from) <= MS(r.to), {
62
+ message: "a skipped range ends no earlier than it starts",
63
+ });
64
+
65
+ /** How far one root's enumeration got. Counts, times and provider ids only — never a name. */
66
+ export const RootCoverageSchema = z
67
+ .object({
68
+ rootKind: z.enum(ENUMERATION_ROOT_KINDS),
69
+ /** The shared drive's provider id; null for the user and domain corpora. */
70
+ rootId: z.string().min(1).max(128).nullable(),
71
+ status: z.enum(ENUMERATION_COMPLETENESS),
72
+ enumeratedCount: z.number().int().min(0),
73
+ /** Null exactly when nothing was enumerated under the root. */
74
+ observedOldestModifiedTime: IsoDateTime.nullable(),
75
+ observedNewestModifiedTime: IsoDateTime.nullable(),
76
+ skippedRanges: z.array(EnumerationSkippedRangeSchema).max(16),
77
+ })
78
+ .superRefine((root, ctx) => {
79
+ if ((root.rootId !== null) !== (root.rootKind === "shared_drive")) {
80
+ ctx.addIssue({
81
+ code: "custom",
82
+ message: "a root id names exactly a shared drive",
83
+ });
84
+ }
85
+ if (root.status === "complete" && root.skippedRanges.length > 0) {
86
+ ctx.addIssue({
87
+ code: "custom",
88
+ message: "a complete root skips nothing",
89
+ });
90
+ }
91
+ const empty = root.enumeratedCount === 0;
92
+ const timesAbsent =
93
+ root.observedOldestModifiedTime === null &&
94
+ root.observedNewestModifiedTime === null;
95
+ const timesPresent =
96
+ root.observedOldestModifiedTime !== null &&
97
+ root.observedNewestModifiedTime !== null;
98
+ if (empty ? !timesAbsent : !timesPresent) {
99
+ ctx.addIssue({
100
+ code: "custom",
101
+ message:
102
+ "observed modified times are present exactly when the root enumerated a file",
103
+ });
104
+ }
105
+ if (
106
+ timesPresent &&
107
+ MS(root.observedOldestModifiedTime as string) >
108
+ MS(root.observedNewestModifiedTime as string)
109
+ ) {
110
+ ctx.addIssue({
111
+ code: "custom",
112
+ message: "the oldest observed time is not after the newest",
113
+ });
114
+ }
115
+ });
116
+ export type RootCoverage = z.infer<typeof RootCoverageSchema>;
117
+
118
+ /**
119
+ * A run's stage counters. Each narrows the one before it:
120
+ * enumerated >= metadata-eligible >= T0-scored >= T1-attended >= peeked;
121
+ * T0-scored >= classified; T0-scored >= retained >= shown.
122
+ */
123
+ export const DiscoveryStageCountersSchema = z
124
+ .object({
125
+ filesEnumerated: z.number().int().min(0),
126
+ filesMetadataEligible: z.number().int().min(0),
127
+ filesT0Scored: z.number().int().min(0),
128
+ filesT1Attended: z.number().int().min(0),
129
+ filesPeeked: z.number().int().min(0),
130
+ filesClassified: z.number().int().min(0),
131
+ filesShown: z.number().int().min(0),
132
+ filesRetained: z.number().int().min(0),
133
+ })
134
+ .superRefine((c, ctx) => {
135
+ const narrows =
136
+ c.filesMetadataEligible <= c.filesEnumerated &&
137
+ c.filesT0Scored <= c.filesMetadataEligible &&
138
+ c.filesT1Attended <= c.filesT0Scored &&
139
+ c.filesPeeked <= c.filesT1Attended &&
140
+ c.filesClassified <= c.filesT0Scored &&
141
+ c.filesRetained <= c.filesT0Scored &&
142
+ c.filesShown <= c.filesRetained;
143
+ if (!narrows) {
144
+ ctx.addIssue({
145
+ code: "custom",
146
+ message: "stage counters narrow from enumerated to shown",
147
+ });
148
+ }
149
+ });
150
+ export type DiscoveryStageCounters = z.infer<
151
+ typeof DiscoveryStageCountersSchema
152
+ >;
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * Re-exports the closed Source Sync vocabularies (the sets the backend persists
5
5
  * and mirrors as database CHECK constraints, and the app handles exhaustively)
6
- * including the discovery relevance vocabulary, the discovery and candidate-set response schemas, the candidate-edit and
6
+ * including the discovery relevance vocabulary, the index-first enumeration
7
+ * vocabulary (completeness, root coverage, stage counters), the attention
8
+ * evidence schemas, the discovery and candidate-set response schemas, the candidate-edit and
7
9
  * per-scope approval request and response schemas, the per-document change
8
10
  * plan and apply schemas, the source-resolution rule schemas, the staleness
9
11
  * finding and standing-order schemas, and the Drive-reach status and upgrade
@@ -31,6 +33,7 @@ export type {
31
33
  SourceSyncScopeApproverKind,
32
34
  DocumentType,
33
35
  KnownDocumentType,
36
+ OperatingKnowledgeFamily,
34
37
  } from "./vocabulary";
35
38
 
36
39
  export {
@@ -45,6 +48,7 @@ export {
45
48
  SOURCE_SYNC_SCOPE_APPROVAL_STATES,
46
49
  SOURCE_SYNC_SCOPE_APPROVER_KINDS,
47
50
  DOCUMENT_TYPES,
51
+ OPERATING_KNOWLEDGE_FAMILIES,
48
52
  } from "./vocabulary";
49
53
 
50
54
  export type {
@@ -86,6 +90,36 @@ export {
86
90
  refineCandidateRelevance,
87
91
  } from "./relevance";
88
92
 
93
+ export type {
94
+ EnumerationCompleteness,
95
+ EnumerationRootKind,
96
+ RootCoverage,
97
+ DiscoveryStageCounters,
98
+ } from "./enumeration";
99
+
100
+ export {
101
+ ENUMERATION_COMPLETENESS,
102
+ ENUMERATION_ROOT_KINDS,
103
+ combineEnumerationCompleteness,
104
+ EnumerationSkippedRangeSchema,
105
+ RootCoverageSchema,
106
+ DiscoveryStageCountersSchema,
107
+ } from "./enumeration";
108
+
109
+ export type {
110
+ AttentionSignalKind,
111
+ AttentionSignalSource,
112
+ AttentionSignal,
113
+ AttentionEvidence,
114
+ } from "./attention-evidence";
115
+
116
+ export {
117
+ ATTENTION_SIGNAL_KINDS,
118
+ AttentionSignalSourceSchema,
119
+ AttentionSignalSchema,
120
+ AttentionEvidenceSchema,
121
+ } from "./attention-evidence";
122
+
89
123
  export type {
90
124
  SourceSyncCandidateItem,
91
125
  SourceSyncCandidateItemPatch,
@@ -7,6 +7,12 @@
7
7
  */
8
8
 
9
9
  import { z } from "zod";
10
+ import {
11
+ combineEnumerationCompleteness,
12
+ DiscoveryStageCountersSchema,
13
+ ENUMERATION_COMPLETENESS,
14
+ RootCoverageSchema,
15
+ } from "./enumeration";
10
16
 
11
17
  /** The funnel's verdict. `dropped` is persisted (drop ledger / re-scored observation) and never reaches the wire item. */
12
18
  export const SOURCE_RELEVANCE_TIERS = ["dropped", "hidden", "shown"] as const;
@@ -165,17 +171,50 @@ export type CandidateAttention = z.infer<typeof CandidateAttentionSchema>;
165
171
 
166
172
  /**
167
173
  * The last completed discovery run's counts, so no surface shows one ambiguous total.
168
- * The set response carries it NULL before any run has completed.
174
+ * The set response carries it NULL before any run has completed. The three enumeration
175
+ * fields are all present or all absent: NULL on all three = the run predates index-first
176
+ * discovery (control ADR index-first-operating-knowledge-discovery).
169
177
  */
170
- export const CandidateDiscoverySummarySchema = z.object({
171
- foundThisRun: z.number().int().min(0),
172
- retainedDecisions: z.number().int().min(0),
173
- notRediscovered: z.number().int().min(0),
174
- pendingClassification: z.number().int().min(0),
175
- filesDropped: z.number().int().min(0),
176
- coveragePercent: z.number().int().min(0).max(100),
177
- scanOutcome: z.enum(SCAN_OUTCOMES),
178
- });
178
+ export const CandidateDiscoverySummarySchema = z
179
+ .object({
180
+ foundThisRun: z.number().int().min(0),
181
+ retainedDecisions: z.number().int().min(0),
182
+ notRediscovered: z.number().int().min(0),
183
+ pendingClassification: z.number().int().min(0),
184
+ filesDropped: z.number().int().min(0),
185
+ coveragePercent: z.number().int().min(0).max(100),
186
+ scanOutcome: z.enum(SCAN_OUTCOMES),
187
+ /** How completely the run enumerated; only `complete` may be called a full index. */
188
+ enumerationCompleteness: z.enum(ENUMERATION_COMPLETENESS).nullable(),
189
+ /** One entry per enumerated root. */
190
+ rootCoverage: z.array(RootCoverageSchema).min(1).max(64).nullable(),
191
+ /** What each stage did, by count (filesEnumerated is metadata only — never "indexed"). */
192
+ stageCounters: DiscoveryStageCountersSchema.nullable(),
193
+ })
194
+ .superRefine((s, ctx) => {
195
+ const present = [
196
+ s.enumerationCompleteness,
197
+ s.rootCoverage,
198
+ s.stageCounters,
199
+ ].filter((v) => v !== null).length;
200
+ if (present !== 0 && present !== 3) {
201
+ ctx.addIssue({
202
+ code: "custom",
203
+ message: "enumeration fields are all present or all absent",
204
+ });
205
+ }
206
+ if (
207
+ s.rootCoverage !== null &&
208
+ s.enumerationCompleteness !== null &&
209
+ combineEnumerationCompleteness(s.rootCoverage.map((r) => r.status)) !==
210
+ s.enumerationCompleteness
211
+ ) {
212
+ ctx.addIssue({
213
+ code: "custom",
214
+ message: "enumeration completeness is the combination of its roots",
215
+ });
216
+ }
217
+ });
179
218
  export type CandidateDiscoverySummary = z.infer<
180
219
  typeof CandidateDiscoverySummarySchema
181
220
  >;
@@ -191,6 +230,11 @@ type CandidateRelevanceFacts = {
191
230
  selectionSource: SelectionSource | null;
192
231
  documentType: string | null;
193
232
  documentTypeConfidence: number | null;
233
+ withheld: boolean;
234
+ attention: { t1: AttentionLane | null; t2: AttentionLane | null };
235
+ operatingKnowledgeFamily: string | null;
236
+ canonicalPath: string | null;
237
+ attentionEvidence: readonly unknown[] | null;
194
238
  };
195
239
 
196
240
  /**
@@ -198,7 +242,10 @@ type CandidateRelevanceFacts = {
198
242
  * a hidden line is never included; classification decides whether a recommendation and a
199
243
  * class exist; `selectionSource` is present exactly when a treatment is committed. A
200
244
  * document type exists only on a classified line, and carries a confidence exactly when
201
- * it is a known type (control ADR source-discovery-observation-vs-policy).
245
+ * it is a known type (control ADR source-discovery-observation-vs-policy). An
246
+ * operating-knowledge family needs a known type; attention evidence and a canonical path
247
+ * exist only on a line T1 attended and never on a withheld one (control ADR
248
+ * index-first-operating-knowledge-discovery).
202
249
  */
203
250
  export function refineCandidateRelevance(
204
251
  item: CandidateRelevanceFacts,
@@ -248,4 +295,31 @@ export function refineCandidateRelevance(
248
295
  "a known document type carries a confidence; unknown and absent carry none",
249
296
  });
250
297
  }
298
+ if (item.operatingKnowledgeFamily !== null && !knownType) {
299
+ ctx.addIssue({
300
+ code: "custom",
301
+ message: "an operating-knowledge family needs a known document type",
302
+ });
303
+ }
304
+ if (
305
+ item.withheld &&
306
+ (item.attentionEvidence !== null || item.canonicalPath !== null)
307
+ ) {
308
+ ctx.addIssue({
309
+ code: "custom",
310
+ message: "a withheld line carries no attention evidence and no path",
311
+ });
312
+ }
313
+ if (item.attentionEvidence !== null && item.attention.t1 === null) {
314
+ ctx.addIssue({
315
+ code: "custom",
316
+ message: "attention evidence exists only on a line T1 attended",
317
+ });
318
+ }
319
+ if (item.canonicalPath !== null && item.attention.t1 === null) {
320
+ ctx.addIssue({
321
+ code: "custom",
322
+ message: "a canonical path exists only on a line T1 attended",
323
+ });
324
+ }
251
325
  }
@@ -113,7 +113,9 @@ export type SourceSyncScopeApproverKind =
113
113
 
114
114
  /**
115
115
  * What a source IS — a STRUCTURAL name the model observes, never a relevance verdict
116
- * (control ADR source-discovery-observation-vs-policy). Product policy in the backend maps a
116
+ * (control ADR source-discovery-observation-vs-policy). The set names operating
117
+ * knowledge — charters, playbooks, policies, decision logs, postmortems and the like —
118
+ * beside strategy (control ADR index-first-operating-knowledge-discovery). Product policy in the backend maps a
117
119
  * resolved type to a family, a source class, a sensitivity and a relevance policy; none of
118
120
  * that travels on the wire. A medium is not a meaning: `image_or_screenshot` is graded on
119
121
  * content. `unknown` is a real answer (the model could not tell, or two reads disagreed).
@@ -151,7 +153,41 @@ export const DOCUMENT_TYPES = [
151
153
  "coursework",
152
154
  "empty_template",
153
155
  "image_or_screenshot",
156
+ // Operating knowledge (control ADR index-first-operating-knowledge-discovery):
157
+ "team_charter",
158
+ "process_playbook",
159
+ "policy_handbook",
160
+ "onboarding_guide",
161
+ "values_culture",
162
+ "positioning_messaging",
163
+ "decision_log",
164
+ "postmortem",
165
+ "metrics_definition",
166
+ "operating_cadence",
154
167
  "unknown",
155
168
  ] as const;
156
169
  export type DocumentType = (typeof DOCUMENT_TYPES)[number];
157
170
  export type KnownDocumentType = Exclude<DocumentType, "unknown">;
171
+
172
+ /**
173
+ * WHAT KIND of operating knowledge a source is: what the company is trying to do
174
+ * (direction), how it is organized to do it (operating_model), how it runs
175
+ * (practices), who it is and for whom (identity_market), and what it decided and
176
+ * learned (decisions_learning). Control ADR index-first-operating-knowledge-discovery.
177
+ *
178
+ * A NAME the product derives from the resolved document type — the backend's
179
+ * versioned policy table owns the type -> family mapping, exactly as it owns
180
+ * sourceClass. It is NOT the backend's relevance-policy family (strategy,
181
+ * reference, corporate_record, …), which never travels on the wire. Board and
182
+ * financial material are ordinary members of `direction`; no family is required
183
+ * to exist for a discovery to be valid.
184
+ */
185
+ export const OPERATING_KNOWLEDGE_FAMILIES = [
186
+ "direction",
187
+ "operating_model",
188
+ "practices",
189
+ "identity_market",
190
+ "decisions_learning",
191
+ ] as const;
192
+ export type OperatingKnowledgeFamily =
193
+ (typeof OPERATING_KNOWLEDGE_FAMILIES)[number];
@@ -78,6 +78,14 @@ that renders them alongside action items at the top of `/me/work`.
78
78
  - **`proactive.org_structure_review_ready` is the org structure notice's row.**
79
79
  It arrives UNREAD, because a `notice` has no other surface to hold the badge.
80
80
  The renderer links it to the place the review is opened.
81
+ - **`proactive.workflow_needs_attention` and `proactive.workflow_finished` are
82
+ the workflow settlement rows** (control ADR-CTRL-540 phase 2), the durable
83
+ twins of `workflow.needs_attention` (explained, so born READ) and
84
+ `workflow.finished` (a notice, so UNREAD). A row holds identifiers only — the
85
+ workflow id and type; a finished run's counts render at read time under the
86
+ reader's authority. Both supersede per workflow type (condition field
87
+ `workflowType`, renewing): one active row per workflow type per reader, and a
88
+ newer settlement is news again.
81
89
  - **This is a mention row, not a chat-unread model.** Being named is a discrete
82
90
  fact you come back to; "this chat has new messages" is not modeled here or
83
91
  anywhere, and the proactive `unread` flag on a chat summary stays a one-time
@@ -229,6 +229,19 @@ export const USER_NOTIFICATION_KINDS = [
229
229
  * where the review is opened.
230
230
  */
231
231
  "proactive.org_structure_review_ready",
232
+
233
+ // ===========================================================================
234
+ // Workflow settlements (control ADR-CTRL-540 phase 2)
235
+ // ===========================================================================
236
+ //
237
+ // The durable twins of `workflow.needs_attention` / `workflow.finished`,
238
+ // rendered by `src/proactive/` in the backend, DISTINCT from the kind strings.
239
+ // A row holds identifiers only (the workflow id and type); a `notice` row's
240
+ // counts render at read time under the reader's authority.
241
+ /** A failure a person can act on. Explained: the chat holds the badge, born read. */
242
+ "proactive.workflow_needs_attention",
243
+ /** A finished run nobody watched. A notice: this row IS the attention, unread. */
244
+ "proactive.workflow_finished",
232
245
  ] as const;
233
246
 
234
247
  export type UserNotificationKind = (typeof USER_NOTIFICATION_KINDS)[number];
@@ -32,6 +32,14 @@ const STANDING = {
32
32
  resolution: RESOLVED_FOR_ALL,
33
33
  retention: "standard",
34
34
  } as const;
35
+ /** One active row per workflow type per reader; a newer settlement renews it. */
36
+ const PER_WORKFLOW_TYPE = {
37
+ condition: { scope: "fields", fields: ["workflowType"] },
38
+ supersedes: true,
39
+ attentionOnSupersede: "renew",
40
+ resolution: "never",
41
+ retention: "standard",
42
+ } as const;
35
43
 
36
44
  /**
37
45
  * Every kind's lifecycle. Annotated (not `satisfies`) so a missing kind and an
@@ -120,4 +128,6 @@ export const NOTIFICATION_LIFECYCLE: Readonly<Record<UserNotificationKind, Notif
120
128
  resolution: RESOLVED_FOR_ALL,
121
129
  retention: "standard",
122
130
  },
131
+ "proactive.workflow_needs_attention": PER_WORKFLOW_TYPE,
132
+ "proactive.workflow_finished": PER_WORKFLOW_TYPE,
123
133
  };
@@ -40,6 +40,30 @@ so every producer, validator and renderer agrees on it.
40
40
  adding its outcome member; the union must cover every registered type.
41
41
  - **`unknown` is a safety valve, not a status.** It names an unrecognized
42
42
  terminal transition, never a workflow whose state could not be read.
43
+ - **The offer is a function of reason** (control ADR affordance-system). Every
44
+ outcome carries a REQUIRED `affordances` list — the ordered next steps the
45
+ ending offers — fixed by its reason's per-type `*_AFFORDANCES_OF_REASON`
46
+ table and frozen with the outcome. The tables are total (an empty list is an
47
+ explicit decision) and the schema refuses any other list, a missing key and
48
+ more than `MAX_OFFERED_AFFORDANCES`. Producers read the offer through
49
+ `affordancesOfReason` only.
50
+ - **`review.open` is projected, never stored.** It is derived from a `review`
51
+ disposition; an offer that contains it is refused. `affordancesOfOutcome` is
52
+ the single projection every consumer renders from: the frozen offer, then
53
+ `review.open` when the disposition is `review`.
54
+ - **A deferred settlement's route is a function of reason** (control
55
+ ADR-CTRL-540 phase 2). An ending with no launching conversation takes its
56
+ reason's entry in the total per-type `*_DEFERRED_ROUTE_OF_REASON` table:
57
+ `explained` only for a `failed` or `abandoned` ending that offers at least one
58
+ affordance (told fact-free via `toExplainedOutcome`), `notice` for anything
59
+ whose telling needs counts, `covered` when a domain proactive kind normally
60
+ announces it (always with a `fallback: "notice"` and an audience, because
61
+ the backend verifies the covering occurrence at route time), and `silent`
62
+ exactly for `stopped_by_user`. A `produced`, `empty` or `partial` ending is
63
+ never explained.
64
+ - **Workflows never imports proactive.** The covering kinds are literal
65
+ strings pinned against `PROACTIVE_EVENT_KIND_IDS` by a test; the dependency
66
+ runs proactive → workflows only.
43
67
 
44
68
  ## Public API
45
69
 
@@ -47,8 +71,20 @@ so every producer, validator and renderer agrees on it.
47
71
  - `WorkflowOutcomeSchema` / `WorkflowOutcome` and the per-type member schemas,
48
72
  reason tables, fact schemas and `WorkflowOutcomeDispositionSchema`
49
73
  (`outcome.ts`).
74
+ - `DISCOVER_AFFORDANCES_OF_REASON`, `SYNTHESIZE_AFFORDANCES_OF_REASON`,
75
+ `ORG_STRUCTURE_AFFORDANCES_OF_REASON` (`outcome.ts`).
76
+ - `WORKFLOW_AFFORDANCES_OF_REASON`, `affordancesOfReason`,
77
+ `affordancesOfOutcome` (`affordances-of-outcome.ts`).
78
+ - `SETTLEMENT_AUDIENCE_POLICIES`, `SETTLEMENT_COVERING_KINDS`,
79
+ `SETTLEMENT_SILENT_REASONS`, `DEFERRED_ROUTES`, `DeferredRouteSchema` /
80
+ `DeferredRoute`, the `*_DEFERRED_ROUTE_OF_REASON` tables,
81
+ `WORKFLOW_DEFERRED_ROUTE_OF_REASON`, `ExplainedWorkflowOutcome` and its
82
+ `Explained*Reason` types, `deferredRouteOf`, `toExplainedOutcome`
83
+ (`deferred-route.ts`).
50
84
 
51
85
  ## Dependencies
52
86
 
53
- `zod` (outcome schemas) and `../input-requests/types` (the review
54
- disposition's request type). Otherwise pure vocabulary data and one guard.
87
+ `zod` (outcome schemas), `../input-requests/types` (the review disposition's
88
+ request type) and `../affordances/affordance` (the offered-affordance schema,
89
+ its sanity bound and `sameAffordances`). Otherwise pure vocabulary data, one
90
+ guard and pure helpers.
@@ -17,7 +17,24 @@ wire value against it.
17
17
  (compile-time), each reason table's kinds are valid, the schema accepts a
18
18
  well-formed member and refuses a kind that does not match its reason, a
19
19
  review disposition on a non-produced outcome, a reason from another type,
20
- and extra keys (the facts stay closed).
20
+ and extra keys (the facts stay closed). Every reason has an explicit offer
21
+ of offerable affordances, and the schema refuses an offer that differs from
22
+ its reason's table, a missing `affordances` key, `review.open` inside a
23
+ frozen offer and more than `MAX_OFFERED_AFFORDANCES` entries.
24
+
25
+ - `affordances-of-outcome.test.ts` — `affordancesOfOutcome` projects
26
+ `review.open` after the frozen offer for a `review` disposition and nothing
27
+ extra for `narrate`; `affordancesOfReason` returns each workflow type's
28
+ table entry (both org-structure types share one table); every projected
29
+ list is a valid `Affordance` list.
30
+
31
+ - `deferred-route.test.ts` — every reason of every workflow type has a valid
32
+ deferred route; `explained` only for a `failed`/`abandoned` ending with a
33
+ non-empty offer; `produced`/`empty`/`partial` never explained; `silent` ⇔
34
+ `stopped_by_user`; every covering kind is a real proactive kind and every
35
+ covered route falls back to a notice with an audience; `toExplainedOutcome`
36
+ strips facts and stays schema-valid; and no `src/workflows/*.ts` file
37
+ imports from proactive.
21
38
 
22
39
  ## Invariants
23
40
 
@@ -33,4 +50,6 @@ None — test-only.
33
50
 
34
51
  ## Dependencies
35
52
 
36
- `vitest` and `../types`.
53
+ `vitest`, `node:fs`, `../types`, `../outcome`, `../affordances-of-outcome`,
54
+ `../deferred-route`, `../../affordances/affordance` and
55
+ `../../proactive/kinds` (test-only: pins the covering kinds).