@cynodia/axiom-compiler 0.5.2-alpha.1 → 0.6.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
@@ -20,13 +20,13 @@ Main exports: `compileToIR`, `compileToHtml`, `createThemeStylesheet`,
20
20
  ## Installation
21
21
 
22
22
  ```bash
23
- npm install @cynodia/axiom-compiler@alpha
23
+ npm install @cynodia/axiom-compiler
24
24
  ```
25
25
 
26
26
  Most applications should install the facade package instead, which re-exports this one:
27
27
 
28
28
  ```bash
29
- npm install @cynodia/axiom@alpha
29
+ npm install @cynodia/axiom
30
30
  ```
31
31
 
32
32
 
package/dist/codegen.d.ts CHANGED
@@ -1,7 +1,23 @@
1
1
  import type { ApplicationGraph, ApplicationIR, Appearance } from '@cynodia/axiom-core';
2
2
  import type { CompileOptions } from './normalize.js';
3
+ /**
4
+ * How a generated page reaches its authority.
5
+ *
6
+ * `true` means the standard semantic endpoint on the current origin, which is what the
7
+ * reference host serves — so an application with server-authoritative state needs no
8
+ * endpoint string and no gateway code of its own.
9
+ */
10
+ export type RemoteOption = boolean | {
11
+ endpoint?: string;
12
+ timeoutMs?: number;
13
+ };
3
14
  export interface HtmlOptions extends CompileOptions {
4
15
  title?: string;
16
+ /**
17
+ * Configure the page to talk to an authority. Omitted, it defaults to `true` whenever the
18
+ * IR contains a remote action, because such a page cannot work without one.
19
+ */
20
+ remote?: RemoteOption;
5
21
  /**
6
22
  * Pins the appearance of the emitted page. Omitted, the theme decides — and a theme set
7
23
  * to `system` follows the reader's own preference.
package/dist/codegen.js CHANGED
@@ -21,6 +21,24 @@ export function compileIRToHtml(ir, options = {}) {
21
21
  const title = options.title ?? ir.name;
22
22
  const stylesheet = createThemeStylesheet(ir.theme);
23
23
  const appearance = options.appearance ?? (ir.theme.appearance === 'system' ? undefined : ir.theme.appearance);
24
+ // A page with remote actions is not usable without a gateway, so one is configured by
25
+ // default rather than left for an author to discover.
26
+ const needsRemote = (ir.remoteActionIds ?? []).length > 0;
27
+ const remote = options.remote ?? needsRemote;
28
+ const remoteConfig = remote === false
29
+ ? null
30
+ : {
31
+ endpoint: (remote === true ? undefined : remote.endpoint) ?? '/axiom',
32
+ ...(remote !== true && remote.timeoutMs !== undefined ? { timeoutMs: remote.timeoutMs } : {}),
33
+ };
34
+ const bootstrap = remoteConfig
35
+ ? [
36
+ `const __axiomRemote = createHttpRemoteGateway(${JSON.stringify(remoteConfig)});`,
37
+ 'const __axiomApp = createAxiomRuntime({ ir: __AXIOM_IR__, rootElement: __axiomRoot, host: createBrowserHost(), remote: __axiomRemote });',
38
+ ]
39
+ : [
40
+ 'const __axiomApp = createAxiomRuntime({ ir: __AXIOM_IR__, rootElement: __axiomRoot, host: createBrowserHost() });',
41
+ ];
24
42
  return [
25
43
  '<!DOCTYPE html>',
26
44
  `<html lang="en"${appearance ? ` data-axiom-appearance="${appearance}"` : ''}>`,
@@ -38,8 +56,10 @@ export function compileIRToHtml(ir, options = {}) {
38
56
  runtimeSource,
39
57
  `const __AXIOM_IR__ = ${payload};`,
40
58
  'const __axiomRoot = document.getElementById("app");',
41
- 'const __axiomApp = createAxiomRuntime({ ir: __AXIOM_IR__, rootElement: __axiomRoot, host: createBrowserHost() });',
59
+ ...bootstrap,
42
60
  'globalThis.__AXIOM_APP__ = __axiomApp;',
61
+ // `start()` is the whole startup sequence: it renders synchronously and then loads
62
+ // authoritative state when a gateway is configured.
43
63
  '__axiomApp.start();',
44
64
  ' </script>',
45
65
  '</body>',
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.1-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.1-alpha.1",
35
+ "@cynodia/axiom-runtime": "0.6.1-alpha.1"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsc -b tsconfig.json tsconfig.test.json",