@cynodia/axiom-core 0.7.0-alpha.2 → 0.8.1-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/README.md CHANGED
@@ -9,14 +9,18 @@ The Application Graph and its semantic model: nodes, fields, structured types,
9
9
  expressions, **locations** (addressable writable positions), edge derivation, validation
10
10
  and type inference. It also owns the presentation and UX layer — semantic roles, layout and
11
11
  spacing tokens, device classes, value formats and the `Theme` — with presentation
12
- resolution and validation.
12
+ resolution and validation. As of 0.8 it also owns the vocabulary for external
13
+ integrations, effects, triggers and events: `IntegrationDef`, `IntegrationOperationDef`,
14
+ `EventDef`, `TriggerDef`, and the `integration-query`/`integration-effect` operation
15
+ kinds — see [`docs/INTEGRATIONS.md`](https://github.com/cynodia/axiom/blob/main/docs/INTEGRATIONS.md).
13
16
 
14
17
  Presentation lives here because it is part of the canonical graph: it is intent, not
15
18
  styling, and it names no colour, length or CSS property.
16
19
 
17
20
  Main exports: `ApplicationGraph`, `TypeRef` builders, expression builders, `Location`
18
21
  builders, `Presentation`, `Theme`, `resolvePresentationMap`, `validateGraph`,
19
- `VALIDATION_CODES`, `ApplicationIR`.
22
+ `VALIDATION_CODES`, `ApplicationIR`, `ServerIR`, `IntegrationDef`, `IntegrationOperationDef`,
23
+ `EventDef`, `TriggerDef`.
20
24
 
21
25
  ## Installation
22
26
 
@@ -43,6 +43,8 @@ export interface AuthorityContext {
43
43
  export declare function authorityContext(nodes: readonly AnyNode[], principalEntityId?: NodeId): AuthorityContext;
44
44
  /** Every state a set of expressions reads, following derived state transitively. */
45
45
  export declare function statesReadBy(expressions: readonly Expression[], context: AuthorityContext): Set<NodeId>;
46
+ /** Whether an action calls out to an integration anywhere in its top-level operations. */
47
+ export declare function actionUsesIntegration(action: ActionDef): boolean;
46
48
  /** The states an action writes, following `for-each`, `invoke` and declared native effects. */
47
49
  export declare function statesWrittenBy(action: ActionDef, context: AuthorityContext, visited?: Set<NodeId>): Set<NodeId>;
48
50
  /** The states an action reads: its guards, its values, its selectors, its authorization. */
package/dist/authority.js CHANGED
@@ -112,10 +112,23 @@ function operationExpressions(operation) {
112
112
  found.push(...locationExpressions(operation.resultTarget));
113
113
  }
114
114
  break;
115
+ case 'integration-query':
116
+ found.push(...Object.values(operation.arguments ?? {}));
117
+ break;
118
+ case 'integration-effect':
119
+ found.push(...Object.values(operation.arguments ?? {}));
120
+ if (operation.idempotencyKey) {
121
+ found.push(operation.idempotencyKey);
122
+ }
123
+ break;
115
124
  default:
116
125
  }
117
126
  return found;
118
127
  }
128
+ /** Whether an action calls out to an integration anywhere in its top-level operations. */
129
+ export function actionUsesIntegration(action) {
130
+ return (action.operations ?? []).some((operation) => operation.kind === 'integration-query' || operation.kind === 'integration-effect');
131
+ }
119
132
  /** The states an action writes, following `for-each`, `invoke` and declared native effects. */
120
133
  export function statesWrittenBy(action, context, visited = new Set()) {
121
134
  const found = new Set();
@@ -152,6 +165,12 @@ export function statesWrittenBy(action, context, visited = new Set()) {
152
165
  }
153
166
  }
154
167
  break;
168
+ case 'integration-query':
169
+ case 'integration-effect':
170
+ // Neither writes Axiom state directly: a query's result is a transaction-local
171
+ // scope binding, and an effect's outcome reaches state only through a follow-up
172
+ // action invoked from its success/failure event.
173
+ break;
155
174
  default:
156
175
  }
157
176
  }
