@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.
- package/dist/diagnostics.d.ts +37 -0
- package/dist/diagnostics.js +39 -0
- package/dist/graph.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/live-query.d.ts +22 -6
- package/dist/live-query.js +80 -12
- package/dist/semantic-identity.js +1 -0
- 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 +282 -1
- package/dist/workflows.d.ts +131 -0
- package/dist/workflows.js +121 -0
- package/package.json +1 -1
package/dist/diagnostics.d.ts
CHANGED
|
@@ -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). */
|
package/dist/diagnostics.js
CHANGED
|
@@ -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.
|
|
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';
|
package/dist/live-query.d.ts
CHANGED
|
@@ -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
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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
|
package/dist/live-query.js
CHANGED
|
@@ -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
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
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]);
|
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
|
@@ -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
|
+
}
|