@company-semantics/contracts 62.6.0 → 62.8.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": "62.6.0",
3
+ "version": "62.8.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = '52d89a463432' as const;
3
- export const SPEC_HASH_FULL = '52d89a4634328ae80200c221d6aa25d8c34c9d09208a2c460d82a523ec79144d' as const;
2
+ export const SPEC_HASH = '71dd78eccef8' as const;
3
+ export const SPEC_HASH_FULL = '71dd78eccef89c2b48c1ac446802164817d850a8c3a5680a3096956f17e59f08' as const;
@@ -4298,6 +4298,11 @@ export interface components {
4298
4298
  kind: "structure-review";
4299
4299
  /** Format: uuid */
4300
4300
  proposalId: string;
4301
+ } | {
4302
+ /** @constant */
4303
+ kind: "workflow";
4304
+ /** Format: uuid */
4305
+ workflowId: string;
4301
4306
  };
4302
4307
  } | {
4303
4308
  /** @constant */
@@ -4326,6 +4331,39 @@ export interface components {
4326
4331
  kind: "structure-review";
4327
4332
  /** Format: uuid */
4328
4333
  proposalId: string;
4334
+ } | {
4335
+ /** @constant */
4336
+ kind: "workflow";
4337
+ /** Format: uuid */
4338
+ workflowId: string;
4339
+ };
4340
+ } | {
4341
+ /** @constant */
4342
+ trigger: "launch";
4343
+ id?: string;
4344
+ message?: unknown;
4345
+ messageId?: unknown;
4346
+ messages?: unknown;
4347
+ runtimeProfile?: string;
4348
+ pageContext?: {
4349
+ route: string;
4350
+ /** @enum {string} */
4351
+ view: "chat" | "chats" | "company-md" | "execution-detail" | "internal-admin" | "org-settings" | "settings" | "teams" | "teamwork" | "teamwork-member" | "timeline" | "upgrade" | "user-md" | "work";
4352
+ /** @enum {string} */
4353
+ scope: "none" | "me" | "org";
4354
+ entity?: {
4355
+ /** @enum {string} */
4356
+ type: "document" | "person" | "chat" | "execution" | "work-item";
4357
+ id: string;
4358
+ };
4359
+ };
4360
+ /** @enum {string} */
4361
+ origin?: "user" | "proactive";
4362
+ launch: {
4363
+ /** @constant */
4364
+ kind: "workflow";
4365
+ /** Format: uuid */
4366
+ workflowId: string;
4329
4367
  };
4330
4368
  };
