@cynodia/axiom-server 0.6.0-alpha.1 → 0.6.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/README.md CHANGED
@@ -18,16 +18,33 @@ SQLite), `TransportAdapter` (in-process and HTTP), and `ServerHost` for time, id
18
18
  and authentication. Nothing in an ApplicationGraph mentions HTTP, SQL or a route.
19
19
 
20
20
  Main exports: `createAxiomServer`, `createMemoryPersistence`, `createSqlitePersistence`,
21
- `createServerHost`, `createDirectTransport`, `createHttpTransport`, `createRemoteGateway`,
22
- `serveOverHttp`, `SERVER_DIAGNOSTIC_CODES`.
21
+ `createServerHost`, `createDeterministicServerHost`, `createDirectTransport`,
22
+ `createHttpTransport`, `createRemoteGateway`, `serveOverHttp`, `serveAxiomApplication`,
23
+ `SERVER_DIAGNOSTIC_CODES`.
23
24
 
24
- `conformance/*.json` ships with this package: portable fixtures a Server IR, the state to
25
- start from, invocations and expected results that any conforming runtime can be held to.
25
+ `serveAxiomApplication` is the whole deployment story: it serves the generated page at `GET /`
26
+ and the semantic endpoint at `POST /axiom`, from one process, for any Axiom application. No
27
+ route, controller, handler or SQL statement is written by an application author.
28
+
29
+ ### Portable artifacts
30
+
31
+ `axiom.server.v1` is a frozen, language-independent contract, and this package ships what an
32
+ implementation in another language needs to conform to it:
33
+
34
+ ```
35
+ @cynodia/axiom-server/conformance the fixture manifest
36
+ @cynodia/axiom-server/conformance/<name>.json one fixture: IR, state, invocations, expectations
37
+ @cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for the IR
38
+ @cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
39
+ ```
40
+
41
+ The fixtures are pure data. Running them requires no part of this implementation — which is
42
+ the point: the semantic contract, the schemas and these files are the whole specification.
26
43
 
27
44
  ## Installation
28
45
 
29
46
  ```bash
30
- npm install @cynodia/axiom-server@alpha
47
+ npm install @cynodia/axiom-server
31
48
  ```
32
49
 
33
50
  This package is **not** re-exported by the `@cynodia/axiom` facade, because it imports
@@ -727,6 +727,7 @@
727
727
  },
728
728
  "expect": {
729
729
  "ok": false,
730
+ "changedStates": [],
730
731
  "diagnosticCodes": [
731
732
  "AUTHORIZATION_DENIED"
732
733
  ]
@@ -740,6 +741,7 @@
740
741
  },
741
742
  "expect": {
742
743
  "ok": false,
744
+ "changedStates": [],
743
745
  "diagnosticCodes": [
744
746
  "AUTHORIZATION_DENIED"
745
747
  ]
@@ -753,7 +755,11 @@
753
755
  "param_stock": 1
754
756
  },
755
757
  "expect": {
756
- "ok": true
758
+ "ok": true,
759
+ "changedStates": [
760
+ "state_parts",
761
+ "state_total"
762
+ ]
757
763
  }
758
764
  }
759
765
  ],
@@ -738,6 +738,7 @@
738
738
  },
739
739
  "expect": {
740
740
  "ok": false,
741
+ "changedStates": [],
741
742
  "diagnosticCodes": [
742
743
  "CONSTRAINT_VIOLATION"
743
744
  ]
@@ -735,7 +735,11 @@
735
735
  ]
736
736
  },
737
737
  "expect": {
738
- "ok": true
738
+ "ok": true,
739
+ "changedStates": [
740
+ "state_parts",
741
+ "state_total"
742
+ ]
739
743
  }
740
744
  }
741
745
  ],
@@ -727,6 +727,7 @@
727
727
  },
728
728
  "expect": {
729
729
  "ok": false,
730
+ "changedStates": [],
730
731
  "diagnosticCodes": [
731
732
  "PRECONDITION_FAILED"
732
733
  ],
@@ -743,6 +744,7 @@
743
744
  },
744
745
  "expect": {
745
746
  "ok": false,
747
+ "changedStates": [],
746
748
  "diagnosticCodes": [
747
749
  "PRECONDITION_FAILED"
748
750
  ],
@@ -727,7 +727,12 @@
727
727
  "param_qty": 3
728
728
  },
