@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.
@@ -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
- const EXECUTABLE_KINDS = [
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
- if (typeof step.id === 'string' && !reachable.has(String(step.id))) {
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 ${String(step.id)} is unreachable from entry`,
1059
+ message: `Workflow ${workflow.id} step ${sid} is unreachable from entry`,
1059
1060
  nodeId: workflow.id,
1060
1061
  });
1061
1062
  }
@@ -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
- return workflow.steps.find((step) => String(step.id) === String(stepId));
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.steps.filter((s) => s.type === 'action').map((s) => s.action);
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.steps.filter((s) => s.type === 'wait-event').map((s) => s.event);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-core",
3
- "version": "0.14.0-alpha.1",
3
+ "version": "0.14.0-alpha.3",
4
4
  "description": "Application Graph, semantic types, locations and validation for Axiom.",
5
5
  "license": "MIT",
6
6
  "author": "AskTech AS",