@@ -194,6 +213,12 @@ export function statesReadByAction(action, context, visited = new Set()) {
194
213
  * cannot disagree with what the action actually does.
195
214
  */
196
215
  export function actionAuthority(action, context) {
216
+ // Integrations default server-only (spec §65: secrets, trust, CORS, auditability,
217
+ // deterministic authority), so an action that calls one is unconditionally server —
218
+ // independent of what it writes.
219
+ if (actionUsesIntegration(action)) {
220
+ return 'server';
221
+ }
197
222
  for (const stateId of statesWrittenBy(action, context)) {
198
223
  const state = context.states.get(stateId);
199
224
  if (state && stateAuthority(state) === 'server') {
@@ -143,6 +143,27 @@ export function deriveEdges(nodes) {
143
143
  // bindings, no caller scope. Its parameters resolve to nothing here by design.
144
144
  reads(node.id, node.expression, new Map());
145
145
  break;
146
+ case 'integration-operation':
147
+ link(node.id, node.integrationId, 'references');
148
+ break;
149
+ case 'trigger':
150
+ link(node.id, node.actionId, 'invokes');
151
+ if (node.when.kind === 'event') {
152
+ link(node.id, node.when.eventId, 'references');
153
+ }
154
+ if (node.when.kind === 'lifecycle' && node.when.routeId) {
155
+ link(node.id, node.when.routeId, 'depends-on');
156
+ }
157
+ for (const argument of Object.values(node.arguments ?? {})) {
158
+ reads(node.id, argument, rootScope);
159
+ }
160
+ if (node.enabledWhen) {
161
+ reads(node.id, node.enabledWhen, rootScope);
162
+ }
163
+ break;
164
+ case 'integration':
165
+ case 'event':
166
+ break;
146
167
  default:
147
168
  }
148
169
  }
@@ -415,6 +436,27 @@ function linkOperations(actionId, operations, linker, scope) {
415
436
  }
416
437
  }
417
438
  break;
439
+ case 'integration-query':
440
+ linker.link(actionId, operation.operationId, 'references');
441
+ for (const argument of Object.values(operation.arguments ?? {})) {
442
+ linker.reads(actionId, argument, scope);
443
+ }
444
+ break;
445
+ case 'integration-effect':
446
+ linker.link(actionId, operation.operationId, 'references');
447
+ for (const argument of Object.values(operation.arguments ?? {})) {
448
+ linker.reads(actionId, argument, scope);
449
+ }
450
+ if (operation.idempotencyKey) {
451
+ linker.reads(actionId, operation.idempotencyKey, scope);
452
+ }
453
+ if (operation.succeededEventId) {
454
+ linker.link(actionId, operation.succeededEventId, 'references');
455
+ }
456
+ if (operation.failedEventId) {
457
+ linker.link(actionId, operation.failedEventId, 'references');
458
+ }
459
+ break;
418
460
  default:
419
461
  }
420
462
  }
@@ -88,5 +88,24 @@ export declare const VALIDATION_CODES: {
88
88
  readonly serverOnlyStateObserved: "SERVER_ONLY_STATE_OBSERVED";
89
89
  readonly invalidPrincipalEntity: "INVALID_PRINCIPAL_ENTITY";
90
90
  readonly missingActionArgument: "MISSING_ACTION_ARGUMENT";
91
+ /** An `IntegrationOperationDef.integrationId`, or an operation's `operationId`, that does not resolve. */
92
+ readonly unknownIntegration: "UNKNOWN_INTEGRATION";
93
+ readonly unknownIntegrationOperation: "UNKNOWN_INTEGRATION_OPERATION";
94
+ /** An `integration-query` operation naming an effect, or an `integration-effect` naming a query. */
95
+ readonly integrationOperationModeMismatch: "INTEGRATION_OPERATION_MODE_MISMATCH";
96
+ /** A missing required argument, or an argument the operation declares no parameter for. */
97
+ readonly integrationArgumentMismatch: "INTEGRATION_ARGUMENT_MISMATCH";
98
+ readonly triggerActionNotFound: "TRIGGER_ACTION_NOT_FOUND";
99
+ readonly triggerIntervalNotPositive: "TRIGGER_INTERVAL_NOT_POSITIVE";
100
+ /** An event id that does not resolve to an `EventDef` — a trigger's `eventId`, or an effect's success/failure event. */
101
+ readonly unknownEvent: "UNKNOWN_EVENT";
102
+ /** An `event` trigger targeting a client-authority action, or a `route-enter`/`route-leave` trigger targeting a server-authority one. */
103
+ readonly triggerWrongAuthority: "TRIGGER_WRONG_AUTHORITY";
104
+ /** A trigger — which always invokes with `source: 'system'` — targets an action whose `invocation.allowedSources` excludes `'system'`; the trigger could never succeed. */
105
+ readonly triggerTargetSourceMismatch: "TRIGGER_TARGET_SOURCE_MISMATCH";
106
+ /** `ActionDef.invocation.allowedSources` is present but empty — the action could never be invoked at all. */
107
+ readonly invalidInvocationSource: "INVALID_INVOCATION_SOURCE";
108
+ /** A client-authority trigger of a kind the intended trigger runtime does not execute — it would validate and compile but never fire (spec 8.1 §31-36). */
109
+ readonly clientTriggerUnsupported: "CLIENT_TRIGGER_UNSUPPORTED";
91
110
  };
