@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.
- package/dist/diagnostics.d.ts +30 -0
- package/dist/diagnostics.js +32 -0
- package/dist/graph.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/semantic-identity.d.ts +18 -0
- package/dist/semantic-identity.js +20 -2
- package/dist/server-ir.d.ts +15 -1
- package/dist/server-ir.js +15 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.js +1 -0
- package/dist/validate.js +251 -0
- package/dist/workflows.d.ts +164 -0
- package/dist/workflows.js +258 -0
- package/package.json +1 -1
package/dist/diagnostics.d.ts
CHANGED
|
@@ -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). */
|
package/dist/diagnostics.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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,
|
package/dist/server-ir.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
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
|
+
}
|