@cynodia/axiom-core 0.13.1-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.
@@ -156,6 +156,36 @@ export declare const VALIDATION_CODES: {
156
156
  readonly invalidQueryOperation: "INVALID_QUERY_OPERATION";
157
157
  /** A `provider-record` location whose entity does not resolve, or whose identity field is not that entity's identity. */
158
158
  readonly invalidProviderRecordLocation: "INVALID_PROVIDER_RECORD_LOCATION";
159
+ /** `WorkflowDef.entry` does not name a step of that workflow. */
160
+ readonly workflowEntryNotFound: "WORKFLOW_ENTRY_NOT_FOUND";
161
+ /** A control-flow edge (`next`/`onError`/`then`/`else`/`onTimeout`) names a step that does not exist. */
162
+ readonly workflowStepNotFound: "WORKFLOW_STEP_NOT_FOUND";
163
+ /** Two steps in one workflow share an id, or a step id collides with a graph node id. */
164
+ readonly workflowDuplicateStepId: "WORKFLOW_DUPLICATE_STEP_ID";
165
+ /** A step whose `type` is outside `WORKFLOW_STEP_TYPES`, or whose shape is invalid for its type. */
166
+ readonly workflowInvalidStep: "WORKFLOW_INVALID_STEP";
167
+ /** The workflow control-flow graph contains a cycle (retries are runtime policy, not a cycle). */
168
+ readonly workflowCycleNotAllowed: "WORKFLOW_CYCLE_NOT_ALLOWED";
169
+ /** An `action` step references an `action` that does not exist. */
170
+ readonly workflowActionNotFound: "WORKFLOW_ACTION_NOT_FOUND";
171
+ /** A `wait-event` step references an `event` that does not exist. */
172
+ readonly workflowEventNotFound: "WORKFLOW_EVENT_NOT_FOUND";
173
+ /** A `bind` / expression names a `WorkflowBinding` the workflow does not declare, or reads one before its producer. */
174
+ readonly workflowBindingNotFound: "WORKFLOW_BINDING_NOT_FOUND";
175
+ /** Two steps declare themselves the producer of the same binding, or a `bind` assigns a binding twice. */
176
+ readonly workflowDuplicateBinding: "WORKFLOW_DUPLICATE_BINDING";
177
+ /** A `WorkflowRetryPolicy` with a non-positive `maxAttempts`/`initialDelaySeconds`, a `backoffMultiplier` < 1, or `maxDelaySeconds` < `initialDelaySeconds`. */
178
+ readonly workflowInvalidRetryPolicy: "WORKFLOW_INVALID_RETRY_POLICY";
179
+ /** A `timer` step with neither `after` nor `at`, both, or a non-positive `after.seconds`. */
180
+ readonly workflowInvalidTimer: "WORKFLOW_INVALID_TIMER";
181
+ /** A step unreachable from `entry` (spec14 §122 — an authoring mistake, not a warning). */
182
+ readonly workflowUnreachableStep: "WORKFLOW_UNREACHABLE_STEP";
183
+ /** A control-flow path that can neither reach `complete`/`fail` nor end in an intentional durable wait (spec14 §123). */
184
+ readonly workflowNoTerminal: "WORKFLOW_NO_TERMINAL";
185
+ /** A workflow expression references an id outside the workflow expression scope (inputs / bindings / EVENT / PRINCIPAL). */
186
+ readonly workflowExpressionScope: "WORKFLOW_EXPRESSION_SCOPE";
187
+ /** A workflow expression reads a nondeterministic builtin (`now`/`uuid`/`random`) outside captured-time semantics. */
188
+ readonly workflowNondeterministic: "WORKFLOW_NONDETERMINISTIC";
159
189
  /** A `MigrationDef` whose `fromSchema`/`toSchema` are not consecutive positive integers, or a migration whose `fromSchema` is at or beyond `graph.schemaVersion`. */
160
190
  readonly invalidMigrationVersion: "INVALID_MIGRATION_VERSION";
161
191
  /** No contiguous `MigrationDef` chain connects schema 1 to `graph.schemaVersion` — a step is missing (spec11 §13). */
@@ -148,6 +148,38 @@ export const VALIDATION_CODES = {
148
148
  invalidQueryOperation: 'INVALID_QUERY_OPERATION',
149
149
  /** A `provider-record` location whose entity does not resolve, or whose identity field is not that entity's identity. */
150
150
  invalidProviderRecordLocation: 'INVALID_PROVIDER_RECORD_LOCATION',
151
+ // Durable workflows (0.14, spec14 §124). `validateGraph` rejects an internally
152
+ // inconsistent `WorkflowDef` before it can be compiled or executed.
153
+ /** `WorkflowDef.entry` does not name a step of that workflow. */
154
+ workflowEntryNotFound: 'WORKFLOW_ENTRY_NOT_FOUND',
155
+ /** A control-flow edge (`next`/`onError`/`then`/`else`/`onTimeout`) names a step that does not exist. */
156
+ workflowStepNotFound: 'WORKFLOW_STEP_NOT_FOUND',
157
+ /** Two steps in one workflow share an id, or a step id collides with a graph node id. */
158
+ workflowDuplicateStepId: 'WORKFLOW_DUPLICATE_STEP_ID',
159
+ /** A step whose `type` is outside `WORKFLOW_STEP_TYPES`, or whose shape is invalid for its type. */
160
+ workflowInvalidStep: 'WORKFLOW_INVALID_STEP',
161
+ /** The workflow control-flow graph contains a cycle (retries are runtime policy, not a cycle). */
162
+ workflowCycleNotAllowed: 'WORKFLOW_CYCLE_NOT_ALLOWED',
163
+ /** An `action` step references an `action` that does not exist. */
164
+ workflowActionNotFound: 'WORKFLOW_ACTION_NOT_FOUND',
165
+ /** A `wait-event` step references an `event` that does not exist. */
166
+ workflowEventNotFound: 'WORKFLOW_EVENT_NOT_FOUND',
167
+ /** A `bind` / expression names a `WorkflowBinding` the workflow does not declare, or reads one before its producer. */
168
+ workflowBindingNotFound: 'WORKFLOW_BINDING_NOT_FOUND',
169
+ /** Two steps declare themselves the producer of the same binding, or a `bind` assigns a binding twice. */
170
+ workflowDuplicateBinding: 'WORKFLOW_DUPLICATE_BINDING',
171
+ /** A `WorkflowRetryPolicy` with a non-positive `maxAttempts`/`initialDelaySeconds`, a `backoffMultiplier` < 1, or `maxDelaySeconds` < `initialDelaySeconds`. */
172
+ workflowInvalidRetryPolicy: 'WORKFLOW_INVALID_RETRY_POLICY',
173
+ /** A `timer` step with neither `after` nor `at`, both, or a non-positive `after.seconds`. */
174
+ workflowInvalidTimer: 'WORKFLOW_INVALID_TIMER',
175
+ /** A step unreachable from `entry` (spec14 §122 — an authoring mistake, not a warning). */
176
+ workflowUnreachableStep: 'WORKFLOW_UNREACHABLE_STEP',
177
+ /** A control-flow path that can neither reach `complete`/`fail` nor end in an intentional durable wait (spec14 §123). */
178
+ workflowNoTerminal: 'WORKFLOW_NO_TERMINAL',
179
+ /** A workflow expression references an id outside the workflow expression scope (inputs / bindings / EVENT / PRINCIPAL). */
180
+ workflowExpressionScope: 'WORKFLOW_EXPRESSION_SCOPE',
181
+ /** A workflow expression reads a nondeterministic builtin (`now`/`uuid`/`random`) outside captured-time semantics. */
182
+ workflowNondeterministic: 'WORKFLOW_NONDETERMINISTIC',
151
183
  // Schema evolution & semantic migrations (0.11). `validateGraph` rejects an internally
152
184
  // inconsistent migration declaration before any persisted data is touched (spec11 §77, §78).
153
185
  /** A `MigrationDef` whose `fromSchema`/`toSchema` are not consecutive positive integers, or a migration whose `fromSchema` is at or beyond `graph.schemaVersion`. */
package/dist/graph.js CHANGED
@@ -23,7 +23,7 @@ export class ApplicationGraph {
23
23
  /** Bumped by every change, so the derived edge index can never serve stale data. */
24
24
  revision = 0;
25
25
  semanticIndex;
26
- constructor(id, name, version = '0.13.1') {
26
+ constructor(id, name, version = '0.14.0') {
27
27
  this.data = { id, name, version, nodes: {}, edges: {} };
28
28
  }
29
29
  get id() {
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export * from './subscriptions.js';
15
15
  export * from './storage.js';
16
16
  export * from './query.js';
17
17
  export * from './live-query.js';
18
+ export * from './workflows.js';
18
19
  export * from './relationships.js';
19
20
  export * from './read-policy.js';
20
21
  export * from './migration.js';
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export * from './subscriptions.js';
15
15
  export * from './storage.js';
16
16
  export * from './query.js';
17
17
  export * from './live-query.js';
18
+ export * from './workflows.js';
18
19
  export * from './relationships.js';
19
20
  export * from './read-policy.js';
20
21
  export * from './migration.js';
@@ -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',
@@ -65,6 +82,7 @@ const EXECUTABLE_KINDS = [
65
82
  'transition-constraint',
66
83
  'storage',
67
84
  'relationship',
85
+ 'workflow',
68
86
  ];
69
87
  /** Keys removed everywhere in the tree — human metadata, never executable meaning. */
70
88
  const NON_SEMANTIC_KEYS = new Set(['name', 'description', 'label', 'metadata', AUTHORING_METADATA_KEY]);
@@ -95,7 +113,7 @@ export function semanticProjection(graph) {
95
113
  if (group.length === 0) {
96
114
  continue;
97
115
  }
98
- nodes[kind] = byId(group).map((node) => stripNonSemantic(node));
116
+ nodes[kind] = byId(group).map((node) => stripNonSemantic(kind === 'workflow' ? canonicalWorkflowForFingerprint(node) : node));
99
117
  }
100
118
  return {
101
119
  fingerprintVersion: SEMANTIC_FINGERPRINT_VERSION,
@@ -11,6 +11,7 @@ import type { QueryDef } from './query.js';
11
11
  import type { RelationshipDef } from './relationships.js';
12
12
  import type { ReadPolicyDef } from './read-policy.js';
13
13
  import type { MigrationDef } from './migration.js';
14
+ import type { WorkflowDef } from './workflows.js';
14
15
  /**
15
16
  * The contracts a Server IR may declare. A runtime that does not recognize the value MUST
16
17
  * refuse the IR rather than interpret it partially.
@@ -26,7 +27,7 @@ import type { MigrationDef } from './migration.js';
26
27
  * Every existing application therefore still compiles to a byte-identical
27
28
  * `axiom.server.v1` document, and the frozen conformance fixtures stay frozen.
28
29
  */
29
- export declare const SERVER_IR_CONTRACTS: readonly ["axiom.server.v1", "axiom.server.v2", "axiom.server.v3", "axiom.server.v4", "axiom.server.v5", "axiom.server.v6", "axiom.server.v7"];
30
+ export declare const SERVER_IR_CONTRACTS: readonly ["axiom.server.v1", "axiom.server.v2", "axiom.server.v3", "axiom.server.v4", "axiom.server.v5", "axiom.server.v6", "axiom.server.v7", "axiom.server.v8"];
30
31
  export type ServerIRContract = (typeof SERVER_IR_CONTRACTS)[number];
31
32
  /** The oldest contract, and the one a document declares unless it needs more. */
32
33
  export declare const SERVER_IR_CONTRACT: ServerIRContract;
@@ -108,8 +109,18 @@ export declare function usesExternalIOVocabulary(ir: {
108
109
  */
109
110
  export declare function usesMigrationVocabulary(ir: {
110
111
  migrations?: readonly unknown[];
112
+ workflows?: readonly unknown[];
111
113
  schemaVersion?: number;
112
114
  }): boolean;
115
+ /**
116
+ * Whether a document carries 0.14 durable-workflow vocabulary. A pre-v8 runtime has no
117
+ * `WorkflowDef` execution model at all, so any `WorkflowDef` present requires
118
+ * `axiom.server.v8` — computed from the document, never asserted by hand, so a graph that
119
+ * declares no workflow compiles to the byte-identical v1–v7 document it always did.
120
+ */
121
+ export declare function usesWorkflowVocabulary(ir: {
122
+ workflows?: readonly unknown[];
123
+ }): boolean;
113
124
  /**
114
125
  * Whether a document uses 0.10's semantic data-access vocabulary — a `QueryDef`, a
115
126
  * `RelationshipDef`, a `ReadPolicyDef`, or a `query` operation inside an action. A v5
@@ -139,6 +150,7 @@ export declare function serverIRExpressions(ir: {
139
150
  queries?: readonly QueryDef[];
140
151
  readPolicies?: readonly ReadPolicyDef[];
141
152
  migrations?: readonly MigrationDef[];
153
+ workflows?: readonly WorkflowDef[];
142
154
  }): Expression[];
143
155
  /**
144
156
  * The normalized form an authority executes: everything required to decide a mutation, and
@@ -237,5 +249,7 @@ export interface ServerIR {
237
249
  * `Expression` transform leaves, never SQL, a callback or a provider handle (spec11 §88).
238
250
  */
239
251
  migrations?: MigrationDef[];
252
+ /** Durable workflow definitions (spec14, `axiom.server.v8`). */
253
+ workflows?: WorkflowDef[];
240
254
  }
241
255
  //# sourceMappingURL=server-ir.d.ts.map
package/dist/server-ir.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { walkExpression } from './expressions.js';
2
2
  import { queryExpressions } from './query.js';
3
3
  import { migrationExpressions } from './migration.js';
4
+ import { workflowExpressions } from './workflows.js';
4
5
  /**
5
6
  * The contracts a Server IR may declare. A runtime that does not recognize the value MUST
6
7
  * refuse the IR rather than interpret it partially.
@@ -24,11 +25,12 @@ export const SERVER_IR_CONTRACTS = [
24
25
  'axiom.server.v5',
25
26
  'axiom.server.v6',
26
27
  'axiom.server.v7',
28
+ 'axiom.server.v8',
27
29
  ];
28
30
  /** The oldest contract, and the one a document declares unless it needs more. */
29
31
  export const SERVER_IR_CONTRACT = 'axiom.server.v1';
30
32
  /** The newest contract this implementation produces and executes. */
31
- export const SERVER_IR_LATEST_CONTRACT = 'axiom.server.v7';
33
+ export const SERVER_IR_LATEST_CONTRACT = 'axiom.server.v8';
32
34
  /** Operation kinds no contract before `axiom.server.v5` contains. */
33
35
  export const SERVER_IR_V5_OPERATION_KINDS = [
34
36
  'blob-metadata',
@@ -133,6 +135,15 @@ export function usesExternalIOVocabulary(ir) {
133
135
  export function usesMigrationVocabulary(ir) {
134
136
  return (ir.migrations?.length ?? 0) > 0 || (ir.schemaVersion ?? 1) > 1;
135
137
  }
138
+ /**
139
+ * Whether a document carries 0.14 durable-workflow vocabulary. A pre-v8 runtime has no
140
+ * `WorkflowDef` execution model at all, so any `WorkflowDef` present requires
141
+ * `axiom.server.v8` — computed from the document, never asserted by hand, so a graph that
142
+ * declares no workflow compiles to the byte-identical v1–v7 document it always did.
143
+ */
144
+ export function usesWorkflowVocabulary(ir) {
145
+ return (ir.workflows?.length ?? 0) > 0;
146
+ }
136
147
  /**
137
148
  * Whether a document uses 0.10's semantic data-access vocabulary — a `QueryDef`, a
138
149
  * `RelationshipDef`, a `ReadPolicyDef`, or a `query` operation inside an action. A v5
@@ -200,6 +211,9 @@ export function serverIRExpressions(ir) {
200
211
  for (const migration of ir.migrations ?? []) {
201
212
  found.push(...migrationExpressions(migration));
202
213
  }
214
+ for (const workflow of ir.workflows ?? []) {
215
+ found.push(...workflowExpressions(workflow));
216
+ }
203
217
  return found;
204
218
  }
205
219
  function actionExpressions(action) {
package/dist/types.d.ts CHANGED
@@ -11,11 +11,12 @@ import type { QueryDef } from './query.js';
11
11
  import type { RelationshipDef } from './relationships.js';
12
12
  import type { ReadPolicyDef } from './read-policy.js';
13
13
  import type { MigrationDef } from './migration.js';
14
- export type SemanticNodeKind = 'entity' | 'state' | 'action' | 'constraint' | 'transition-constraint' | 'route' | 'expression' | 'integration' | 'integration-operation' | 'event' | 'trigger' | 'subscription' | 'storage' | 'query' | 'relationship' | 'read-policy' | 'migration';
14
+ import type { WorkflowDef } from './workflows.js';
15
+ export type SemanticNodeKind = 'entity' | 'state' | 'action' | 'constraint' | 'transition-constraint' | 'route' | 'expression' | 'integration' | 'integration-operation' | 'event' | 'trigger' | 'subscription' | 'storage' | 'query' | 'relationship' | 'read-policy' | 'migration' | 'workflow';
15
16
  /** Every semantic node kind, enumerated so tests can walk them. */
16
17
  export declare const SEMANTIC_NODE_KINDS: readonly SemanticNodeKind[];
17
18
  export type NodeKind = SemanticNodeKind | UINodeKind;
18
- export type AnyNode = EntityDef | StateDef | ActionDef | ConstraintDef | TransitionConstraintDef | RouteDef | ExpressionDef | IntegrationDef | IntegrationOperationDef | EventDef | TriggerDef | SubscriptionDef | StorageDef | QueryDef | RelationshipDef | ReadPolicyDef | MigrationDef | UINode;
19
+ export type AnyNode = EntityDef | StateDef | ActionDef | ConstraintDef | TransitionConstraintDef | RouteDef | ExpressionDef | IntegrationDef | IntegrationOperationDef | EventDef | TriggerDef | SubscriptionDef | StorageDef | QueryDef | RelationshipDef | ReadPolicyDef | MigrationDef | WorkflowDef | UINode;
19
20
  export type NodeOfKind<K extends NodeKind> = Extract<AnyNode, {
20
21
  kind: K;
21
22
  }>;
package/dist/types.js CHANGED
@@ -17,4 +17,5 @@ export const SEMANTIC_NODE_KINDS = [
17
17
  'relationship',
18
18
  'read-policy',
19
19
  'migration',
20
+ 'workflow',
20
21
  ];
package/dist/validate.js CHANGED
@@ -5,6 +5,7 @@ import { SUBSCRIPTION_BACKPRESSURE_POLICIES, subscriptionBackpressure, subscript
5
5
  import { BLOB_REF_FIELDS } from './storage.js';
6
6
  import { queryPaginationStrategy, sortKeyDirection } from './query.js';
7
7
  import { queryStateReferences } from './live-query.js';
8
+ import { WORKFLOW_EVENT_SCOPE, WORKFLOW_PRINCIPAL_SCOPE, WORKFLOW_STEP_TYPES, workflowHasCycle, workflowReachableSteps, workflowStepById, workflowStepExpressions, workflowStepSuccessors, } from './workflows.js';
8
9
  import { relationshipIsToOne } from './relationships.js';
9
10
  import { validateMigrations } from './validate-migration.js';
10
11
  import { collectionType, entityType } from './type-ref.js';
@@ -172,6 +173,9 @@ function validateNode(node, context) {
172
173
  case 'read-policy':
173
174
  validateReadPolicy(node, context);
174
175
  return;
176
+ case 'workflow':
177
+ validateWorkflow(node, context);
178
+ return;
175
179
  case 'migration':
176
180
  // A `MigrationDef` is validated by the graph-level `validateMigrations` pass, which
177
181
  // needs every migration and `graph.schemaVersion` at once — chain contiguity and
@@ -894,6 +898,253 @@ function policyRowScope(rowScopeId, entityId, ownerId, context, entity) {
894
898
  * endpoints consistent with the declared cardinality. Axiom never *infers* a link, so this
895
899
  * is where an inconsistent explicit one is caught.
896
900
  */
901
+ /**
902
+ * A `WorkflowDef`'s structural soundness (spec14 §121-§125). Every failure is a structured
903
+ * diagnostic — never a thrown `TypeError` on a malformed step.
904
+ */
905
+ function validateWorkflow(workflow, context) {
906
+ const steps = Array.isArray(workflow.steps) ? workflow.steps : [];
907
+ const stepIds = new Set();
908
+ for (const step of steps) {
909
+ if (!step || typeof step !== 'object' || typeof step.id !== 'string') {
910
+ context.errors.push({
911
+ code: VALIDATION_CODES.workflowInvalidStep,
912
+ message: `Workflow ${workflow.id} has a step with no id`,
913
+ nodeId: workflow.id,
914
+ });
915
+ continue;
916
+ }
917
+ const id = String(step.id);
918
+ if (stepIds.has(id) || context.nodes.has(step.id)) {
919
+ context.errors.push({
920
+ code: VALIDATION_CODES.workflowDuplicateStepId,
921
+ message: `Workflow ${workflow.id} step id ${id} is declared twice, or collides with a graph node`,
922
+ nodeId: workflow.id,
923
+ });
924
+ }
925
+ stepIds.add(id);
926
+ if (!WORKFLOW_STEP_TYPES.includes(step.type)) {
927
+ context.errors.push({
928
+ code: VALIDATION_CODES.workflowInvalidStep,
929
+ message: `Workflow ${workflow.id} step ${id} has an unknown type ${String(step.type)}`,
930
+ nodeId: workflow.id,
931
+ });
932
+ }
933
+ }
934
+ if (!workflow.entry || !stepIds.has(String(workflow.entry))) {
935
+ context.errors.push({
936
+ code: VALIDATION_CODES.workflowEntryNotFound,
937
+ message: `Workflow ${workflow.id} entry ${String(workflow.entry)} is not one of its steps`,
938
+ nodeId: workflow.id,
939
+ });
940
+ }
941
+ const edge = (target, from) => {
942
+ if (target !== undefined && !stepIds.has(String(target))) {
943
+ context.errors.push({
944
+ code: VALIDATION_CODES.workflowStepNotFound,
945
+ message: `Workflow ${workflow.id} step ${from} points at ${String(target)}, which is not a step`,
946
+ nodeId: workflow.id,
947
+ });
948
+ }
949
+ };
950
+ // Bindings: declared once, produced by exactly one declared step.
951
+ const declaredBindings = new Map();
952
+ for (const binding of workflow.bindings ?? []) {
953
+ const bid = String(binding.id);
954
+ if (declaredBindings.has(bid)) {
955
+ context.errors.push({
956
+ code: VALIDATION_CODES.workflowDuplicateBinding,
957
+ message: `Workflow ${workflow.id} declares binding ${bid} twice`,
958
+ nodeId: workflow.id,
959
+ });
960
+ }
961
+ declaredBindings.set(bid, binding);
962
+ if (!stepIds.has(String(binding.producedBy))) {
963
+ context.errors.push({
964
+ code: VALIDATION_CODES.workflowStepNotFound,
965
+ message: `Workflow ${workflow.id} binding ${bid} is producedBy ${String(binding.producedBy)}, which is not a step`,
966
+ nodeId: workflow.id,
967
+ });
968
+ }
969
+ validateTypeRef(binding.valueType, workflow.id, context);
970
+ }
971
+ const inputIds = new Set((workflow.inputs ?? []).map((input) => String(input.id)));
972
+ for (const input of workflow.inputs ?? [])
973
+ validateTypeRef(input.valueType, workflow.id, context);
974
+ const boundBy = new Map(); // bindingId -> step id that binds it
975
+ for (const step of steps) {
976
+ if (!step || typeof step.id !== 'string')
977
+ continue;
978
+ const from = String(step.id);
979
+ const eventScopeOk = step.type === 'wait-event';
980
+ // Control-flow edges resolve.
981
+ if (step.type === 'action') {
982
+ edge(step.next, from);
983
+ edge(step.onError, from);
984
+ requireKind(step.action, 'action', workflow.id, context, VALIDATION_CODES.workflowActionNotFound);
985
+ if (step.retry)
986
+ validateWorkflowRetry(step.retry, workflow.id, from, context);
987
+ }
988
+ else if (step.type === 'wait-event') {
989
+ edge(step.next, from);
990
+ edge(step.onTimeout, from);
991
+ requireKind(step.event, 'event', workflow.id, context, VALIDATION_CODES.workflowEventNotFound);
992
+ if (step.timeout && !(step.timeout.seconds > 0)) {
993
+ context.errors.push({
994
+ code: VALIDATION_CODES.workflowInvalidTimer,
995
+ message: `Workflow ${workflow.id} step ${from} has a non-positive timeout`,
996
+ nodeId: workflow.id,
997
+ });
998
+ }
999
+ for (const bindingId of Object.keys(step.bind ?? {})) {
1000
+ if (!declaredBindings.has(bindingId)) {
1001
+ context.errors.push({
1002
+ code: VALIDATION_CODES.workflowBindingNotFound,
1003
+ message: `Workflow ${workflow.id} step ${from} binds ${bindingId}, which is not a declared WorkflowBinding`,
1004
+ nodeId: workflow.id,
1005
+ });
1006
+ }
1007
+ else if (String(declaredBindings.get(bindingId).producedBy) !== from) {
1008
+ context.errors.push({
1009
+ code: VALIDATION_CODES.workflowDuplicateBinding,
1010
+ message: `Workflow ${workflow.id} step ${from} binds ${bindingId}, but its declared producer is ${String(declaredBindings.get(bindingId).producedBy)}`,
1011
+ nodeId: workflow.id,
1012
+ });
1013
+ }
1014
+ if (boundBy.has(bindingId)) {
1015
+ context.errors.push({
1016
+ code: VALIDATION_CODES.workflowDuplicateBinding,
1017
+ message: `Workflow ${workflow.id} binding ${bindingId} is assigned by more than one step`,
1018
+ nodeId: workflow.id,
1019
+ });
1020
+ }
1021
+ boundBy.set(bindingId, from);
1022
+ }
1023
+ }
1024
+ else if (step.type === 'timer') {
1025
+ edge(step.next, from);
1026
+ const hasAfter = step.after !== undefined;
1027
+ const hasAt = step.at !== undefined;
1028
+ if (hasAfter === hasAt) {
1029
+ context.errors.push({
1030
+ code: VALIDATION_CODES.workflowInvalidTimer,
1031
+ message: `Workflow ${workflow.id} timer ${from} must declare exactly one of after / at`,
1032
+ nodeId: workflow.id,
1033
+ });
1034
+ }
1035
+ else if (hasAfter && !(step.after.seconds > 0)) {
1036
+ context.errors.push({
1037
+ code: VALIDATION_CODES.workflowInvalidTimer,
1038
+ message: `Workflow ${workflow.id} timer ${from} has a non-positive after.seconds`,
1039
+ nodeId: workflow.id,
1040
+ });
1041
+ }
1042
+ }
1043
+ else if (step.type === 'branch') {
1044
+ edge(step.then, from);
1045
+ edge(step.else, from);
1046
+ }
1047
+ // Expression scope — inputs / bindings / (EVENT inside wait-event) / PRINCIPAL only.
1048
+ for (const expression of workflowStepExpressions(step)) {
1049
+ validateWorkflowExpression(expression, workflow.id, from, inputIds, declaredBindings, eventScopeOk, context);
1050
+ }
1051
+ }
1052
+ // Reachability + acyclicity + terminal reachability.
1053
+ const reachable = workflowReachableSteps(workflow);
1054
+ for (const step of steps) {
1055
+ const sid = step && typeof step === 'object' ? step.id : undefined;
1056
+ if (typeof sid === 'string' && !reachable.has(sid)) {
1057
+ context.errors.push({
1058
+ code: VALIDATION_CODES.workflowUnreachableStep,
1059
+ message: `Workflow ${workflow.id} step ${sid} is unreachable from entry`,
1060
+ nodeId: workflow.id,
1061
+ });
1062
+ }
1063
+ }
1064
+ if (stepIds.has(String(workflow.entry)) && workflowHasCycle(workflow)) {
1065
+ context.errors.push({
1066
+ code: VALIDATION_CODES.workflowCycleNotAllowed,
1067
+ message: `Workflow ${workflow.id} has a control-flow cycle; retries are runtime policy, not graph edges`,
1068
+ nodeId: workflow.id,
1069
+ });
1070
+ }
1071
+ // Every reachable non-terminal step must be able to reach a terminal step (or an
1072
+ // intentional wait-event with no timeout is an acceptable "may never resolve" leaf).
1073
+ if (stepIds.has(String(workflow.entry)) && !workflowHasCycle(workflow)) {
1074
+ const canTerminate = new Map();
1075
+ const reaches = (id, seen) => {
1076
+ if (canTerminate.has(id))
1077
+ return canTerminate.get(id);
1078
+ if (seen.has(id))
1079
+ return false;
1080
+ seen.add(id);
1081
+ const step = workflowStepById(workflow, id);
1082
+ if (!step)
1083
+ return false;
1084
+ if (step.type === 'complete' || step.type === 'fail') {
1085
+ canTerminate.set(id, true);
1086
+ return true;
1087
+ }
1088
+ if (step.type === 'wait-event' && !step.timeout) {
1089
+ // An unbounded wait is an intentional durable leaf (spec14 §123).
1090
+ const ok = workflowStepSuccessors(step).some((n) => reaches(String(n), new Set(seen)));
1091
+ canTerminate.set(id, ok || true);
1092
+ return true;
1093
+ }
1094
+ const ok = workflowStepSuccessors(step).some((n) => reaches(String(n), new Set(seen)));
1095
+ canTerminate.set(id, ok);
1096
+ return ok;
1097
+ };
1098
+ for (const id of reachable) {
1099
+ if (!reaches(id, new Set())) {
1100
+ context.errors.push({
1101
+ code: VALIDATION_CODES.workflowNoTerminal,
1102
+ message: `Workflow ${workflow.id} step ${id} cannot reach complete or fail`,
1103
+ nodeId: workflow.id,
1104
+ });
1105
+ }
1106
+ }
1107
+ }
1108
+ }
1109
+ function validateWorkflowRetry(retry, workflowId, stepId, context) {
1110
+ const bad = !(retry.maxAttempts >= 1) ||
1111
+ !(retry.initialDelaySeconds >= 0) ||
1112
+ !(retry.backoffMultiplier >= 1) ||
1113
+ !(retry.maxDelaySeconds >= retry.initialDelaySeconds);
1114
+ if (bad) {
1115
+ context.errors.push({
1116
+ code: VALIDATION_CODES.workflowInvalidRetryPolicy,
1117
+ message: `Workflow ${workflowId} step ${stepId} has an invalid retry policy`,
1118
+ nodeId: workflowId,
1119
+ });
1120
+ }
1121
+ }
1122
+ const WORKFLOW_NONDETERMINISTIC_BUILTINS = new Set(['now', 'uuid', 'random']);
1123
+ function validateWorkflowExpression(expression, workflowId, stepId, inputIds, bindings, eventScopeOk, context) {
1124
+ walkExpression(expression, (node) => {
1125
+ if (node.kind === 'ref') {
1126
+ const id = String(node.targetId);
1127
+ const inScope = inputIds.has(id) ||
1128
+ bindings.has(id) ||
1129
+ id === WORKFLOW_PRINCIPAL_SCOPE ||
1130
+ (eventScopeOk && id === WORKFLOW_EVENT_SCOPE);
1131
+ if (!inScope) {
1132
+ context.errors.push({
1133
+ code: VALIDATION_CODES.workflowExpressionScope,
1134
+ message: `Workflow ${workflowId} step ${stepId} references ${id}, which is outside workflow expression scope (inputs / bindings${eventScopeOk ? ' / EVENT' : ''} / PRINCIPAL)`,
1135
+ nodeId: workflowId,
1136
+ });
1137
+ }
1138
+ }
1139
+ if (node.kind === 'call' && WORKFLOW_NONDETERMINISTIC_BUILTINS.has(node.function)) {
1140
+ context.errors.push({
1141
+ code: VALIDATION_CODES.workflowNondeterministic,
1142
+ message: `Workflow ${workflowId} step ${stepId} calls ${node.function}; workflow expressions must be deterministic`,
1143
+ nodeId: workflowId,
1144
+ });
1145
+ }
1146
+ });
1147
+ }
897
1148
  function validateRelationship(relationship, context) {
898
1149
  const fromEntity = requireRelationshipEndpoint(relationship.from, relationship.id, context);
899
1150
  const toEntity = requireRelationshipEndpoint(relationship.to, relationship.id, context);
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Durable workflows (spec14).
3
+ *
4
+ * A `WorkflowDef` is a **long-running semantic computation with a durable control
5
+ * position** — not a background promise, a persisted callback, a job-queue entry, a cron
6
+ * task or a mutable JSON blob. The graph owns the orchestration meaning; the runtime owns
7
+ * scheduling, persistence, retries, leases, fencing, crash recovery and physical execution.
8
+ *
9
+ * The node is portable plain data: a closed step vocabulary, `Expression` trees for every
10
+ * leaf, and no JavaScript body of any kind (spec14 §7, §8). It compiles into
11
+ * `axiom.server.v8` and is inspectable through `AgentAPI.analyzeWorkflow`.
12
+ */
13
+ import type { Expression } from './expressions.js';
14
+ import type { NodeId } from './ids.js';
15
+ import type { NodeBase } from './nodes.js';
16
+ import type { TypeRef } from './type-ref.js';
17
+ /** The matched event payload — resolvable only inside a `wait-event` step's `where` / `bind`. */
18
+ export declare const WORKFLOW_EVENT_SCOPE: "EVENT";
19
+ /** The workflow's bound principal. */
20
+ export declare const WORKFLOW_PRINCIPAL_SCOPE: "PRINCIPAL";
21
+ export interface WorkflowInput {
22
+ id: NodeId;
23
+ valueType: TypeRef;
24
+ required?: boolean;
25
+ }
26
+ /**
27
+ * A typed, durable, **single-assignment** value produced after start. Exactly one step
28
+ * (`producedBy`) assigns it; every other step may only read it. There is deliberately no
29
+ * mutable workflow blob (spec14 §26-§28).
30
+ */
31
+ export interface WorkflowBinding {
32
+ id: NodeId;
33
+ valueType: TypeRef;
34
+ /** The step id whose activation assigns this binding (a `wait-event` step). */
35
+ producedBy: NodeId;
36
+ }
37
+ /** Portable relative time. No cron, no ISO-8601 parsing in the graph (spec14 §44, §84). */
38
+ export interface WorkflowDuration {
39
+ seconds: number;
40
+ }
41
+ /** Portable retry policy for an `action` step (spec14 §38). */
42
+ export interface WorkflowRetryPolicy {
43
+ maxAttempts: number;
44
+ initialDelaySeconds: number;
45
+ backoffMultiplier: number;
46
+ maxDelaySeconds: number;
47
+ }
48
+ export declare const WORKFLOW_STEP_TYPES: readonly ["action", "wait-event", "timer", "branch", "complete", "fail"];
49
+ export type WorkflowStepType = (typeof WORKFLOW_STEP_TYPES)[number];
50
+ /** Invoke a canonical `ActionDef` under the workflow principal (spec14 §29-§43). */
51
+ export interface WorkflowActionStep {
52
+ type: 'action';
53
+ id: NodeId;
54
+ action: NodeId;
55
+ /** Argument expressions, in workflow expression scope (inputs / bindings / PRINCIPAL). */
56
+ arguments?: Record<string, Expression>;
57
+ next: NodeId;
58
+ /** Where a terminal action failure routes; absent ⇒ the workflow fails. */
59
+ onError?: NodeId;
60
+ retry?: WorkflowRetryPolicy;
61
+ }
62
+ /** Wait for a matching canonical `EventDef` occurrence (spec14 §50-§66). */
63
+ export interface WorkflowWaitEventStep {
64
+ type: 'wait-event';
65
+ id: NodeId;
66
+ event: NodeId;
67
+ /** Deterministic boolean correlation over `ref(EVENT)` / inputs / bindings. */
68
+ where?: Expression;
69
+ /** Assigns declared `WorkflowBinding`s from the matched event (`bindingId -> Expression`). */
70
+ bind?: Record<string, Expression>;
71
+ next: NodeId;
72
+ timeout?: WorkflowDuration;
73
+ onTimeout?: NodeId;
74
+ }
75
+ /** Wait until a durable time (spec14 §44-§49). Exactly one of `after` / `at`. */
76
+ export interface WorkflowTimerStep {
77
+ type: 'timer';
78
+ id: NodeId;
79
+ after?: WorkflowDuration;
80
+ /** An expression over inputs resolving to an epoch-ms number, captured once on activation. */
81
+ at?: Expression;
82
+ next: NodeId;
83
+ }
84
+ /** Deterministic edge choice over durable workflow context (spec14 §67-§70). */
85
+ export interface WorkflowBranchStep {
86
+ type: 'branch';
87
+ id: NodeId;
88
+ when: Expression;
89
+ then: NodeId;
90
+ else: NodeId;
91
+ }
92
+ /** Terminal → `completed` (spec14 §71, §72). */
93
+ export interface WorkflowCompleteStep {
94
+ type: 'complete';
95
+ id: NodeId;
96
+ output?: Record<string, Expression>;
97
+ }
98
+ /** Terminal → `failed` (spec14 §73). */
99
+ export interface WorkflowFailStep {
100
+ type: 'fail';
101
+ id: NodeId;
102
+ error?: Record<string, Expression>;
103
+ }
104
+ export type WorkflowStep = WorkflowActionStep | WorkflowWaitEventStep | WorkflowTimerStep | WorkflowBranchStep | WorkflowCompleteStep | WorkflowFailStep;
105
+ export interface WorkflowDef extends NodeBase {
106
+ kind: 'workflow';
107
+ inputs?: WorkflowInput[];
108
+ bindings?: WorkflowBinding[];
109
+ entry: NodeId;
110
+ steps: WorkflowStep[];
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;
119
+ export declare function workflowStepById(workflow: WorkflowDef, stepId: NodeId | string): WorkflowStep | undefined;
120
+ /** Every step id a step can hand control to (control-flow successors). Total over bad input. */
121
+ export declare function workflowStepSuccessors(step: WorkflowStep): NodeId[];
122
+ export declare function workflowIsTerminalStep(step: WorkflowStep): boolean;
123
+ /** Every `Expression` embedded in a step — the leaves dependency / scope analysis walks. Total over bad input. */
124
+ export declare function workflowStepExpressions(step: WorkflowStep): Expression[];
125
+ /** Every `Expression` in a workflow — inputs carry none, so this is the step leaves. */
126
+ export declare function workflowExpressions(workflow: WorkflowDef): Expression[];
127
+ /** The `ActionDef` ids a workflow invokes. */
128
+ export declare function workflowActionIds(workflow: WorkflowDef): NodeId[];
129
+ /** The `EventDef` ids a workflow waits on. */
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[];
157
+ /**
158
+ * Step ids reachable from `entry` by control flow (spec14 §122). A step not in this set is
159
+ * unreachable and an authoring mistake.
160
+ */
161
+ export declare function workflowReachableSteps(workflow: WorkflowDef): Set<string>;
162
+ /** Whether the control-flow graph has a cycle (retries are runtime policy, not a cycle — §11). */
163
+ export declare function workflowHasCycle(workflow: WorkflowDef): boolean;
164
+ //# sourceMappingURL=workflows.d.ts.map
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Durable workflows (spec14).
3
+ *
4
+ * A `WorkflowDef` is a **long-running semantic computation with a durable control
5
+ * position** — not a background promise, a persisted callback, a job-queue entry, a cron
6
+ * task or a mutable JSON blob. The graph owns the orchestration meaning; the runtime owns
7
+ * scheduling, persistence, retries, leases, fencing, crash recovery and physical execution.
8
+ *
9
+ * The node is portable plain data: a closed step vocabulary, `Expression` trees for every
10
+ * leaf, and no JavaScript body of any kind (spec14 §7, §8). It compiles into
11
+ * `axiom.server.v8` and is inspectable through `AgentAPI.analyzeWorkflow`.
12
+ */
13
+ // ------------------------------------------------------------------- reserved scope ids
14
+ /** The matched event payload — resolvable only inside a `wait-event` step's `where` / `bind`. */
15
+ export const WORKFLOW_EVENT_SCOPE = 'EVENT';
16
+ /** The workflow's bound principal. */
17
+ export const WORKFLOW_PRINCIPAL_SCOPE = 'PRINCIPAL';
18
+ // -------------------------------------------------------------------------------- steps
19
+ export const WORKFLOW_STEP_TYPES = [
20
+ 'action',
21
+ 'wait-event',
22
+ 'timer',
23
+ 'branch',
24
+ 'complete',
25
+ 'fail',
26
+ ];
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
+ }
41
+ export function workflowStepById(workflow, stepId) {
42
+ const steps = Array.isArray(workflow?.steps) ? workflow.steps : [];
43
+ return steps.find((step) => !!step && typeof step === 'object' && String(step.id) === String(stepId));
44
+ }
45
+ /** Every step id a step can hand control to (control-flow successors). Total over bad input. */
46
+ export function workflowStepSuccessors(step) {
47
+ if (!isWorkflowStep(step))
48
+ return [];
49
+ switch (step.type) {
50
+ case 'action':
51
+ return step.onError ? [step.next, step.onError] : [step.next];
52
+ case 'wait-event':
53
+ return step.onTimeout ? [step.next, step.onTimeout] : [step.next];
54
+ case 'timer':
55
+ return [step.next];
56
+ case 'branch':
57
+ return [step.then, step.else];
58
+ case 'complete':
59
+ case 'fail':
60
+ return [];
61
+ default:
62
+ return [];
63
+ }
64
+ }
65
+ export function workflowIsTerminalStep(step) {
66
+ return isWorkflowStep(step) && (step.type === 'complete' || step.type === 'fail');
67
+ }
68
+ /** Every `Expression` embedded in a step — the leaves dependency / scope analysis walks. Total over bad input. */
69
+ export function workflowStepExpressions(step) {
70
+ if (!isWorkflowStep(step))
71
+ return [];
72
+ switch (step.type) {
73
+ case 'action':
74
+ return Object.values(step.arguments ?? {});
75
+ case 'wait-event':
76
+ return [...(step.where ? [step.where] : []), ...Object.values(step.bind ?? {})];
77
+ case 'timer':
78
+ return step.at ? [step.at] : [];
79
+ case 'branch':
80
+ return [step.when];
81
+ case 'complete':
82
+ return Object.values(step.output ?? {});
83
+ case 'fail':
84
+ return Object.values(step.error ?? {});
85
+ default:
86
+ return [];
87
+ }
88
+ }
89
+ /** Every `Expression` in a workflow — inputs carry none, so this is the step leaves. */
90
+ export function workflowExpressions(workflow) {
91
+ return workflow.steps.flatMap(workflowStepExpressions);
92
+ }
93
+ /** The `ActionDef` ids a workflow invokes. */
94
+ export function workflowActionIds(workflow) {
95
+ return (Array.isArray(workflow?.steps) ? workflow.steps : [])
96
+ .filter((s) => isWorkflowStep(s) && s.type === 'action')
97
+ .map((s) => s.action);
98
+ }
99
+ /** The `EventDef` ids a workflow waits on. */
100
+ export function workflowEventIds(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;
215
+ }
216
+ /**
217
+ * Step ids reachable from `entry` by control flow (spec14 §122). A step not in this set is
218
+ * unreachable and an authoring mistake.
219
+ */
220
+ export function workflowReachableSteps(workflow) {
221
+ const seen = new Set();
222
+ const stack = [String(workflow.entry)];
223
+ while (stack.length > 0) {
224
+ const id = stack.pop();
225
+ if (seen.has(id))
226
+ continue;
227
+ const step = workflowStepById(workflow, id);
228
+ if (!step)
229
+ continue;
230
+ seen.add(id);
231
+ for (const next of workflowStepSuccessors(step))
232
+ stack.push(String(next));
233
+ }
234
+ return seen;
235
+ }
236
+ /** Whether the control-flow graph has a cycle (retries are runtime policy, not a cycle — §11). */
237
+ export function workflowHasCycle(workflow) {
238
+ const WHITE = 0;
239
+ const GREY = 1;
240
+ const BLACK = 2;
241
+ const colour = new Map();
242
+ const visit = (id) => {
243
+ const step = workflowStepById(workflow, id);
244
+ if (!step)
245
+ return false;
246
+ colour.set(id, GREY);
247
+ for (const next of workflowStepSuccessors(step)) {
248
+ const c = colour.get(String(next)) ?? WHITE;
249
+ if (c === GREY)
250
+ return true;
251
+ if (c === WHITE && visit(String(next)))
252
+ return true;
253
+ }
254
+ colour.set(id, BLACK);
255
+ return false;
256
+ };
257
+ return visit(String(workflow.entry));
258
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-core",
3
- "version": "0.13.1-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",