92
111
  //# sourceMappingURL=diagnostics.d.ts.map
@@ -76,4 +76,24 @@ export const VALIDATION_CODES = {
76
76
  serverOnlyStateObserved: 'SERVER_ONLY_STATE_OBSERVED',
77
77
  invalidPrincipalEntity: 'INVALID_PRINCIPAL_ENTITY',
78
78
  missingActionArgument: 'MISSING_ACTION_ARGUMENT',
79
+ // Integrations, effects, triggers and events.
80
+ /** An `IntegrationOperationDef.integrationId`, or an operation's `operationId`, that does not resolve. */
81
+ unknownIntegration: 'UNKNOWN_INTEGRATION',
82
+ unknownIntegrationOperation: 'UNKNOWN_INTEGRATION_OPERATION',
83
+ /** An `integration-query` operation naming an effect, or an `integration-effect` naming a query. */
84
+ integrationOperationModeMismatch: 'INTEGRATION_OPERATION_MODE_MISMATCH',
85
+ /** A missing required argument, or an argument the operation declares no parameter for. */
86
+ integrationArgumentMismatch: 'INTEGRATION_ARGUMENT_MISMATCH',
87
+ triggerActionNotFound: 'TRIGGER_ACTION_NOT_FOUND',
88
+ triggerIntervalNotPositive: 'TRIGGER_INTERVAL_NOT_POSITIVE',
89
+ /** An event id that does not resolve to an `EventDef` — a trigger's `eventId`, or an effect's success/failure event. */
90
+ unknownEvent: 'UNKNOWN_EVENT',
91
+ /** An `event` trigger targeting a client-authority action, or a `route-enter`/`route-leave` trigger targeting a server-authority one. */
92
+ triggerWrongAuthority: 'TRIGGER_WRONG_AUTHORITY',
93
+ /** A trigger — which always invokes with `source: 'system'` — targets an action whose `invocation.allowedSources` excludes `'system'`; the trigger could never succeed. */
94
+ triggerTargetSourceMismatch: 'TRIGGER_TARGET_SOURCE_MISMATCH',
95
+ /** `ActionDef.invocation.allowedSources` is present but empty — the action could never be invoked at all. */
96
+ invalidInvocationSource: 'INVALID_INVOCATION_SOURCE',
97
+ /** A client-authority trigger of a kind the intended trigger runtime does not execute — it would validate and compile but never fire (spec 8.1 §31-36). */
98
+ clientTriggerUnsupported: 'CLIENT_TRIGGER_UNSUPPORTED',
79
99
  };
