@company-semantics/contracts 91.0.0 → 92.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.
@@ -140,11 +140,6 @@ function candidateSet(overrides: Record<string, unknown> = {}) {
140
140
  filesPendingClassification: 0,
141
141
  },
142
142
  scopes: [],
143
- estimate: {
144
- lowMicroUsd: "250000",
145
- highMicroUsd: "1250000",
146
- capMicroUsd: "2000000",
147
- },
148
143
  discovery: {
149
144
  foundThisRun: 12,
150
145
  retainedDecisions: 3,
@@ -264,38 +259,16 @@ describe("SourceSyncCandidateItemSchema", () => {
264
259
  });
265
260
 
266
261
  describe("SourceSyncCandidateSetResponseSchema", () => {
267
- it("accepts an estimate as integer micro-USD strings and refuses a decimal", () => {
262
+ it("accepts the draft set", () => {
268
263
  expect(
269
264
  SourceSyncCandidateSetResponseSchema.safeParse(candidateSet()).success,
270
265
  ).toBe(true);
271
- const decimal = candidateSet({
272
- estimate: {
273
- lowMicroUsd: "0.25",
274
- highMicroUsd: "1250000",
275
- capMicroUsd: "2000000",
276
- },
277
- });
278
- expect(
279
- SourceSyncCandidateSetResponseSchema.safeParse(decimal).success,
280
- ).toBe(false);
281
- const jsonNumber = candidateSet({
282
- estimate: {
283
- lowMicroUsd: "250000",
284
- highMicroUsd: "1250000",
285
- capMicroUsd: 2000000,
286
- },
287
- });
288
- expect(
289
- SourceSyncCandidateSetResponseSchema.safeParse(jsonNumber).success,
290
- ).toBe(false);
291
266
  });
292
267
 
293
- it("accepts a null estimate before the estimator has run", () => {
294
- expect(
295
- SourceSyncCandidateSetResponseSchema.safeParse(
296
- candidateSet({ estimate: null }),
297
- ).success,
298
- ).toBe(true);
268
+ it("carries no cost estimate: the review authorizes files, never a spend", () => {
269
+ expect(SourceSyncCandidateSetResponseSchema.shape).not.toHaveProperty(
270
+ "estimate",
271
+ );
299
272
  });
300
273
 
301
274
  it("refuses an unknown set state", () => {
@@ -30,9 +30,6 @@ import {
30
30
  import { CandidateDiscoverySummarySchema } from "./relevance";
31
31
  import { SourceSyncCandidateItemSchema } from "./candidate-item";
32
32
 
33
- /** Integer micro-USD as a string — the billing domain's money convention on the wire. */
34
- const MicroUsd = z.string().regex(/^[0-9]+$/);
35
-
36
33
  /** A person as the review names them: a display label, never an id or an address. */
37
34
  const PersonLabel = z.string().min(1).max(255);
38
35
 
@@ -109,14 +106,6 @@ export const SourceSyncCandidateSetResponseSchema = z.object({
109
106
  totals: SourceSyncCandidateTotalsSchema,
110
107
  /** Every scope of the set the caller may see, and where its approval stands. */
111
108
  scopes: z.array(SourceSyncScopeApprovalProjectionSchema).max(500),
112
- /** A RANGE, and the cap that is the promise. Null until the estimator has run. */
113
- estimate: z
114
- .object({
115
- lowMicroUsd: MicroUsd,
116
- highMicroUsd: MicroUsd,
117
- capMicroUsd: MicroUsd,
118
- })
119
- .nullable(),
120
109
  /** The last completed discovery run's counts; null before any run completed. */
121
110
  discovery: z.nullable(CandidateDiscoverySummarySchema),
122
111
  });
@@ -134,13 +123,14 @@ export const SourceSyncAddCandidateRequestSchema = z
134
123
 
135
124
  /**
136
125
  * Approve the set AS REVIEWED. `revision` is the revision the person was looking at; the server
137
- * refuses a stale one, freezes the items, and hashes them PER SCOPE with that scope's share of the cap.
126
+ * refuses a stale one, freezes the items, and hashes them PER SCOPE.
127
+ *
128
+ * An approval authorizes the files, never a spend: there is no cap and no estimate here. Spend is
129
+ * bounded by the org's standing spending controls and a server-derived run ceiling.
138
130
  */
139
131
  export const SourceSyncApproveRequestSchema = z
140
132
  .object({
141
133
  revision: z.number().int().min(0),
142
- /** The promise: synthesis stops at this. Integer micro-USD. */
143
- spendCapMicroUsd: MicroUsd,
144
134
  /** Absent: every scope the caller may approve (all of them for the single v1 approver). */
145
135
  scopeKeys: z.array(z.string().min(1).max(64)).min(1).max(500).optional(),
146
136
  })
@@ -151,7 +141,7 @@ export const SourceSyncScopeApprovalSchema = z.object({
151
141
  /** 'company' or 'unit:<uuid>'. */
152
142
  scopeKey: z.string().min(1).max(64),
153
143
  approvalId: z.string().uuid(),
154
- /** Hash over THIS scope's ordered (source, target, use, observation) tuples and its share of the cap. */
144
+ /** Hash over THIS scope's ordered (source, target, use, observation) tuples. */
155
145
  candidateSetHash: z.string().regex(/^[0-9a-f]{64}$/),
156
146
  });
157
147
 
@@ -285,6 +275,7 @@ export const SOURCE_SYNC_SYNTHESIS_PHASES = [
285
275
  "drafting",
286
276
  "waiting_for_review",
287
277
  "paused_billing",
278
+ "paused_ceiling",
288
279
  "applied",
289
280
  "applied_partial",
290
281
  "failed",
@@ -32,7 +32,8 @@ so every producer, validator and renderer agrees on it.
32
32
  - **Source Sync registers two flows.** `source-sync/discover` (Stage 1)
33
33
  retains nothing; `source-sync/synthesize` (Stage 4) runs only after a person
34
34
  approved the exact set it reads.
35
- - **Every type has a typed outcome** (`outcome.ts`, control ADR-CTRL-540). A
35
+ - **Every type has a typed outcome** (`outcome.ts`, tables in
36
+ `outcome-reasons.ts`; control ADR-CTRL-540). A
36
37
  terminal transition is classified once, by the backend, into a per-type
37
38
  member of `WorkflowOutcome`: a closed `reason`, typed count-only `facts`, a
38
39
  `kind` that is a function of the reason (`*_OUTCOME_KIND_OF_REASON`), and an
@@ -68,11 +69,12 @@ so every producer, validator and renderer agrees on it.
68
69
  ## Public API
69
70
 
70
71
  - `ANALYSIS_WORKFLOW_TYPES`, `WorkflowType`, `isWorkflowType` (`types.ts`).
71
- - `WorkflowOutcomeSchema` / `WorkflowOutcome` and the per-type member schemas,
72
- reason tables, fact schemas and `WorkflowOutcomeDispositionSchema`
73
- (`outcome.ts`).
72
+ - `WorkflowOutcomeSchema` / `WorkflowOutcome`, the per-type member schemas and
73
+ `WorkflowOutcomeDispositionSchema` (`outcome.ts`).
74
+ - `WORKFLOW_OUTCOME_KINDS` and the per-type reason tables and fact schemas
75
+ (`outcome-reasons.ts`).
74
76
  - `DISCOVER_AFFORDANCES_OF_REASON`, `SYNTHESIZE_AFFORDANCES_OF_REASON`,
75
- `ORG_STRUCTURE_AFFORDANCES_OF_REASON` (`outcome.ts`).
77
+ `ORG_STRUCTURE_AFFORDANCES_OF_REASON` (`outcome-reasons.ts`).
76
78
  - `WORKFLOW_AFFORDANCES_OF_REASON`, `affordancesOfReason`,
77
79
  `affordancesOfOutcome` (`affordances-of-outcome.ts`).
78
80
  - `SETTLEMENT_AUDIENCE_POLICIES`, `SETTLEMENT_COVERING_KINDS`,
@@ -6,11 +6,8 @@ import {
6
6
  affordancesOfOutcome,
7
7
  affordancesOfReason,
8
8
  } from "../affordances-of-outcome";
9
- import {
10
- ORG_STRUCTURE_AFFORDANCES_OF_REASON,
11
- WorkflowOutcomeSchema,
12
- type WorkflowOutcome,
13
- } from "../outcome";
9
+ import { WorkflowOutcomeSchema, type WorkflowOutcome } from "../outcome";
10
+ import { ORG_STRUCTURE_AFFORDANCES_OF_REASON } from "../outcome-reasons";
14
11
  import { ANALYSIS_WORKFLOW_TYPES } from "../types";
15
12
 
16
13
  const review = {
@@ -16,6 +16,7 @@ import {
16
16
  toExplainedOutcome,
17
17
  type DeferredRoute,
18
18
  } from "../deferred-route";
19
+ import { WorkflowOutcomeSchema, type WorkflowOutcome } from "../outcome";
19
20
  import {
20
21
  DISCOVER_AFFORDANCES_OF_REASON,
21
22
  DISCOVER_OUTCOME_KIND_OF_REASON,
@@ -23,9 +24,7 @@ import {
23
24
  ORG_STRUCTURE_OUTCOME_KIND_OF_REASON,
24
25
  SYNTHESIZE_AFFORDANCES_OF_REASON,
25
26
  SYNTHESIZE_OUTCOME_KIND_OF_REASON,
26
- WorkflowOutcomeSchema,
27
- type WorkflowOutcome,
28
- } from "../outcome";
27
+ } from "../outcome-reasons";
29
28
 
30
29
  // (kind table, offer table, route table) per outcome family. Built here rather
31
30
  // than exported so the rules stay pinned without widening the public surface.
@@ -5,6 +5,7 @@ import {
5
5
  OfferedAffordanceSchema,
6
6
  } from "../../affordances/affordance";
7
7
  import type { WorkflowType } from "../types";
8
+ import { WorkflowOutcomeSchema, type WorkflowOutcome } from "../outcome";
8
9
  import {
9
10
  DISCOVER_AFFORDANCES_OF_REASON,
10
11
  DISCOVER_OUTCOME_KIND_OF_REASON,
@@ -13,9 +14,7 @@ import {
13
14
  SYNTHESIZE_AFFORDANCES_OF_REASON,
14
15
  SYNTHESIZE_OUTCOME_KIND_OF_REASON,
15
16
  WORKFLOW_OUTCOME_KINDS,
16
- WorkflowOutcomeSchema,
17
- type WorkflowOutcome,
18
- } from "../outcome";
17
+ } from "../outcome-reasons";
19
18
 
20
19
  // Compile-time: every registered workflow type has an outcome member, and the
21
20
  // union names no type that is not registered.
@@ -9,13 +9,12 @@
9
9
  * offered next steps are one list built one way.
10
10
  */
11
11
  import type { Affordance, OfferedAffordance } from "../affordances/affordance";
12
+ import type { WorkflowOutcome, WorkflowOutcomeOf } from "./outcome";
12
13
  import {
13
14
  DISCOVER_AFFORDANCES_OF_REASON,
14
15
  ORG_STRUCTURE_AFFORDANCES_OF_REASON,
15
16
  SYNTHESIZE_AFFORDANCES_OF_REASON,
16
- type WorkflowOutcome,
17
- type WorkflowOutcomeOf,
18
- } from "./outcome";
17
+ } from "./outcome-reasons";
19
18
  import type { WorkflowType } from "./types";
20
19
 
21
20
  /** Every workflow type's offer table, total over the type union. */
@@ -29,16 +29,18 @@
29
29
  import { z } from "zod";
30
30
 
31
31
  import type {
32
- DiscoverOutcomeReason,
33
32
  DiscoverWorkflowOutcome,
34
33
  HrisConnectWorkflowOutcome,
35
- OrgStructureOutcomeReason,
36
34
  ReanalyzeWorkflowOutcome,
37
- SynthesizeOutcomeReason,
38
35
  SynthesizeWorkflowOutcome,
39
36
  WorkflowOutcome,
40
37
  WorkflowOutcomeOf,
41
38
  } from "./outcome";
39
+ import type {
40
+ DiscoverOutcomeReason,
41
+ OrgStructureOutcomeReason,
42
+ SynthesizeOutcomeReason,
43
+ } from "./outcome-reasons";
42
44
  import type { WorkflowType } from "./types";
43
45
 
44
46
  /** Who a routed settlement is addressed to; resolved and frozen by the backend. */
@@ -148,6 +150,8 @@ export const SYNTHESIZE_DEFERRED_ROUTE_OF_REASON = {
148
150
  updates_applied: { route: "covered", by: "sourceSync.docs_updated", fallback: "notice", audience: "creator" },
149
151
  stopped_by_user: STOPPED,
150
152
  paused_billing: EXPLAINED,
153
+ run_ceiling_reached: NOTICE_ADMINS,
154
+ pricing_unconfigured: NOTICE_ADMINS,
151
155
  synthesis_incomplete: NOTICE_ADMINS,
152
156
  approval_mismatch: NOTICE_ADMINS,
153
157
  landing_refused: NOTICE_ADMINS,
@@ -14,35 +14,41 @@ export { ANALYSIS_WORKFLOW_TYPES, isWorkflowType } from "./types";
14
14
  export type {
15
15
  DiscoverOutcomeFacts,
16
16
  DiscoverOutcomeReason,
17
- DiscoverWorkflowOutcome,
18
- HrisConnectWorkflowOutcome,
19
17
  OrgStructureOutcomeFacts,
20
18
  OrgStructureOutcomeReason,
21
- ReanalyzeWorkflowOutcome,
22
19
  SynthesizeOutcomeFacts,
23
20
  SynthesizeOutcomeReason,
24
- SynthesizeWorkflowOutcome,
25
- WorkflowOutcome,
26
- WorkflowOutcomeDisposition,
27
21
  WorkflowOutcomeKind,
28
- WorkflowOutcomeOf,
29
- } from "./outcome";
22
+ } from "./outcome-reasons";
30
23
 
31
24
  export {
32
25
  DISCOVER_AFFORDANCES_OF_REASON,
33
26
  DISCOVER_OUTCOME_KIND_OF_REASON,
34
27
  DiscoverOutcomeFactsSchema,
35
- DiscoverWorkflowOutcomeSchema,
36
- HrisConnectWorkflowOutcomeSchema,
37
28
  ORG_STRUCTURE_AFFORDANCES_OF_REASON,
38
29
  ORG_STRUCTURE_OUTCOME_KIND_OF_REASON,
39
30
  OrgStructureOutcomeFactsSchema,
40
- ReanalyzeWorkflowOutcomeSchema,
41
31
  SYNTHESIZE_AFFORDANCES_OF_REASON,
42
32
  SYNTHESIZE_OUTCOME_KIND_OF_REASON,
43
33
  SynthesizeOutcomeFactsSchema,
44
- SynthesizeWorkflowOutcomeSchema,
45
34
  WORKFLOW_OUTCOME_KINDS,
35
+ } from "./outcome-reasons";
36
+
37
+ export type {
38
+ DiscoverWorkflowOutcome,
39
+ HrisConnectWorkflowOutcome,
40
+ ReanalyzeWorkflowOutcome,
41
+ SynthesizeWorkflowOutcome,
42
+ WorkflowOutcome,
43
+ WorkflowOutcomeDisposition,
44
+ WorkflowOutcomeOf,
45
+ } from "./outcome";
46
+
47
+ export {
48
+ DiscoverWorkflowOutcomeSchema,
49
+ HrisConnectWorkflowOutcomeSchema,
50
+ ReanalyzeWorkflowOutcomeSchema,
51
+ SynthesizeWorkflowOutcomeSchema,
46
52
  WorkflowOutcomeDispositionSchema,
47
53
  WorkflowOutcomeSchema,
48
54
  } from "./outcome";
@@ -0,0 +1,248 @@
1
+ /**
2
+ * The per-type vocabulary of a workflow outcome (control ADR-CTRL-540): the
3
+ * shared `kind` list and, for every workflow type, its closed `reason` union,
4
+ * the `kind` each reason fixes, the affordances each reason offers, and the
5
+ * typed `facts` it reports.
6
+ *
7
+ * Tables only. `./outcome.ts` builds the outcome schemas from them and states
8
+ * the invariants they uphold (per-type and closed, kind and offer as functions
9
+ * of reason, facts are counts).
10
+ *
11
+ * An ending is one row in a `*_OUTCOME_KIND_OF_REASON` table and one in the
12
+ * matching `*_AFFORDANCES_OF_REASON` table, whose `satisfies` refuses a
13
+ * missing row.
14
+ */
15
+ import { z } from "zod";
16
+
17
+ import type { OfferedAffordance } from "../affordances/affordance";
18
+
19
+ /** The shape of an ending, shared by every workflow type. */
20
+ export const WORKFLOW_OUTCOME_KINDS = [
21
+ /** The run produced what it is for. */
22
+ "produced",
23
+ /** The run finished and produced nothing. */
24
+ "empty",
25
+ /** The run finished short of full coverage (a budget or ceiling). */
26
+ "partial",
27
+ /** A person stopped the run; Continue may begin a new execution epoch. */
28
+ "stopped",
29
+ /** The run failed. */
30
+ "failed",
31
+ /** The run went silent and the reconciler closed it. */
32
+ "abandoned",
33
+ /** A terminal transition the classifier did not recognize. */
34
+ "unknown",
35
+ ] as const;
36
+
37
+ export type WorkflowOutcomeKind = (typeof WORKFLOW_OUTCOME_KINDS)[number];
38
+
39
+ const Count = z.number().int().nonnegative();
40
+
41
+ // The affordances an ending may offer, named once for the tables below.
42
+ const FIND_FILES = { id: "sourceSync.findFiles" } as const;
43
+ const CONNECT_GOOGLE_DRIVE = { id: "integrations.connectGoogleDrive" } as const;
44
+ const ENABLE_SOURCE_SYNC = { id: "sourceSync.enable" } as const;
45
+ const OPEN_BILLING = { id: "billing.open" } as const;
46
+ const REANALYZE_STRUCTURE = { id: "orgStructure.reanalyze" } as const;
47
+ const RESYNC_DIRECTORY = { id: "hris.resyncDirectory" } as const;
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // source-sync/discover
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /** Why a Drive discovery run ended the way it did. */
54
+ export const DISCOVER_OUTCOME_KIND_OF_REASON = {
55
+ candidates_found: "produced",
56
+ nothing_found: "empty",
57
+ nothing_relevant: "empty",
58
+ budget_exhausted: "partial",
59
+ file_ceiling: "partial",
60
+ stopped_by_user: "stopped",
61
+ plan_coverage_refused: "failed",
62
+ billing_refused: "failed",
63
+ drive_auth: "failed",
64
+ drive_unavailable: "failed",
65
+ no_connection: "failed",
66
+ source_sync_off: "failed",
67
+ internal_error: "failed",
68
+ abandoned: "abandoned",
69
+ unrecognized_terminal_state: "unknown",
70
+ } as const satisfies Record<string, WorkflowOutcomeKind>;
71
+
72
+ export type DiscoverOutcomeReason =
73
+ keyof typeof DISCOVER_OUTCOME_KIND_OF_REASON;
74
+
75
+ /**
76
+ * What each discovery ending offers (control ADR affordance-system). TOTAL: an
77
+ * empty list is a decision that no grounded action changes that ending — a
78
+ * re-run of the same search is not "broadening" it, and nothing raises a budget.
79
+ */
80
+ export const DISCOVER_AFFORDANCES_OF_REASON = {
81
+ candidates_found: [],
82
+ nothing_found: [],
83
+ nothing_relevant: [],
84
+ budget_exhausted: [],
85
+ file_ceiling: [],
86
+ stopped_by_user: [],
87
+ plan_coverage_refused: [],
88
+ billing_refused: [OPEN_BILLING],
89
+ drive_auth: [CONNECT_GOOGLE_DRIVE],
90
+ drive_unavailable: [FIND_FILES],
91
+ no_connection: [CONNECT_GOOGLE_DRIVE],
92
+ source_sync_off: [ENABLE_SOURCE_SYNC],
93
+ internal_error: [FIND_FILES],
94
+ abandoned: [FIND_FILES],
95
+ unrecognized_terminal_state: [],
96
+ } as const satisfies Record<
97
+ DiscoverOutcomeReason,
98
+ readonly OfferedAffordance[]
99
+ >;
100
+
101
+ /** The counts a discovery run reports at its end. */
102
+ export const DiscoverOutcomeFactsSchema = z
103
+ .object({
104
+ probesTotal: Count,
105
+ probesDone: Count,
106
+ filesSeen: Count,
107
+ filesShown: Count,
108
+ filesHidden: Count,
109
+ filesDropped: Count,
110
+ coveragePercent: z.number().min(0).max(100).nullable(),
111
+ })
112
+ .strict();
113
+
114
+ export type DiscoverOutcomeFacts = z.infer<typeof DiscoverOutcomeFactsSchema>;
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // source-sync/synthesize
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /** Why a synthesis (landing then drafting) run ended the way it did. */
121
+ export const SYNTHESIZE_OUTCOME_KIND_OF_REASON = {
122
+ updates_ready: "produced",
123
+ updates_applied: "produced",
124
+ stopped_by_user: "stopped",
125
+ paused_billing: "failed",
126
+ run_ceiling_reached: "failed",
127
+ pricing_unconfigured: "failed",
128
+ synthesis_incomplete: "failed",
129
+ approval_mismatch: "failed",
130
+ landing_refused: "failed",
131
+ standing_order_refused: "failed",
132
+ no_connection: "failed",
133
+ source_sync_off: "failed",
134
+ internal_error: "failed",
135
+ abandoned: "abandoned",
136
+ unrecognized_terminal_state: "unknown",
137
+ } as const satisfies Record<string, WorkflowOutcomeKind>;
138
+
139
+ export type SynthesizeOutcomeReason =
140
+ keyof typeof SYNTHESIZE_OUTCOME_KIND_OF_REASON;
141
+
142
+ /** What each synthesis ending offers. TOTAL; an empty list is explicit. */
143
+ export const SYNTHESIZE_AFFORDANCES_OF_REASON = {
144
+ updates_ready: [],
145
+ updates_applied: [],
146
+ stopped_by_user: [],
147
+ paused_billing: [OPEN_BILLING],
148
+ run_ceiling_reached: [],
149
+ pricing_unconfigured: [],
150
+ synthesis_incomplete: [],
151
+ approval_mismatch: [],
152
+ landing_refused: [],
153
+ standing_order_refused: [],
154
+ no_connection: [CONNECT_GOOGLE_DRIVE],
155
+ source_sync_off: [ENABLE_SOURCE_SYNC],
156
+ internal_error: [],
157
+ abandoned: [],
158
+ unrecognized_terminal_state: [],
159
+ } as const satisfies Record<
160
+ SynthesizeOutcomeReason,
161
+ readonly OfferedAffordance[]
162
+ >;
163
+
164
+ /** The counts a synthesis run reports at its end. */
165
+ export const SynthesizeOutcomeFactsSchema = z
166
+ .object({
167
+ /** Approved scopes the run read. */
168
+ scopeCount: Count,
169
+ /** Authority or size violations that left the batch incomplete. */
170
+ violationCount: Count,
171
+ })
172
+ .strict();
173
+
174
+ export type SynthesizeOutcomeFacts = z.infer<
175
+ typeof SynthesizeOutcomeFactsSchema
176
+ >;
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // org-structure/hris-connect and org-structure/reanalyze
180
+ // ---------------------------------------------------------------------------
181
+
182
+ /**
183
+ * Why an org-structure analysis ended the way it did. Shared by both
184
+ * org-structure workflow types: they run the same analysis. The directory-only
185
+ * endings (`directory_synced`, `analysis_already_running`, `sync_failed`) occur
186
+ * only on `org-structure/hris-connect`, which syncs the directory first.
187
+ */
188
+ export const ORG_STRUCTURE_OUTCOME_KIND_OF_REASON = {
189
+ proposal_ready: "produced",
190
+ directory_synced: "produced",
191
+ analysis_already_running: "produced",
192
+ proposal_blocked: "failed",
193
+ no_sync_run: "empty",
194
+ no_persons: "empty",
195
+ reporting_not_keyed: "empty",
196
+ stopped_by_user: "stopped",
197
+ sync_failed: "failed",
198
+ snapshot_too_large: "failed",
199
+ model_failed: "failed",
200
+ run_too_large: "failed",
201
+ placement_failed: "failed",
202
+ internal_error: "failed",
203
+ abandoned: "abandoned",
204
+ unrecognized_terminal_state: "unknown",
205
+ } as const satisfies Record<string, WorkflowOutcomeKind>;
206
+
207
+ export type OrgStructureOutcomeReason =
208
+ keyof typeof ORG_STRUCTURE_OUTCOME_KIND_OF_REASON;
209
+
210
+ /**
211
+ * What each org-structure ending offers; shared by both org-structure workflow
212
+ * types, like the kind table. TOTAL; an empty list is explicit.
213
+ */
214
+ export const ORG_STRUCTURE_AFFORDANCES_OF_REASON = {
215
+ proposal_ready: [],
216
+ directory_synced: [],
217
+ analysis_already_running: [],
218
+ proposal_blocked: [],
219
+ no_sync_run: [RESYNC_DIRECTORY],
220
+ no_persons: [],
221
+ reporting_not_keyed: [],
222
+ stopped_by_user: [],
223
+ sync_failed: [RESYNC_DIRECTORY],
224
+ snapshot_too_large: [],
225
+ model_failed: [REANALYZE_STRUCTURE],
226
+ run_too_large: [],
227
+ placement_failed: [REANALYZE_STRUCTURE],
228
+ internal_error: [REANALYZE_STRUCTURE],
229
+ abandoned: [REANALYZE_STRUCTURE],
230
+ unrecognized_terminal_state: [],
231
+ } as const satisfies Record<
232
+ OrgStructureOutcomeReason,
233
+ readonly OfferedAffordance[]
234
+ >;
235
+
236
+ /** The counts a finished org-structure proposal carries. */
237
+ export const OrgStructureOutcomeFactsSchema = z
238
+ .object({
239
+ departmentCount: Count,
240
+ teamCount: Count,
241
+ placedCount: Count,
242
+ reviewItemCount: Count,
243
+ })
244
+ .strict();
245
+
246
+ export type OrgStructureOutcomeFacts = z.infer<
247
+ typeof OrgStructureOutcomeFactsSchema
248
+ >;