@cynodia/axiom-core 0.14.0-alpha.1 → 0.14.0-alpha.3
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/dist/semantic-identity.d.ts +18 -0
- package/dist/semantic-identity.js +19 -2
- package/dist/validate.js +3 -2
- package/dist/workflows.d.ts +49 -2
- package/dist/workflows.js +269 -7
- package/package.json +1 -1
|
@@ -33,6 +33,14 @@ import type { ApplicationGraph } from './graph.js';
|
|
|
33
33
|
* - `StorageDef` — `readAuthorization` / `uploadAuthorization` expressions, `retry`.
|
|
34
34
|
* - `RelationshipDef` — endpoints and cardinality (also in the schema fingerprint; repeated
|
|
35
35
|
* here so a semantic-only comparison is self-contained).
|
|
36
|
+
* - `WorkflowDef` — `inputs`, `bindings`, `entry`, and every step's kind, control-flow edges
|
|
37
|
+
* and step-specific executable semantics (the `ActionDef` / `EventDef` a step targets, an
|
|
38
|
+
* `action` step's argument expressions / `retry` policy, a `wait-event` step's correlation
|
|
39
|
+
* `where` / `bind` / `timeout`, a `timer` step's `after` / `at`, a `branch` step's `when`
|
|
40
|
+
* and edges, `complete` / `fail` output/error expressions). A workflow's referenced
|
|
41
|
+
* `ActionDef` / `EventDef` bodies are covered transitively — they are their own executable
|
|
42
|
+
* nodes in this same projection (spec14pt3 §5, §7, §30-§33). Presentation-only fields
|
|
43
|
+
* (`name` / `description` / `label`) are stripped exactly as elsewhere.
|
|
36
44
|
* - `graph.schemaVersion`.
|
|
37
45
|
*
|
|
38
46
|
* ### Exclusions (what does NOT change it)
|
|
@@ -49,6 +57,16 @@ import type { ApplicationGraph } from './graph.js';
|
|
|
49
57
|
*/
|
|
50
58
|
/** The projection algorithm's own version, mixed into the hash (spec12 §46). */
|
|
51
59
|
export declare const SEMANTIC_FINGERPRINT_VERSION = 1;
|
|
60
|
+
/**
|
|
61
|
+
* Every graph node kind that carries executable meaning — the single source of truth for
|
|
62
|
+
* "which graph changes alter executable semantic meaning" (spec14pt3 §5 G1). Both the
|
|
63
|
+
* graph-level {@link semanticFingerprint} and the ServerIR-side authority-compatibility
|
|
64
|
+
* fingerprint MUST derive from this same list; a `packages/server` test pins that they do,
|
|
65
|
+
* so a future primitive cannot be added to one and silently omitted from the other
|
|
66
|
+
* (spec14pt3 §189, §190 — the deeper architectural correction behind Phase 22 F3).
|
|
67
|
+
*/
|
|
68
|
+
export declare const EXECUTABLE_KINDS: readonly ["action", "integration", "integration-operation", "trigger", "event", "subscription", "read-policy", "query", "expression", "constraint", "transition-constraint", "storage", "relationship", "workflow"];
|
|
69
|
+
export type ExecutableKind = (typeof EXECUTABLE_KINDS)[number];
|
|
52
70
|
export interface SemanticProjection {
|
|
53
71
|
fingerprintVersion: number;
|
|
54
72
|
schemaVersion: number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { canonicalJSON } from './schema-identity.js';
|
|
3
3
|
import { AUTHORING_METADATA_KEY } from './authoring-metadata.js';
|
|
4
|
+
import { canonicalWorkflowForFingerprint } from './workflows.js';
|
|
4
5
|
/**
|
|
5
6
|
* Application **semantic** identity (spec12 §45, §46).
|
|
6
7
|
*
|
|
@@ -35,6 +36,14 @@ import { AUTHORING_METADATA_KEY } from './authoring-metadata.js';
|
|
|
35
36
|
* - `StorageDef` — `readAuthorization` / `uploadAuthorization` expressions, `retry`.
|
|
36
37
|
* - `RelationshipDef` — endpoints and cardinality (also in the schema fingerprint; repeated
|
|
37
38
|
* here so a semantic-only comparison is self-contained).
|
|
39
|
+
* - `WorkflowDef` — `inputs`, `bindings`, `entry`, and every step's kind, control-flow edges
|
|
40
|
+
* and step-specific executable semantics (the `ActionDef` / `EventDef` a step targets, an
|
|
41
|
+
* `action` step's argument expressions / `retry` policy, a `wait-event` step's correlation
|
|
42
|
+
* `where` / `bind` / `timeout`, a `timer` step's `after` / `at`, a `branch` step's `when`
|
|
43
|
+
* and edges, `complete` / `fail` output/error expressions). A workflow's referenced
|
|
44
|
+
* `ActionDef` / `EventDef` bodies are covered transitively — they are their own executable
|
|
45
|
+
* nodes in this same projection (spec14pt3 §5, §7, §30-§33). Presentation-only fields
|
|
46
|
+
* (`name` / `description` / `label`) are stripped exactly as elsewhere.
|
|
38
47
|
* - `graph.schemaVersion`.
|
|
39
48
|
*
|
|
40
49
|
* ### Exclusions (what does NOT change it)
|
|
@@ -51,7 +60,15 @@ import { AUTHORING_METADATA_KEY } from './authoring-metadata.js';
|
|
|
51
60
|
*/
|
|
52
61
|
/** The projection algorithm's own version, mixed into the hash (spec12 §46). */
|
|
53
62
|
export const SEMANTIC_FINGERPRINT_VERSION = 1;
|
|
54
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Every graph node kind that carries executable meaning — the single source of truth for
|
|
65
|
+
* "which graph changes alter executable semantic meaning" (spec14pt3 §5 G1). Both the
|
|
66
|
+
* graph-level {@link semanticFingerprint} and the ServerIR-side authority-compatibility
|
|
67
|
+
* fingerprint MUST derive from this same list; a `packages/server` test pins that they do,
|
|
68
|
+
* so a future primitive cannot be added to one and silently omitted from the other
|
|
69
|
+
* (spec14pt3 §189, §190 — the deeper architectural correction behind Phase 22 F3).
|
|
70
|
+
*/
|
|
71
|
+
export const EXECUTABLE_KINDS = [
|
|
55
72
|
'action',
|
|
56
73
|
'integration',
|
|
57
74
|
'integration-operation',
|
|
@@ -96,7 +113,7 @@ export function semanticProjection(graph) {
|
|
|
96
113
|
if (group.length === 0) {
|
|
97
114
|
continue;
|
|
98
115
|
}
|
|
99
|
-
nodes[kind] = byId(group).map((node) => stripNonSemantic(node));
|
|
116
|
+
nodes[kind] = byId(group).map((node) => stripNonSemantic(kind === 'workflow' ? canonicalWorkflowForFingerprint(node) : node));
|
|
100
117
|
}
|
|
101
118
|
return {
|
|
102
119
|
fingerprintVersion: SEMANTIC_FINGERPRINT_VERSION,
|
package/dist/validate.js
CHANGED
|
@@ -1052,10 +1052,11 @@ function validateWorkflow(workflow, context) {
|
|
|
1052
1052
|
// Reachability + acyclicity + terminal reachability.
|
|
1053
1053
|
const reachable = workflowReachableSteps(workflow);
|
|
1054
1054
|
for (const step of steps) {
|
|
1055
|
-
|
|
1055
|
+
const sid = step && typeof step === 'object' ? step.id : undefined;
|
|
1056
|
+
if (typeof sid === 'string' && !reachable.has(sid)) {
|
|
1056
1057
|
context.errors.push({
|
|
1057
1058
|
code: VALIDATION_CODES.workflowUnreachableStep,
|
|
1058
|
-
message: `Workflow ${workflow.id} step ${
|
|
1059
|
+
message: `Workflow ${workflow.id} step ${sid} is unreachable from entry`,
|
|
1059
1060
|
nodeId: workflow.id,
|
|
1060
1061
|
});
|
|
1061
1062
|
}
|
package/dist/workflows.d.ts
CHANGED
|
@@ -109,11 +109,18 @@ export interface WorkflowDef extends NodeBase {
|
|
|
109
109
|
entry: NodeId;
|
|
110
110
|
steps: WorkflowStep[];
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Whether a value is a structurally recognizable workflow step — an object with an `id` and
|
|
114
|
+
* one of the six {@link WORKFLOW_STEP_TYPES}. The accessors below are total over *any* input
|
|
115
|
+
* (a malformed graph, a hand-tampered Server IR) precisely so a bad step produces a
|
|
116
|
+
* structured diagnostic rather than a native `TypeError` (spec14pt3 F1 / F2 §39-§48).
|
|
117
|
+
*/
|
|
118
|
+
export declare function isWorkflowStep(step: unknown): step is WorkflowStep;
|
|
112
119
|
export declare function workflowStepById(workflow: WorkflowDef, stepId: NodeId | string): WorkflowStep | undefined;
|
|
113
|
-
/** Every step id a step can hand control to (control-flow successors). */
|
|
120
|
+
/** Every step id a step can hand control to (control-flow successors). Total over bad input. */
|
|
114
121
|
export declare function workflowStepSuccessors(step: WorkflowStep): NodeId[];
|
|
115
122
|
export declare function workflowIsTerminalStep(step: WorkflowStep): boolean;
|
|
116
|
-
/** Every `Expression` embedded in a step — the leaves dependency / scope analysis walks. */
|
|
123
|
+
/** Every `Expression` embedded in a step — the leaves dependency / scope analysis walks. Total over bad input. */
|
|
117
124
|
export declare function workflowStepExpressions(step: WorkflowStep): Expression[];
|
|
118
125
|
/** Every `Expression` in a workflow — inputs carry none, so this is the step leaves. */
|
|
119
126
|
export declare function workflowExpressions(workflow: WorkflowDef): Expression[];
|
|
@@ -121,6 +128,46 @@ export declare function workflowExpressions(workflow: WorkflowDef): Expression[]
|
|
|
121
128
|
export declare function workflowActionIds(workflow: WorkflowDef): NodeId[];
|
|
122
129
|
/** The `EventDef` ids a workflow waits on. */
|
|
123
130
|
export declare function workflowEventIds(workflow: WorkflowDef): NodeId[];
|
|
131
|
+
/**
|
|
132
|
+
* A `WorkflowDef` reduced to a form where **authoring order is not semantic** (spec14pt3
|
|
133
|
+
* §19, §64): `steps`, `inputs` and `bindings` are ordered by id. Control flow is by explicit
|
|
134
|
+
* `entry` / `next` / `then` / `else` / `onError` / `onTimeout` edges, never by array
|
|
135
|
+
* position, so two workflows that differ only in the order their steps were declared are the
|
|
136
|
+
* same executable meaning. Both the graph-level `semanticFingerprint` and the ServerIR-side
|
|
137
|
+
* authority-compatibility fingerprint pass workflows through this before hashing, so they
|
|
138
|
+
* agree (spec14pt3 §5 G1). Non-`id` fields are untouched; human metadata is stripped by the
|
|
139
|
+
* caller's projection exactly as elsewhere.
|
|
140
|
+
*/
|
|
141
|
+
export declare function canonicalWorkflowForFingerprint<T extends Partial<WorkflowDef>>(workflow: T): T;
|
|
142
|
+
export interface WorkflowStructuralProblem {
|
|
143
|
+
code: 'WORKFLOW_INVALID_STEP' | 'WORKFLOW_ENTRY_NOT_FOUND' | 'WORKFLOW_STEP_NOT_FOUND' | 'WORKFLOW_INVALID_TIMER' | 'WORKFLOW_BINDING_NOT_FOUND' | 'WORKFLOW_EXPRESSION_SCOPE' | 'WORKFLOW_NONDETERMINISTIC';
|
|
144
|
+
message: string;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The complete **runtime-boundary** admission check on a `WorkflowDef` — total over *any*
|
|
148
|
+
* value, including a hand-tampered `ServerIR` where `workflows`, `steps`, `inputs` or
|
|
149
|
+
* `bindings` are the wrong shape entirely (spec14pt3 §44-§49, spec14pt4 §11-§19, §31).
|
|
150
|
+
* `validateGraph` remains the authoring-time authority; this is what the authoritative
|
|
151
|
+
* runtime runs on a `ServerIR` it did not compile itself, so a structurally invalid or
|
|
152
|
+
* referentially inconsistent workflow fails closed with a structured result instead of
|
|
153
|
+
* reaching a native exception, a silently dropped binding or a permanently wedged
|
|
154
|
+
* `running` instance.
|
|
155
|
+
*
|
|
156
|
+
* It checks, in order (never traverse before proving shape):
|
|
157
|
+
* 1. container shape — `workflow` is an object; `steps` / `inputs` / `bindings` are arrays
|
|
158
|
+
* of the right element shape;
|
|
159
|
+
* 2. step shape + control-flow edges (`entry` / `next` / `then` / `else` / `onError` /
|
|
160
|
+
* `onTimeout` resolve; `action` / `event` targets present; timer / terminal shape);
|
|
161
|
+
* 3. reference integrity — a `wait-event` `bind` key is a declared `WorkflowBinding`; a
|
|
162
|
+
* `WorkflowBinding.producedBy` resolves to a step; every workflow expression `ref`
|
|
163
|
+
* resolves in that location's closed scope (inputs / bindings / `PRINCIPAL`, plus
|
|
164
|
+
* `EVENT` only inside a `wait-event` `where` / `bind`); no `now` / `uuid` / `random`.
|
|
165
|
+
*
|
|
166
|
+
* Cross-node references (`ActionDef` / `EventDef` *existence in the graph*) stay a
|
|
167
|
+
* compile-time concern — a missing target id surfaces as a structured `UNKNOWN_SERVER_ACTION`
|
|
168
|
+
* refusal at invoke time, not a wedge.
|
|
169
|
+
*/
|
|
170
|
+
export declare function workflowStructuralProblems(workflow: unknown): WorkflowStructuralProblem[];
|
|
124
171
|
/**
|
|
125
172
|
* Step ids reachable from `entry` by control flow (spec14 §122). A step not in this set is
|
|
126
173
|
* unreachable and an authoring mistake.
|
package/dist/workflows.js
CHANGED
|
@@ -25,11 +25,27 @@ export const WORKFLOW_STEP_TYPES = [
|
|
|
25
25
|
'fail',
|
|
26
26
|
];
|
|
27
27
|
// ---------------------------------------------------------------------------- accessors
|
|
28
|
+
/**
|
|
29
|
+
* Whether a value is a structurally recognizable workflow step — an object with an `id` and
|
|
30
|
+
* one of the six {@link WORKFLOW_STEP_TYPES}. The accessors below are total over *any* input
|
|
31
|
+
* (a malformed graph, a hand-tampered Server IR) precisely so a bad step produces a
|
|
32
|
+
* structured diagnostic rather than a native `TypeError` (spec14pt3 F1 / F2 §39-§48).
|
|
33
|
+
*/
|
|
34
|
+
export function isWorkflowStep(step) {
|
|
35
|
+
return (!!step &&
|
|
36
|
+
typeof step === 'object' &&
|
|
37
|
+
!Array.isArray(step) &&
|
|
38
|
+
typeof step.id === 'string' &&
|
|
39
|
+
WORKFLOW_STEP_TYPES.includes(step.type));
|
|
40
|
+
}
|
|
28
41
|
export function workflowStepById(workflow, stepId) {
|
|
29
|
-
|
|
42
|
+
const steps = Array.isArray(workflow?.steps) ? workflow.steps : [];
|
|
43
|
+
return steps.find((step) => !!step && typeof step === 'object' && String(step.id) === String(stepId));
|
|
30
44
|
}
|
|
31
|
-
/** Every step id a step can hand control to (control-flow successors). */
|
|
45
|
+
/** Every step id a step can hand control to (control-flow successors). Total over bad input. */
|
|
32
46
|
export function workflowStepSuccessors(step) {
|
|
47
|
+
if (!isWorkflowStep(step))
|
|
48
|
+
return [];
|
|
33
49
|
switch (step.type) {
|
|
34
50
|
case 'action':
|
|
35
51
|
return step.onError ? [step.next, step.onError] : [step.next];
|
|
@@ -42,13 +58,17 @@ export function workflowStepSuccessors(step) {
|
|
|
42
58
|
case 'complete':
|
|
43
59
|
case 'fail':
|
|
44
60
|
return [];
|
|
61
|
+
default:
|
|
62
|
+
return [];
|
|
45
63
|
}
|
|
46
64
|
}
|
|
47
65
|
export function workflowIsTerminalStep(step) {
|
|
48
|
-
return step.type === 'complete' || step.type === 'fail';
|
|
66
|
+
return isWorkflowStep(step) && (step.type === 'complete' || step.type === 'fail');
|
|
49
67
|
}
|
|
50
|
-
/** Every `Expression` embedded in a step — the leaves dependency / scope analysis walks. */
|
|
68
|
+
/** Every `Expression` embedded in a step — the leaves dependency / scope analysis walks. Total over bad input. */
|
|
51
69
|
export function workflowStepExpressions(step) {
|
|
70
|
+
if (!isWorkflowStep(step))
|
|
71
|
+
return [];
|
|
52
72
|
switch (step.type) {
|
|
53
73
|
case 'action':
|
|
54
74
|
return Object.values(step.arguments ?? {});
|
|
@@ -62,19 +82,261 @@ export function workflowStepExpressions(step) {
|
|
|
62
82
|
return Object.values(step.output ?? {});
|
|
63
83
|
case 'fail':
|
|
64
84
|
return Object.values(step.error ?? {});
|
|
85
|
+
default:
|
|
86
|
+
return [];
|
|
65
87
|
}
|
|
66
88
|
}
|
|
67
89
|
/** Every `Expression` in a workflow — inputs carry none, so this is the step leaves. */
|
|
68
90
|
export function workflowExpressions(workflow) {
|
|
69
|
-
return workflow.steps.flatMap(workflowStepExpressions);
|
|
91
|
+
return (Array.isArray(workflow?.steps) ? workflow.steps : []).flatMap(workflowStepExpressions);
|
|
70
92
|
}
|
|
71
93
|
/** The `ActionDef` ids a workflow invokes. */
|
|
72
94
|
export function workflowActionIds(workflow) {
|
|
73
|
-
return workflow
|
|
95
|
+
return (Array.isArray(workflow?.steps) ? workflow.steps : [])
|
|
96
|
+
.filter((s) => isWorkflowStep(s) && s.type === 'action')
|
|
97
|
+
.map((s) => s.action);
|
|
74
98
|
}
|
|
75
99
|
/** The `EventDef` ids a workflow waits on. */
|
|
76
100
|
export function workflowEventIds(workflow) {
|
|
77
|
-
return workflow
|
|
101
|
+
return (Array.isArray(workflow?.steps) ? workflow.steps : [])
|
|
102
|
+
.filter((s) => isWorkflowStep(s) && s.type === 'wait-event')
|
|
103
|
+
.map((s) => s.event);
|
|
104
|
+
}
|
|
105
|
+
// ----------------------------------------------------- canonical form for fingerprinting
|
|
106
|
+
/**
|
|
107
|
+
* A `WorkflowDef` reduced to a form where **authoring order is not semantic** (spec14pt3
|
|
108
|
+
* §19, §64): `steps`, `inputs` and `bindings` are ordered by id. Control flow is by explicit
|
|
109
|
+
* `entry` / `next` / `then` / `else` / `onError` / `onTimeout` edges, never by array
|
|
110
|
+
* position, so two workflows that differ only in the order their steps were declared are the
|
|
111
|
+
* same executable meaning. Both the graph-level `semanticFingerprint` and the ServerIR-side
|
|
112
|
+
* authority-compatibility fingerprint pass workflows through this before hashing, so they
|
|
113
|
+
* agree (spec14pt3 §5 G1). Non-`id` fields are untouched; human metadata is stripped by the
|
|
114
|
+
* caller's projection exactly as elsewhere.
|
|
115
|
+
*/
|
|
116
|
+
export function canonicalWorkflowForFingerprint(workflow) {
|
|
117
|
+
// Total over a hand-tampered slice: a non-object workflow value is returned unchanged and
|
|
118
|
+
// the engine's admission validator refuses it structurally (spec14pt4 §31).
|
|
119
|
+
if (!workflow || typeof workflow !== 'object' || Array.isArray(workflow))
|
|
120
|
+
return workflow;
|
|
121
|
+
const byId = (list) => [...(list ?? [])].sort((a, b) => {
|
|
122
|
+
const ai = String(a?.id ?? '');
|
|
123
|
+
const bi = String(b?.id ?? '');
|
|
124
|
+
return ai < bi ? -1 : ai > bi ? 1 : 0;
|
|
125
|
+
});
|
|
126
|
+
return {
|
|
127
|
+
...workflow,
|
|
128
|
+
...(Array.isArray(workflow.steps) ? { steps: byId(workflow.steps) } : {}),
|
|
129
|
+
...(Array.isArray(workflow.inputs) ? { inputs: byId(workflow.inputs) } : {}),
|
|
130
|
+
...(Array.isArray(workflow.bindings) ? { bindings: byId(workflow.bindings) } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const WORKFLOW_NONDETERMINISTIC_BUILTINS = new Set(['now', 'uuid', 'random']);
|
|
134
|
+
function isPlainObject(value) {
|
|
135
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Walk an expression tree, total over arbitrary input, collecting every scope `ref` target
|
|
139
|
+
* id and every non-deterministic builtin call. `literal` node payloads are not recursed
|
|
140
|
+
* into (a literal object value that happens to look like a `ref` is data, not a reference).
|
|
141
|
+
*/
|
|
142
|
+
function walkExpression(expression, refs, nondeterministic, seen = new Set()) {
|
|
143
|
+
if (!isPlainObject(expression) || seen.has(expression))
|
|
144
|
+
return;
|
|
145
|
+
seen.add(expression);
|
|
146
|
+
if (expression.kind === 'ref' && expression.targetId !== undefined) {
|
|
147
|
+
refs.add(String(expression.targetId));
|
|
148
|
+
}
|
|
149
|
+
if (expression.kind === 'call' &&
|
|
150
|
+
typeof expression.function === 'string' &&
|
|
151
|
+
WORKFLOW_NONDETERMINISTIC_BUILTINS.has(expression.function)) {
|
|
152
|
+
nondeterministic.add(expression.function);
|
|
153
|
+
}
|
|
154
|
+
if (expression.kind === 'literal')
|
|
155
|
+
return;
|
|
156
|
+
for (const value of Object.values(expression)) {
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
for (const entry of value)
|
|
159
|
+
walkExpression(entry, refs, nondeterministic, seen);
|
|
160
|
+
}
|
|
161
|
+
else if (isPlainObject(value)) {
|
|
162
|
+
walkExpression(value, refs, nondeterministic, seen);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The complete **runtime-boundary** admission check on a `WorkflowDef` — total over *any*
|
|
168
|
+
* value, including a hand-tampered `ServerIR` where `workflows`, `steps`, `inputs` or
|
|
169
|
+
* `bindings` are the wrong shape entirely (spec14pt3 §44-§49, spec14pt4 §11-§19, §31).
|
|
170
|
+
* `validateGraph` remains the authoring-time authority; this is what the authoritative
|
|
171
|
+
* runtime runs on a `ServerIR` it did not compile itself, so a structurally invalid or
|
|
172
|
+
* referentially inconsistent workflow fails closed with a structured result instead of
|
|
173
|
+
* reaching a native exception, a silently dropped binding or a permanently wedged
|
|
174
|
+
* `running` instance.
|
|
175
|
+
*
|
|
176
|
+
* It checks, in order (never traverse before proving shape):
|
|
177
|
+
* 1. container shape — `workflow` is an object; `steps` / `inputs` / `bindings` are arrays
|
|
178
|
+
* of the right element shape;
|
|
179
|
+
* 2. step shape + control-flow edges (`entry` / `next` / `then` / `else` / `onError` /
|
|
180
|
+
* `onTimeout` resolve; `action` / `event` targets present; timer / terminal shape);
|
|
181
|
+
* 3. reference integrity — a `wait-event` `bind` key is a declared `WorkflowBinding`; a
|
|
182
|
+
* `WorkflowBinding.producedBy` resolves to a step; every workflow expression `ref`
|
|
183
|
+
* resolves in that location's closed scope (inputs / bindings / `PRINCIPAL`, plus
|
|
184
|
+
* `EVENT` only inside a `wait-event` `where` / `bind`); no `now` / `uuid` / `random`.
|
|
185
|
+
*
|
|
186
|
+
* Cross-node references (`ActionDef` / `EventDef` *existence in the graph*) stay a
|
|
187
|
+
* compile-time concern — a missing target id surfaces as a structured `UNKNOWN_SERVER_ACTION`
|
|
188
|
+
* refusal at invoke time, not a wedge.
|
|
189
|
+
*/
|
|
190
|
+
export function workflowStructuralProblems(workflow) {
|
|
191
|
+
const problems = [];
|
|
192
|
+
const wid = isPlainObject(workflow) ? String(workflow.id ?? '<unknown>') : '<unknown>';
|
|
193
|
+
// 1. Container shape — before any traversal.
|
|
194
|
+
if (!isPlainObject(workflow)) {
|
|
195
|
+
return [{ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} is not an object` }];
|
|
196
|
+
}
|
|
197
|
+
if (!Array.isArray(workflow.steps)) {
|
|
198
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} steps is not an array` });
|
|
199
|
+
}
|
|
200
|
+
if (workflow.inputs !== undefined && !Array.isArray(workflow.inputs)) {
|
|
201
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} inputs is not an array` });
|
|
202
|
+
}
|
|
203
|
+
if (workflow.bindings !== undefined && !Array.isArray(workflow.bindings)) {
|
|
204
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} bindings is not an array` });
|
|
205
|
+
}
|
|
206
|
+
const steps = Array.isArray(workflow.steps) ? workflow.steps : [];
|
|
207
|
+
const rawInputs = Array.isArray(workflow.inputs) ? workflow.inputs : [];
|
|
208
|
+
const rawBindings = Array.isArray(workflow.bindings) ? workflow.bindings : [];
|
|
209
|
+
const inputIds = new Set();
|
|
210
|
+
for (const input of rawInputs) {
|
|
211
|
+
if (!isPlainObject(input) || typeof input.id !== 'string') {
|
|
212
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} has an input that is not an object with a string id` });
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
inputIds.add(input.id);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// 2. Steps.
|
|
219
|
+
const ids = new Set();
|
|
220
|
+
for (const step of steps) {
|
|
221
|
+
if (!isWorkflowStep(step)) {
|
|
222
|
+
problems.push({
|
|
223
|
+
code: 'WORKFLOW_INVALID_STEP',
|
|
224
|
+
message: `Workflow ${wid} has a step that is not an object with an id and a known kind`,
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
ids.add(String(step.id));
|
|
229
|
+
}
|
|
230
|
+
const bindingIds = new Set();
|
|
231
|
+
for (const binding of rawBindings) {
|
|
232
|
+
if (!isPlainObject(binding) || typeof binding.id !== 'string') {
|
|
233
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} has a binding that is not an object with a string id` });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
bindingIds.add(binding.id);
|
|
237
|
+
if (binding.producedBy === undefined || !ids.has(String(binding.producedBy))) {
|
|
238
|
+
problems.push({
|
|
239
|
+
code: 'WORKFLOW_STEP_NOT_FOUND',
|
|
240
|
+
message: `Workflow ${wid} binding ${binding.id} is producedBy ${String(binding.producedBy)}, which is not a step`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (workflow.entry === undefined || !ids.has(String(workflow.entry))) {
|
|
245
|
+
problems.push({
|
|
246
|
+
code: 'WORKFLOW_ENTRY_NOT_FOUND',
|
|
247
|
+
message: `Workflow ${wid} entry ${String(workflow.entry)} is not one of its steps`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const edge = (target, from) => {
|
|
251
|
+
if (target !== undefined && !ids.has(String(target))) {
|
|
252
|
+
problems.push({
|
|
253
|
+
code: 'WORKFLOW_STEP_NOT_FOUND',
|
|
254
|
+
message: `Workflow ${wid} step ${from} points at ${String(target)}, which is not a step`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const scopeCheck = (expression, from, eventInScope) => {
|
|
259
|
+
const refs = new Set();
|
|
260
|
+
const nondeterministic = new Set();
|
|
261
|
+
walkExpression(expression, refs, nondeterministic);
|
|
262
|
+
for (const id of refs) {
|
|
263
|
+
const ok = inputIds.has(id) ||
|
|
264
|
+
bindingIds.has(id) ||
|
|
265
|
+
id === WORKFLOW_PRINCIPAL_SCOPE ||
|
|
266
|
+
(eventInScope && id === WORKFLOW_EVENT_SCOPE);
|
|
267
|
+
if (!ok) {
|
|
268
|
+
problems.push({
|
|
269
|
+
code: 'WORKFLOW_EXPRESSION_SCOPE',
|
|
270
|
+
message: `Workflow ${wid} step ${from} references ${id}, which is not in scope (inputs / bindings${eventInScope ? ' / EVENT' : ''} / PRINCIPAL)`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const fn of nondeterministic) {
|
|
275
|
+
problems.push({
|
|
276
|
+
code: 'WORKFLOW_NONDETERMINISTIC',
|
|
277
|
+
message: `Workflow ${wid} step ${from} calls ${fn}(), which is not deterministic and not allowed in a workflow expression`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
for (const step of steps) {
|
|
282
|
+
if (!isWorkflowStep(step))
|
|
283
|
+
continue;
|
|
284
|
+
const from = String(step.id);
|
|
285
|
+
switch (step.type) {
|
|
286
|
+
case 'action':
|
|
287
|
+
if (typeof step.action !== 'string') {
|
|
288
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} action step ${from} has no action target` });
|
|
289
|
+
}
|
|
290
|
+
edge(step.next, from);
|
|
291
|
+
edge(step.onError, from);
|
|
292
|
+
break;
|
|
293
|
+
case 'wait-event':
|
|
294
|
+
if (typeof step.event !== 'string') {
|
|
295
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} wait-event step ${from} has no event target` });
|
|
296
|
+
}
|
|
297
|
+
edge(step.next, from);
|
|
298
|
+
edge(step.onTimeout, from);
|
|
299
|
+
if (step.timeout !== undefined && !(Number(step.timeout?.seconds) > 0)) {
|
|
300
|
+
problems.push({ code: 'WORKFLOW_INVALID_TIMER', message: `Workflow ${wid} wait-event step ${from} has a non-positive timeout` });
|
|
301
|
+
}
|
|
302
|
+
for (const bindingId of isPlainObject(step.bind) ? Object.keys(step.bind) : []) {
|
|
303
|
+
if (!bindingIds.has(bindingId)) {
|
|
304
|
+
problems.push({
|
|
305
|
+
code: 'WORKFLOW_BINDING_NOT_FOUND',
|
|
306
|
+
message: `Workflow ${wid} step ${from} binds ${bindingId}, which is not a declared WorkflowBinding`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
case 'timer': {
|
|
312
|
+
const hasAfter = step.after !== undefined;
|
|
313
|
+
const hasAt = step.at !== undefined;
|
|
314
|
+
if (hasAfter === hasAt) {
|
|
315
|
+
problems.push({ code: 'WORKFLOW_INVALID_TIMER', message: `Workflow ${wid} timer step ${from} must declare exactly one of after / at` });
|
|
316
|
+
}
|
|
317
|
+
else if (hasAfter && !(Number(step.after?.seconds) > 0)) {
|
|
318
|
+
problems.push({ code: 'WORKFLOW_INVALID_TIMER', message: `Workflow ${wid} timer step ${from} has a non-positive after.seconds` });
|
|
319
|
+
}
|
|
320
|
+
edge(step.next, from);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case 'branch':
|
|
324
|
+
if (step.when === undefined) {
|
|
325
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} branch step ${from} has no when expression` });
|
|
326
|
+
}
|
|
327
|
+
edge(step.then, from);
|
|
328
|
+
edge(step.else, from);
|
|
329
|
+
break;
|
|
330
|
+
case 'complete':
|
|
331
|
+
case 'fail':
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
// 3. Expression reference / scope integrity (spec14pt4 §17, §18).
|
|
335
|
+
for (const expression of workflowStepExpressions(step)) {
|
|
336
|
+
scopeCheck(expression, from, step.type === 'wait-event');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return problems;
|
|
78
340
|
}
|
|
79
341
|
/**
|
|
80
342
|
* Step ids reachable from `entry` by control flow (spec14 §122). A step not in this set is
|