@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.
@@ -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,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
- 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,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.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
+ 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
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.2",
4
4
  "description": "Application Graph, semantic types, locations and validation for Axiom.",
5
5
  "license": "MIT",
6
6
  "author": "AskTech AS",