@cynodia/axiom-core 0.14.0-alpha.2 → 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.
@@ -140,20 +140,34 @@ export declare function workflowEventIds(workflow: WorkflowDef): NodeId[];
140
140
  */
141
141
  export declare function canonicalWorkflowForFingerprint<T extends Partial<WorkflowDef>>(workflow: T): T;
142
142
  export interface WorkflowStructuralProblem {
143
- code: 'WORKFLOW_INVALID_STEP' | 'WORKFLOW_ENTRY_NOT_FOUND' | 'WORKFLOW_STEP_NOT_FOUND' | 'WORKFLOW_INVALID_TIMER';
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
144
  message: string;
145
145
  }
146
146
  /**
147
- * A **runtime-boundary** structural check on a `WorkflowDef`, independent of an
148
- * `ApplicationGraph` (spec14pt3 §44-§49). `validateGraph` remains the authoring-time
149
- * authority; this is what the authoritative runtime runs on a `ServerIR` it did not compile
150
- * itself — stored, transported or hand-tampered — so structurally invalid workflow IR fails
151
- * closed with a structured result instead of reaching a native `TypeError`, a silently
152
- * skipped step or a permanently wedged instance. It checks only what the engine dereferences
153
- * to execute; cross-node references (`ActionDef` / `EventDef` ids) are a compile-time
154
- * concern and are not re-checked here.
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.
155
169
  */
156
- export declare function workflowStructuralProblems(workflow: WorkflowDef): WorkflowStructuralProblem[];
170
+ export declare function workflowStructuralProblems(workflow: unknown): WorkflowStructuralProblem[];
157
171
  /**
158
172
  * Step ids reachable from `entry` by control flow (spec14 §122). A step not in this set is
159
173
  * unreachable and an authoring mistake.
package/dist/workflows.js CHANGED
@@ -88,7 +88,7 @@ export function workflowStepExpressions(step) {
88
88
  }
89
89
  /** Every `Expression` in a workflow — inputs carry none, so this is the step leaves. */
90
90
  export function workflowExpressions(workflow) {
91
- return workflow.steps.flatMap(workflowStepExpressions);
91
+ return (Array.isArray(workflow?.steps) ? workflow.steps : []).flatMap(workflowStepExpressions);
92
92
  }
93
93
  /** The `ActionDef` ids a workflow invokes. */
94
94
  export function workflowActionIds(workflow) {
@@ -114,6 +114,10 @@ export function workflowEventIds(workflow) {
114
114
  * caller's projection exactly as elsewhere.
115
115
  */
116
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;
117
121
  const byId = (list) => [...(list ?? [])].sort((a, b) => {
118
122
  const ai = String(a?.id ?? '');
119
123
  const bi = String(b?.id ?? '');
@@ -126,20 +130,92 @@ export function canonicalWorkflowForFingerprint(workflow) {
126
130
  ...(Array.isArray(workflow.bindings) ? { bindings: byId(workflow.bindings) } : {}),
127
131
  };
128
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
+ }
129
137
  /**
130
- * A **runtime-boundary** structural check on a `WorkflowDef`, independent of an
131
- * `ApplicationGraph` (spec14pt3 §44-§49). `validateGraph` remains the authoring-time
132
- * authority; this is what the authoritative runtime runs on a `ServerIR` it did not compile
133
- * itself — stored, transported or hand-tampered — so structurally invalid workflow IR fails
134
- * closed with a structured result instead of reaching a native `TypeError`, a silently
135
- * skipped step or a permanently wedged instance. It checks only what the engine dereferences
136
- * to execute; cross-node references (`ActionDef` / `EventDef` ids) are a compile-time
137
- * concern and are not re-checked here.
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.
138
189
  */
139
190
  export function workflowStructuralProblems(workflow) {
140
191
  const problems = [];
141
- const wid = String(workflow?.id ?? '<unknown>');
142
- const steps = Array.isArray(workflow?.steps) ? workflow.steps : [];
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.
143
219
  const ids = new Set();
144
220
  for (const step of steps) {
145
221
  if (!isWorkflowStep(step)) {
@@ -151,10 +227,24 @@ export function workflowStructuralProblems(workflow) {
151
227
  }
152
228
  ids.add(String(step.id));
153
229
  }
154
- if (workflow?.entry === undefined || !ids.has(String(workflow.entry))) {
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))) {
155
245
  problems.push({
156
246
  code: 'WORKFLOW_ENTRY_NOT_FOUND',
157
- message: `Workflow ${wid} entry ${String(workflow?.entry)} is not one of its steps`,
247
+ message: `Workflow ${wid} entry ${String(workflow.entry)} is not one of its steps`,
158
248
  });
159
249
  }
160
250
  const edge = (target, from) => {
@@ -165,6 +255,29 @@ export function workflowStructuralProblems(workflow) {
165
255
  });
166
256
  }
167
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
+ };
168
281
  for (const step of steps) {
169
282
  if (!isWorkflowStep(step))
170
283
  continue;
@@ -186,6 +299,14 @@ export function workflowStructuralProblems(workflow) {
186
299
  if (step.timeout !== undefined && !(Number(step.timeout?.seconds) > 0)) {
187
300
  problems.push({ code: 'WORKFLOW_INVALID_TIMER', message: `Workflow ${wid} wait-event step ${from} has a non-positive timeout` });
188
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
+ }
189
310
  break;
190
311
  case 'timer': {
191
312
  const hasAfter = step.after !== undefined;
@@ -210,6 +331,10 @@ export function workflowStructuralProblems(workflow) {
210
331
  case 'fail':
211
332
  break;
212
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
+ }
213
338
  }
214
339
  return problems;
215
340
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-core",
3
- "version": "0.14.0-alpha.2",
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",