@cynodia/axiom-runtime 0.7.0-alpha.2 → 0.8.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/dom.d.ts CHANGED
@@ -52,6 +52,17 @@ export interface ConfirmationRequest {
52
52
  /** A single line for hosts that can only ask a plain question. */
53
53
  message: string;
54
54
  }
55
+ export interface IntegrationQuerySuccess {
56
+ ok: true;
57
+ value: unknown;
58
+ }
59
+ export interface IntegrationQueryFailure {
60
+ ok: false;
61
+ code: string;
62
+ message: string;
63
+ retryable?: boolean;
64
+ }
65
+ export type IntegrationQueryOutcome = IntegrationQuerySuccess | IntegrationQueryFailure;
55
66
  /** Everything the runtime needs from its environment, so nothing is read from globals. */
56
67
  export interface HostEnvironment {
57
68
  document: DomDocument;
@@ -65,5 +76,15 @@ export interface HostEnvironment {
65
76
  uuid(): string;
66
77
  storage?: StorageAdapter;
67
78
  report?(message: string): void;
79
+ /**
80
+ * Executes an integration query operation by id and returns its result.
81
+ *
82
+ * Only the authoritative runtime ever calls this, executing an `integration-query`
83
+ * operation: no client-compiled action ever contains one (integrations default
84
+ * server-only), so a browser host never needs to implement it.
85
+ */
86
+ queryIntegration?(operationId: string, args: Record<string, unknown>, options: {
87
+ timeoutMs?: number;
88
+ }): Promise<IntegrationQueryOutcome>;
68
89
  }
69
90
  //# sourceMappingURL=dom.d.ts.map
package/dist/runtime.d.ts CHANGED
@@ -36,6 +36,14 @@ export declare const RUNTIME_DIAGNOSTIC_CODES: {
36
36
  readonly REMOTE_ACTION_UNAVAILABLE: "REMOTE_ACTION_UNAVAILABLE";
37
37
  /** The authority could not be reached. Authoritative state was not loaded. */
38
38
  readonly AUTHORITY_UNREACHABLE: "AUTHORITY_UNREACHABLE";
39
+ /** No host capable of executing an integration query is configured. */
40
+ readonly INTEGRATION_UNAVAILABLE: "INTEGRATION_UNAVAILABLE";
41
+ /** An integration query did not answer within its declared timeout. */
42
+ readonly INTEGRATION_TIMEOUT: "INTEGRATION_TIMEOUT";
43
+ /** A provider's response did not conform to the operation's declared result type. */
44
+ readonly INTEGRATION_RESULT_INVALID: "INTEGRATION_RESULT_INVALID";
45
+ /** An integration query failed. Never carries a provider secret. */
46
+ readonly INTEGRATION_QUERY_FAILED: "INTEGRATION_QUERY_FAILED";
39
47
  };
40
48
  export type RuntimeDiagnosticCode = (typeof RUNTIME_DIAGNOSTIC_CODES)[keyof typeof RUNTIME_DIAGNOSTIC_CODES];
41
49
  /**
@@ -56,6 +64,24 @@ export interface RuntimeDiagnostic {
56
64
  /** Structured context, so an agent never has to read the message. */
57
65
  details?: Record<string, unknown>;
58
66
  }