729
729
  "expect": {
730
- "ok": true
730
+ "ok": true,
731
+ "changedStates": [
732
+ "state_parts",
733
+ "state_ledger",
734
+ "state_total"
735
+ ]
731
736
  }
732
737
  },
733
738
  {
@@ -739,7 +744,12 @@
739
744
  },
740
745
  "expect": {
741
746
  "ok": true,
742
- "replayed": true
747
+ "replayed": true,
748
+ "changedStates": [
749
+ "state_parts",
750
+ "state_ledger",
751
+ "state_total"
752
+ ]
743
753
  }
744
754
  }
745
755
  ],
@@ -0,0 +1,143 @@
1
+ {
2
+ "conformance": "axiom.conformance.v1",
3
+ "contract": "axiom.server.v1",
4
+ "protocol": "axiom.protocol.v1",
5
+ "release": "0.6.2-alpha.1",
6
+ "description": "Portable conformance fixtures for the Axiom Server IR. Each entry is a self-contained JSON document: the Server IR, the state to start from, the principals, the invocations to perform and the results required. Running them needs no part of this implementation.",
7
+ "areas": [
8
+ "action guards",
9
+ "argument validation",
10
+ "authorization",
11
+ "concurrent mutation",
12
+ "constraints",
13
+ "derived state",
14
+ "expression evaluation",
15
+ "failure modes",
16
+ "for-each provisional writes",
17
+ "idempotency",
18
+ "mutation",
19
+ "persistence",
20
+ "restart",
21
+ "rollback",
22
+ "transition constraints"
23
+ ],
24
+ "fixtures": [
25
+ {
26
+ "name": "mutation-commits",
27
+ "file": "mutation-commits.json",
28
+ "covers": [
29
+ "expression evaluation",
30
+ "mutation",
31
+ "derived state"
32
+ ],
33
+ "description": "An action within its guards commits every write, and derived state follows.",
34
+ "concurrent": false,
35
+ "restartAndReassert": false,
36
+ "invocations": 1
37
+ },
38
+ {
39
+ "name": "guard-refuses",
40
+ "file": "guard-refuses.json",
41
+ "covers": [
42
+ "action guards",
43
+ "failure modes"
44
+ ],
45
+ "description": "A guard that does not hold refuses the invocation, naming its failure mode.",
46
+ "concurrent": false,
47
+ "restartAndReassert": false,
48
+ "invocations": 2
49
+ },
50
+ {
51
+ "name": "constraint-rolls-back",
52
+ "file": "constraint-rolls-back.json",
53
+ "covers": [
54
+ "constraints",
55
+ "rollback",
56
+ "for-each provisional writes"
57
+ ],
58
+ "description": "Iteration N sees the writes of iterations before it, so two lines for one part are counted together — and the invariant then rolls the whole action back.",
59
+ "concurrent": false,
60
+ "restartAndReassert": false,
61
+ "invocations": 1
62
+ },
63
+ {
64
+ "name": "for-each-provisional",
65
+ "file": "for-each-provisional.json",
66
+ "covers": [
67
+ "for-each provisional writes"
68
+ ],
69
+ "description": "The same two-line shape, within stock: 10 − 4 − 4, not 10 − 4 twice.",
70
+ "concurrent": false,
71
+ "restartAndReassert": false,
72
+ "invocations": 1
73
+ },
74
+ {
75
+ "name": "transition-constraint",
76
+ "file": "transition-constraint.json",
77
+ "covers": [
78
+ "transition constraints"
79
+ ],
80
+ "description": "A rule about how state may change refuses a raise, and permits a fall.",
81
+ "concurrent": false,
82
+ "restartAndReassert": false,
83
+ "invocations": 2
84
+ },
85
+ {
86
+ "name": "authorization",
87
+ "file": "authorization.json",
88
+ "covers": [
89
+ "authorization"
90
+ ],
91
+ "description": "Authorization is evaluated on the authority, with the caller bound to PRINCIPAL.",
92
+ "concurrent": false,
93
+ "restartAndReassert": false,
94
+ "invocations": 3
95
+ },
96
+ {
97
+ "name": "argument-validation",
98
+ "file": "argument-validation.json",
99
+ "covers": [
100
+ "argument validation"
101
+ ],
102
+ "description": "Untrusted arguments are checked against declared parameter types.",
103
+ "concurrent": false,
104
+ "restartAndReassert": false,
105
+ "invocations": 4
106
+ },
107
+ {
108
+ "name": "idempotent-retry",
109
+ "file": "idempotent-retry.json",
110
+ "covers": [
111
+ "persistence",
112
+ "idempotency"
113
+ ],
114
+ "description": "A repeated request id is answered from the record, not executed again.",
115
+ "concurrent": false,
116
+ "restartAndReassert": false,
117
+ "invocations": 2
118
+ },
119
+ {
120
+ "name": "concurrent-invocations",
121
+ "file": "concurrent-invocations.json",
122
+ "covers": [
123
+ "concurrent mutation"
124
+ ],
125
+ "description": "Three callers each want four of a stock of five. Exactly one may commit; the rest are refused by the guard against authoritative state.",
126
+ "concurrent": true,
127
+ "restartAndReassert": false,
128
+ "invocations": 3
129
+ },
130
+ {
131
+ "name": "restart",
132
+ "file": "restart.json",
133
+ "covers": [
134
+ "persistence",
135
+ "restart"
136
+ ],
137
+ "description": "Committed state is restored exactly on restart, and a rolled-back write never reappears.",
138
+ "concurrent": false,
139
+ "restartAndReassert": true,
140
+ "invocations": 2
141
+ }
142
+ ]
143
+ }
@@ -730,7 +730,8 @@
730
730
  "ok": true,