@@ -0,0 +1,43 @@
1
+ import type { FieldId, NodeId } from './ids.js';
2
+ import type { EntityDef } from './nodes.js';
3
+ import type { TypeRef } from './type-ref.js';
4
+ /**
5
+ * Reserved field ids for an `integration-effect`'s `succeededEventId`/`failedEventId`
6
+ * payload — spec 8.1 §37-41's structured envelope, replacing the 0.8.0 shape (the raw
7
+ * adapter result for success, a formatted `"<code>: <message>"` string for failure) that
8
+ * forced a follow-up action to parse text to correlate a failure back to the effect that
9
+ * caused it.
10
+ *
11
+ * One shape covers both outcomes (spec §40's "structured `EffectOutcome` envelope for both
12
+ * success/failure"), rather than a distinct entity per outcome: field ids are unique across
13
+ * a whole graph — like `GROUP_KEY_FIELD`/`GROUP_ITEMS_FIELD` — so two entities could not both
14
+ * declare `effectId`/`operationId`/`integrationId` without colliding. A success dispatch
15
+ * populates `result`; a failure dispatch populates `code`/`message`/`retryable`; neither
16
+ * ever populates both, and none of `result`/`code`/`message`/`retryable` is `required`, so
17
+ * either shape validates.
18
+ *
19
+ * Unlike `GROUP_KEY_FIELD`/`GROUP_ITEMS_FIELD`, these are not runtime-enforced reservations
20
+ * — a payload is validated as an ordinary `entity` type, the same as any other event. They
21
+ * are a naming convention plus the builder below, so every application's effect outcome
22
+ * events share one shape an agent only has to learn once.
23
+ */
24
+ export declare const EFFECT_ID_FIELD: FieldId;
25
+ export declare const EFFECT_INTEGRATION_ID_FIELD: FieldId;
26
+ export declare const EFFECT_OPERATION_ID_FIELD: FieldId;
27
+ export declare const EFFECT_CODE_FIELD: FieldId;
28
+ export declare const EFFECT_MESSAGE_FIELD: FieldId;
29
+ export declare const EFFECT_RETRYABLE_FIELD: FieldId;
30
+ export declare const EFFECT_IDEMPOTENCY_KEY_FIELD: FieldId;
31
+ export declare const EFFECT_CORRELATION_ID_FIELD: FieldId;
32
+ export declare const EFFECT_RESULT_FIELD: FieldId;
33
+ /**
34
+ * The canonical entity shape for an `integration-effect`'s `succeededEventId` and
35
+ * `failedEventId` payload alike. Declare it once with
36
+ * `graph.addNode(effectOutcomeEntity(ENTITY_ID, resultType))` and reference it from both
37
+ * `EventDef`s with `entityType(ENTITY_ID)`. `resultType` is the effect operation's own
38
+ * declared result type — if a graph's effect operations return incompatible result types,
39
+ * they need either a common supertype here or their own outcome entity each (field ids are
40
+ * graph-global, so at most one `effectOutcomeEntity` may share a given result shape).
41
+ */
42
+ export declare function effectOutcomeEntity(id: NodeId, resultType: TypeRef): EntityDef;
43
+ //# sourceMappingURL=effect-outcome.d.ts.map
@@ -0,0 +1,57 @@
1
+ import { fieldId } from './ids.js';
2
+ import { primitiveType } from './type-ref.js';
3
+ /**
4
+ * Reserved field ids for an `integration-effect`'s `succeededEventId`/`failedEventId`
5
+ * payload — spec 8.1 §37-41's structured envelope, replacing the 0.8.0 shape (the raw
6
+ * adapter result for success, a formatted `"<code>: <message>"` string for failure) that
7
+ * forced a follow-up action to parse text to correlate a failure back to the effect that
8
+ * caused it.
9
+ *
10
+ * One shape covers both outcomes (spec §40's "structured `EffectOutcome` envelope for both
11
+ * success/failure"), rather than a distinct entity per outcome: field ids are unique across
12
+ * a whole graph — like `GROUP_KEY_FIELD`/`GROUP_ITEMS_FIELD` — so two entities could not both
13
+ * declare `effectId`/`operationId`/`integrationId` without colliding. A success dispatch
14
+ * populates `result`; a failure dispatch populates `code`/`message`/`retryable`; neither
15
+ * ever populates both, and none of `result`/`code`/`message`/`retryable` is `required`, so
16
+ * either shape validates.
17
+ *
18
+ * Unlike `GROUP_KEY_FIELD`/`GROUP_ITEMS_FIELD`, these are not runtime-enforced reservations
19
+ * — a payload is validated as an ordinary `entity` type, the same as any other event. They
20
+ * are a naming convention plus the builder below, so every application's effect outcome
21
+ * events share one shape an agent only has to learn once.
22
+ */
23
+ export const EFFECT_ID_FIELD = fieldId('field_effect_id');
24
+ export const EFFECT_INTEGRATION_ID_FIELD = fieldId('field_effect_integration_id');
25
+ export const EFFECT_OPERATION_ID_FIELD = fieldId('field_effect_operation_id');
26
+ export const EFFECT_CODE_FIELD = fieldId('field_effect_code');
27
+ export const EFFECT_MESSAGE_FIELD = fieldId('field_effect_message');
28
+ export const EFFECT_RETRYABLE_FIELD = fieldId('field_effect_retryable');
29
+ export const EFFECT_IDEMPOTENCY_KEY_FIELD = fieldId('field_effect_idempotency_key');
30
+ export const EFFECT_CORRELATION_ID_FIELD = fieldId('field_effect_correlation_id');
31
+ export const EFFECT_RESULT_FIELD = fieldId('field_effect_result');
32
+ /**
33
+ * The canonical entity shape for an `integration-effect`'s `succeededEventId` and
34
+ * `failedEventId` payload alike. Declare it once with
35
+ * `graph.addNode(effectOutcomeEntity(ENTITY_ID, resultType))` and reference it from both
36
+ * `EventDef`s with `entityType(ENTITY_ID)`. `resultType` is the effect operation's own
37
+ * declared result type — if a graph's effect operations return incompatible result types,
38
+ * they need either a common supertype here or their own outcome entity each (field ids are
39
+ * graph-global, so at most one `effectOutcomeEntity` may share a given result shape).
40
+ */
41
+ export function effectOutcomeEntity(id, resultType) {
42
+ return {
43
+ id,
44
+ kind: 'entity',
45
+ fields: [
46
+ { id: EFFECT_ID_FIELD, valueType: primitiveType('string'), required: true },
47
+ { id: EFFECT_INTEGRATION_ID_FIELD, valueType: primitiveType('string'), required: true },
48
+ { id: EFFECT_OPERATION_ID_FIELD, valueType: primitiveType('string'), required: true },
49
+ { id: EFFECT_RESULT_FIELD, valueType: resultType },
50
+ { id: EFFECT_CODE_FIELD, valueType: primitiveType('string') },
51
+ { id: EFFECT_MESSAGE_FIELD, valueType: primitiveType('string') },
52
+ { id: EFFECT_RETRYABLE_FIELD, valueType: primitiveType('boolean') },
53
+ { id: EFFECT_IDEMPOTENCY_KEY_FIELD, valueType: primitiveType('string') },
54
+ { id: EFFECT_CORRELATION_ID_FIELD, valueType: primitiveType('string') },
55
+ ],
56
+ };
57
+ }
@@ -0,0 +1,13 @@
1
+ import type { NodeBase } from './nodes.js';
2
+ import type { TypeRef } from './type-ref.js';
3
+ /**
4
+ * A semantic fact that occurred — a webhook delivery, an effect's outcome. An event is
5
+ * not work: it names something that happened, with a typed payload, and it is `TriggerDef`
6
+ * that says what happens next. Nothing resolves an event payload as `unknown`; it is
7
+ * validated against `payloadType` before it reaches any action.
8
+ */
9
+ export interface EventDef extends NodeBase {
10
+ kind: 'event';
11
+ payloadType: TypeRef;
12
+ }
13
+ //# sourceMappingURL=events.d.ts.map
package/dist/events.js ADDED
@@ -0,0 +1 @@
1
+ export {};
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.7.0') {
26
+ constructor(id, name, version = '0.8.1') {
27
27
  this.data = { id, name, version, nodes: {}, edges: {} };
28
28
  }
