@cynodia/axiom-core 0.13.0-alpha.1 → 0.14.0-alpha.1

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.
@@ -133,6 +133,13 @@ export declare const VALIDATION_CODES: {
133
133
  readonly duplicateReadPolicy: "DUPLICATE_READ_POLICY";
134
134
  /** A `QueryDef.filter` that is not a boolean expression, or reads outside `rowScopeId` / parameters / `PRINCIPAL`. */
135
135
  readonly invalidQueryPredicate: "INVALID_QUERY_PREDICATE";
136
+ /**
137
+ * A `QueryDef` clause or a `ReadPolicyDef` predicate references a `StateDef`. A query
138
+ * executes on the `DataProvider`, which binds no authority state; the reference would
139
+ * evaluate to nothing at run time (spec13.1 F2). Bind a runtime-varying value through a
140
+ * query parameter instead.
141
+ */
142
+ readonly queryStateRefNotAllowed: "QUERY_STATE_REF_NOT_ALLOWED";
136
143
  /** A `QuerySortKey` whose projected key is not an orderable type. */
137
144
  readonly invalidQuerySort: "INVALID_QUERY_SORT";
138
145
  /** A projected field that is not on the projection entity, or whose value type is incompatible. */
@@ -149,6 +156,36 @@ export declare const VALIDATION_CODES: {
149
156
  readonly invalidQueryOperation: "INVALID_QUERY_OPERATION";
150
157
  /** A `provider-record` location whose entity does not resolve, or whose identity field is not that entity's identity. */
151
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";
152
189
  /** A `MigrationDef` whose `fromSchema`/`toSchema` are not consecutive positive integers, or a migration whose `fromSchema` is at or beyond `graph.schemaVersion`. */
153
190
  readonly invalidMigrationVersion: "INVALID_MIGRATION_VERSION";
154
191
  /** No contiguous `MigrationDef` chain connects schema 1 to `graph.schemaVersion` — a step is missing (spec11 §13). */
@@ -125,6 +125,13 @@ export const VALIDATION_CODES = {
125
125
  duplicateReadPolicy: 'DUPLICATE_READ_POLICY',
126
126
  /** A `QueryDef.filter` that is not a boolean expression, or reads outside `rowScopeId` / parameters / `PRINCIPAL`. */
127
127
  invalidQueryPredicate: 'INVALID_QUERY_PREDICATE',
128
+ /**
129
+ * A `QueryDef` clause or a `ReadPolicyDef` predicate references a `StateDef`. A query
130
+ * executes on the `DataProvider`, which binds no authority state; the reference would
131
+ * evaluate to nothing at run time (spec13.1 F2). Bind a runtime-varying value through a
132
+ * query parameter instead.
133
+ */
134
+ queryStateRefNotAllowed: 'QUERY_STATE_REF_NOT_ALLOWED',
128
135
  /** A `QuerySortKey` whose projected key is not an orderable type. */
129
136
  invalidQuerySort: 'INVALID_QUERY_SORT',
130
137
  /** A projected field that is not on the projection entity, or whose value type is incompatible. */
@@ -141,6 +148,38 @@ export const VALIDATION_CODES = {
141
148
  invalidQueryOperation: 'INVALID_QUERY_OPERATION',
142
149
  /** A `provider-record` location whose entity does not resolve, or whose identity field is not that entity's identity. */
143
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',
144
183
  // Schema evolution & semantic migrations (0.11). `validateGraph` rejects an internally
145
184
  // inconsistent migration declaration before any persisted data is touched (spec11 §77, §78).
146
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.0') {
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';
@@ -72,9 +72,24 @@ export declare function applyDelta(rows: unknown[], delta: LiveQueryDelta, ident
72
72
  export interface QueryDependencySet {
73
73
  entityIds: Set<string>;
74
74
  stateIds: Set<string>;
75
+ /**
76
+ * `StateDef` ids a `QueryDef` clause (or the effective `ReadPolicy` predicate) references
77
+ * — which the provider cannot bind, so the query is not validly executable (spec13.1 F2).
78
+ * Never a real dependency; a query with any of these is not live-capable.
79
+ */
80
+ unsupportedStateRefs: Set<string>;
75
81
  /** A dependency that is not statically enumerable — re-evaluate on every commit (spec13 §29). */
76
82
  broad: boolean;
77
83
  }
84
+ /**
85
+ * The `StateDef` ids referenced by a `QueryDef`'s clauses outside the query's legal
86
+ * execution scope (`rowScopeId`, parameters, relationship binds, `PRINCIPAL`, nested
87
+ * iteration scopes). A query executes on the `DataProvider`, which binds no authority
88
+ * state, so any such reference would evaluate to nothing — it is a validation error
89
+ * (spec13.1 F2, §76). `includePolicy` also walks a `ReadPolicyDef` predicate, whose runtime
90
+ * scope is likewise state-free.
91
+ */
92
+ export declare function queryStateReferences(query: QueryDef, knownStateIds: ReadonlySet<string>, policy?: ReadPolicyDef): string[];
78
93
  /**
79
94
  * Conservative static dependencies of a live `QueryDef` (spec13 §26-§31). Includes the
80
95
  * source entity, every entity a used / source-touching relationship reaches, and every
@@ -109,11 +124,12 @@ export type LiveCapability = {
109
124
  /** Builtins whose value is not a pure function of committed state (spec13 §145, §146). */
110
125
  export declare const LIVE_QUERY_NONDETERMINISTIC_BUILTINS: readonly string[];
111
126
  /**
112
- * Classify a `QueryDef`'s live capability (spec13 §145, §146, §148, §149). A query whose
113
- * filter / sort / projection / group / aggregate reads a nondeterministic builtin
114
- * (`now`, `uuid`) is `not-live-capable`; an aggregate / grouped query, or one whose source
115
- * entity has no identity field, is `live-capable-reset-only` (whole-result resets only);
116
- * otherwise `live-capable` (incremental `insert`/`remove`/`update`/`move`).
127
+ * Classify a `QueryDef`'s live capability (spec13 §145, §146, §148, §149; spec13.1 §78). A
128
+ * query whose clause references a `StateDef` (when `knownStateIds` is supplied) is
129
+ * `not-live-capable` — it is not validly executable (F2). A query that reads a
130
+ * nondeterministic builtin (`now`, `uuid`) is `not-live-capable`; an aggregate / grouped
131
+ * query, or one whose source entity has no identity field, is `live-capable-reset-only`;
132
+ * otherwise `live-capable`.
117
133
  */
118
- export declare function queryLiveCapability(query: QueryDef, sourceIdentityFieldId: string | undefined): LiveCapability;
134
+ export declare function queryLiveCapability(query: QueryDef, sourceIdentityFieldId: string | undefined, knownStateIds?: ReadonlySet<string>): LiveCapability;
119
135
  //# sourceMappingURL=live-query.d.ts.map
@@ -182,6 +182,61 @@ export function applyDelta(rows, delta, identityFieldId) {
182
182
  }
183
183
  return result;
184
184
  }
185
+ /**
186
+ * The `StateDef` ids referenced by a `QueryDef`'s clauses outside the query's legal
187
+ * execution scope (`rowScopeId`, parameters, relationship binds, `PRINCIPAL`, nested
188
+ * iteration scopes). A query executes on the `DataProvider`, which binds no authority
189
+ * state, so any such reference would evaluate to nothing — it is a validation error
190
+ * (spec13.1 F2, §76). `includePolicy` also walks a `ReadPolicyDef` predicate, whose runtime
191
+ * scope is likewise state-free.
192
+ */
193
+ export function queryStateReferences(query, knownStateIds, policy) {
194
+ const found = new Set();
195
+ const localScopes = new Set([
196
+ String(query.rowScopeId),
197
+ ...(query.parameters ?? []).map((p) => String(p.id)),
198
+ 'PRINCIPAL',
199
+ ]);
200
+ for (const use of query.relationships ?? []) {
201
+ if (use.bindAs)
202
+ localScopes.add(String(use.bindAs));
203
+ }
204
+ if (policy)
205
+ localScopes.add(String(policy.rowScopeId));
206
+ const scan = (expression) => {
207
+ if (!expression)
208
+ return;
209
+ walkExpression(expression, (node) => {
210
+ if (node.kind === 'filter' ||
211
+ node.kind === 'find' ||
212
+ node.kind === 'map' ||
213
+ node.kind === 'sort' ||
214
+ node.kind === 'every' ||
215
+ node.kind === 'some') {
216
+ const scopeId = node.scopeId;
217
+ if (scopeId)
218
+ localScopes.add(String(scopeId));
219
+ }
220
+ if (node.kind === 'ref') {
221
+ const id = String(node.targetId);
222
+ if (!localScopes.has(id) && knownStateIds.has(id))
223
+ found.add(id);
224
+ }
225
+ });
226
+ };
227
+ scan(query.filter);
228
+ for (const key of query.sort ?? [])
229
+ scan(key.key);
230
+ for (const field of query.projection?.fields ?? [])
231
+ scan(field.value);
232
+ for (const expression of query.groupBy ?? [])
233
+ scan(expression);
234
+ for (const aggregate of query.aggregate ?? [])
235
+ scan(aggregate.key);
236
+ if (policy)
237
+ scan(policy.predicate);
238
+ return [...found];
239
+ }
185
240
  /**
186
241
  * Conservative static dependencies of a live `QueryDef` (spec13 §26-§31). Includes the
187
242
  * source entity, every entity a used / source-touching relationship reaches, and every
@@ -192,6 +247,7 @@ export function applyDelta(rows, delta, identityFieldId) {
192
247
  export function queryDependencies(query, policy, relationships, knownStateIds) {
193
248
  const entityIds = new Set([String(query.source)]);
194
249
  const stateIds = new Set();
250
+ const unsupportedStateRefs = new Set(queryStateReferences(query, knownStateIds, policy));
195
251
  let broad = false;
196
252
  const localScopes = new Set([
197
253
  String(query.rowScopeId),
@@ -202,6 +258,8 @@ export function queryDependencies(query, policy, relationships, knownStateIds) {
202
258
  if (use.bindAs)
203
259
  localScopes.add(String(use.bindAs));
204
260
  }
261
+ if (policy)
262
+ localScopes.add(String(policy.rowScopeId));
205
263
  const scan = (expression) => {
206
264
  if (!expression)
207
265
  return;
@@ -220,6 +278,10 @@ export function queryDependencies(query, policy, relationships, knownStateIds) {
220
278
  const id = String(node.targetId);
221
279
  if (localScopes.has(id))
222
280
  return;
281
+ // A `StateDef` ref is not a real dependency — the query cannot bind it (spec13.1 F2);
282
+ // it is collected in `unsupportedStateRefs` above, not here.
283
+ if (unsupportedStateRefs.has(id))
284
+ return;
223
285
  if (knownStateIds.has(id))
224
286
  stateIds.add(id);
225
287
  else
@@ -236,12 +298,8 @@ export function queryDependencies(query, policy, relationships, knownStateIds) {
236
298
  scan(expression);
237
299
  for (const aggregate of query.aggregate ?? [])
238
300
  scan(aggregate.key);
239
- if (policy) {
240
- // The policy predicate binds a candidate row to its own scope and the caller to
241
- // PRINCIPAL — neither is a StateDef dependency (spec 0.10 §56).
242
- localScopes.add(String(policy.rowScopeId));
301
+ if (policy)
243
302
  scan(policy.predicate);
244
- }
245
303
  for (const relationship of relationships) {
246
304
  if (String(relationship.from.entityId) === String(query.source) ||
247
305
  (query.relationships ?? []).some((use) => String(use.relationshipId) === String(relationship.id))) {
@@ -251,7 +309,7 @@ export function queryDependencies(query, policy, relationships, knownStateIds) {
251
309
  }
252
310
  if (policy)
253
311
  entityIds.add(String(policy.entityId));
254
- return { entityIds, stateIds, broad };
312
+ return { entityIds, stateIds, unsupportedStateRefs, broad };
255
313
  }
256
314
  /** Conservative "may this commit have changed the query's result?" (spec13 §27, §63, §64). */
257
315
  export function commitAffectsQuery(changeset, deps) {
@@ -279,13 +337,23 @@ function usesNondeterministic(expression) {
279
337
  return found;
280
338
  }
281
339
  /**
282
- * Classify a `QueryDef`'s live capability (spec13 §145, §146, §148, §149). A query whose
283
- * filter / sort / projection / group / aggregate reads a nondeterministic builtin
284
- * (`now`, `uuid`) is `not-live-capable`; an aggregate / grouped query, or one whose source
285
- * entity has no identity field, is `live-capable-reset-only` (whole-result resets only);
286
- * otherwise `live-capable` (incremental `insert`/`remove`/`update`/`move`).
340
+ * Classify a `QueryDef`'s live capability (spec13 §145, §146, §148, §149; spec13.1 §78). A
341
+ * query whose clause references a `StateDef` (when `knownStateIds` is supplied) is
342
+ * `not-live-capable` — it is not validly executable (F2). A query that reads a
343
+ * nondeterministic builtin (`now`, `uuid`) is `not-live-capable`; an aggregate / grouped
344
+ * query, or one whose source entity has no identity field, is `live-capable-reset-only`;
345
+ * otherwise `live-capable`.
287
346
  */
288
- export function queryLiveCapability(query, sourceIdentityFieldId) {
347
+ export function queryLiveCapability(query, sourceIdentityFieldId, knownStateIds) {
348
+ if (knownStateIds) {
349
+ const stateRefs = queryStateReferences(query, knownStateIds);
350
+ if (stateRefs.length > 0) {
351
+ return {
352
+ capability: 'not-live-capable',
353
+ reason: `query expression references a StateDef (${stateRefs.join(', ')}), which QueryDef execution scope does not bind`,
354
+ };
355
+ }
356
+ }
289
357
  const expressions = [
290
358
  query.filter,
291
359
  ...(query.sort ?? []).map((k) => k.key),
@@ -65,6 +65,7 @@ const EXECUTABLE_KINDS = [
65
65
  'transition-constraint',
66
66
  'storage',
67
67
  'relationship',
68
+ 'workflow',
68
69
  ];
69
70
  /** Keys removed everywhere in the tree — human metadata, never executable meaning. */
70
71
  const NON_SEMANTIC_KEYS = new Set(['name', 'description', 'label', 'metadata', AUTHORING_METADATA_KEY]);
@@ -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
@@ -1,9 +1,11 @@
1
- import { AGGREGATE_FUNCTIONS, BUILTIN_FUNCTIONS, expressionDefsIn } from './expressions.js';
1
+ import { AGGREGATE_FUNCTIONS, BUILTIN_FUNCTIONS, expressionDefsIn, walkExpression } from './expressions.js';
2
2
  import { VALIDATION_CODES } from './diagnostics.js';
3
3
  import { EDGE_KINDS, actionGuards, isMutationOperation } from './nodes.js';
4
4
  import { SUBSCRIPTION_BACKPRESSURE_POLICIES, subscriptionBackpressure, subscriptionQueueLimit, } from './subscriptions.js';
5
5
  import { BLOB_REF_FIELDS } from './storage.js';
6
6
  import { queryPaginationStrategy, sortKeyDirection } from './query.js';
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';
7
9
  import { relationshipIsToOne } from './relationships.js';
8
10
  import { validateMigrations } from './validate-migration.js';
9
11
  import { collectionType, entityType } from './type-ref.js';
@@ -171,6 +173,9 @@ function validateNode(node, context) {
171
173
  case 'read-policy':
172
174
  validateReadPolicy(node, context);
173
175
  return;
176
+ case 'workflow':
177
+ validateWorkflow(node, context);
178
+ return;
174
179
  case 'migration':
175
180
  // A `MigrationDef` is validated by the graph-level `validateMigrations` pass, which
176
181
  // needs every migration and `graph.schemaVersion` at once — chain contiguity and
@@ -854,6 +859,24 @@ function validateReadPolicy(policy, context) {
854
859
  nodeId: policy.id,
855
860
  });
856
861
  }
862
+ // A read policy predicate is AND-ed into a query filter and evaluated by the data
863
+ // provider, in the same state-free scope as the query itself (spec13.1 F2, §82).
864
+ for (const [id, node] of context.nodes) {
865
+ if (node.kind !== 'state')
866
+ continue;
867
+ let referenced = false;
868
+ walkExpression(policy.predicate, (expr) => {
869
+ if (expr.kind === 'ref' && expr.targetId === id && expr.targetId !== policy.rowScopeId)
870
+ referenced = true;
871
+ });
872
+ if (referenced) {
873
+ context.errors.push({
874
+ code: VALIDATION_CODES.queryStateRefNotAllowed,
875
+ message: `Read policy ${policy.id} references StateDef ${String(id)}; a policy predicate runs on the data provider and cannot bind authority state.`,
876
+ nodeId: policy.id,
877
+ });
878
+ }
879
+ }
857
880
  }
858
881
  /** Builds the single-row scope a read policy or a query filter is evaluated in. */
859
882
  function policyRowScope(rowScopeId, entityId, ownerId, context, entity) {
@@ -875,6 +898,252 @@ function policyRowScope(rowScopeId, entityId, ownerId, context, entity) {
875
898
  * endpoints consistent with the declared cardinality. Axiom never *infers* a link, so this
876
899
  * is where an inconsistent explicit one is caught.
877
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
+ if (typeof step.id === 'string' && !reachable.has(String(step.id))) {
1056
+ context.errors.push({
1057
+ code: VALIDATION_CODES.workflowUnreachableStep,
1058
+ message: `Workflow ${workflow.id} step ${String(step.id)} is unreachable from entry`,
1059
+ nodeId: workflow.id,
1060
+ });
1061
+ }
1062
+ }
1063
+ if (stepIds.has(String(workflow.entry)) && workflowHasCycle(workflow)) {
1064
+ context.errors.push({
1065
+ code: VALIDATION_CODES.workflowCycleNotAllowed,
1066
+ message: `Workflow ${workflow.id} has a control-flow cycle; retries are runtime policy, not graph edges`,
1067
+ nodeId: workflow.id,
1068
+ });
1069
+ }
1070
+ // Every reachable non-terminal step must be able to reach a terminal step (or an
1071
+ // intentional wait-event with no timeout is an acceptable "may never resolve" leaf).
1072
+ if (stepIds.has(String(workflow.entry)) && !workflowHasCycle(workflow)) {
1073
+ const canTerminate = new Map();
1074
+ const reaches = (id, seen) => {
1075
+ if (canTerminate.has(id))
1076
+ return canTerminate.get(id);
1077
+ if (seen.has(id))
1078
+ return false;
1079
+ seen.add(id);
1080
+ const step = workflowStepById(workflow, id);
1081
+ if (!step)
1082
+ return false;
1083
+ if (step.type === 'complete' || step.type === 'fail') {
1084
+ canTerminate.set(id, true);
1085
+ return true;
1086
+ }
1087
+ if (step.type === 'wait-event' && !step.timeout) {
1088
+ // An unbounded wait is an intentional durable leaf (spec14 §123).
1089
+ const ok = workflowStepSuccessors(step).some((n) => reaches(String(n), new Set(seen)));
1090
+ canTerminate.set(id, ok || true);
1091
+ return true;
1092
+ }
1093
+ const ok = workflowStepSuccessors(step).some((n) => reaches(String(n), new Set(seen)));
1094
+ canTerminate.set(id, ok);
1095
+ return ok;
1096
+ };
1097
+ for (const id of reachable) {
1098
+ if (!reaches(id, new Set())) {
1099
+ context.errors.push({
1100
+ code: VALIDATION_CODES.workflowNoTerminal,
1101
+ message: `Workflow ${workflow.id} step ${id} cannot reach complete or fail`,
1102
+ nodeId: workflow.id,
1103
+ });
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ function validateWorkflowRetry(retry, workflowId, stepId, context) {
1109
+ const bad = !(retry.maxAttempts >= 1) ||
1110
+ !(retry.initialDelaySeconds >= 0) ||
1111
+ !(retry.backoffMultiplier >= 1) ||
1112
+ !(retry.maxDelaySeconds >= retry.initialDelaySeconds);
1113
+ if (bad) {
1114
+ context.errors.push({
1115
+ code: VALIDATION_CODES.workflowInvalidRetryPolicy,
1116
+ message: `Workflow ${workflowId} step ${stepId} has an invalid retry policy`,
1117
+ nodeId: workflowId,
1118
+ });
1119
+ }
1120
+ }
1121
+ const WORKFLOW_NONDETERMINISTIC_BUILTINS = new Set(['now', 'uuid', 'random']);
1122
+ function validateWorkflowExpression(expression, workflowId, stepId, inputIds, bindings, eventScopeOk, context) {
1123
+ walkExpression(expression, (node) => {
1124
+ if (node.kind === 'ref') {
1125
+ const id = String(node.targetId);
1126
+ const inScope = inputIds.has(id) ||
1127
+ bindings.has(id) ||
1128
+ id === WORKFLOW_PRINCIPAL_SCOPE ||
1129
+ (eventScopeOk && id === WORKFLOW_EVENT_SCOPE);
1130
+ if (!inScope) {
1131
+ context.errors.push({
1132
+ code: VALIDATION_CODES.workflowExpressionScope,
1133
+ message: `Workflow ${workflowId} step ${stepId} references ${id}, which is outside workflow expression scope (inputs / bindings${eventScopeOk ? ' / EVENT' : ''} / PRINCIPAL)`,
1134
+ nodeId: workflowId,
1135
+ });
1136
+ }
1137
+ }
1138
+ if (node.kind === 'call' && WORKFLOW_NONDETERMINISTIC_BUILTINS.has(node.function)) {
1139
+ context.errors.push({
1140
+ code: VALIDATION_CODES.workflowNondeterministic,
1141
+ message: `Workflow ${workflowId} step ${stepId} calls ${node.function}; workflow expressions must be deterministic`,
1142
+ nodeId: workflowId,
1143
+ });
1144
+ }
1145
+ });
1146
+ }
878
1147
  function validateRelationship(relationship, context) {
879
1148
  const fromEntity = requireRelationshipEndpoint(relationship.from, relationship.id, context);
880
1149
  const toEntity = requireRelationshipEndpoint(relationship.to, relationship.id, context);
@@ -998,6 +1267,18 @@ function validateQuery(query, context) {
998
1267
  scope.types.set(use.bindAs, relationshipIsToOne(relationship) ? target : collectionType(target));
999
1268
  }
1000
1269
  }
1270
+ // A QueryDef executes on the DataProvider, which binds no authority state. A `ref` to a
1271
+ // `StateDef` in any clause would evaluate to nothing at run time, so every layer — this
1272
+ // validator, the compiler, dependency analysis, live-capability, AgentAPI and the runtime
1273
+ // — must reject it rather than one accepting what the others cannot execute (spec13.1 F2).
1274
+ const knownStateIds = new Set([...context.nodes.values()].filter((node) => node.kind === 'state').map((node) => String(node.id)));
1275
+ for (const stateId of queryStateReferences(query, knownStateIds)) {
1276
+ context.errors.push({
1277
+ code: VALIDATION_CODES.queryStateRefNotAllowed,
1278
+ message: `Query ${query.id} references StateDef ${stateId}; a QueryDef clause runs on the data provider and cannot bind authority state. Pass a runtime-varying value as a query parameter instead.`,
1279
+ nodeId: query.id,
1280
+ });
1281
+ }
1001
1282
  if (query.filter) {
1002
1283
  validateExpression(query.filter, query.id, context, scope);
1003
1284
  if (isKnownNonBoolean(inferExpressionType(query.filter, context.semantics, scope.types))) {
@@ -0,0 +1,131 @@
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
+ export declare function workflowStepById(workflow: WorkflowDef, stepId: NodeId | string): WorkflowStep | undefined;
113
+ /** Every step id a step can hand control to (control-flow successors). */
114
+ export declare function workflowStepSuccessors(step: WorkflowStep): NodeId[];
115
+ export declare function workflowIsTerminalStep(step: WorkflowStep): boolean;
116
+ /** Every `Expression` embedded in a step — the leaves dependency / scope analysis walks. */
117
+ export declare function workflowStepExpressions(step: WorkflowStep): Expression[];
118
+ /** Every `Expression` in a workflow — inputs carry none, so this is the step leaves. */
119
+ export declare function workflowExpressions(workflow: WorkflowDef): Expression[];
120
+ /** The `ActionDef` ids a workflow invokes. */
121
+ export declare function workflowActionIds(workflow: WorkflowDef): NodeId[];
122
+ /** The `EventDef` ids a workflow waits on. */
123
+ export declare function workflowEventIds(workflow: WorkflowDef): NodeId[];
124
+ /**
125
+ * Step ids reachable from `entry` by control flow (spec14 §122). A step not in this set is
126
+ * unreachable and an authoring mistake.
127
+ */
128
+ export declare function workflowReachableSteps(workflow: WorkflowDef): Set<string>;
129
+ /** Whether the control-flow graph has a cycle (retries are runtime policy, not a cycle — §11). */
130
+ export declare function workflowHasCycle(workflow: WorkflowDef): boolean;
131
+ //# sourceMappingURL=workflows.d.ts.map
@@ -0,0 +1,121 @@
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
+ export function workflowStepById(workflow, stepId) {
29
+ return workflow.steps.find((step) => String(step.id) === String(stepId));
30
+ }
31
+ /** Every step id a step can hand control to (control-flow successors). */
32
+ export function workflowStepSuccessors(step) {
33
+ switch (step.type) {
34
+ case 'action':
35
+ return step.onError ? [step.next, step.onError] : [step.next];
36
+ case 'wait-event':
37
+ return step.onTimeout ? [step.next, step.onTimeout] : [step.next];
38
+ case 'timer':
39
+ return [step.next];
40
+ case 'branch':
41
+ return [step.then, step.else];
42
+ case 'complete':
43
+ case 'fail':
44
+ return [];
45
+ }
46
+ }
47
+ export function workflowIsTerminalStep(step) {
48
+ return step.type === 'complete' || step.type === 'fail';
49
+ }
50
+ /** Every `Expression` embedded in a step — the leaves dependency / scope analysis walks. */
51
+ export function workflowStepExpressions(step) {
52
+ switch (step.type) {
53
+ case 'action':
54
+ return Object.values(step.arguments ?? {});
55
+ case 'wait-event':
56
+ return [...(step.where ? [step.where] : []), ...Object.values(step.bind ?? {})];
57
+ case 'timer':
58
+ return step.at ? [step.at] : [];
59
+ case 'branch':
60
+ return [step.when];
61
+ case 'complete':
62
+ return Object.values(step.output ?? {});
63
+ case 'fail':
64
+ return Object.values(step.error ?? {});
65
+ }
66
+ }
67
+ /** Every `Expression` in a workflow — inputs carry none, so this is the step leaves. */
68
+ export function workflowExpressions(workflow) {
69
+ return workflow.steps.flatMap(workflowStepExpressions);
70
+ }
71
+ /** The `ActionDef` ids a workflow invokes. */
72
+ export function workflowActionIds(workflow) {
73
+ return workflow.steps.filter((s) => s.type === 'action').map((s) => s.action);
74
+ }
75
+ /** The `EventDef` ids a workflow waits on. */
76
+ export function workflowEventIds(workflow) {
77
+ return workflow.steps.filter((s) => s.type === 'wait-event').map((s) => s.event);
78
+ }
79
+ /**
80
+ * Step ids reachable from `entry` by control flow (spec14 §122). A step not in this set is
81
+ * unreachable and an authoring mistake.
82
+ */
83
+ export function workflowReachableSteps(workflow) {
84
+ const seen = new Set();
85
+ const stack = [String(workflow.entry)];
86
+ while (stack.length > 0) {
87
+ const id = stack.pop();
88
+ if (seen.has(id))
89
+ continue;
90
+ const step = workflowStepById(workflow, id);
91
+ if (!step)
92
+ continue;
93
+ seen.add(id);
94
+ for (const next of workflowStepSuccessors(step))
95
+ stack.push(String(next));
96
+ }
97
+ return seen;
98
+ }
99
+ /** Whether the control-flow graph has a cycle (retries are runtime policy, not a cycle — §11). */
100
+ export function workflowHasCycle(workflow) {
101
+ const WHITE = 0;
102
+ const GREY = 1;
103
+ const BLACK = 2;
104
+ const colour = new Map();
105
+ const visit = (id) => {
106
+ const step = workflowStepById(workflow, id);
107
+ if (!step)
108
+ return false;
109
+ colour.set(id, GREY);
110
+ for (const next of workflowStepSuccessors(step)) {
111
+ const c = colour.get(String(next)) ?? WHITE;
112
+ if (c === GREY)
113
+ return true;
114
+ if (c === WHITE && visit(String(next)))
115
+ return true;
116
+ }
117
+ colour.set(id, BLACK);
118
+ return false;
119
+ };
120
+ return visit(String(workflow.entry));
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-core",
3
- "version": "0.13.0-alpha.1",
3
+ "version": "0.14.0-alpha.1",
4
4
  "description": "Application Graph, semantic types, locations and validation for Axiom.",
5
5
  "license": "MIT",
6
6
  "author": "AskTech AS",