731
731
  "changedStates": [
732
732
  "state_parts",
733
- "state_ledger"
733
+ "state_ledger",
734
+ "state_total"
734
735
  ]
735
736
  }
736
737
  }
@@ -727,7 +727,12 @@
727
727
  "param_qty": 3
728
728
  },
729
729
  "expect": {
730
- "ok": true
730
+ "ok": true,
731
+ "changedStates": [
732
+ "state_parts",
733
+ "state_ledger",
734
+ "state_total"
735
+ ]
731
736
  }
732
737
  },
733
738
  {
@@ -737,7 +742,8 @@
737
742
  "param_qty": 99
738
743
  },
739
744
  "expect": {
740
- "ok": false
745
+ "ok": false,
746
+ "changedStates": []
741
747
  }
742
748
  }
743
749
  ],
@@ -727,6 +727,7 @@
727
727
  },
728
728
  "expect": {
729
729
  "ok": false,
730
+ "changedStates": [],
730
731
  "diagnosticCodes": [
731
732
  "TRANSITION_CONSTRAINT_VIOLATION"
732
733
  ]
@@ -740,7 +741,11 @@
740
741
  "param_stock": 4
741
742
  },
742
743
  "expect": {
743
- "ok": true
744
+ "ok": true,
745
+ "changedStates": [
746
+ "state_parts",
747
+ "state_total"
748
+ ]
744
749
  }
745
750
  }
746
751
  ],
package/dist/host.d.ts CHANGED
@@ -53,5 +53,22 @@ export declare function createServerHost(overrides?: Partial<ServerHost>): Serve
53
53
  * A deterministic host, for conformance runs and tests. `now` and `uuid` count rather than
54
54
  * varying, so an expected result is stable.
55
55
  */
56
+ /**
57
+ * The host model the conformance suite runs against.
58
+ *
59
+ * `now()` and `uuid()` are the only two places semantics can depend on something outside the
60
+ * graph, so a portable fixture needs both pinned. The model is one counter, shared, starting
61
+ * at zero and incremented **before** each value is produced, so the nth host call in an
62
+ * execution — whichever of the two it is — is always the same value:
63
+ *
64
+ * ```
65
+ * uuid() → "id-<n>"
66
+ * now() → "2026-01-01T00:00:<n, two digits>.000Z"
67
+ * ```
68
+ *
69
+ * A runtime in another language reproduces this exactly by counting host calls in execution
70
+ * order. The counter is per host instance, and a fixture that restarts the authority keeps
71
+ * the same host — a restart does not rewind it.
72
+ */
56
73
  export declare function createDeterministicServerHost(overrides?: Partial<ServerHost>): ServerHost;
57
74
  //# sourceMappingURL=host.d.ts.map