29
29
  get id() {
package/dist/index.d.ts CHANGED
@@ -7,7 +7,12 @@ export * from './type-ref.js';
7
7
  export * from './group.js';
8
8
  export * from './expressions.js';
9
9
  export * from './nodes.js';
10
+ export * from './integrations.js';
11
+ export * from './effect-outcome.js';
12
+ export * from './events.js';
13
+ export * from './triggers.js';
10
14
  export * from './renderer-capabilities.js';
15
+ export * from './trigger-capabilities.js';
11
16
  export * from './authoring-metadata.js';
12
17
  export * from './ui.js';
13
18
  export * from './types.js';
package/dist/index.js CHANGED
@@ -7,7 +7,12 @@ export * from './type-ref.js';
7
7
  export * from './group.js';
8
8
  export * from './expressions.js';
9
9
  export * from './nodes.js';
10
+ export * from './integrations.js';
11
+ export * from './effect-outcome.js';
12
+ export * from './events.js';
13
+ export * from './triggers.js';
10
14
  export * from './renderer-capabilities.js';
15
+ export * from './trigger-capabilities.js';
11
16
  export * from './authoring-metadata.js';
12
17
  export * from './ui.js';
13
18
  export * from './types.js';
@@ -0,0 +1,57 @@
1
+ import type { NodeId } from './ids.js';
2
+ import type { NodeBase } from './nodes.js';
3
+ import type { TypeRef } from './type-ref.js';
4
+ /**
5
+ * An external capability domain — a shipping provider, a device fleet, a payments
6
+ * processor. It names the capability, never the SDK, host name, secret or HTTP client
7
+ * that eventually implements it: those are supplied by a host adapter, never by the
8
+ * graph.
9
+ */
10
+ export interface IntegrationDef extends NodeBase {
11
+ kind: 'integration';
12
+ }
13
+ /**
14
+ * A query observes an external system and does not intentionally mutate it. An effect
15
+ * may mutate or otherwise cause an irreversible external consequence. The distinction is
16
+ * load-bearing: a query's result may be used mid-transaction, while an effect is never
17
+ * rollback-capable and is dispatched only after the transaction that requested it commits.
18
+ */
19
+ export type IntegrationOperationMode = 'query' | 'effect';
20
+ export declare const INTEGRATION_OPERATION_MODES: readonly IntegrationOperationMode[];
21
+ export type RetryPolicyKind = 'none' | 'fixed' | 'exponential';
22
+ export declare const RETRY_POLICY_KINDS: readonly RetryPolicyKind[];
23
+ export interface RetryPolicy {
24
+ policy: RetryPolicyKind;
25
+ maxAttempts?: number;
26
+ delayMs?: number;
27
+ }
28
+ export interface IntegrationOperationParameter {
29
+ id: NodeId;
30
+ name?: string;
31
+ valueType: TypeRef;
32
+ required?: boolean;
33
+ }
34
+ /**
35
+ * A typed semantic operation an integration exposes.
36
+ *
37
+ * Its result must conform to `resultType` — a provider response that does not is rejected
38
+ * at the adapter/runtime boundary, never handed to the application as `unknown`.
39
+ */
40
+ export interface IntegrationOperationDef extends NodeBase {
41
+ kind: 'integration-operation';
42
+ integrationId: NodeId;
43
+ mode: IntegrationOperationMode;
44
+ parameters?: IntegrationOperationParameter[];
45
+ resultType: TypeRef;
46
+ /**
47
+ * Whether this operation may be invoked from client-executed code. Absent means
48
+ * server-only, which is the default for every integration (spec §65): client safety is
49
+ * never inferred from the absence of a declared secret, only declared explicitly.
50
+ */
51
+ clientSafe?: boolean;
52
+ /** Whether invoking this effect twice with the same idempotency key has one effect. */
53
+ idempotent?: boolean;
54
+ /** Retry policy for a failed effect. Absent means `'none'`. Meaningless for a query. */
55
+ retry?: RetryPolicy;
56
+ }
57
+ //# sourceMappingURL=integrations.d.ts.map
@@ -0,0 +1,2 @@
1
+ export const INTEGRATION_OPERATION_MODES = ['query', 'effect'];
2
+ export const RETRY_POLICY_KINDS = ['none', 'fixed', 'exponential'];
package/dist/ir.d.ts CHANGED
@@ -7,6 +7,7 @@ import type { TypeRef } from './type-ref.js';
7
7
  import type { Authority } from './authority.js';
8
8
  import type { ResolvedPresentation } from './presentation.js';
9
9
  import type { Theme } from './theme.js';
10
+ import type { TriggerDef } from './triggers.js';
10
11
  export interface RouteSegment {
11
12
  kind: 'static' | 'parameter';
12
13
  value: string;
@@ -92,5 +93,13 @@ export interface ApplicationIR {
92
93
  * second renderer remains possible.
93
94
  */
94
95
  presentation: Record<NodeId, ResolvedPresentation>;
96
+ /**
97
+ * Client-authority triggers only — `interval`/`delay`/`lifecycle('application-start' |
98
+ * 'runtime-ready')` triggers whose target action executes locally, plus
99
+ * `lifecycle('route-enter' | 'route-leave')` triggers, which are inherently a client
100
+ * concept. No `event`-kind trigger, no integration, and no secret ever reaches this IR
101
+ * (spec §80).
102
+ */
103
+ triggers: TriggerDef[];
95
104
  }
96
105
  //# sourceMappingURL=ir.d.ts.map
package/dist/nodes.d.ts CHANGED
@@ -174,6 +174,27 @@ export interface ActionGuard {
174
174
  condition: Expression;
175
175
  failureMode?: FailureMode;
176
176
  }
177
+ /**
178
+ * `'client'` is an ordinary `InvokeRequest`; `'system'` is a trigger-, event- or
179
+ * effect-outcome-originated invocation. Server-computed and client-unforgeable — see
180
+ * `ExecutionContext.source` in `@cynodia/axiom-server`.
181
+ */
182
+ export type InvocationSource = 'client' | 'system';
183
+ export declare const INVOCATION_SOURCES: readonly InvocationSource[];
184
+ /** Both sources, the default when `ActionDef.invocation` is absent (spec 8.1 §5). */
185
+ export declare const DEFAULT_ALLOWED_INVOCATION_SOURCES: readonly InvocationSource[];
186
+ /**
187
+ * Restricts who may invoke an action, independently of `authorization`'s identity check.
188
+ *
189
+ * `authorization` answers "may this caller invoke this action"; `invocation` answers "may
190
+ * an invocation reaching the authority this way invoke this action at all" — the trust
191
+ * boundary a trigger-only or effect-outcome-only action needs, since otherwise any anonymous
192
+ * client that knows its id can invoke it directly (spec 8.1 §3-9).
193
+ */
194
+ export interface ActionInvocationPolicy {
195
+ /** Absent means both — identical to omitting `invocation` entirely. */
196
+ allowedSources?: readonly InvocationSource[];
197
+ }
177
198
  /**
178
199
  * Behaviour expressed as data, executed as a transaction.
179
200
  *
@@ -210,13 +231,21 @@ export interface ActionDef extends NodeBase {
210
231
  authorization?: Expression;
211
232
  /** What the confirmation says, when a plain message is not enough. */
212
233
  confirmation?: ConfirmationPresentation;
234
+ /** Restricts which invocation sources may reach this action. Absent means both. */
235
+ invocation?: ActionInvocationPolicy;
213
236
  }
214
237
  /**
215
238
  * The conditions an action checks, however they were written. `guards` pairs each
216
239
  * condition with its failure; the older parallel arrays are matched by position.
217
240
  */
218
241
  export declare function actionGuards(action: ActionDef): ActionGuard[];
219
- export type Operation = SetOperation | InsertOperation | RemoveOperation | ForEachOperation | InvokeOperation | NavigateOperation | NativeOperation;
242
+ /** The invocation sources this action accepts — both, unless `invocation` restricts it. */
243
+ export declare function allowedInvocationSources(action: ActionDef): readonly InvocationSource[];
244
+ /** Whether an ordinary client `InvokeRequest` may name this action at all. */
245
+ export declare function isClientInvocable(action: ActionDef): boolean;
246
+ /** Whether this action accepts only trigger-, event- or effect-outcome-originated calls. */
247
+ export declare function isSystemOnlyAction(action: ActionDef): boolean;
248
+ export type Operation = SetOperation | InsertOperation | RemoveOperation | ForEachOperation | InvokeOperation | NavigateOperation | NativeOperation | IntegrationQueryOperation | IntegrationEffectOperation;
220
249
  export type OperationKind = Operation['kind'];
221
250
  /** Every operation kind the runtime is required to execute. */
222
251
  export declare const OPERATION_KINDS: readonly OperationKind[];
@@ -290,6 +319,44 @@ export interface NavigateOperation {
290
319
  /** Keyed by route parameter id. */
291
320
  parameters?: Record<string, Expression>;
292
321
  }
322
+ /**
323
+ * Executes an integration query and binds its result into scope for later operations in
324
+ * the same action.
325
+ *
326
+ * A query is awaited mid-transaction — its result may inform the mutations that follow —
327
+ * but evaluating it is not an ordinary Expression, because it is not deterministic over
328
+ * semantic state alone. `bindAs` introduces a scope the way a `for-each`'s `scopeId` does:
329
+ * later operations refer to the whole result as `ref(bindAs)`. Never legal inside
330
+ * `for-each`.
331
+ */
332
+ export interface IntegrationQueryOperation {
333
+ kind: 'integration-query';
334
+ /** The `IntegrationOperationDef` to call. Must declare `mode: 'query'`. */
335
+ operationId: NodeId;
336
+ arguments?: Record<string, Expression>;
337
+ bindAs: NodeId;
338
+ timeoutMs?: number;
339
+ }
340
+ /**
341
+ * Records intent to perform an external effect. It never calls the adapter during the
342
+ * transaction: reaching this operation only appends an effect intent, discarded on
343
+ * rollback exactly like a mutation is. Dispatch to the adapter happens only after the
344
+ * surrounding transaction commits — the outbox invariant (spec §18) — so an effect is
345
+ * never rollback-capable and never mid-transaction. Never legal inside `for-each`.
346
+ *
347
+ * `succeededEventId`/`failedEventId`, when declared, are dispatched through the ordinary
348
+ * event pipeline once the effect's outcome is known — an effect's result is never folded
349
+ * back into the transaction that requested it.
350
+ */
351
+ export interface IntegrationEffectOperation {
352
+ kind: 'integration-effect';
353
+ /** The `IntegrationOperationDef` to call. Must declare `mode: 'effect'`. */
354
+ operationId: NodeId;
355
+ arguments?: Record<string, Expression>;
356
+ idempotencyKey?: Expression;
357
+ succeededEventId?: NodeId;
358
+ failedEventId?: NodeId;
359
+ }
293
360
  export type NativeEffect = {
294
361
  kind: 'reads-state';
295
362
  stateId: NodeId;
package/dist/nodes.js CHANGED
@@ -1,3 +1,6 @@
1
+ export const INVOCATION_SOURCES = ['client', 'system'];
2
+ /** Both sources, the default when `ActionDef.invocation` is absent (spec 8.1 §5). */
3
+ export const DEFAULT_ALLOWED_INVOCATION_SOURCES = INVOCATION_SOURCES;
1
4
  /**
2
5
  * The conditions an action checks, however they were written. `guards` pairs each
3
6
  * condition with its failure; the older parallel arrays are matched by position.
@@ -11,6 +14,19 @@ export function actionGuards(action) {
11
14
  ...(action.failureModes?.[index] ? { failureMode: action.failureModes[index] } : {}),
12
15
  }));
13
16
  }
17
+ /** The invocation sources this action accepts — both, unless `invocation` restricts it. */
18
+ export function allowedInvocationSources(action) {
19
+ return action.invocation?.allowedSources ?? DEFAULT_ALLOWED_INVOCATION_SOURCES;
20
+ }
21
+ /** Whether an ordinary client `InvokeRequest` may name this action at all. */
22
+ export function isClientInvocable(action) {
23
+ return allowedInvocationSources(action).includes('client');
24
+ }
25
+ /** Whether this action accepts only trigger-, event- or effect-outcome-originated calls. */
26
+ export function isSystemOnlyAction(action) {
27
+ const sources = allowedInvocationSources(action);
28
+ return sources.includes('system') && !sources.includes('client');
29
+ }
14
30
  /** Every operation kind the runtime is required to execute. */
15
31
  export const OPERATION_KINDS = [
16
32
  'set',
@@ -20,6 +36,8 @@ export const OPERATION_KINDS = [
20
36
  'invoke',
21
37
  'navigate',
22
38
  'native',
39
+ 'integration-query',
40
+ 'integration-effect',
23
41
  ];
24
42
  export function isMutationOperation(operation) {
25
43
  return operation.kind === 'set' || operation.kind === 'insert' || operation.kind === 'remove';