@company-semantics/contracts 95.0.0 → 97.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.
- package/package.json +1 -1
- package/src/affordances/README.md +24 -1
- package/src/affordances/__tests__/README.md +6 -0
- package/src/affordances/__tests__/affordances.test.ts +6 -1
- package/src/affordances/__tests__/catalog-wave.test.ts +2 -2
- package/src/affordances/__tests__/workflow-affordances.test.ts +98 -0
- package/src/affordances/affordance.ts +15 -0
- package/src/affordances/args.ts +13 -0
- package/src/affordances/ids.ts +14 -0
- package/src/affordances/index.ts +1 -0
- package/src/api/generated-spec-hash.ts +2 -2
- package/src/api/generated.ts +496 -4
- package/src/generated/openapi-routes.ts +4 -0
- package/src/root-exports/delivery.ts +35 -2
- package/src/workflows/README.md +27 -2
- package/src/workflows/__tests__/README.md +18 -2
- package/src/workflows/__tests__/execution-affordances.test.ts +122 -0
- package/src/workflows/__tests__/projection-fields.test.ts +148 -0
- package/src/workflows/__tests__/types.test.ts +19 -1
- package/src/workflows/execution-affordances.ts +107 -0
- package/src/workflows/index.ts +26 -2
- package/src/workflows/projection-fields.ts +65 -0
- package/src/workflows/types.ts +27 -0
package/src/workflows/README.md
CHANGED
|
@@ -62,13 +62,37 @@ so every producer, validator and renderer agrees on it.
|
|
|
62
62
|
the backend verifies the covering occurrence at route time), and `silent`
|
|
63
63
|
exactly for `stopped_by_user`. A `produced`, `empty` or `partial` ending is
|
|
64
64
|
never explained.
|
|
65
|
+
- **`ANALYSIS_WORKFLOW_STATES` is the backend's six states, in order**
|
|
66
|
+
(`pending`, `running`, `draining`, `stopped`, `complete`, `failed`). A
|
|
67
|
+
workflow id is the logical run; the backend's execution epoch is the attempt.
|
|
68
|
+
- **Execution actions come from one total table** (ADR-CONTRACTS-227).
|
|
69
|
+
`executionAffordancesFor` maps a workflow's state, its type's
|
|
70
|
+
`WorkflowExecutionSupport` and its frozen outcome offer to the `workflow.*`
|
|
71
|
+
affordances: `pending`/`running` → stop; `draining` → nothing; `stopped` →
|
|
72
|
+
continue, runAgain; `failed` → retry, runAgain; `complete` → runAgain. An
|
|
73
|
+
unsupported action is never offered, and `workflow.runAgain` is dropped
|
|
74
|
+
beside a frozen domain relaunch (`DOMAIN_RELAUNCH_AFFORDANCE_IDS`) — one
|
|
75
|
+
relaunch control, the domain's. It never emits `review.open`.
|
|
76
|
+
- **Projection fields, not the projection.** The workflow projection is
|
|
77
|
+
backend-local; `WorkflowExecutionProjectionFieldsSchema` is the one wire
|
|
78
|
+
shape for the fields it composes (`executionAffordances`, `relaunchedFrom`,
|
|
79
|
+
`epochStartedAt`, `epochCompletedAt`), every key required. Contracts does not
|
|
80
|
+
touch the projection's `capabilities` flags.
|
|
65
81
|
- **Workflows never imports proactive.** The covering kinds are literal
|
|
66
82
|
strings pinned against `PROACTIVE_EVENT_KIND_IDS` by a test; the dependency
|
|
67
83
|
runs proactive → workflows only.
|
|
68
84
|
|
|
69
85
|
## Public API
|
|
70
86
|
|
|
71
|
-
- `ANALYSIS_WORKFLOW_TYPES`, `WorkflowType`, `isWorkflowType
|
|
87
|
+
- `ANALYSIS_WORKFLOW_TYPES`, `WorkflowType`, `isWorkflowType`,
|
|
88
|
+
`ANALYSIS_WORKFLOW_STATES`, `AnalysisWorkflowState` (`types.ts`).
|
|
89
|
+
- `WORKFLOW_EXECUTION_ACTIONS`, `WorkflowExecutionAction`,
|
|
90
|
+
`WorkflowExecutionSupport`, `WorkflowExecutionAffordance`,
|
|
91
|
+
`DOMAIN_RELAUNCH_AFFORDANCE_IDS`, `ExecutionAffordancesInput`,
|
|
92
|
+
`executionAffordancesFor` (`execution-affordances.ts`).
|
|
93
|
+
- `WorkflowExecutionAffordanceSchema`,
|
|
94
|
+
`WorkflowExecutionProjectionFieldsSchema` /
|
|
95
|
+
`WorkflowExecutionProjectionFields` (`projection-fields.ts`).
|
|
72
96
|
- `WorkflowOutcomeSchema` / `WorkflowOutcome`, the per-type member schemas and
|
|
73
97
|
`WorkflowOutcomeDispositionSchema` (`outcome.ts`).
|
|
74
98
|
- `WORKFLOW_OUTCOME_KINDS` and the per-type reason tables and fact schemas
|
|
@@ -88,5 +112,6 @@ so every producer, validator and renderer agrees on it.
|
|
|
88
112
|
|
|
89
113
|
`zod` (outcome schemas), `../input-requests/types` (the review disposition's
|
|
90
114
|
request type) and `../affordances/affordance` (the offered-affordance schema,
|
|
91
|
-
its sanity bound and `sameAffordances`)
|
|
115
|
+
its sanity bound and `sameAffordances`) and `../affordances/args` (the
|
|
116
|
+
`workflow.*` args schema). Otherwise pure vocabulary data, one
|
|
92
117
|
guard and pure helpers.
|
|
@@ -11,7 +11,22 @@ wire value against it.
|
|
|
11
11
|
`<domain>/<flow>` with exactly one separator, which is the collision guard
|
|
12
12
|
between domains. For `isWorkflowType`, the load-bearing cases are the
|
|
13
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.
|
|
14
|
+
its own, a trailing separator, an unknown flow, and non-string inputs. Also
|
|
15
|
+
pins `ANALYSIS_WORKFLOW_STATES` as the backend's six states in order.
|
|
16
|
+
|
|
17
|
+
- `execution-affordances.test.ts` — pins `WORKFLOW_EXECUTION_ACTIONS` and
|
|
18
|
+
`DOMAIN_RELAUNCH_AFFORDANCE_IDS`; `executionAffordancesFor` offers the
|
|
19
|
+
per-state table with full support, never offers an unsupported action,
|
|
20
|
+
drops `workflow.runAgain` beside a frozen domain relaunch (one relaunch
|
|
21
|
+
control), never emits `review.open`, and every instance is a valid
|
|
22
|
+
`Affordance` carrying `{ workflowId }`.
|
|
23
|
+
|
|
24
|
+
- `projection-fields.test.ts` — `WorkflowExecutionAffordanceSchema` accepts
|
|
25
|
+
every instance the offer table emits and refuses non-`workflow.*` ids,
|
|
26
|
+
non-uuid `workflowId`s and extra keys; `WorkflowExecutionProjectionFieldsSchema`
|
|
27
|
+
requires every key (also in its JSON Schema), refuses extra keys, more than
|
|
28
|
+
four offers and malformed lineage/timing, and both schemas reach the package
|
|
29
|
+
root.
|
|
15
30
|
|
|
16
31
|
- `outcome.test.ts` — the outcome union covers every registered workflow type
|
|
17
32
|
(compile-time), each reason table's kinds are valid, the schema accepts a
|
|
@@ -51,5 +66,6 @@ None — test-only.
|
|
|
51
66
|
## Dependencies
|
|
52
67
|
|
|
53
68
|
`vitest`, `node:fs`, `../types`, `../outcome`, `../affordances-of-outcome`,
|
|
54
|
-
`../deferred-route`,
|
|
69
|
+
`../execution-affordances`, `../projection-fields`, `../deferred-route`, `../index`,
|
|
70
|
+
`../../index` (root reachability), `../../affordances/affordance` and
|
|
55
71
|
`../../proactive/kinds` (test-only: pins the covering kinds).
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AffordanceSchema,
|
|
5
|
+
type Affordance,
|
|
6
|
+
} from "../../affordances/affordance";
|
|
7
|
+
import { ANALYSIS_WORKFLOW_STATES, type AnalysisWorkflowState } from "../types";
|
|
8
|
+
import {
|
|
9
|
+
DOMAIN_RELAUNCH_AFFORDANCE_IDS,
|
|
10
|
+
WORKFLOW_EXECUTION_ACTIONS,
|
|
11
|
+
executionAffordancesFor,
|
|
12
|
+
type WorkflowExecutionSupport,
|
|
13
|
+
} from "../execution-affordances";
|
|
14
|
+
|
|
15
|
+
const workflowId = "3f1d2c4b-5a6e-4f70-8a91-b2c3d4e5f607";
|
|
16
|
+
|
|
17
|
+
const ALL: WorkflowExecutionSupport = {
|
|
18
|
+
stop: true,
|
|
19
|
+
continue: true,
|
|
20
|
+
retry: true,
|
|
21
|
+
runAgain: true,
|
|
22
|
+
};
|
|
23
|
+
const NONE: WorkflowExecutionSupport = {
|
|
24
|
+
stop: false,
|
|
25
|
+
continue: false,
|
|
26
|
+
retry: false,
|
|
27
|
+
runAgain: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const idsFor = (
|
|
31
|
+
state: AnalysisWorkflowState,
|
|
32
|
+
support: WorkflowExecutionSupport = ALL,
|
|
33
|
+
outcomeAffordances: readonly Affordance[] = [],
|
|
34
|
+
) =>
|
|
35
|
+
executionAffordancesFor({
|
|
36
|
+
workflowId,
|
|
37
|
+
state,
|
|
38
|
+
support,
|
|
39
|
+
outcomeAffordances,
|
|
40
|
+
}).map((a) => a.id);
|
|
41
|
+
|
|
42
|
+
describe("WORKFLOW_EXECUTION_ACTIONS / DOMAIN_RELAUNCH_AFFORDANCE_IDS", () => {
|
|
43
|
+
it("pins the four actions and the two domain relaunches", () => {
|
|
44
|
+
expect([...WORKFLOW_EXECUTION_ACTIONS]).toEqual([
|
|
45
|
+
"stop",
|
|
46
|
+
"continue",
|
|
47
|
+
"retry",
|
|
48
|
+
"runAgain",
|
|
49
|
+
]);
|
|
50
|
+
expect([...DOMAIN_RELAUNCH_AFFORDANCE_IDS]).toEqual([
|
|
51
|
+
"sourceSync.findFiles",
|
|
52
|
+
"orgStructure.reanalyze",
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("executionAffordancesFor", () => {
|
|
58
|
+
it("offers per state with full support", () => {
|
|
59
|
+
expect(idsFor("pending")).toEqual(["workflow.stop"]);
|
|
60
|
+
expect(idsFor("running")).toEqual(["workflow.stop"]);
|
|
61
|
+
expect(idsFor("draining")).toEqual([]);
|
|
62
|
+
expect(idsFor("stopped")).toEqual([
|
|
63
|
+
"workflow.continue",
|
|
64
|
+
"workflow.runAgain",
|
|
65
|
+
]);
|
|
66
|
+
expect(idsFor("failed")).toEqual(["workflow.retry", "workflow.runAgain"]);
|
|
67
|
+
expect(idsFor("complete")).toEqual(["workflow.runAgain"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("a draining run offers nothing", () => {
|
|
71
|
+
expect(idsFor("draining")).toEqual([]);
|
|
72
|
+
expect(
|
|
73
|
+
idsFor("draining", ALL, [
|
|
74
|
+
{ id: "sourceSync.findFiles" },
|
|
75
|
+
{ id: "orgStructure.reanalyze" },
|
|
76
|
+
]),
|
|
77
|
+
).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("never offers an action whose support flag is false", () => {
|
|
81
|
+
for (const state of ANALYSIS_WORKFLOW_STATES) {
|
|
82
|
+
expect(idsFor(state, NONE)).toEqual([]);
|
|
83
|
+
for (const action of WORKFLOW_EXECUTION_ACTIONS) {
|
|
84
|
+
const support = { ...ALL, [action]: false };
|
|
85
|
+
expect(idsFor(state, support)).not.toContain(`workflow.${action}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("run again is withheld when the outcome already offers a domain relaunch", () => {
|
|
91
|
+
expect(idsFor("complete", ALL, [{ id: "sourceSync.findFiles" }])).toEqual(
|
|
92
|
+
[],
|
|
93
|
+
);
|
|
94
|
+
expect(idsFor("failed", ALL, [{ id: "orgStructure.reanalyze" }])).toEqual([
|
|
95
|
+
"workflow.retry",
|
|
96
|
+
]);
|
|
97
|
+
expect(idsFor("stopped", ALL, [{ id: "billing.open" }])).toEqual([
|
|
98
|
+
"workflow.continue",
|
|
99
|
+
"workflow.runAgain",
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("never emits review.open and every instance is schema-valid", () => {
|
|
104
|
+
const review: Affordance = {
|
|
105
|
+
id: "review.open",
|
|
106
|
+
args: { requestType: "x", requestId: "y" },
|
|
107
|
+
} as Affordance;
|
|
108
|
+
for (const state of ANALYSIS_WORKFLOW_STATES) {
|
|
109
|
+
const offered = executionAffordancesFor({
|
|
110
|
+
workflowId,
|
|
111
|
+
state,
|
|
112
|
+
support: ALL,
|
|
113
|
+
outcomeAffordances: [review],
|
|
114
|
+
});
|
|
115
|
+
for (const affordance of offered) {
|
|
116
|
+
expect(affordance.id).not.toBe("review.open");
|
|
117
|
+
expect(affordance.args).toEqual({ workflowId });
|
|
118
|
+
expect(AffordanceSchema.safeParse(affordance).success).toBe(true);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
import * as root from "../../index";
|
|
5
|
+
import * as barrel from "../index";
|
|
6
|
+
import { ANALYSIS_WORKFLOW_STATES } from "../types";
|
|
7
|
+
import {
|
|
8
|
+
executionAffordancesFor,
|
|
9
|
+
type WorkflowExecutionAffordance,
|
|
10
|
+
} from "../execution-affordances";
|
|
11
|
+
import {
|
|
12
|
+
WorkflowExecutionAffordanceSchema,
|
|
13
|
+
WorkflowExecutionProjectionFieldsSchema,
|
|
14
|
+
type WorkflowExecutionProjectionFields,
|
|
15
|
+
} from "../projection-fields";
|
|
16
|
+
|
|
17
|
+
type Equals<A, B> =
|
|
18
|
+
(<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
|
|
19
|
+
? true
|
|
20
|
+
: false;
|
|
21
|
+
|
|
22
|
+
// Compile-time: the schema's member is exactly the offer table's output.
|
|
23
|
+
const memberMatchesOfferTable: Equals<
|
|
24
|
+
z.infer<typeof WorkflowExecutionAffordanceSchema>["id"],
|
|
25
|
+
WorkflowExecutionAffordance["id"]
|
|
26
|
+
> = true;
|
|
27
|
+
void memberMatchesOfferTable;
|
|
28
|
+
|
|
29
|
+
const workflowId = "3f1d2c4b-5a6e-4f70-8a91-b2c3d4e5f607";
|
|
30
|
+
|
|
31
|
+
const valid: WorkflowExecutionProjectionFields = {
|
|
32
|
+
executionAffordances: [
|
|
33
|
+
{ id: "workflow.continue", args: { workflowId } },
|
|
34
|
+
{ id: "workflow.runAgain", args: { workflowId } },
|
|
35
|
+
],
|
|
36
|
+
relaunchedFrom: null,
|
|
37
|
+
epochStartedAt: "2026-10-09T10:00:00.000Z",
|
|
38
|
+
epochCompletedAt: null,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("WorkflowExecutionAffordanceSchema", () => {
|
|
42
|
+
it("accepts every instance the offer table emits", () => {
|
|
43
|
+
const support = { stop: true, continue: true, retry: true, runAgain: true };
|
|
44
|
+
for (const state of ANALYSIS_WORKFLOW_STATES) {
|
|
45
|
+
for (const affordance of executionAffordancesFor({
|
|
46
|
+
workflowId,
|
|
47
|
+
state,
|
|
48
|
+
support,
|
|
49
|
+
outcomeAffordances: [],
|
|
50
|
+
})) {
|
|
51
|
+
expect(
|
|
52
|
+
WorkflowExecutionAffordanceSchema.safeParse(affordance).success,
|
|
53
|
+
).toBe(true);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("refuses non-workflow ids, non-uuid ids and extra keys", () => {
|
|
59
|
+
for (const bad of [
|
|
60
|
+
{ id: "review.open", args: { workflowId } },
|
|
61
|
+
{ id: "sourceSync.findFiles" },
|
|
62
|
+
{ id: "workflow.stop", args: { workflowId: "not-a-uuid" } },
|
|
63
|
+
{ id: "workflow.stop", args: { workflowId, extra: 1 } },
|
|
64
|
+
{ id: "workflow.stop", args: { workflowId }, extra: 1 },
|
|
65
|
+
{ id: "workflow.stop" },
|
|
66
|
+
]) {
|
|
67
|
+
expect(WorkflowExecutionAffordanceSchema.safeParse(bad).success).toBe(
|
|
68
|
+
false,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("WorkflowExecutionProjectionFieldsSchema", () => {
|
|
75
|
+
it("accepts a well-formed value, including a relaunch with settled timing", () => {
|
|
76
|
+
expect(
|
|
77
|
+
WorkflowExecutionProjectionFieldsSchema.safeParse(valid).success,
|
|
78
|
+
).toBe(true);
|
|
79
|
+
expect(
|
|
80
|
+
WorkflowExecutionProjectionFieldsSchema.safeParse({
|
|
81
|
+
executionAffordances: [],
|
|
82
|
+
relaunchedFrom: workflowId,
|
|
83
|
+
epochStartedAt: "2026-10-09T10:00:00.000Z",
|
|
84
|
+
epochCompletedAt: "2026-10-09T10:05:00.000Z",
|
|
85
|
+
}).success,
|
|
86
|
+
).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("requires every key", () => {
|
|
90
|
+
for (const key of Object.keys(valid)) {
|
|
91
|
+
const rest: Record<string, unknown> = { ...valid };
|
|
92
|
+
delete rest[key];
|
|
93
|
+
expect(
|
|
94
|
+
WorkflowExecutionProjectionFieldsSchema.safeParse(rest).success,
|
|
95
|
+
).toBe(false);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("projection fields without executionAffordances are refused", () => {
|
|
100
|
+
const rest: Record<string, unknown> = { ...valid };
|
|
101
|
+
delete rest.executionAffordances;
|
|
102
|
+
expect(
|
|
103
|
+
WorkflowExecutionProjectionFieldsSchema.safeParse(rest).success,
|
|
104
|
+
).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("refuses extra keys, more than four offers and malformed values", () => {
|
|
108
|
+
for (const bad of [
|
|
109
|
+
{ ...valid, capabilities: {} },
|
|
110
|
+
{
|
|
111
|
+
...valid,
|
|
112
|
+
executionAffordances: Array.from({ length: 5 }, () => ({
|
|
113
|
+
id: "workflow.stop",
|
|
114
|
+
args: { workflowId },
|
|
115
|
+
})),
|
|
116
|
+
},
|
|
117
|
+
{ ...valid, relaunchedFrom: "not-a-uuid" },
|
|
118
|
+
{ ...valid, epochStartedAt: "yesterday" },
|
|
119
|
+
{ ...valid, epochCompletedAt: 0 },
|
|
120
|
+
]) {
|
|
121
|
+
expect(
|
|
122
|
+
WorkflowExecutionProjectionFieldsSchema.safeParse(bad).success,
|
|
123
|
+
).toBe(false);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("converts to JSON Schema with every key required", () => {
|
|
128
|
+
const json = z.toJSONSchema(WorkflowExecutionProjectionFieldsSchema, {
|
|
129
|
+
io: "input",
|
|
130
|
+
}) as { required?: string[] };
|
|
131
|
+
expect([...(json.required ?? [])].sort()).toEqual([
|
|
132
|
+
"epochCompletedAt",
|
|
133
|
+
"epochStartedAt",
|
|
134
|
+
"executionAffordances",
|
|
135
|
+
"relaunchedFrom",
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("reaches the package root", () => {
|
|
140
|
+
for (const name of [
|
|
141
|
+
"WorkflowExecutionAffordanceSchema",
|
|
142
|
+
"WorkflowExecutionProjectionFieldsSchema",
|
|
143
|
+
] as const) {
|
|
144
|
+
expect((root as Record<string, unknown>)[name]).toBe(barrel[name]);
|
|
145
|
+
expect(barrel[name]).toBeDefined();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ANALYSIS_WORKFLOW_STATES,
|
|
4
|
+
ANALYSIS_WORKFLOW_TYPES,
|
|
5
|
+
isWorkflowType,
|
|
6
|
+
} from "../types";
|
|
7
|
+
|
|
8
|
+
describe("ANALYSIS_WORKFLOW_STATES", () => {
|
|
9
|
+
// Pins the backend's six states in the backend's order.
|
|
10
|
+
it("names the backend's six lifecycle states in order", () => {
|
|
11
|
+
expect([...ANALYSIS_WORKFLOW_STATES]).toEqual([
|
|
12
|
+
"pending",
|
|
13
|
+
"running",
|
|
14
|
+
"draining",
|
|
15
|
+
"stopped",
|
|
16
|
+
"complete",
|
|
17
|
+
"failed",
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
3
21
|
|
|
4
22
|
describe("ANALYSIS_WORKFLOW_TYPES", () => {
|
|
5
23
|
it("contains exactly the two registered org-structure flows", () => {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workflow execution offer table (control ADR affordance-system;
|
|
3
|
+
* ADR-CONTRACTS-227).
|
|
4
|
+
*
|
|
5
|
+
* `executionAffordancesFor` is the ONE rule deciding which execution actions
|
|
6
|
+
* (`workflow.stop | continue | retry | runAgain`) a workflow offers right now,
|
|
7
|
+
* from its lifecycle state, its type's execution support and the frozen offer
|
|
8
|
+
* of its settled outcome. The backend projection serves its result; the app
|
|
9
|
+
* renders it; neither re-derives it.
|
|
10
|
+
*
|
|
11
|
+
* INVARIANTS:
|
|
12
|
+
* - TOTAL over `AnalysisWorkflowState`: a new state fails to compile until the
|
|
13
|
+
* table decides it.
|
|
14
|
+
* - An action whose support flag is false is never offered.
|
|
15
|
+
* - ONE relaunch control: `workflow.runAgain` is dropped whenever the frozen
|
|
16
|
+
* outcome already offers a domain relaunch (`DOMAIN_RELAUNCH_AFFORDANCE_IDS`).
|
|
17
|
+
* - Never emits `review.open` — viewing results stays the outcome's projection.
|
|
18
|
+
* - Pure; returns a new array.
|
|
19
|
+
*/
|
|
20
|
+
import type { Affordance, OfferedAffordance } from "../affordances/affordance";
|
|
21
|
+
import type { AnalysisWorkflowState } from "./types";
|
|
22
|
+
|
|
23
|
+
/** The execution actions a workflow type may support. */
|
|
24
|
+
export const WORKFLOW_EXECUTION_ACTIONS = [
|
|
25
|
+
/** Stop a pending or running workflow. */
|
|
26
|
+
"stop",
|
|
27
|
+
/** Resume a stopped workflow from where it stopped. */
|
|
28
|
+
"continue",
|
|
29
|
+
/** Re-run a failed workflow's remaining work. */
|
|
30
|
+
"retry",
|
|
31
|
+
/** Launch a fresh run of a settled or stopped workflow. */
|
|
32
|
+
"runAgain",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
/** One execution action. */
|
|
36
|
+
export type WorkflowExecutionAction =
|
|
37
|
+
(typeof WORKFLOW_EXECUTION_ACTIONS)[number];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Which execution actions a workflow type supports. Contracts names the
|
|
41
|
+
* vocabulary; the backend's per-type table holds the values.
|
|
42
|
+
*/
|
|
43
|
+
export type WorkflowExecutionSupport = Readonly<
|
|
44
|
+
Record<WorkflowExecutionAction, boolean>
|
|
45
|
+
>;
|
|
46
|
+
|
|
47
|
+
/** An execution affordance instance — one of the four `workflow.*` ids. */
|
|
48
|
+
export type WorkflowExecutionAffordance = Extract<
|
|
49
|
+
Affordance,
|
|
50
|
+
{ readonly id: `workflow.${WorkflowExecutionAction}` }
|
|
51
|
+
>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Affordances that relaunch a workflow's domain work. When a frozen outcome
|
|
55
|
+
* offers one, it IS the relaunch control and `workflow.runAgain` is dropped.
|
|
56
|
+
*/
|
|
57
|
+
export const DOMAIN_RELAUNCH_AFFORDANCE_IDS = [
|
|
58
|
+
/** A discovery ending's relaunch. */
|
|
59
|
+
"sourceSync.findFiles",
|
|
60
|
+
/** An org-structure ending's relaunch. */
|
|
61
|
+
"orgStructure.reanalyze",
|
|
62
|
+
] as const satisfies readonly OfferedAffordance["id"][];
|
|
63
|
+
|
|
64
|
+
/** The actions each state may offer, in render order, before support filtering. */
|
|
65
|
+
const EXECUTION_ACTIONS_OF_STATE = {
|
|
66
|
+
pending: ["stop"],
|
|
67
|
+
running: ["stop"],
|
|
68
|
+
draining: [],
|
|
69
|
+
stopped: ["continue", "runAgain"],
|
|
70
|
+
failed: ["retry", "runAgain"],
|
|
71
|
+
complete: ["runAgain"],
|
|
72
|
+
} as const satisfies Record<
|
|
73
|
+
AnalysisWorkflowState,
|
|
74
|
+
readonly WorkflowExecutionAction[]
|
|
75
|
+
>;
|
|
76
|
+
|
|
77
|
+
/** Input to {@link executionAffordancesFor}. */
|
|
78
|
+
export type ExecutionAffordancesInput = {
|
|
79
|
+
readonly workflowId: string;
|
|
80
|
+
readonly state: AnalysisWorkflowState;
|
|
81
|
+
readonly support: WorkflowExecutionSupport;
|
|
82
|
+
/** The settled outcome's frozen offer; `[]` before settlement. */
|
|
83
|
+
readonly outcomeAffordances: readonly Affordance[];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** The execution affordances a workflow offers now, in render order. */
|
|
87
|
+
export function executionAffordancesFor({
|
|
88
|
+
workflowId,
|
|
89
|
+
state,
|
|
90
|
+
support,
|
|
91
|
+
outcomeAffordances,
|
|
92
|
+
}: ExecutionAffordancesInput): readonly WorkflowExecutionAffordance[] {
|
|
93
|
+
const domainRelaunchOffered = outcomeAffordances.some((affordance) =>
|
|
94
|
+
(DOMAIN_RELAUNCH_AFFORDANCE_IDS as readonly string[]).includes(
|
|
95
|
+
affordance.id,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
const actions: readonly WorkflowExecutionAction[] =
|
|
99
|
+
EXECUTION_ACTIONS_OF_STATE[state];
|
|
100
|
+
return actions
|
|
101
|
+
.filter((action) => support[action])
|
|
102
|
+
.filter((action) => !(action === "runAgain" && domainRelaunchOffered))
|
|
103
|
+
.map((action) => ({
|
|
104
|
+
id: `workflow.${action}` as const,
|
|
105
|
+
args: { workflowId },
|
|
106
|
+
}));
|
|
107
|
+
}
|
package/src/workflows/index.ts
CHANGED
|
@@ -7,9 +7,33 @@
|
|
|
7
7
|
* @see ./types.ts for why the list is namespaced, central and closed
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export type { WorkflowType } from "./types";
|
|
10
|
+
export type { AnalysisWorkflowState, WorkflowType } from "./types";
|
|
11
11
|
|
|
12
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
ANALYSIS_WORKFLOW_STATES,
|
|
14
|
+
ANALYSIS_WORKFLOW_TYPES,
|
|
15
|
+
isWorkflowType,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
ExecutionAffordancesInput,
|
|
20
|
+
WorkflowExecutionAction,
|
|
21
|
+
WorkflowExecutionAffordance,
|
|
22
|
+
WorkflowExecutionSupport,
|
|
23
|
+
} from "./execution-affordances";
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
DOMAIN_RELAUNCH_AFFORDANCE_IDS,
|
|
27
|
+
WORKFLOW_EXECUTION_ACTIONS,
|
|
28
|
+
executionAffordancesFor,
|
|
29
|
+
} from "./execution-affordances";
|
|
30
|
+
|
|
31
|
+
export type { WorkflowExecutionProjectionFields } from "./projection-fields";
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
WorkflowExecutionAffordanceSchema,
|
|
35
|
+
WorkflowExecutionProjectionFieldsSchema,
|
|
36
|
+
} from "./projection-fields";
|
|
13
37
|
|
|
14
38
|
export type {
|
|
15
39
|
DiscoverOutcomeFacts,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow execution fields of the analysis-workflow projection (control ADR
|
|
3
|
+
* affordance-system; ADR-CONTRACTS-227).
|
|
4
|
+
*
|
|
5
|
+
* The projection itself (`AnalysisWorkflowProjectionResponseSchema`) is
|
|
6
|
+
* backend-local; the backend COMPOSES these fields into it. This module is the
|
|
7
|
+
* ONE location for their schemas, so the execution offer, the relaunch lineage
|
|
8
|
+
* and the per-epoch timing have one wire shape everywhere.
|
|
9
|
+
*
|
|
10
|
+
* INVARIANTS:
|
|
11
|
+
* - Every key is REQUIRED; absence is `null` (or `[]`), never a missing key.
|
|
12
|
+
* - `executionAffordances` carries only the four `workflow.*` instances, at most
|
|
13
|
+
* one per action — `executionAffordancesFor`'s result, never re-derived.
|
|
14
|
+
* - Contracts neither adds nor removes the projection's other keys.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
import { WorkflowExecutionArgsSchema } from "../affordances/args";
|
|
19
|
+
|
|
20
|
+
/** One execution affordance instance — the four `workflow.*` members only. */
|
|
21
|
+
export const WorkflowExecutionAffordanceSchema = z.discriminatedUnion("id", [
|
|
22
|
+
z
|
|
23
|
+
.object({
|
|
24
|
+
id: z.literal("workflow.stop"),
|
|
25
|
+
args: WorkflowExecutionArgsSchema,
|
|
26
|
+
})
|
|
27
|
+
.strict(),
|
|
28
|
+
z
|
|
29
|
+
.object({
|
|
30
|
+
id: z.literal("workflow.continue"),
|
|
31
|
+
args: WorkflowExecutionArgsSchema,
|
|
32
|
+
})
|
|
33
|
+
.strict(),
|
|
34
|
+
z
|
|
35
|
+
.object({
|
|
36
|
+
id: z.literal("workflow.retry"),
|
|
37
|
+
args: WorkflowExecutionArgsSchema,
|
|
38
|
+
})
|
|
39
|
+
.strict(),
|
|
40
|
+
z
|
|
41
|
+
.object({
|
|
42
|
+
id: z.literal("workflow.runAgain"),
|
|
43
|
+
args: WorkflowExecutionArgsSchema,
|
|
44
|
+
})
|
|
45
|
+
.strict(),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/** The projection fields the backend composes into its workflow projection. */
|
|
49
|
+
export const WorkflowExecutionProjectionFieldsSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
/** The execution actions offered now, in render order; one per action. */
|
|
52
|
+
executionAffordances: z.array(WorkflowExecutionAffordanceSchema).max(4),
|
|
53
|
+
/** The workflow this run relaunched, or null for an original run. */
|
|
54
|
+
relaunchedFrom: z.string().uuid().nullable(),
|
|
55
|
+
/** When the current epoch started, or null before it starts. */
|
|
56
|
+
epochStartedAt: z.string().datetime().nullable(),
|
|
57
|
+
/** When the current epoch settled, or null while it is open. */
|
|
58
|
+
epochCompletedAt: z.string().datetime().nullable(),
|
|
59
|
+
})
|
|
60
|
+
.strict();
|
|
61
|
+
|
|
62
|
+
/** The workflow execution projection fields. */
|
|
63
|
+
export type WorkflowExecutionProjectionFields = z.infer<
|
|
64
|
+
typeof WorkflowExecutionProjectionFieldsSchema
|
|
65
|
+
>;
|
package/src/workflows/types.ts
CHANGED
|
@@ -53,3 +53,30 @@ export function isWorkflowType(value: unknown): value is WorkflowType {
|
|
|
53
53
|
(ANALYSIS_WORKFLOW_TYPES as readonly string[]).includes(value)
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The lifecycle states of an analysis workflow — the backend's six, in the
|
|
59
|
+
* backend's order (`analysis_workflows.state`; the projection enumerates the
|
|
60
|
+
* same). Domain-agnostic: every workflow type moves through the same states.
|
|
61
|
+
*
|
|
62
|
+
* INVARIANTS:
|
|
63
|
+
* - CLOSED. Tables keyed by state (`executionAffordancesFor`) are total over
|
|
64
|
+
* this union, so a new state fails to compile until each table decides it.
|
|
65
|
+
*/
|
|
66
|
+
export const ANALYSIS_WORKFLOW_STATES = [
|
|
67
|
+
/** Created, not yet picked up. */
|
|
68
|
+
"pending",
|
|
69
|
+
/** Executing. */
|
|
70
|
+
"running",
|
|
71
|
+
/** A stop was requested; in-flight work is finishing. */
|
|
72
|
+
"draining",
|
|
73
|
+
/** Stopped before completing; the run's progress is kept. */
|
|
74
|
+
"stopped",
|
|
75
|
+
/** Settled successfully. */
|
|
76
|
+
"complete",
|
|
77
|
+
/** Settled with an error. */
|
|
78
|
+
"failed",
|
|
79
|
+
] as const;
|
|
80
|
+
|
|
81
|
+
/** One analysis-workflow lifecycle state. */
|
|
82
|
+
export type AnalysisWorkflowState = (typeof ANALYSIS_WORKFLOW_STATES)[number];
|