package/dist/host.js CHANGED
@@ -12,6 +12,23 @@ export function createServerHost(overrides = {}) {
12
12
  * A deterministic host, for conformance runs and tests. `now` and `uuid` count rather than
13
13
  * varying, so an expected result is stable.
14
14
  */
15
+ /**
16
+ * The host model the conformance suite runs against.
17
+ *
18
+ * `now()` and `uuid()` are the only two places semantics can depend on something outside the
19
+ * graph, so a portable fixture needs both pinned. The model is one counter, shared, starting
20
+ * at zero and incremented **before** each value is produced, so the nth host call in an
21
+ * execution — whichever of the two it is — is always the same value:
22
+ *
23
+ * ```
24
+ * uuid() → "id-<n>"
25
+ * now() → "2026-01-01T00:00:<n, two digits>.000Z"
26
+ * ```
27
+ *
28
+ * A runtime in another language reproduces this exactly by counting host calls in execution
29
+ * order. The counter is per host instance, and a fixture that restarts the authority keeps
30
+ * the same host — a restart does not rewind it.
31
+ */
15
32
  export function createDeterministicServerHost(overrides = {}) {
16
33
  let counter = 0;
17
34
  return {
@@ -1,4 +1,7 @@
1
1
  import type { AxiomServer } from './server.js';
2
+ import type { PrincipalRecord } from './host.js';
3
+ import type { PersistenceAdapter } from './persistence.js';
4
+ import type { ServerIR } from './deps.js';
2
5
  /**
3
6
  * The reference Node host.
4
7
  *
@@ -11,6 +14,11 @@ export interface NodeHostOptions {
11
14
  port?: number;
12
15
  /** The single semantic endpoint. */
13
16
  path?: string;
17
+ /**
18
+ * The generated browser page. When given, `GET /` serves it — so one process hands out
19
+ * the client and answers it. Omit to run a bare authority.
20
+ */
21
+ page?: string;
14
22
  }
15
23
  export interface RunningNodeHost {
16
24
  /** The port actually bound, which matters when `port: 0` was requested. */
@@ -19,4 +27,41 @@ export interface RunningNodeHost {
19
27
  close(): Promise<void>;
20
28
  }
21
29
  export declare function serveOverHttp(options: NodeHostOptions): Promise<RunningNodeHost>;
30
+ /**
31
+ * One graph, one process: the generated page and the authority that answers it.
32
+ *
33
+ * This is the whole deployment story for an application with server-authoritative state.
34
+ * The caller supplies the two compiled artifacts — the Server IR and the page — plus how to
35
+ * authenticate a credential and where to keep state. It defines no route, no verb and no
36
+ * handler, because there are none to define: `GET /` is the page and `POST /axiom` is the
37
+ * semantic endpoint, for every Axiom application there will ever be.
38
+ *
39
+ * ```ts
40
+ * const running = await serveAxiomApplication({
41
+ * serverIR: compileToServerIR(graph),
42
+ * page: compileToHtml(graph),
43
+ * persistence: createSqlitePersistence({ file: 'app.db' }),
44
+ * authenticate: (credential) => users.get(credential ?? '') ?? null,
45
+ * port: 3000,
46
+ * });
47
+ * ```
48
+ */
49
+ export interface AxiomApplicationOptions {
50
+ serverIR: ServerIR;
51
+ /** The generated browser page, exactly as `compileToHtml` produced it. */
52
+ page: string;
53
+ persistence?: PersistenceAdapter;
54
+ /** Resolves opaque credential material to a principal record, or null for anonymous. */
55
+ authenticate?: (credential: string | null) => PrincipalRecord | null | Promise<PrincipalRecord | null>;
56
+ port?: number;
57
+ /** The semantic endpoint. Change it only if something else already owns `/axiom`. */
58
+ path?: string;
59
+ }
60
+ export interface RunningAxiomApplication extends RunningNodeHost {
61
+ /** The page's address, which is what a person opens. */
62
+ pageUrl: string;
63
+ /** The authority itself, for tests and administrative inspection. */
64
+ server: AxiomServer;
65
+ }
66
+ export declare function serveAxiomApplication(options: AxiomApplicationOptions): Promise<RunningAxiomApplication>;
22
67
  //# sourceMappingURL=node-host.d.ts.map
package/dist/node-host.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { dispatch } from './transport.js';
3
+ import { createAxiomServer } from './server.js';
4
+ import { createServerHost } from './host.js';
3
5
  const MAX_BODY_BYTES = 1024 * 1024;
4
6
  async function readBody(request) {
5
7
  const chunks = [];
@@ -22,7 +24,15 @@ export async function serveOverHttp(options) {
22
24
  await options.server.start();
23
25
  const http = createServer((request, response) => {
24
26
  void (async () => {
25
- if (request.method !== 'POST' || (request.url ?? '').split('?')[0] !== path) {
27
+ const target = (request.url ?? '').split('?')[0];
28
+ if (options.page !== undefined && request.method === 'GET' && (target === '/' || target === '/index.html')) {
29
+ // The generated page and the endpoint it talks to, from one process. Nothing here is
30
+ // application-specific: every Axiom application is served by exactly this handler.
31
+ response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
32
+ response.end(options.page);
33
+ return;
34
+ }
35
+ if (request.method !== 'POST' || target !== path) {
26
36
  response.writeHead(404, { 'content-type': 'application/json' });
27
37
  response.end(JSON.stringify({ kind: 'error', diagnostics: [] }));
28
38
  return;
@@ -61,3 +71,19 @@ export async function serveOverHttp(options) {
61
71
  }),
62
72
  };
63
73
  }
74
+ export async function serveAxiomApplication(options) {
75
+ const server = createAxiomServer({
76
+ ir: options.serverIR,
77
+ ...(options.persistence ? { persistence: options.persistence } : {}),
78
+ ...(options.authenticate
79
+ ? { host: createServerHost({ authenticate: options.authenticate }) }
80
+ : {}),
81
+ });
82
+ const running = await serveOverHttp({
83
+ server,
84
+ page: options.page,
85
+ ...(options.port === undefined ? {} : { port: options.port }),
86
+ ...(options.path === undefined ? {} : { path: options.path }),
87
+ });
88
+ return { ...running, pageUrl: `http://127.0.0.1:${running.port}/`, server };
89
+ }
@@ -13,7 +13,15 @@ export interface SnapshotRequest {
13
13
  kind: 'snapshot';
14
14
  protocol: typeof PROTOCOL_VERSION;
15
15
  credential?: Credential;
16
- /** When given, only states changed after this revision are returned. */
16
+ /**
17
+ * Ask for an incremental snapshot: a revision this caller already holds.
18
+ *
19
+ * The response then omits every observable state the authority can **prove** has not
20
+ * changed since that revision, and sets `partial`. Omission always means unchanged;
21
+ * inclusion never promises changed. A value that is not a non-negative safe integer is a
22
+ * malformed request; a value the authority cannot reason about — ahead of its own
23
+ * revision — is answered with a complete snapshot, which is always a valid answer.
24
+ */
17
25
  sinceRevision?: number;
18
26
  }
19
27
  export interface InvokeRequest {
@@ -30,10 +38,19 @@ export interface InvokeRequest {
30
38
  requestId?: string;
31
39
  }
32
40
  export type ServerRequest = SnapshotRequest | InvokeRequest;
33
- /** The authoritative value of every observable state the caller may see. */
41
+ /**
42
+ * The authoritative value of the observable states the caller may see.
43
+ *
44
+ * Complete unless `partial` is set. A complete snapshot names every observable state; a
45
+ * partial one names those that may have changed since the `sinceRevision` that was asked
46
+ * for, and a state it does not name is unchanged since then. `revision` is the authority's
47
+ * current revision either way, so a client can use it as the next `sinceRevision`.
48
+ */
34
49
  export interface StateSnapshot {
35
50
  revision: number;
36
51
  states: Record<NodeId, unknown>;
52
+ /** Present and true only for the answer to a `sinceRevision` request. */
53
+ partial?: boolean;
37
54
  }
38
55
  export interface InvokeResponse {
39
56
  kind: 'result';
package/dist/server.d.ts CHANGED
@@ -42,17 +42,5 @@ export interface AxiomServer {
42
42
  mutationLog(): MutationLogEntry[];
43
43
  stop(): Promise<void>;
44
44
  }
45
- /**
46
- * The authoritative runtime.
47
- *
48
- * It executes the **same semantic engine** the client runs, given an IR that contains no UI
49
- * and no routes. That is deliberate rather than convenient: transaction boundaries,
50
- * provisional writes, `for-each` ordering, constraint and transition evaluation, rollback
51
- * and the mutation log are not reimplemented here, so a graph cannot behave differently
52
- * merely because execution moved to the authority.
53
- *
54
- * Requests are serialized. One action runs at a time, and its persistence commit completes
55
- * before the next begins, so two callers cannot both commit from the same snapshot.
56
- */
57
45
  export declare function createAxiomServer(options: AxiomServerOptions): AxiomServer;
58
46
  //# sourceMappingURL=server.d.ts.map