@cynodia/axiom-compiler 0.5.2-alpha.1 → 0.6.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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/normalize.js +60 -4
- package/dist/server.d.ts +18 -0
- package/dist/server.js +90 -0
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/normalize.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { actionGuards, inferExpressionType, inferLocationType, itemTypeOf, locationFieldIds, locationRootStateId, resolvePresentationMap, semanticContextFromGraph, validateGraph, } from '@cynodia/axiom-core';
|
|
1
|
+
import { actionAuthority, actionGuards, authorityContext, inferExpressionType, inferLocationType, itemTypeOf, locationFieldIds, locationRootStateId, referencedIds, resolvePresentationMap, semanticContextFromGraph, stateAuthority, validateGraph, } from '@cynodia/axiom-core';
|
|
2
2
|
import { isUINode } from '@cynodia/axiom-core';
|
|
3
3
|
export class GraphValidationError extends Error {
|
|
4
4
|
problems;
|
|
@@ -43,6 +43,18 @@ export function compileToIR(graph, options = {}) {
|
|
|
43
43
|
throw new GraphValidationError(result);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
// The authority boundary decides what may cross into the client at all.
|
|
47
|
+
const authorityOf = authorityContext(graph.listNodes(), graph.principalEntityId);
|
|
48
|
+
const hiddenStateIds = new Set();
|
|
49
|
+
for (const state of authorityOf.states.values()) {
|
|
50
|
+
if (state.serverOnly) {
|
|
51
|
+
hiddenStateIds.add(state.id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const remoteActionIds = [];
|
|
55
|
+
const authority = {};
|
|
56
|
+
/** Nothing that reads state the client may not observe may reach the client. */
|
|
57
|
+
const readsHiddenState = (expressions) => expressions.some((expression) => referencedIds(expression).some((id) => hiddenStateIds.has(id)));
|
|
46
58
|
const nodes = {};
|
|
47
59
|
const actions = {};
|
|
48
60
|
const uiNodes = {};
|
|
@@ -61,10 +73,43 @@ export function compileToIR(graph, options = {}) {
|
|
|
61
73
|
case 'entity':
|
|
62
74
|
entities.push(node);
|
|
63
75
|
break;
|
|
64
|
-
case 'state':
|
|
65
|
-
|
|
76
|
+
case 'state': {
|
|
77
|
+
if (hiddenStateIds.has(node.id)) {
|
|
78
|
+
// Server-only state is not merely unwritable here; it is absent.
|
|
79
|
+
delete nodes[node.id];
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
authority[node.id] = stateAuthority(node);
|
|
83
|
+
states.push(stateAuthority(node) === 'server'
|
|
84
|
+
? // The authority owns the value. A seed shipped to the client would be a
|
|
85
|
+
// second, unauthoritative source of truth.
|
|
86
|
+
{ ...node, initialValue: undefined }
|
|
87
|
+
: node);
|
|
66
88
|
break;
|
|
89
|
+
}
|
|
67
90
|
case 'action': {
|
|
91
|
+
if (actionAuthority(node, authorityOf) === 'server') {
|
|
92
|
+
// A remote action reaches the client as its name, its parameters and nothing
|
|
93
|
+
// else: no operations to replay, no guards to fake, no authorization to satisfy.
|
|
94
|
+
remoteActionIds.push(node.id);
|
|
95
|
+
const remote = {
|
|
96
|
+
id: node.id,
|
|
97
|
+
kind: 'action',
|
|
98
|
+
...(node.name ? { name: node.name } : {}),
|
|
99
|
+
...(node.parameters ? { parameters: node.parameters } : {}),
|
|
100
|
+
...(node.destructive !== undefined ? { destructive: node.destructive } : {}),
|
|
101
|
+
...(node.requiresConfirmation !== undefined
|
|
102
|
+
? { requiresConfirmation: node.requiresConfirmation }
|
|
103
|
+
: {}),
|
|
104
|
+
...(node.confirmationMessage ? { confirmationMessage: node.confirmationMessage } : {}),
|
|
105
|
+
...(node.confirmation ? { confirmation: node.confirmation } : {}),
|
|
106
|
+
...(node.metadata ? { metadata: node.metadata } : {}),
|
|
107
|
+
operations: [],
|
|
108
|
+
};
|
|
109
|
+
actions[node.id] = remote;
|
|
110
|
+
nodes[node.id] = remote;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
68
113
|
// Guards are authoring sugar: the IR carries conditions and failures aligned.
|
|
69
114
|
const guards = actionGuards(node);
|
|
70
115
|
actions[node.id] = {
|
|
@@ -76,9 +121,17 @@ export function compileToIR(graph, options = {}) {
|
|
|
76
121
|
break;
|
|
77
122
|
}
|
|
78
123
|
case 'constraint':
|
|
124
|
+
if (readsHiddenState([node.expression])) {
|
|
125
|
+
delete nodes[node.id];
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
79
128
|
constraints.push(node);
|
|
80
129
|
break;
|
|
81
130
|
case 'transition-constraint':
|
|
131
|
+
if (readsHiddenState([node.expression])) {
|
|
132
|
+
delete nodes[node.id];
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
82
135
|
transitionConstraints.push(node);
|
|
83
136
|
break;
|
|
84
137
|
case 'route':
|
|
@@ -149,11 +202,14 @@ export function compileToIR(graph, options = {}) {
|
|
|
149
202
|
constraints,
|
|
150
203
|
transitionConstraints,
|
|
151
204
|
routes,
|
|
152
|
-
|
|
205
|
+
// An edge naming a node the client does not receive would tell it that node exists.
|
|
206
|
+
edges: graph.semanticEdges().filter((edge) => nodes[edge.from] && nodes[edge.to]),
|
|
153
207
|
locationTypes,
|
|
154
208
|
locationRoots,
|
|
155
209
|
locationRequired,
|
|
156
210
|
repeatIdentityFields,
|
|
211
|
+
authority,
|
|
212
|
+
remoteActionIds,
|
|
157
213
|
theme,
|
|
158
214
|
presentation,
|
|
159
215
|
};
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ApplicationGraph, ServerIR } from '@cynodia/axiom-core';
|
|
2
|
+
import type { CompileOptions } from './normalize.js';
|
|
3
|
+
/**
|
|
4
|
+
* Compiles the authoritative half of a graph.
|
|
5
|
+
*
|
|
6
|
+
* What an authority needs is not what a client needs, and the difference is the point: the
|
|
7
|
+
* Server IR carries the rules a client must never be trusted with, and none of the UI,
|
|
8
|
+
* presentation, theme or routing that decides nothing.
|
|
9
|
+
*
|
|
10
|
+
* The result is plain JSON — deterministic, closure-free and free of anything specific to
|
|
11
|
+
* a language or host — so a conforming runtime in another language can execute it and reach
|
|
12
|
+
* the same semantic result.
|
|
13
|
+
*/
|
|
14
|
+
export declare function compileToServerIR(graph: ApplicationGraph, options?: CompileOptions): ServerIR;
|
|
15
|
+
export declare function serializeServerIR(ir: ServerIR): string;
|
|
16
|
+
/** Whether a graph has any authoritative half at all. */
|
|
17
|
+
export declare function hasServerAuthority(graph: ApplicationGraph): boolean;
|
|
18
|
+
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { SERVER_IR_CONTRACT, actionAuthority, actionGuards, authorityContext, isObservable, serverStateClosure, stateAuthority, validateGraph, } from '@cynodia/axiom-core';
|
|
2
|
+
import { GraphValidationError } from './normalize.js';
|
|
3
|
+
/**
|
|
4
|
+
* Compiles the authoritative half of a graph.
|
|
5
|
+
*
|
|
6
|
+
* What an authority needs is not what a client needs, and the difference is the point: the
|
|
7
|
+
* Server IR carries the rules a client must never be trusted with, and none of the UI,
|
|
8
|
+
* presentation, theme or routing that decides nothing.
|
|
9
|
+
*
|
|
10
|
+
* The result is plain JSON — deterministic, closure-free and free of anything specific to
|
|
11
|
+
* a language or host — so a conforming runtime in another language can execute it and reach
|
|
12
|
+
* the same semantic result.
|
|
13
|
+
*/
|
|
14
|
+
export function compileToServerIR(graph, options = {}) {
|
|
15
|
+
if (options.validate !== false) {
|
|
16
|
+
const result = validateGraph(graph);
|
|
17
|
+
if (!result.valid) {
|
|
18
|
+
throw new GraphValidationError(result);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const nodes = graph.listNodes();
|
|
22
|
+
const context = authorityContext(nodes, graph.principalEntityId);
|
|
23
|
+
const needed = serverStateClosure(context);
|
|
24
|
+
const entities = [];
|
|
25
|
+
const constraints = [];
|
|
26
|
+
const transitionConstraints = [];
|
|
27
|
+
for (const node of nodes) {
|
|
28
|
+
if (node.kind === 'entity') {
|
|
29
|
+
entities.push(node);
|
|
30
|
+
}
|
|
31
|
+
else if (node.kind === 'constraint') {
|
|
32
|
+
constraints.push(node);
|
|
33
|
+
}
|
|
34
|
+
else if (node.kind === 'transition-constraint') {
|
|
35
|
+
transitionConstraints.push(node);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const states = [];
|
|
39
|
+
const observableStateIds = [];
|
|
40
|
+
for (const state of context.states.values()) {
|
|
41
|
+
if (!needed.has(state.id)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
states.push(state);
|
|
45
|
+
if (stateAuthority(state) === 'server' && isObservable(state)) {
|
|
46
|
+
observableStateIds.push(state.id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const actions = {};
|
|
50
|
+
for (const action of context.actions.values()) {
|
|
51
|
+
if (actionAuthority(action, context) !== 'server') {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Guards are authoring sugar in both halves: the IR carries conditions and failures
|
|
55
|
+
// aligned by position, exactly as the client IR does. An authority that read `guards`
|
|
56
|
+
// and not `preconditions` would silently not check them.
|
|
57
|
+
const guards = actionGuards(action);
|
|
58
|
+
actions[action.id] = {
|
|
59
|
+
...action,
|
|
60
|
+
preconditions: guards.map((guard) => guard.condition),
|
|
61
|
+
failureModes: guards.map((guard) => guard.failureMode ?? { code: 'precondition-failed' }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const fields = {};
|
|
65
|
+
for (const entry of graph.listFields()) {
|
|
66
|
+
fields[entry.field.id] = entry;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
contract: SERVER_IR_CONTRACT,
|
|
70
|
+
id: graph.id,
|
|
71
|
+
name: graph.name,
|
|
72
|
+
version: graph.version,
|
|
73
|
+
entities,
|
|
74
|
+
fields,
|
|
75
|
+
states,
|
|
76
|
+
actions,
|
|
77
|
+
constraints,
|
|
78
|
+
transitionConstraints,
|
|
79
|
+
...(graph.principalEntityId ? { principalEntityId: graph.principalEntityId } : {}),
|
|
80
|
+
observableStateIds,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function serializeServerIR(ir) {
|
|
84
|
+
return JSON.stringify(ir);
|
|
85
|
+
}
|
|
86
|
+
/** Whether a graph has any authoritative half at all. */
|
|
87
|
+
export function hasServerAuthority(graph) {
|
|
88
|
+
const context = authorityContext(graph.listNodes(), graph.principalEntityId);
|
|
89
|
+
return [...context.states.values()].some((state) => stateAuthority(state) === 'server');
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-alpha.1",
|
|
4
4
|
"description": "Normalizes an Axiom application graph and emits a self-contained page.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AskTech AS",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@cynodia/axiom-core": "0.
|
|
35
|
-
"@cynodia/axiom-runtime": "0.
|
|
34
|
+
"@cynodia/axiom-core": "0.6.0-alpha.1",
|
|
35
|
+
"@cynodia/axiom-runtime": "0.6.0-alpha.1"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc -b tsconfig.json tsconfig.test.json",
|