67
+ /**
68
+ * Recorded intent to perform an external effect — appended when an `integration-effect`
69
+ * operation is reached, and discarded on rollback exactly like a mutation log entry is.
70
+ * Distinct from `MutationLogEntry`: an effect is not a state mutation (spec §73), and
71
+ * `outcome` here means whether the **intent was committed**, not whether the external
72
+ * effect itself succeeded — that is a separate, later question a host answers.
73
+ */
74
+ export interface EffectIntentRecord {
75
+ id: string;
76
+ transactionId?: string;
77
+ actionId?: NodeId;
78
+ operationId: NodeId;
79
+ arguments: Record<string, unknown>;
80
+ idempotencyKey?: string;
81
+ succeededEventId?: NodeId;
82
+ failedEventId?: NodeId;
83
+ outcome?: 'committed' | 'rolled-back';
84
+ }
59
85
  export interface ActionResult {
60
86
  ok: boolean;
61
87
  diagnostics: RuntimeDiagnostic[];
@@ -175,6 +201,12 @@ export interface AxiomRuntime {
175
201
  getActionOutcome(id: NodeId): ActionOutcome | undefined;
176
202
  /** Every mutation this runtime has applied, in order, with its semantic location. */
177
203
  getMutationLog(): MutationLogEntry[];
204
+ /**
205
+ * Every `integration-effect` intent recorded so far, in order — a log distinct from the
206
+ * mutation log because an effect is not a state mutation. `outcome` reflects whether the
207
+ * *intent* was committed, not whether the external effect itself has run yet.
208
+ */
209
+ getEffectIntents(): EffectIntentRecord[];
178
210
  registerNativeOperation(implementationId: string, implementation: NativeImplementation): void;
179
211
  /**
180
212
  * Invokes an action and waits for its outcome. For a remote action this awaits the
@@ -207,6 +239,18 @@ export interface AxiomRuntime {
207
239
  ok: false;
208
240
  diagnostic: RuntimeDiagnostic;
209
241
  };
242
+ /**
243
+ * Evaluates an expression with extra ids bound in scope, keyed by id — how a trigger's
244
+ * `arguments`/`enabledWhen` resolve `ref()` of the trigger's own id to read an event
245
+ * payload, the same way a `for-each` body resolves its scope id.
246
+ */
247
+ evaluateWithBindings(expression: Expression, bindings: Record<string, unknown>): {
248
+ ok: true;
249
+ value: unknown;
250
+ } | {
251
+ ok: false;
252
+ diagnostic: RuntimeDiagnostic;
253
+ };
210
254
  }
211
255
  export declare function createAxiomRuntime(options: AxiomRuntimeOptions): AxiomRuntime;
212
256
  /** Builds a host bound to the browser globals. Used by generated pages. */
package/dist/runtime.js CHANGED
@@ -40,6 +40,14 @@ export const RUNTIME_DIAGNOSTIC_CODES = {
40
40
  REMOTE_ACTION_UNAVAILABLE: 'REMOTE_ACTION_UNAVAILABLE',
41
41
  /** The authority could not be reached. Authoritative state was not loaded. */
42
42
  AUTHORITY_UNREACHABLE: 'AUTHORITY_UNREACHABLE',
43
+ /** No host capable of executing an integration query is configured. */
44
+ INTEGRATION_UNAVAILABLE: 'INTEGRATION_UNAVAILABLE',
45
+ /** An integration query did not answer within its declared timeout. */
46
+ INTEGRATION_TIMEOUT: 'INTEGRATION_TIMEOUT',
47
+ /** A provider's response did not conform to the operation's declared result type. */
48
+ INTEGRATION_RESULT_INVALID: 'INTEGRATION_RESULT_INVALID',
49
+ /** An integration query failed. Never carries a provider secret. */
50
+ INTEGRATION_QUERY_FAILED: 'INTEGRATION_QUERY_FAILED',
43
51
  };
44
52
  const MISSING = Symbol('missing');
45
53
  function unwrapType(type) {
@@ -125,6 +133,7 @@ export function createAxiomRuntime(options) {
125
133
  let started = false;
126
134
  let transactionCounter = 0;
127
135
  const mutationLog = [];
136
+ const effectIntentLog = [];
128
137
  const inputValidation = options.inputValidation ?? 'immediate';
129
138
  const remote = options.remote;
130
139
  const remoteActionIds = new Set(ir.remoteActionIds ?? []);
@@ -436,6 +445,11 @@ export function createAxiomRuntime(options) {
436
445
  entry.outcome = outcome;
437
446
  }
438
447
  }
448
+ for (const entry of effectIntentLog) {
449
+ if (entry.transactionId === transaction.id && entry.outcome === undefined) {
450
+ entry.outcome = outcome;
451
+ }
452
+ }
439
453
  }
440
454
  /**
441
455
  * Classifies an evaluation failure. The code says what kind of failure it was, so an
@@ -1068,6 +1082,33 @@ export function createAxiomRuntime(options) {
1068
1082
  }
1069
1083
  return;
1070
1084
  }
1085
+ case 'integration-query':
1086
+ // The result was already resolved and bound into the action's scope before this
1087
+ // transaction opened — see `runActionAsync`. Nothing to do here; this case exists
1088
+ // only so the kind is recognized rather than falling to `default`.
1089
+ return;
1090
+ case 'integration-effect': {
1091
+ // Never calls an adapter here: reaching this operation only records intent, in the
1092
+ // same log a mutation is recorded in, discarded on rollback the same way. Dispatch
1093
+ // to the adapter happens only after the surrounding transaction commits.
1094
+ const args = {};
1095
+ for (const [key, argument] of Object.entries(operation.arguments ?? {})) {
1096
+ args[key] = cloneValue(evaluate(argument, scope));
1097
+ }
1098
+ effectIntentLog.push({
1099
+ id: host.uuid(),
1100
+ transactionId: context.transactionId,
1101
+ ...(context.sourceNodeId ? { actionId: context.sourceNodeId } : {}),
1102
+ operationId: operation.operationId,
1103
+ arguments: args,
1104
+ ...(operation.idempotencyKey
1105
+ ? { idempotencyKey: toText(evaluate(operation.idempotencyKey, scope)) }
1106
+ : {}),
1107
+ ...(operation.succeededEventId ? { succeededEventId: operation.succeededEventId } : {}),
1108
+ ...(operation.failedEventId ? { failedEventId: operation.failedEventId } : {}),
1109
+ });
1110
+ return;
1111
+ }
1071
1112
  default:
1072
1113
  result.push({
1073
1114
  code: RUNTIME_DIAGNOSTIC_CODES.UNSUPPORTED_OPERATION,
@@ -1204,14 +1245,14 @@ export function createAxiomRuntime(options) {
1204
1245
  }
1205
1246
  renderApplication();
1206
1247
  }
1207
- function runAction(actionId, args = {}) {
1248
+ function runAction(actionId, args = {}, presetBindings) {
1208
1249
  const remoteAction = remoteActionIds.has(actionId) ? ir.actions[actionId] : undefined;
1209
1250
  if (remoteAction) {
1210
1251
  return runRemoteAction(remoteAction, args);
1211
1252
  }
1212
1253
  return collecting((collected) => {
1213
1254
  const started = collected.length;
1214
- const result = runActionCollecting(actionId, args, collected);
1255
+ const result = runActionCollecting(actionId, args, collected, presetBindings);
1215
1256
  if (ir.actions[actionId]) {
1216
1257
  // The record is this invocation's own diagnostics, so a later invocation of another
1217
1258
  // action can never appear to belong to this one.
@@ -1226,9 +1267,87 @@ export function createAxiomRuntime(options) {
1226
1267
  return result;
1227
1268
  });
1228
1269
  }
1270
+ function actionHasIntegrationQuery(action) {
1271
+ return (action.operations ?? []).some((operation) => operation.kind === 'integration-query');
1272
+ }
1273
+ /**
1274
+ * Resolves every top-level `integration-query` operation before the transaction opens,
1275
+ * then runs the action exactly as `runAction` would with the results bound into scope.
1276
+ *
1277
+ * Queries are awaited here, ahead of guards and the transaction — the same "none of this
1278
+ * mutates anything" phase preconditions already occupy — because a query's own validation
1279
+ * scope proves guards can never reference `ref(bindAs)`. `integration-effect` operations
1280
+ * need no async pre-step: they only record intent, synchronously, during the ordinary
1281
+ * operation loop.
1282
+ */
1283
+ async function runActionAsync(actionId, args = {}) {
1284
+ const action = ir.actions[actionId];
1285
+ if (!action || !actionHasIntegrationQuery(action) || remoteActionIds.has(actionId)) {
1286
+ return runAction(actionId, args);
1287
+ }
1288
+ const scope = rootScope();
1289
+ for (const parameter of action.parameters ?? []) {
1290
+ scope.values.set(parameter.id, args[parameter.id] ?? null);
1291
+ }
1292
+ const presetBindings = new Map();
1293
+ for (const operation of action.operations ?? []) {
1294
+ if (operation.kind !== 'integration-query') {
1295
+ continue;
1296
+ }
1297
+ const queryArgs = {};
1298
+ for (const [key, argument] of Object.entries(operation.arguments ?? {})) {
1299
+ queryArgs[key] = evaluate(argument, scope);
1300
+ }
1301
+ let outcome;
1302
+ try {
1303
+ outcome = await host.queryIntegration?.(operation.operationId, queryArgs, {
1304
+ ...(operation.timeoutMs !== undefined ? { timeoutMs: operation.timeoutMs } : {}),
1305
+ });
1306
+ }
1307
+ catch (error) {
1308
+ outcome = {
1309
+ ok: false,
1310
+ code: RUNTIME_DIAGNOSTIC_CODES.INTEGRATION_QUERY_FAILED,
1311
+ message: error instanceof Error ? error.message : String(error),
1312
+ };
1313
+ }
1314
+ if (!outcome) {
1315
+ const diagnostic = {
1316
+ code: RUNTIME_DIAGNOSTIC_CODES.INTEGRATION_UNAVAILABLE,
1317
+ message: `${action.name ?? action.id} calls an integration query, but this host cannot perform one`,
1318
+ severity: 'error',
1319
+ nodeId: action.id,
1320
+ actionId: action.id,
1321
+ };
1322
+ report(diagnostic);
1323
+ recordOutcome(actionId, 'failed', [diagnostic]);
1324
+ renderApplication();
1325
+ return { ok: false, diagnostics: [diagnostic] };
1326
+ }
1327
+ if (!outcome.ok) {
1328
+ const diagnostic = {
1329
+ code: RUNTIME_DIAGNOSTIC_CODES[outcome.code]
1330
+ ? outcome.code
1331
+ : RUNTIME_DIAGNOSTIC_CODES.INTEGRATION_QUERY_FAILED,
1332
+ message: outcome.message,
1333
+ severity: 'error',
1334
+ nodeId: action.id,
1335
+ actionId: action.id,
1336
+ details: { operationId: String(operation.operationId), retryable: outcome.retryable === true },
1337
+ };
1338
+ report(diagnostic);
1339
+ recordOutcome(actionId, 'failed', [diagnostic]);
1340
+ renderApplication();
1341
+ return { ok: false, diagnostics: [diagnostic] };
1342
+ }
1343
+ scope.values.set(operation.bindAs, outcome.value);
1344
+ presetBindings.set(operation.bindAs, outcome.value);
1345
+ }
1346
+ return runAction(actionId, args, presetBindings);
1347
+ }
1229
1348
  /** Set when the most recent invocation stopped because a confirmation was declined. */
1230
1349
  let cancelled = false;
1231
- function runActionCollecting(actionId, args, collected) {
1350
+ function runActionCollecting(actionId, args, collected, presetBindings) {
1232
1351
  const action = ir.actions[actionId];
1233
1352
  if (!action) {
1234
1353
  const failure = {
@@ -1243,6 +1362,11 @@ export function createAxiomRuntime(options) {
1243
1362
  for (const parameter of action.parameters ?? []) {
1244
1363
  scope.values.set(parameter.id, args[parameter.id] ?? null);
1245
1364
  }
1365
+ // Integration query results resolved before this transaction opened (see
1366
+ // `runActionAsync`) — bound the same way a parameter is, so `ref(bindAs)` resolves.
1367
+ for (const [id, value] of presetBindings ?? []) {
1368
+ scope.values.set(id, value);
1369
+ }
1246
1370
  const failures = [];
1247
1371
  for (const parameter of action.parameters ?? []) {
1248
1372
  if (parameter.required && !isPresent(scope.values.get(parameter.id))) {
@@ -2323,10 +2447,17 @@ export function createAxiomRuntime(options) {
2323
2447
  getMutationLog() {
2324
2448
  return mutationLog.map((entry) => ({ ...entry }));
2325
2449
  },
2450
+ getEffectIntents() {
2451
+ return effectIntentLog.map((entry) => ({ ...entry }));
2452
+ },
2326
2453
  registerNativeOperation(implementationId, implementation) {
2327
2454
  natives.set(implementationId, implementation);
2328
2455
  },
2329
2456
  async invokeActionAsync(id, args = {}) {
2457
+ const action = ir.actions[id];
2458
+ if (action && actionHasIntegrationQuery(action) && !remoteActionIds.has(id)) {
2459
+ return runActionAsync(id, args);
2460
+ }
2330
2461
  const result = runAction(id, args);
2331
2462
  if (!result.pending) {
2332
2463
  return result;
@@ -2343,6 +2474,14 @@ export function createAxiomRuntime(options) {
2343
2474
  const outcome = tryEvaluate(expression, rootScope(), {});
2344
2475
  return outcome.ok ? { ok: true, value: outcome.value } : { ok: false, diagnostic: outcome.diagnostic };
2345
2476
  },
2477
+ evaluateWithBindings(expression, bindings) {
2478
+ let scope = rootScope();
2479
+ for (const [id, value] of Object.entries(bindings)) {
2480
+ scope = childScope(scope, id, value);
2481
+ }
2482
+ const outcome = tryEvaluate(expression, scope, {});
2483
+ return outcome.ok ? { ok: true, value: outcome.value } : { ok: false, diagnostic: outcome.diagnostic };
2484
+ },
2346
2485
  };
2347
2486
  }
2348
2487
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-runtime",
3
- "version": "0.7.0-alpha.2",
3
+ "version": "0.8.0-alpha.1",
4
4
  "description": "Domain-independent runtime that executes an Axiom application graph.",
5
5
  "license": "MIT",
6
6
  "author": "AskTech AS",
@@ -31,7 +31,7 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "@cynodia/axiom-core": "0.7.0-alpha.2"
34
+ "@cynodia/axiom-core": "0.8.0-alpha.1"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc -b tsconfig.json tsconfig.test.json",