4331
4369
  ProactiveChatResolution: {
package/src/index.ts CHANGED
@@ -367,6 +367,17 @@ export type {
367
367
  ProactiveChatMessage,
368
368
  } from "./proactive/index";
369
369
 
370
+ // Analysis workflows — the registered types a durable long-running analysis
371
+ // can be (PRD-00976). The workflow primitive itself is domain-agnostic; its
372
+ // TYPE is the one place a domain names itself, and that name is a public wire
373
+ // discriminator carried on the workflow row, in the data-workflow-progress
374
+ // chat part and by the generic projection read. Namespaced `<domain>/<flow>`
375
+ // so domains cannot collide, and a CLOSED const array so the discriminator is
376
+ // validated and handled exhaustively. See src/workflows/README.md.
377
+ export { ANALYSIS_WORKFLOW_TYPES, isWorkflowType } from "./workflows/index";
378
+
379
+ export type { WorkflowType } from "./workflows/index";
380
+
370
381
  // Comment wire contract — where a thread hangs (the anchor union), what a
371
382
  // thread and its comments look like on the wire, and who a comment may name.
372
383
  // The anchor is the single most load-bearing shape in this package: the backend
@@ -1032,6 +1043,10 @@ export type {
1032
1043
  SuggestedRepliesData,
1033
1044
  SuggestedRepliesPart,
1034
1045
  SuggestedRepliesDataPart,
1046
+ // Workflow progress surface types (non-governed anchor, data-workflow-progress, PRD-00976)
1047
+ WorkflowProgressData,
1048
+ WorkflowProgressPart,
1049
+ WorkflowProgressDataPart,
1035
1050
  // Execution result types (Phase 5)
1036
1051
  ExecutionArtifactStatus,
1037
1052
  ExecutionResultSummary,
@@ -15,6 +15,7 @@ Canonical vocabulary for structured assistant message output. Defines the type s
15
15
  - Execution results carry `state` resolved from ExecutionState (single authority, no redundant status)
16
16
  - Undo creates append-only audit rows — original execution is never mutated
17
17
  - `SuggestedRepliesPart` is a surface but NOT a governed one: a chip fires an ordinary user turn (no `executionId`, no `submitEndpoint`), is stateless (clicking never consumes it) and advisory (the composer stays enabled) — so it is outside the at-most-one-governed-surface-per-turn rule
18
+ - `WorkflowProgressPart` is the second non-governed surface: a two-field ANCHOR (`workflowId`, `workflowType`) to a durable background workflow. It presents no decision, carries no result snapshot (minted at t=0; parts are never rewritten) and no `startedAt` (operational state stays off immutable content), and names no domain vocabulary — live state is hydrated from the generic workflow projection read
18
19
 
19
20
  <!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
20
21
 
@@ -82,6 +83,9 @@ Canonical vocabulary for structured assistant message output. Defines the type s
82
83
  - `UndoResultData` _(type)_ — Undo result data payload.
83
84
  - `UndoResultDataPart` _(type)_ — Undo result data part (wire format).
84
85
  - `WireSurfaceBuilder` — Factory for creating wire-format surface parts.
86
+ - `WorkflowProgressData` _(type)_ — Workflow progress surface data payload — the anchor's handle and its discriminator.
87
+ - `WorkflowProgressDataPart` _(type)_ — Workflow progress data part (wire format, as persisted in `chat_messages.parts`).
88
+ - `WorkflowProgressPart` _(type)_ — Workflow progress message part (semantic type, as rendered).
85
89
  - `addPart` — Add any message part to the builder.
86
90
  - `addSurface` — Add a surface part to the builder.
87
91
  - `addText` — Add a text part to the builder.
@@ -102,6 +106,7 @@ Canonical vocabulary for structured assistant message output. Defines the type s
102
106
 
103
107
  - `execution`
104
108
  - `mcp`
109
+ - `workflows`
105
110
 
106
111
  **External packages:**
107
112
 
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { WireSurfaceBuilder } from "../wire";
3
+ import { isSurfacePart, isTextPart } from "../types";
4
+ import type { AssistantMessagePart, SurfacePart } from "../types";
5
+ import { createPartBuilder, addSurface, addText } from "../builder";
6
+ import type {
7
+ WorkflowProgressData,
8
+ WorkflowProgressPart,
9
+ } from "../workflow-progress";
10
+
11
+ describe("WireSurfaceBuilder.workflowProgress", () => {
12
+ const data: WorkflowProgressData = {
13
+ workflowId: "3f1c2d4e-0000-0000-0000-000000000000",
14
+ workflowType: "org-structure/hris-connect",
15
+ };
16
+
17
+ it("returns data part with type data-workflow-progress", () => {
18
+ const result = WireSurfaceBuilder.workflowProgress(data);
19
+ expect(result.type).toBe("data-workflow-progress");
20
+ });
21
+
22
+ it("data matches input WorkflowProgressData exactly", () => {
23
+ const result = WireSurfaceBuilder.workflowProgress(data);
24
+ expect(result.data).toEqual(data);
25
+ });
26
+
27
+ // The part is an ANCHOR: a handle plus a discriminator and nothing else.
28
+ // Pinning the key set catches the two documented regressions — a `result`
29
+ // snapshot (minted at t=0, it could only ever be empty or stale) or a
30
+ // `startedAt` (operational state duplicated into immutable content) creeping
31
+ // onto a durably persisted, share-visible payload.
32
+ it("workflow-progress carries exactly the anchor keys", () => {
33
+ const result = WireSurfaceBuilder.workflowProgress(data);
34
+ expect(Object.keys(result.data).sort()).toEqual([
35
+ "workflowId",
36
+ "workflowType",
37
+ ]);
38
+ expect(result.data).not.toHaveProperty("result");
39
+ expect(result.data).not.toHaveProperty("startedAt");
40
+ });
41
+
42
+ // Non-governed: nothing on the payload would make it count against the
43
+ // at-most-one-governed-surface-per-turn budget.
44
+ it("carries no governed-surface fields (no executionId, no submitEndpoint)", () => {
45
+ const result = WireSurfaceBuilder.workflowProgress(data);
46
+ expect(result.data).not.toHaveProperty("executionId");
47
+ expect(result.data).not.toHaveProperty("submitEndpoint");
48
+ });
49
+
50
+ // Stateless: building twice from the same facts yields identical output, so
51
+ // a historical message re-rendered later cannot differ from the original.
52
+ it("is deterministic across repeated builds", () => {
53
+ expect(WireSurfaceBuilder.workflowProgress(data)).toEqual(
54
+ WireSurfaceBuilder.workflowProgress(data),
55
+ );
56
+ });
57
+ });
58
+
59
+ // The semantic part is what the app renders after normalizing the wire part.
60
+ // Typing the fixture as WorkflowProgressPart and then assigning it to
61
+ // SurfacePart is the compile-time proof that the union gained the member.
62
+ const anchor: WorkflowProgressPart = {
63
+ type: "workflow-progress",
64
+ data: {
65
+ workflowId: "3f1c2d4e-0000-0000-0000-000000000000",
66
+ workflowType: "org-structure/reanalyse",
67
+ },
68
+ };
69
+
70
+ describe("WorkflowProgressPart in the SurfacePart union", () => {
71
+ it("is assignable to SurfacePart and AssistantMessagePart", () => {
72
+ const asSurface: SurfacePart = anchor;
73
+ const asPart: AssistantMessagePart = anchor;
74
+ expect(asSurface.type).toBe("workflow-progress");
75
+ expect(asPart.type).toBe("workflow-progress");
76
+ });
77
+
78
+ it("is classified as a surface part, not a text part", () => {
79
+ expect(isSurfacePart(anchor)).toBe(true);
80
+ expect(isTextPart(anchor)).toBe(false);
81
+ });
82
+
83
+ // The builder's narrative-before-surface state machine applies to the anchor
84
+ // like any other surface: it is emitted after the prose, never streamed.
85
+ it("is accepted by addSurface after narrative text", () => {
86
+ let state = createPartBuilder();
87
+ state = addText(state, "Analysing your org structure now.").state;
88
+ const result = addSurface(state, anchor);
89
+ expect(result.accepted).toBe(true);
90
+ expect(result.state.parts).toEqual([
91
+ { type: "text", text: "Analysing your org structure now." },
92
+ anchor,
93
+ ]);
94
+ });
95
+ });
@@ -71,6 +71,13 @@ export type {
71
71
  SuggestedRepliesDataPart,
72
72
  } from "./suggested-replies";
73
73
 
74
+ // Workflow progress surface types (non-governed anchor to a durable workflow)
75
+ export type {
76
+ WorkflowProgressData,
77
+ WorkflowProgressPart,
78
+ WorkflowProgressDataPart,
79
+ } from "./workflow-progress";
80
+
74
81
  // Execution result types
75
82
  export type {
76
83
  ExecutionArtifactStatus,
@@ -19,6 +19,7 @@ import type { ConfirmationPart } from "./confirmation";
19
19
  import type { InteractiveTaskPart } from "./interactive";
20
20
  import type { StructureReviewPart } from "./structure-review";
21
21
  import type { SuggestedRepliesPart } from "./suggested-replies";
22
+ import type { WorkflowProgressPart } from "./workflow-progress";
22
23
 
23
24
  // =============================================================================
24
25
  // Narrative Parts (Streamable)
@@ -118,6 +119,10 @@ export interface TablePart {
118
119
  * SuggestedRepliesPart is a surface part but NOT a governed one: a chip fires
119
120
  * an ordinary user turn, so it is outside the preview/confirmation/interactive
120
121
  * triad and its at-most-one-governed-surface-per-turn rule.
122
+ *
123
+ * WorkflowProgressPart is the second non-governed member: it anchors a durable
124
+ * background workflow to the occurrence and presents no decision, so it must
125
+ * not consume the governed-surface budget either.
121
126
  */
122
127
  export type SurfacePart =
123
128
  | ToolListPart
@@ -128,7 +133,8 @@ export type SurfacePart =
128
133
  | PreviewPart
129
134
  | InteractiveTaskPart
130
135
  | StructureReviewPart
131
- | SuggestedRepliesPart;
136
+ | SuggestedRepliesPart
137
+ | WorkflowProgressPart;
132
138
 
133
139
  /**
134
140
  * All assistant message part types.
@@ -25,6 +25,10 @@ import type {
25
25
  SuggestedRepliesData,
26
26
  SuggestedRepliesDataPart,
27
27
  } from "./suggested-replies";
28
+ import type {
29
+ WorkflowProgressData,
30
+ WorkflowProgressDataPart,
31
+ } from "./workflow-progress";
28
32
  import type {
29
33
  ExecutionResultData,
30
34
  ExecutionResultDataPart,
@@ -164,6 +168,31 @@ export const WireSurfaceBuilder = {
164
168
  };
165
169
  },
166
170
 
171
+ /**
172
+ * Build a workflow-progress data part for streaming.
173
+ * The anchor for a durable background workflow attached to this assistant
174
+ * occurrence: a handle plus the namespaced workflow type, hydrated
175
+ * client-side from the generic workflow projection read.
176
+ *
177
+ * INVARIANTS:
178
+ * - Not a governed surface: presents no decision, no executionId, no
179
+ * submitEndpoint — so it does not count against the
180
+ * at-most-one-governed-surface-per-turn rule
181
+ * - Anchor only: no result snapshot (minted at t=0, parts are never
182
+ * rewritten) and no startedAt (operational state stays off immutable
183
+ * content)
184
+ * - Emission is orchestration-driven (a typed launch), never a model choice
185
+ *
186
+ * @param data - Workflow progress data (workflowId + workflowType)
187
+ * @returns Wire-format workflow progress part ready for stream
188
+ */
189
+ workflowProgress(data: WorkflowProgressData): WorkflowProgressDataPart {
190
+ return {
191
+ type: "data-workflow-progress",
192
+ data,
193
+ };
194
+ },
195
+
167
196
  /**
168
197
  * Build an execution result data part for streaming.
169
198
  * Reports the outcome of an executed action.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Workflow Progress Surface Types
3
+ *
4
+ * A durable background workflow attached to an assistant occurrence. The part
5
+ * tells a renderer "there is long-running work behind this message; render its
6
+ * progress surface" — and deliberately says nothing about what that work is
7
+ * analysing. It is a SIBLING of `suggested-replies`: a surface part, but NOT a
8
+ * governed one.
9
+ *
10
+ * WORKFLOW PROGRESS INVARIANTS:
11
+ * - AN ANCHOR, NOT A PAYLOAD. Chat message parts are persisted verbatim and
12
+ * shipped verbatim to token-only share readers, so this carries a handle and
13
+ * a type and nothing else. Live and settled state come from one authorized
14
+ * read keyed on `workflowId` (the generic workflow projection), never from
15
+ * the part.
16
+ * - NO `result`. The part is minted at t=0, before the workflow has produced
17
+ * anything, and `chat_messages.parts` is written once and never rewritten.
18
+ * A result snapshot here would be permanently empty or permanently stale.
19
+ * - NO `startedAt`. It would duplicate operational state into immutable
20
+ * message content, and nothing renders it without hydrating the workflow
21
+ * anyway.
22
+ * - NON-GOVERNED. It presents no decision: no `executionId`, no
23
+ * `submitEndpoint`, no side effect of its own. So it must NOT count against
24
+ * the at-most-one-governed-surface-per-turn budget the chat bridge enforces
25
+ * over the preview/confirmation/interactive triad.
26
+ * - NO DOMAIN VOCABULARY. `workflowType` is the namespaced discriminator from
27
+ * `ANALYSIS_WORKFLOW_TYPES`; the part never names a department, proposal,
28
+ * provider or person. A domain adapter on the consuming side owns that.
29
+ * - Emission is orchestration-driven (a typed launch), never a model choice.
30
+ *
31
+ * Purely additive: an app that predates this part normalizes unknown part
32
+ * types to null, so it drops the anchor and still renders the prose.
33
+ *
34
+ * @see ../workflows/types.ts for the registered workflow types
35
+ * @see decisions/ADR-CONT-026.md for the message-parts design
36
+ */
37
+
38
+ import type { WorkflowType } from "../workflows/types";
39
+
40
+ /**
41
+ * Workflow progress surface data payload — the anchor's handle and its
42
+ * discriminator. Both fields required; nothing else is ever carried.
43
+ */
44
+ export interface WorkflowProgressData {
45
+ /** The durable workflow this occurrence is attached to. */
46
+ workflowId: string;
47
+ /** Which registered flow it is — the namespaced wire discriminator. */
48
+ workflowType: WorkflowType;
49
+ }
50
+
51
+ /**
52
+ * Workflow progress message part (semantic type, as rendered).
53
+ */
54
+ export interface WorkflowProgressPart {
55
+ type: "workflow-progress";
56
+ data: WorkflowProgressData;
57
+ }
58
+
59
+ /**
60
+ * Workflow progress data part (wire format, as persisted in
61
+ * `chat_messages.parts`). Uses AI SDK's data-{name} convention.
62
+ */
63
+ export interface WorkflowProgressDataPart {
64
+ type: "data-workflow-progress";
65
+ data: WorkflowProgressData;
66
+ }
@@ -0,0 +1,35 @@
1
+ # workflows/
2
+
3
+ The registry of analysis-workflow types — the one place a domain names the
4
+ long-running work it hands to the durable workflow primitive.
5
+
6
+ ## Purpose
7
+
8
+ An analysis workflow is a durable, domain-agnostic record of long-running AI
9
+ work (the backend's `analysis_workflows` table). Its lifecycle
10
+ (`pending | running | draining | stopped | complete | failed`) knows nothing
11
+ about departments, proposals or providers. The workflow's TYPE is where a
12
+ domain names itself, and that name travels on the wire: it is stored on the
13
+ workflow row, carried by the `data-workflow-progress` chat message part, and
14
+ answered by the generic projection read. This directory holds that vocabulary
15
+ so every producer, validator and renderer agrees on it.
16
+
17
+ ## Invariants
18
+
19
+ - **Namespaced as `<domain>/<flow>`.** `org-structure/hris-connect` and a
20
+ future `org-deck/ingest` cannot collide, and neither learns the other's
21
+ vocabulary.
22
+ - **Centrally registered.** A second consumer adds one member to
23
+ `ANALYSIS_WORKFLOW_TYPES` and a progress adapter on its own side; it touches
24
+ no generic file. These are public wire discriminators, so the coupling is the
25
+ point — a closed const array gives wire validation and exhaustive handling.
26
+ - **Closed and append-only.** A member is persisted on durable rows and inside
27
+ immutable chat parts; removing one orphans every record that carries it.
28
+ - **Activity-free.** A type says WHICH flow, never what the flow is currently
29
+ doing. Activity is domain progress, carried opaquely by the domain.
30
+ - **`isWorkflowType` accepts exact members only.** A bare `hris-connect`
31
+ without its namespace is rejected.
32
+
33
+ ## Dependencies
34
+
35
+ None. Pure vocabulary data and one pure guard; no `zod`, no runtime imports.
@@ -0,0 +1,30 @@
1
+ # workflows/\_\_tests\_\_/
2
+
3
+ ## Purpose
4
+
5
+ Locks the registry of analysis-workflow types and the guard that validates a
6
+ wire value against it.
7
+
8
+ - `types.test.ts` — pins the exact membership of `ANALYSIS_WORKFLOW_TYPES`, so
9
+ an append or removal is a deliberate diff here rather than a silent change
10
+ to a public wire discriminator. Asserts every member is namespaced as
11
+ `<domain>/<flow>` with exactly one separator, which is the collision guard
12
+ between domains. For `isWorkflowType`, the load-bearing cases are the
13
+ rejections: a bare `hris-connect` without its namespace, the namespace on
14
+ its own, a trailing separator, an unknown flow, and non-string inputs.
15
+
16
+ ## Invariants
17
+
18
+ - These assert VOCABULARY and SHAPE, never behaviour. What a workflow of a
19
+ given type does belongs in the owning domain's suites in backend.
20
+ - The membership test is intentionally exact. Loosening it to "contains" would
21
+ let a removed member (which orphans persisted rows and immutable chat parts)
22
+ pass unnoticed.
23
+
24
+ ## Public API
25
+
26
+ None — test-only.
27
+
28
+ ## Dependencies
29
+
30
+ `vitest` and `../types`.
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { ANALYSIS_WORKFLOW_TYPES, isWorkflowType } from "../types";
3
+
4
+ describe("ANALYSIS_WORKFLOW_TYPES", () => {
5
+ it("contains exactly the two registered org-structure flows", () => {
6
+ expect([...ANALYSIS_WORKFLOW_TYPES]).toEqual([
7
+ "org-structure/hris-connect",
8
+ "org-structure/reanalyse",
9
+ ]);
10
+ });
11
+
12
+ it("has no duplicates", () => {
13
+ expect(new Set(ANALYSIS_WORKFLOW_TYPES).size).toBe(
14
+ ANALYSIS_WORKFLOW_TYPES.length,
15
+ );
16
+ });
17
+
18
+ // Namespacing is the collision guard between domains: every member must be
19
+ // `<domain>/<flow>`, with exactly one separator and no empty half.
20
+ it("every member is namespaced as <domain>/<flow>", () => {
21
+ for (const type of ANALYSIS_WORKFLOW_TYPES) {
22
+ expect(type).toMatch(/^[a-z0-9-]+\/[a-z0-9-]+$/);
23
+ }
24
+ });
25
+ });
26
+
27
+ describe("isWorkflowType", () => {
28
+ it("returns true for every member of ANALYSIS_WORKFLOW_TYPES", () => {
29
+ for (const type of ANALYSIS_WORKFLOW_TYPES) {
30
+ expect(isWorkflowType(type)).toBe(true);
31
+ }
32
+ });
33
+
34
+ it("rejects a bare flow name without its namespace", () => {
35
+ expect(isWorkflowType("hris-connect")).toBe(false);
36
+ expect(isWorkflowType("reanalyse")).toBe(false);
37
+ });
38
+
39
+ it("rejects the namespace on its own and unknown flows", () => {
40
+ expect(isWorkflowType("org-structure")).toBe(false);
41
+ expect(isWorkflowType("org-structure/")).toBe(false);
42
+ expect(isWorkflowType("org-structure/unknown")).toBe(false);
43
+ });
44
+
45
+ it("rejects non-string and empty values", () => {
46
+ expect(isWorkflowType("")).toBe(false);
47
+ expect(isWorkflowType(undefined)).toBe(false);
48
+ expect(isWorkflowType(null)).toBe(false);
49
+ expect(isWorkflowType(42)).toBe(false);
50
+ expect(isWorkflowType({ type: "org-structure/hris-connect" })).toBe(false);
51
+ });
52
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Analysis-workflow vocabulary barrel — the registered types a durable
3
+ * long-running analysis can be.
4
+ *
5
+ * Import from '@company-semantics/contracts' (root).
6
+ *
7
+ * @see ./types.ts for why the list is namespaced, central and closed
8
+ */
9
+
10
+ export type { WorkflowType } from "./types";
11
+
12
+ export { ANALYSIS_WORKFLOW_TYPES, isWorkflowType } from "./types";
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Registered analysis-workflow types.
3
+ *
4
+ * An analysis workflow is a durable record of long-running AI work
5
+ * (`analysis_workflows` in the backend). Its lifecycle is domain-agnostic;
6
+ * its TYPE is the one place a domain names itself, and that name is a public
7
+ * wire discriminator: it is stored on the workflow row, carried by the
8
+ * `data-workflow-progress` chat message part, and returned by the generic
9
+ * projection read. Public wire values are validated and handled exhaustively,
10
+ * which is why the list is a closed const array rather than an open string.
11
+ *
12
+ * INVARIANTS:
13
+ * - NAMESPACED as `<domain>/<flow>`. Two domains cannot collide on a bare
14
+ * name: `org-structure/hris-connect` and a future `org-deck/ingest` can both
15
+ * exist without either learning the other's vocabulary.
16
+ * - CENTRALLY REGISTERED. A new consumer adds one member here and a domain
17
+ * progress adapter on its own side; it touches nothing generic. Central
18
+ * registration is the intended coupling, not an accident — the discriminator
19
+ * must be knowable to every renderer and validator at once.
20
+ * - CLOSED and append-only. A member is a persisted value on durable rows and
21
+ * in immutable chat parts; removing one orphans every record that carries it.
22
+ * - Activity-free. A type says WHICH flow; what the flow is currently doing is
23
+ * domain progress and never lives in this vocabulary.
24
+ */
25
+ export const ANALYSIS_WORKFLOW_TYPES = [
26
+ /** Analyse an org structure freshly connected from an HRIS provider. */
27
+ "org-structure/hris-connect",
28
+ /** Re-run the org-structure analysis against an already-connected source. */
29
+ "org-structure/reanalyse",
30
+ ] as const;
31
+
32
+ /** One registered analysis-workflow type — the wire discriminator. */
33
+ export type WorkflowType = (typeof ANALYSIS_WORKFLOW_TYPES)[number];
34
+
35
+ /**
36
+ * Runtime guard for an untrusted value claiming to be a {@link WorkflowType}.
37
+ * Accepts only exact, namespaced members; a bare flow name is rejected.
38
+ */
39
+ export function isWorkflowType(value: unknown): value is WorkflowType {
40
+ return (
41
+ typeof value === "string" &&
42
+ (ANALYSIS_WORKFLOW_TYPES as readonly string[]).includes(value)
43
+ );
44
+ }