@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 CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './normalize.js';
2
+ export * from './server.js';
2
3
  export * from './stylesheet.js';
3
4
  export * from './codegen.js';
4
5
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './normalize.js';
2
+ export * from './server.js';
2
3
  export * from './stylesheet.js';
3
4
  export * from './codegen.js';
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
- states.push(node);
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
- edges: graph.semanticEdges(),
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
  };
@@ -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.5.2-alpha.1",
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.5.2-alpha.1",
35
- "@cynodia/axiom-runtime": "0.5.2-alpha.1"
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",