@cynodia/axiom-core 0.8.0-alpha.1 → 0.8.2-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.
@@ -101,5 +101,11 @@ export declare const VALIDATION_CODES: {
101
101
  readonly unknownEvent: "UNKNOWN_EVENT";
102
102
  /** An `event` trigger targeting a client-authority action, or a `route-enter`/`route-leave` trigger targeting a server-authority one. */
103
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";
104
110
  };
105
111
  //# sourceMappingURL=diagnostics.d.ts.map
@@ -90,4 +90,10 @@ export const VALIDATION_CODES = {
90
90
  unknownEvent: 'UNKNOWN_EVENT',
91
91
  /** An `event` trigger targeting a client-authority action, or a `route-enter`/`route-leave` trigger targeting a server-authority one. */
92
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',
93
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
+ }
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.8.0') {
26
+ constructor(id, name, version = '0.8.2') {
27
27
  this.data = { id, name, version, nodes: {}, edges: {} };
28
28
  }
29
29
  get id() {
package/dist/index.d.ts CHANGED
@@ -8,9 +8,11 @@ export * from './group.js';
8
8
  export * from './expressions.js';
9
9
  export * from './nodes.js';
10
10
  export * from './integrations.js';
11
+ export * from './effect-outcome.js';
11
12
  export * from './events.js';
12
13
  export * from './triggers.js';
13
14
  export * from './renderer-capabilities.js';
15
+ export * from './trigger-capabilities.js';
14
16
  export * from './authoring-metadata.js';
15
17
  export * from './ui.js';
16
18
  export * from './types.js';
package/dist/index.js CHANGED
@@ -8,9 +8,11 @@ export * from './group.js';
8
8
  export * from './expressions.js';
9
9
  export * from './nodes.js';
10
10
  export * from './integrations.js';
11
+ export * from './effect-outcome.js';
11
12
  export * from './events.js';
12
13
  export * from './triggers.js';
13
14
  export * from './renderer-capabilities.js';
15
+ export * from './trigger-capabilities.js';
14
16
  export * from './authoring-metadata.js';
15
17
  export * from './ui.js';
16
18
  export * from './types.js';
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,12 +231,20 @@ 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[];
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;
219
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. */
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,7 +20,7 @@ import type { TriggerDef } from './triggers.js';
20
20
  * Every existing application therefore still compiles to a byte-identical
21
21
  * `axiom.server.v1` document, and the frozen conformance fixtures stay frozen.
22
22
  */
23
- export declare const SERVER_IR_CONTRACTS: readonly ["axiom.server.v1", "axiom.server.v2", "axiom.server.v3"];
23
+ export declare const SERVER_IR_CONTRACTS: readonly ["axiom.server.v1", "axiom.server.v2", "axiom.server.v3", "axiom.server.v4"];
24
24
  export type ServerIRContract = (typeof SERVER_IR_CONTRACTS)[number];
25
25
  /** The oldest contract, and the one a document declares unless it needs more. */
26
26
  export declare const SERVER_IR_CONTRACT: ServerIRContract;
@@ -46,6 +46,28 @@ export declare function usesIntegrationVocabulary(ir: {
46
46
  events?: readonly unknown[];
47
47
  triggers?: readonly unknown[];
48
48
  }): boolean;
49
+ /**
50
+ * Whether any action declares `invocation` at all, restrictive or not — `axiom.server.v1`
51
+ * is frozen and its schema carries no such property, so even a redundant, fully-permissive
52
+ * `invocation: { allowedSources: ['client', 'system'] }` requires at least `v2`, the same
53
+ * tier a document that merely uses `group`/`expression-ref` requires.
54
+ */
55
+ export declare function usesInvocationVocabulary(ir: {
56
+ actions: Record<string, ActionDef>;
57
+ }): boolean;
58
+ /**
59
+ * Whether a document restricts any action's invocation sources, or gives it a structured
60
+ * effect-outcome payload — both incompatible changes to `axiom.server.v3` execution
61
+ * semantics (spec 8.1 §50-52): a v3 runtime that ignored `invocation.allowedSources` would
62
+ * let a client forge a system-only action, and one expecting the v3 string/raw effect
63
+ * payload would misread the v4 structured envelope. Computed from the document, the same
64
+ * way `usesIntegrationVocabulary` decides v3 — a document that uses neither still labels
65
+ * itself `axiom.server.v3`.
66
+ */
67
+ export declare function usesV4Semantics(ir: {
68
+ actions: Record<string, ActionDef>;
69
+ integrationOperations?: Record<string, IntegrationOperationDef>;
70
+ }): boolean;
49
71
  /** The higher of two contracts, ordered by `SERVER_IR_CONTRACTS`. */
50
72
  export declare function maxContract(a: ServerIRContract, b: ServerIRContract): ServerIRContract;
51
73
  /** Every expression a Server IR document contains, in no particular order. */
package/dist/server-ir.js CHANGED
@@ -14,11 +14,16 @@ import { walkExpression } from './expressions.js';
14
14
  * Every existing application therefore still compiles to a byte-identical
15
15
  * `axiom.server.v1` document, and the frozen conformance fixtures stay frozen.
16
16
  */
17
- export const SERVER_IR_CONTRACTS = ['axiom.server.v1', 'axiom.server.v2', 'axiom.server.v3'];
17
+ export const SERVER_IR_CONTRACTS = [
18
+ 'axiom.server.v1',
19
+ 'axiom.server.v2',
20
+ 'axiom.server.v3',
21
+ 'axiom.server.v4',
22
+ ];
18
23
  /** The oldest contract, and the one a document declares unless it needs more. */
19
24
  export const SERVER_IR_CONTRACT = 'axiom.server.v1';
20
25
  /** The newest contract this implementation produces and executes. */
21
- export const SERVER_IR_LATEST_CONTRACT = 'axiom.server.v3';
26
+ export const SERVER_IR_LATEST_CONTRACT = 'axiom.server.v4';
22
27
  /** Expression kinds that `axiom.server.v1` does not contain. */
23
28
  export const SERVER_IR_V2_EXPRESSION_KINDS = ['group', 'expression-ref'];
24
29
  /**
@@ -49,6 +54,32 @@ export function usesIntegrationVocabulary(ir) {
49
54
  (ir.events?.length ?? 0) > 0 ||
50
55
  (ir.triggers?.length ?? 0) > 0);
51
56
  }
57
+ /**
58
+ * Whether any action declares `invocation` at all, restrictive or not — `axiom.server.v1`
59
+ * is frozen and its schema carries no such property, so even a redundant, fully-permissive
60
+ * `invocation: { allowedSources: ['client', 'system'] }` requires at least `v2`, the same
61
+ * tier a document that merely uses `group`/`expression-ref` requires.
62
+ */
63
+ export function usesInvocationVocabulary(ir) {
64
+ return Object.values(ir.actions).some((action) => action.invocation !== undefined);
65
+ }
66
+ /**
67
+ * Whether a document restricts any action's invocation sources, or gives it a structured
68
+ * effect-outcome payload — both incompatible changes to `axiom.server.v3` execution
69
+ * semantics (spec 8.1 §50-52): a v3 runtime that ignored `invocation.allowedSources` would
70
+ * let a client forge a system-only action, and one expecting the v3 string/raw effect
71
+ * payload would misread the v4 structured envelope. Computed from the document, the same
72
+ * way `usesIntegrationVocabulary` decides v3 — a document that uses neither still labels
73
+ * itself `axiom.server.v3`.
74
+ */
75
+ export function usesV4Semantics(ir) {
76
+ const restrictsInvocation = Object.values(ir.actions).some((action) => {
77
+ const sources = action.invocation?.allowedSources;
78
+ return sources !== undefined && sources.length < 2;
79
+ });
80
+ const usesEffects = Object.values(ir.integrationOperations ?? {}).some((operation) => operation.mode === 'effect');
81
+ return restrictsInvocation || usesEffects;
82
+ }
52
83
  /** The higher of two contracts, ordered by `SERVER_IR_CONTRACTS`. */
53
84
  export function maxContract(a, b) {
54
85
  return SERVER_IR_CONTRACTS.indexOf(b) > SERVER_IR_CONTRACTS.indexOf(a) ? b : a;
@@ -0,0 +1,28 @@
1
+ import type { TriggerKind } from './triggers.js';
2
+ /**
3
+ * What a trigger runtime can actually execute, for client-authority triggers.
4
+ *
5
+ * A server-authority trigger always executes on the authority, which implements every
6
+ * kind, so this only ever gates client-authority triggers — the ones a compiled client IR
7
+ * would otherwise have to execute itself. Before spec 8.1, a client-authority `interval`/
8
+ * `delay`/`lifecycle` trigger validated, compiled into `ApplicationIR.triggers`, and then
9
+ * silently never fired: no client runtime implemented any trigger kind at all. That is
10
+ * exactly the "publicly declared, typechecks, passes validation, has no defined runtime
11
+ * behaviour" shape the framework forbids — the same gap `RendererCapabilities` closed for
12
+ * UI node kinds.
13
+ *
14
+ * A trigger runtime that implements a kind publishes it here; one that has not cannot
15
+ * silently accept it.
16
+ */
17
+ export interface TriggerRuntimeCapabilities {
18
+ /** Names the runtime, so a diagnostic can say which target refused. */
19
+ target: string;
20
+ supportedTriggerKinds: readonly TriggerKind[];
21
+ }
22
+ /**
23
+ * Every kind, for a caller that does not know or care about a target — validation is never
24
+ * rejected for a trigger runtime nobody named. Compiling for a real target supplies the
25
+ * real set.
26
+ */
27
+ export declare const ALL_TRIGGER_KINDS_SUPPORTED: TriggerRuntimeCapabilities;
28
+ //# sourceMappingURL=trigger-capabilities.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { TRIGGER_KINDS } from './triggers.js';
2
+ /**
3
+ * Every kind, for a caller that does not know or care about a target — validation is never
4
+ * rejected for a trigger runtime nobody named. Compiling for a real target supplies the
5
+ * real set.
6
+ */
7
+ export const ALL_TRIGGER_KINDS_SUPPORTED = {
8
+ target: 'any',
9
+ supportedTriggerKinds: TRIGGER_KINDS,
10
+ };
@@ -1,5 +1,6 @@
1
1
  import type { ValidationIssue } from './diagnostics.js';
2
2
  import type { NodeId } from './ids.js';
3
+ import type { TriggerRuntimeCapabilities } from './trigger-capabilities.js';
3
4
  import type { AnyNode } from './types.js';
4
5
  /**
5
6
  * Validation of the authority boundary.
@@ -9,7 +10,7 @@ import type { AnyNode } from './types.js';
9
10
  * correctly — and the point of the boundary is that its correctness does not depend on an
10
11
  * author remembering where to bind an input.
11
12
  */
12
- export declare function validateAuthority(nodes: readonly AnyNode[], principalEntityId: NodeId | undefined): {
13
+ export declare function validateAuthority(nodes: readonly AnyNode[], principalEntityId: NodeId | undefined, triggerRuntime?: TriggerRuntimeCapabilities): {
13
14
  errors: ValidationIssue[];
14
15
  warnings: ValidationIssue[];
15
16
  };
@@ -2,6 +2,8 @@ import { PRINCIPAL, actionAuthority, authorityContext, stateAuthority, statesRea
2
2
  import { referencedIds } from './derive-edges.js';
3
3
  import { VALIDATION_CODES } from './diagnostics.js';
4
4
  import { locationRootStateId } from './location.js';
5
+ import { allowedInvocationSources } from './nodes.js';
6
+ import { ALL_TRIGGER_KINDS_SUPPORTED } from './trigger-capabilities.js';
5
7
  import { isUINode } from './ui.js';
6
8
  /**
7
9
  * Validation of the authority boundary.
@@ -11,7 +13,7 @@ import { isUINode } from './ui.js';
11
13
  * correctly — and the point of the boundary is that its correctness does not depend on an
12
14
  * author remembering where to bind an input.
13
15
  */
14
- export function validateAuthority(nodes, principalEntityId) {
16
+ export function validateAuthority(nodes, principalEntityId, triggerRuntime = ALL_TRIGGER_KINDS_SUPPORTED) {
15
17
  const errors = [];
16
18
  const warnings = [];
17
19
  const context = authorityContext(nodes, principalEntityId);
@@ -164,6 +166,31 @@ export function validateAuthority(nodes, principalEntityId) {
164
166
  details: { actionId: target.id, authority },
165
167
  });
166
168
  }
169
+ // A trigger of any kind always invokes with `source: 'system'` (spec 8.1 §3-9). An
170
+ // action that has opted out of system invocation could never be reached by it.
171
+ if (!allowedInvocationSources(target).includes('system')) {
172
+ errors.push({
173
+ code: VALIDATION_CODES.triggerTargetSourceMismatch,
174
+ message: `Trigger ${node.name ?? node.id} targets ${target.name ?? target.id}, which does not accept 'system'-sourced invocations, so this trigger could never invoke it`,
175
+ nodeId: node.id,
176
+ details: { actionId: target.id },
177
+ });
178
+ }
179
+ // A client-authority trigger executes in the trigger runtime the graph is compiled
180
+ // for. A kind that runtime does not implement would validate, compile, and then
181
+ // silently never fire (spec 8.1 §31-36) — exactly what a renderer capability gate
182
+ // already prevents for UI node kinds.
183
+ if (authority === 'client' && !triggerRuntime.supportedTriggerKinds.includes(node.when.kind)) {
184
+ errors.push({
185
+ code: VALIDATION_CODES.clientTriggerUnsupported,
186
+ message: `Trigger ${node.name ?? node.id} is a client-authority '${node.when.kind}' trigger, which the ` +
187
+ `${triggerRuntime.target} trigger runtime does not execute. Move ${target.name ?? target.id} to ` +
188
+ `server-authoritative execution (so the trigger becomes server-authority), or compile for a ` +
189
+ `trigger runtime that publishes '${node.when.kind}' in its supportedTriggerKinds.`,
190
+ nodeId: node.id,
191
+ details: { actionId: target.id, kind: node.when.kind, target: triggerRuntime.target },
192
+ });
193
+ }
167
194
  }
168
195
  // The principal exists only where an authority evaluates. Reading it anywhere a client
169
196
  // evaluates would be a rule the client could simply not apply.
@@ -1,4 +1,5 @@
1
1
  import type { RendererCapabilities } from './renderer-capabilities.js';
2
+ import type { TriggerRuntimeCapabilities } from './trigger-capabilities.js';
2
3
  import type { ValidationResult } from './diagnostics.js';
3
4
  import type { ApplicationGraph } from './graph.js';
4
5
  /**
@@ -13,6 +14,13 @@ export interface ValidateOptions {
13
14
  * real capabilities, which is where an unrenderable node kind is caught.
14
15
  */
15
16
  renderer?: RendererCapabilities;
17
+ /**
18
+ * The trigger runtime the graph is intended for. Absent, every trigger kind is accepted —
19
+ * a graph is not rejected for a trigger runtime nobody named. `compileToIR` supplies the
20
+ * browser's real (empty) capability set, which is where a client-authority trigger kind
21
+ * no browser runtime executes is caught, rather than silently compiling inert.
22
+ */
23
+ triggerRuntime?: TriggerRuntimeCapabilities;
16
24
  }
17
25
  export declare function validateGraph(graph: ApplicationGraph, options?: ValidateOptions): ValidationResult;
18
26
  //# sourceMappingURL=validate.d.ts.map
package/dist/validate.js CHANGED
@@ -102,7 +102,7 @@ export function validateGraph(graph, options = {}) {
102
102
  warnings.push(...presentation.warnings);
103
103
  // The authority boundary. A graph that could let a client commit server state, or that
104
104
  // would make an authority read state it does not own, cannot execute safely.
105
- const authority = validateAuthority(allNodes, graph.principalEntityId);
105
+ const authority = validateAuthority(allNodes, graph.principalEntityId, options.triggerRuntime);
106
106
  errors.push(...authority.errors);
107
107
  warnings.push(...authority.warnings);
108
108
  return { valid: errors.length === 0, errors, warnings };
@@ -230,6 +230,13 @@ function validateAction(action, context) {
230
230
  if (action.authorization) {
231
231
  validateExpression(action.authorization, action.id, context, new Set());
232
232
  }
233
+ if (action.invocation?.allowedSources && action.invocation.allowedSources.length === 0) {
234
+ context.errors.push({
235
+ code: VALIDATION_CODES.invalidInvocationSource,
236
+ message: `Action ${action.name ?? action.id} declares an empty invocation.allowedSources, so it could never be invoked`,
237
+ nodeId: action.id,
238
+ });
239
+ }
233
240
  const local = emptyScope(new Set((action.parameters ?? []).map((parameter) => parameter.id)));
234
241
  for (const parameter of action.parameters ?? []) {
235
242
  if (parameter.valueType) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-core",
3
- "version": "0.8.0-alpha.1",
3
+ "version": "0.8.2-alpha.1",
4
4
  "description": "Application Graph, semantic types, locations and validation for Axiom.",
5
5
  "license": "MIT",
6
6
  "author": "AskTech AS",