@cynodia/axiom-core 0.14.0-alpha.1 → 0.14.0-alpha.2
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 +35 -2
- package/dist/workflows.js +143 -6
- 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,32 @@ 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';
|
|
144
|
+
message: string;
|
|
145
|
+
}
|
|
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.
|
|
155
|
+
*/
|
|
156
|
+
export declare function workflowStructuralProblems(workflow: WorkflowDef): WorkflowStructuralProblem[];
|
|
124
157
|
/**
|
|
125
158
|
* Step ids reachable from `entry` by control flow (spec14 §122). A step not in this set is
|
|
126
159
|
* 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,6 +82,8 @@ 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. */
|
|
@@ -70,11 +92,126 @@ export function workflowExpressions(workflow) {
|
|
|
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
|
+
const byId = (list) => [...(list ?? [])].sort((a, b) => {
|
|
118
|
+
const ai = String(a?.id ?? '');
|
|
119
|
+
const bi = String(b?.id ?? '');
|
|
120
|
+
return ai < bi ? -1 : ai > bi ? 1 : 0;
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
...workflow,
|
|
124
|
+
...(Array.isArray(workflow.steps) ? { steps: byId(workflow.steps) } : {}),
|
|
125
|
+
...(Array.isArray(workflow.inputs) ? { inputs: byId(workflow.inputs) } : {}),
|
|
126
|
+
...(Array.isArray(workflow.bindings) ? { bindings: byId(workflow.bindings) } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
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
|
+
*/
|
|
139
|
+
export function workflowStructuralProblems(workflow) {
|
|
140
|
+
const problems = [];
|
|
141
|
+
const wid = String(workflow?.id ?? '<unknown>');
|
|
142
|
+
const steps = Array.isArray(workflow?.steps) ? workflow.steps : [];
|
|
143
|
+
const ids = new Set();
|
|
144
|
+
for (const step of steps) {
|
|
145
|
+
if (!isWorkflowStep(step)) {
|
|
146
|
+
problems.push({
|
|
147
|
+
code: 'WORKFLOW_INVALID_STEP',
|
|
148
|
+
message: `Workflow ${wid} has a step that is not an object with an id and a known kind`,
|
|
149
|
+
});
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
ids.add(String(step.id));
|
|
153
|
+
}
|
|
154
|
+
if (workflow?.entry === undefined || !ids.has(String(workflow.entry))) {
|
|
155
|
+
problems.push({
|
|
156
|
+
code: 'WORKFLOW_ENTRY_NOT_FOUND',
|
|
157
|
+
message: `Workflow ${wid} entry ${String(workflow?.entry)} is not one of its steps`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const edge = (target, from) => {
|
|
161
|
+
if (target !== undefined && !ids.has(String(target))) {
|
|
162
|
+
problems.push({
|
|
163
|
+
code: 'WORKFLOW_STEP_NOT_FOUND',
|
|
164
|
+
message: `Workflow ${wid} step ${from} points at ${String(target)}, which is not a step`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
for (const step of steps) {
|
|
169
|
+
if (!isWorkflowStep(step))
|
|
170
|
+
continue;
|
|
171
|
+
const from = String(step.id);
|
|
172
|
+
switch (step.type) {
|
|
173
|
+
case 'action':
|
|
174
|
+
if (typeof step.action !== 'string') {
|
|
175
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} action step ${from} has no action target` });
|
|
176
|
+
}
|
|
177
|
+
edge(step.next, from);
|
|
178
|
+
edge(step.onError, from);
|
|
179
|
+
break;
|
|
180
|
+
case 'wait-event':
|
|
181
|
+
if (typeof step.event !== 'string') {
|
|
182
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} wait-event step ${from} has no event target` });
|
|
183
|
+
}
|
|
184
|
+
edge(step.next, from);
|
|
185
|
+
edge(step.onTimeout, from);
|
|
186
|
+
if (step.timeout !== undefined && !(Number(step.timeout?.seconds) > 0)) {
|
|
187
|
+
problems.push({ code: 'WORKFLOW_INVALID_TIMER', message: `Workflow ${wid} wait-event step ${from} has a non-positive timeout` });
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
case 'timer': {
|
|
191
|
+
const hasAfter = step.after !== undefined;
|
|
192
|
+
const hasAt = step.at !== undefined;
|
|
193
|
+
if (hasAfter === hasAt) {
|
|
194
|
+
problems.push({ code: 'WORKFLOW_INVALID_TIMER', message: `Workflow ${wid} timer step ${from} must declare exactly one of after / at` });
|
|
195
|
+
}
|
|
196
|
+
else if (hasAfter && !(Number(step.after?.seconds) > 0)) {
|
|
197
|
+
problems.push({ code: 'WORKFLOW_INVALID_TIMER', message: `Workflow ${wid} timer step ${from} has a non-positive after.seconds` });
|
|
198
|
+
}
|
|
199
|
+
edge(step.next, from);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case 'branch':
|
|
203
|
+
if (step.when === undefined) {
|
|
204
|
+
problems.push({ code: 'WORKFLOW_INVALID_STEP', message: `Workflow ${wid} branch step ${from} has no when expression` });
|
|
205
|
+
}
|
|
206
|
+
edge(step.then, from);
|
|
207
|
+
edge(step.else, from);
|
|
208
|
+
break;
|
|
209
|
+
case 'complete':
|
|
210
|
+
case 'fail':
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return problems;
|
|
78
215
|
}
|
|
79
216
|
/**
|
|
80
217
|
* Step ids reachable from `entry` by control flow (spec14 §122). A step not in this set is
|