@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 +1 -1
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +38 -0
- package/src/index.ts +15 -0
- package/src/message-parts/README.md +5 -0
- package/src/message-parts/__tests__/workflow-progress.test.ts +95 -0
- package/src/message-parts/index.ts +7 -0
- package/src/message-parts/types.ts +7 -1
- package/src/message-parts/wire.ts +29 -0
- package/src/message-parts/workflow-progress.ts +66 -0
- package/src/workflows/README.md +35 -0
- package/src/workflows/__tests__/README.md +30 -0
- package/src/workflows/__tests__/types.test.ts +52 -0
- package/src/workflows/index.ts +12 -0
- package/src/workflows/types.ts +44 -0
package/package.json
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
|
|
2
|
-
export const SPEC_HASH = '
|
|
3
|
-
export const SPEC_HASH_FULL = '
|
|
2
|
+
export const SPEC_HASH = '71dd78eccef8' as const;
|
|
3
|
+
export const SPEC_HASH_FULL = '71dd78eccef89c2b48c1ac446802164817d850a8c3a5680a3096956f17e59f08' as const;
|
package/src/api/generated.ts
CHANGED
|
@@ -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
|
+
}
|