@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.
- package/dist/workflows.d.ts +24 -10
- package/dist/workflows.js +138 -13
- package/package.json +1 -1
package/dist/workflows.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
148
|
-
* `
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
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:
|
|
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
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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.
|
|
138
189
|
*/
|
|
139
190
|
export function workflowStructuralProblems(workflow) {
|
|
140
191
|
const problems = [];
|
|
141
|
-
const wid = String(workflow
|
|
142
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|