@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.
- package/dist/diagnostics.d.ts +6 -0
- package/dist/diagnostics.js +6 -0
- package/dist/effect-outcome.d.ts +43 -0
- package/dist/effect-outcome.js +57 -0
- package/dist/graph.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/nodes.d.ts +29 -0
- package/dist/nodes.js +16 -0
- package/dist/server-ir.d.ts +23 -1
- package/dist/server-ir.js +33 -2
- package/dist/trigger-capabilities.d.ts +28 -0
- package/dist/trigger-capabilities.js +10 -0
- package/dist/validate-authority.d.ts +2 -1
- package/dist/validate-authority.js +28 -1
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +8 -1
- package/package.json +1 -1
package/dist/diagnostics.d.ts
CHANGED
|
@@ -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
|
package/dist/diagnostics.js
CHANGED
|
@@ -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.
|
|
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',
|
package/dist/server-ir.d.ts
CHANGED
|
@@ -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 = [
|
|
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.
|
|
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.
|
package/dist/validate.d.ts
CHANGED
|
@@ -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) {
|