@cynodia/axiom-server 0.11.0-alpha.1 → 0.11.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.
Files changed (37) hide show
  1. package/README.md +46 -19
  2. package/conformance/migrations/add-optional-field.json +1 -1
  3. package/conformance/migrations/add-required-field-with-default.json +1 -1
  4. package/conformance/migrations/crash-and-resume.json +1 -1
  5. package/conformance/migrations/destructive-removal-approved.json +1 -1
  6. package/conformance/migrations/destructive-removal-refused.json +1 -1
  7. package/conformance/migrations/idempotent-rerun.json +1 -1
  8. package/conformance/migrations/invalid-target-record.json +1 -1
  9. package/conformance/migrations/large-batched-transformation.json +1 -1
  10. package/conformance/migrations/manifest.json +1 -1
  11. package/conformance/migrations/metadata-only-change.json +1 -1
  12. package/conformance/migrations/migration-lock.json +1 -1
  13. package/conformance/migrations/missing-migration-path.json +1 -1
  14. package/conformance/migrations/record-transformation.json +1 -1
  15. package/conformance/migrations/relationship-addition.json +1 -1
  16. package/conformance/migrations/remove-empty-field.json +1 -1
  17. package/conformance/migrations/schema-fingerprint-mismatch.json +1 -1
  18. package/conformance/migrations/transform-field.json +1 -1
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +1 -0
  21. package/dist/migration-execute.d.ts +16 -4
  22. package/dist/migration-execute.js +64 -12
  23. package/dist/migration-executor.d.ts +24 -0
  24. package/dist/migration-executor.js +120 -32
  25. package/dist/migration-gate.d.ts +42 -9
  26. package/dist/migration-gate.js +107 -20
  27. package/dist/migration.d.ts +12 -0
  28. package/dist/migration.js +12 -0
  29. package/dist/server.d.ts +2 -0
  30. package/dist/server.js +56 -27
  31. package/dist/sqlite-contention.d.ts +80 -0
  32. package/dist/sqlite-contention.js +125 -0
  33. package/dist/sqlite-migration.d.ts +14 -0
  34. package/dist/sqlite-migration.js +193 -116
  35. package/package.json +3 -3
  36. package/schema/protocol.v1.schema.json +1 -1
  37. package/schema/server-ir.v7.schema.json +1 -1
package/README.md CHANGED
@@ -22,21 +22,39 @@ SQLite), `TransportAdapter` (in-process and HTTP), `ServerHost` for time, identi
22
22
  scheduling and authentication, and `IntegrationAdapter` for external systems. Nothing in
23
23
  an ApplicationGraph mentions HTTP, SQL, a route or an SDK.
24
24
 
25
- As of 0.8 this package also owns timed and event-driven execution: it schedules and
26
- dispatches `TriggerDef`s, records `integration-effect` intent atomically with the state
27
- write that requested it (the transactional outbox), dispatches an effect's committed
28
- intents to its `IntegrationAdapter` post-commit with retry, and translates verified
29
- webhook deliveries into semantic events. See
30
- [`docs/INTEGRATIONS.md`](https://github.com/cynodia/axiom/blob/main/docs/INTEGRATIONS.md),
31
- [`docs/EFFECTS.md`](https://github.com/cynodia/axiom/blob/main/docs/EFFECTS.md) and
32
- [`docs/TRIGGERS.md`](https://github.com/cynodia/axiom/blob/main/docs/TRIGGERS.md).
25
+ This package also owns the parts of an application that only the authority can run:
26
+
27
+ - **Timed and event-driven execution** (0.8) — scheduling and dispatching `TriggerDef`s,
28
+ the transactional outbox for `integration-effect` intent, post-commit effect delivery
29
+ with retry, and verified-webhook → semantic-event translation. See
30
+ [`docs/INTEGRATIONS.md`](https://github.com/cynodia/axiom/blob/main/docs/INTEGRATIONS.md),
31
+ [`docs/EFFECTS.md`](https://github.com/cynodia/axiom/blob/main/docs/EFFECTS.md),
32
+ [`docs/TRIGGERS.md`](https://github.com/cynodia/axiom/blob/main/docs/TRIGGERS.md).
33
+ - **Inbound streams and binary storage** (0.9) — long-lived `SubscriptionDef` sources
34
+ feeding the event pipeline, and `StorageDef` object stores behind host blob transports.
35
+ See [`docs/SUBSCRIPTIONS.md`](https://github.com/cynodia/axiom/blob/main/docs/SUBSCRIPTIONS.md),
36
+ [`docs/STORAGE.md`](https://github.com/cynodia/axiom/blob/main/docs/STORAGE.md).
37
+ - **The query layer** (0.10) — demand-driven `QueryDef` reads over authoritative data too
38
+ large to materialize, `DataProvider`s (`createMemoryDataProvider` / `createSqliteDataProvider`),
39
+ fingerprinted keyset cursors, and a principal/policy-scoped result cache. See
40
+ [`docs/QUERIES.md`](https://github.com/cynodia/axiom/blob/main/docs/QUERIES.md).
41
+ - **Schema evolution** (0.11) — `planMigration` / `explainMigration`, the host-controlled
42
+ `executeMigration`, the durable `MigrationMetadataStore` and lease lock, keyset-batched
43
+ crash-resumable migration, memory and SQLite `MigrationRowStore`s, and the
44
+ `createAxiomServer` startup gate. Concurrent migrators — including two OS processes on one
45
+ SQLite file — resolve to `completed` / `MIGRATION_IN_PROGRESS` / `alreadyAtTarget`; the
46
+ SQLite provider absorbs physical `SQLITE_BUSY` contention internally (0.11.2). See
47
+ [`docs/MIGRATIONS.md`](https://github.com/cynodia/axiom/blob/main/docs/MIGRATIONS.md).
33
48
 
34
49
  Main exports: `createAxiomServer`, `createMemoryPersistence`, `createSqlitePersistence`,
35
50
  `createServerHost`, `createDeterministicServerHost`, `createDirectTransport`,
36
51
  `createHttpTransport`, `createRemoteGateway`, `serveOverHttp`, `serveAxiomApplication`,
37
52
  `createFakeIntegrationAdapter`, `createHttpIntegrationAdapter`, `createTriggerRuntime`,
38
- `createEffectRunner`, `runConformanceFixture`, `runConformanceSuite`,
39
- `SERVER_DIAGNOSTIC_CODES`.
53
+ `createEffectRunner`, `createMemoryDataProvider`, `createSqliteDataProvider`,
54
+ `createMemoryMigrationStore`, `createSqliteMigrationStore`, `createMemoryRowStore`,
55
+ `createSqliteRowStore`, `planMigration`, `explainMigration`, `executeMigration`,
56
+ `getMigrationStatus`, `migrationAuthority`, `runConformanceFixture`, `runConformanceSuite`,
57
+ `runQueryConformanceFixture`, `runMigrationConformanceFixture`, `SERVER_DIAGNOSTIC_CODES`.
40
58
 
41
59
  `serveAxiomApplication` is the whole deployment story: it serves the generated page at `GET /`
42
60
  and the semantic endpoint at `POST /axiom`, from one process, for any Axiom application. No
@@ -44,17 +62,26 @@ route, controller, handler or SQL statement is written by an application author.
44
62
 
45
63
  ### Portable artifacts
46
64
 
47
- `axiom.server.v1` is a frozen, language-independent contract, and this package ships what an
48
- implementation in another language needs to conform to it:
65
+ The Server IR contract is versioned. **`axiom.server.v1` is frozen** — its bytes and
66
+ semantics never change — but it is **not the current contract**: a document declares the
67
+ oldest contract that can carry its vocabulary, computed from the document. The current
68
+ contract is **`axiom.server.v7`** (0.11 schema-evolution vocabulary); `v1`–`v6` are frozen
69
+ and shipped for compatibility. This package ships what an implementation in another language
70
+ needs to conform:
49
71
 
50
72
  ```
51
- @cynodia/axiom-server/conformance the fixture manifest
52
- @cynodia/axiom-server/conformance/<name>.json one fixture: IR, state, invocations, expectations
53
- @cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for the frozen v1 IR
54
- @cynodia/axiom-server/schema/server-ir.v2.schema.json JSON Schema for v2 (+ group, expression-ref)
55
- @cynodia/axiom-server/schema/server-ir.v3.schema.json JSON Schema for v3 (+ integrations, effects, triggers, events)
56
- @cynodia/axiom-server/schema/server-ir.v4.schema.json JSON Schema for v4 (+ invocation source, structured effect outcome)
57
- @cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
73
+ @cynodia/axiom-server/conformance the base fixture manifest
74
+ @cynodia/axiom-server/conformance/<name>.json one base fixture: IR, state, invocations, expectations
75
+ @cynodia/axiom-server/conformance/queries/<name>.json one query conformance fixture (axiom.conformance.v4)
76
+ @cynodia/axiom-server/conformance/migrations/<name>.json one migration conformance fixture (axiom.conformance.v5)
77
+ @cynodia/axiom-server/schema/server-ir.v1.schema.json JSON Schema for the frozen v1 IR
78
+ @cynodia/axiom-server/schema/server-ir.v2.schema.json v2 (+ group, expression-ref)
79
+ @cynodia/axiom-server/schema/server-ir.v3.schema.json v3 (+ integrations, effects, triggers, events)
80
+ @cynodia/axiom-server/schema/server-ir.v4.schema.json v4 (+ invocation source, structured effect outcome)
81
+ @cynodia/axiom-server/schema/server-ir.v5.schema.json v5 (+ subscriptions, storage, blob operations)
82
+ @cynodia/axiom-server/schema/server-ir.v6.schema.json v6 (+ queries, relationships, read policies)
83
+ @cynodia/axiom-server/schema/server-ir.v7.schema.json v7 (+ migrations, schema version + fingerprint) — current
84
+ @cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
58
85
  ```
59
86
 
60
87
  The fixtures are pure data. Running them requires no part of this implementation — which is
@@ -10,7 +10,7 @@
10
10
  "contract": "axiom.server.v7",
11
11
  "id": "migration-conformance",
12
12
  "name": "Migration Conformance",
13
- "version": "0.11.0-alpha.1",
13
+ "version": "0.11.2-alpha.1",
14
14
  "schemaVersion": 2,
15
15
  "schemaFingerprint": "7e4d24d95c273ff084e7758508a02e35015d5db2ceda46ca61e7ee8a2c2a4929",
16
16
  "migrations": [
@@ -10,7 +10,7 @@
10
10
  "contract": "axiom.server.v7",
11
11
  "id": "migration-conformance",
12
12
  "name": "Migration Conformance",
13
- "version": "0.11.0-alpha.1",
13
+ "version": "0.11.2-alpha.1",
14
14
  "schemaVersion": 2,
15
15
  "schemaFingerprint": "2c478dc5301786b76c6990e38e1659d621d7712fdfa785bf115b1275b9804877",
16
16
  "migrations": [
@@ -12,7 +12,7 @@
12
12
  "contract": "axiom.server.v7",
13
13
  "id": "migration-conformance",
14
14
  "name": "Migration Conformance",
15
- "version": "0.11.0-alpha.1",
15
+ "version": "0.11.2-alpha.1",
16
16
  "schemaVersion": 2,
17
17
  "schemaFingerprint": "2c478dc5301786b76c6990e38e1659d621d7712fdfa785bf115b1275b9804877",
18
18
  "migrations": [
@@ -10,7 +10,7 @@
10
10
  "contract": "axiom.server.v7",
11
11
  "id": "migration-conformance",
12
12
  "name": "Migration Conformance",
13
- "version": "0.11.0-alpha.1",
13
+ "version": "0.11.2-alpha.1",
14
14
  "schemaVersion": 2,
15
15
  "schemaFingerprint": "f9f0f88b1d0cd25eb570c1401fe3f96ca899330d8c210e79ef50a10e91ad236d",
16
16
  "migrations": [
@@ -12,7 +12,7 @@
12
12
  "contract": "axiom.server.v7",
13
13
  "id": "migration-conformance",
14
14
  "name": "Migration Conformance",
15
- "version": "0.11.0-alpha.1",
15
+ "version": "0.11.2-alpha.1",
16
16
  "schemaVersion": 2,
17
17
  "schemaFingerprint": "f9f0f88b1d0cd25eb570c1401fe3f96ca899330d8c210e79ef50a10e91ad236d",
18
18
  "migrations": [
@@ -10,7 +10,7 @@
10
10
  "contract": "axiom.server.v7",
11
11
  "id": "migration-conformance",
12
12
  "name": "Migration Conformance",
13
- "version": "0.11.0-alpha.1",
13
+ "version": "0.11.2-alpha.1",
14
14
  "schemaVersion": 2,
15
15
  "schemaFingerprint": "2c478dc5301786b76c6990e38e1659d621d7712fdfa785bf115b1275b9804877",
16
16
  "migrations": [
@@ -11,7 +11,7 @@
11
11
  "contract": "axiom.server.v7",
12
12
  "id": "migration-conformance",
13
13
  "name": "Migration Conformance",
14
- "version": "0.11.0-alpha.1",
14
+ "version": "0.11.2-alpha.1",
15
15
  "schemaVersion": 2,
16
16
  "schemaFingerprint": "2c478dc5301786b76c6990e38e1659d621d7712fdfa785bf115b1275b9804877",
17
17
  "migrations": [
@@ -11,7 +11,7 @@
11
11
  "contract": "axiom.server.v7",
12
12
  "id": "migration-conformance",
13
13
  "name": "Migration Conformance",
14
- "version": "0.11.0-alpha.1",
14
+ "version": "0.11.2-alpha.1",
15
15
  "schemaVersion": 2,
16
16
  "schemaFingerprint": "2c478dc5301786b76c6990e38e1659d621d7712fdfa785bf115b1275b9804877",
17
17
  "migrations": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "conformance": "axiom.conformance.v5",
3
3
  "baseContract": "axiom.server.v7",
4
- "release": "0.11.0-alpha.1",
4
+ "release": "0.11.2-alpha.1",
5
5
  "description": "Portable migration conformance fixtures (spec11 §84-86). Each file carries a compiled axiom.server.v7 target Server IR with its MigrationDef chain, the schema version the persisted data starts at, the source rows, any destructive approvals, and the exact expected outcome. Running one needs only a row store and the semantics in docs/MIGRATIONS.md + docs/AUTHORITY.md. Every fixture must produce equivalent target data on the memory and SQLite providers (spec11 §83).",
6
6
  "fixtures": [
7
7
  {
@@ -10,7 +10,7 @@
10
10
  "contract": "axiom.server.v7",
11
11
  "id": "migration-conformance",
12
12
  "name": "Migration Conformance",
13
- "version": "0.11.0-alpha.1",
13
+ "version": "0.11.2-alpha.1",
14
14
  "schemaVersion": 2,
15
15
  "schemaFingerprint": "f9f0f88b1d0cd25eb570c1401fe3f96ca899330d8c210e79ef50a10e91ad236d",
16
16
  "migrations": [
@@ -11,7 +11,7 @@
11
11
  "contract": "axiom.server.v7",
12
12
  "id": "migration-conformance",
13
13
  "name": "Migration Conformance",
14
- "version": "0.11.0-alpha.1",
14
+ "version": "0.11.2-alpha.1",
15
15
  "schemaVersion": 2,
16
16
  "schemaFingerprint": "2c478dc5301786b76c6990e38e1659d621d7712fdfa785bf115b1275b9804877",
17
17
  "migrations": [
@@ -10,7 +10,7 @@
10
10
  "contract": "axiom.server.v7",
11
11
  "id": "migration-conformance",
12
12
  "name": "Migration Conformance",
13
- "version": "0.11.0-alpha.1",
13
+ "version": "0.11.2-alpha.1",
14
14
  "schemaVersion": 3,
15
15
  "schemaFingerprint": "5f584b6aacbe13facfb360b562bdfa020f96e5f4e6d7f63c3d0dc7edacc207eb",
16
16
  "migrations": [
@@ -11,7 +11,7 @@
11
11
  "contract": "axiom.server.v7",
12
12
  "id": "migration-conformance",
13
13
  "name": "Migration Conformance",
14
- "version": "0.11.0-alpha.1",
14
+ "version": "0.11.2-alpha.1",
15
15
  "schemaVersion": 2,
16
16
  "schemaFingerprint": "428fde4036f9c14ec63905ce893e907318542fd1a89e6f946b4400713c80a4de",
17
17
  "migrations": [
@@ -10,7 +10,7 @@
10
10
  "contract": "axiom.server.v7",
11
11
  "id": "migration-conformance",
12
12
  "name": "Migration Conformance",
13
- "version": "0.11.0-alpha.1",
13
+ "version": "0.11.2-alpha.1",
14
14
  "schemaVersion": 2,
15
15
  "schemaFingerprint": "4b8e155b1b02f04b28cadd78f580b4ac35f5ba0149e4809e31bbabd145852af4",
16
16
  "migrations": [
@@ -10,7 +10,7 @@
10
10
  "contract": "axiom.server.v7",
11
11
  "id": "migration-conformance",
12
12
  "name": "Migration Conformance",
13
- "version": "0.11.0-alpha.1",
13
+ "version": "0.11.2-alpha.1",
14
14
  "schemaVersion": 2,
15
15
  "schemaFingerprint": "f9f0f88b1d0cd25eb570c1401fe3f96ca899330d8c210e79ef50a10e91ad236d",
16
16
  "migrations": [
@@ -11,7 +11,7 @@
11
11
  "contract": "axiom.server.v7",
12
12
  "id": "migration-conformance",
13
13
  "name": "Migration Conformance",
14
- "version": "0.11.0-alpha.1",
14
+ "version": "0.11.2-alpha.1",
15
15
  "schemaVersion": 2,
16
16
  "schemaFingerprint": "2c478dc5301786b76c6990e38e1659d621d7712fdfa785bf115b1275b9804877",
17
17
  "migrations": [
@@ -10,7 +10,7 @@
10
10
  "contract": "axiom.server.v7",
11
11
  "id": "migration-conformance",
12
12
  "name": "Migration Conformance",
13
- "version": "0.11.0-alpha.1",
13
+ "version": "0.11.2-alpha.1",
14
14
  "schemaVersion": 2,
15
15
  "schemaFingerprint": "f9f0f88b1d0cd25eb570c1401fe3f96ca899330d8c210e79ef50a10e91ad236d",
16
16
  "migrations": [
package/dist/index.d.ts CHANGED
@@ -31,6 +31,8 @@ export * from './migration-eval.js';
31
31
  export * from './migration-executor.js';
32
32
  export * from './migration-execute.js';
33
33
  export * from './migration-conformance.js';
34
+ export { SqliteContentionError, isSqliteContentionError, runWithBusyHandling, DEFAULT_BUSY_TIMEOUT_MS, DEFAULT_BUSY_ATTEMPTS, DEFAULT_BUSY_BACKOFF_MS, } from './sqlite-contention.js';
35
+ export type { BusyHandlingOptions } from './sqlite-contention.js';
34
36
  export { createSqliteMigrationStore, createSqliteRowStore, isSqliteMigrationAvailable, } from './sqlite-migration.js';
35
37
  export type { SqliteMigrationStoreOptions, SqliteRowStoreOptions } from './sqlite-migration.js';
36
38
  export { createSqliteDataProvider, isSqliteAvailable as isSqliteDataProviderAvailable } from './sqlite-data-provider.js';
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ export * from './migration-eval.js';
22
22
  export * from './migration-executor.js';
23
23
  export * from './migration-execute.js';
24
24
  export * from './migration-conformance.js';
25
+ export { SqliteContentionError, isSqliteContentionError, runWithBusyHandling, DEFAULT_BUSY_TIMEOUT_MS, DEFAULT_BUSY_ATTEMPTS, DEFAULT_BUSY_BACKOFF_MS, } from './sqlite-contention.js';
25
26
  export { createSqliteMigrationStore, createSqliteRowStore, isSqliteMigrationAvailable, } from './sqlite-migration.js';
26
27
  export { createSqliteDataProvider, isSqliteAvailable as isSqliteDataProviderAvailable } from './sqlite-data-provider.js';
27
28
  export * from './effects.js';
@@ -18,16 +18,28 @@ import type { SchemaGateResult } from './migration-gate.js';
18
18
  */
19
19
  /**
20
20
  * The authority to run a migration. Deliberately **not** the nullable `PRINCIPAL` an action
21
- * authorization reads (spec11 §74): it is a distinct, host-minted token, so migration
22
- * authority can never be confused with ordinary application authorization.
21
+ * authorization reads (spec11 §74): it is a distinct, host-minted capability.
22
+ *
23
+ * Authorization is by **provenance, not shape** (spec11.1 §16-17). The object returned by
24
+ * `migrationAuthority()` is registered in a process-private `WeakSet` and frozen;
25
+ * `isMigrationPrincipal` checks membership of that set. An object a consumer builds with the
26
+ * same visible fields — `{ kind: 'axiom.migration-authority', grantedBy: 'operator' }` — or a
27
+ * spread copy `{ ...migrationAuthority('operator') }` is **not** in the set and is rejected.
28
+ * `grantedBy` remains visible as a descriptive audit label; it is not the source of
29
+ * authorization.
30
+ *
31
+ * The capability is process-local (spec11.1 §19) and is never serialized (spec11.1 §18):
32
+ * migration durability lives in the `MigrationMetadataStore`, the lease and the checkpoints,
33
+ * not in a portable token.
23
34
  */
24
35
  export interface MigrationPrincipal {
25
36
  readonly kind: 'axiom.migration-authority';
26
- /** A free-form label the host records for the audit trail — an operator id, a deploy job. */
37
+ /** A descriptive audit label — an operator id, a deploy job. Not the source of authorization. */
27
38
  readonly grantedBy: string;
28
39
  }
29
- /** Construct migration authority. Only a host calls this. */
40
+ /** Mint migration authority. Only a host calls this; the result cannot be reconstructed by shape. */
30
41
  export declare function migrationAuthority(grantedBy: string): MigrationPrincipal;
42
+ /** True only for the exact object `migrationAuthority()` returned — provenance, not duck typing. */
31
43
  export declare function isMigrationPrincipal(value: unknown): value is MigrationPrincipal;
32
44
  export interface ExecuteMigrationOptions {
33
45
  ir: ServerIR;
@@ -1,16 +1,22 @@
1
1
  import { MIGRATION_DIAGNOSTIC_CODES } from './migration.js';
2
2
  import { planMigration } from './migration.js';
3
- import { runMigration } from './migration-executor.js';
3
+ import { classifyMigrationContention, runMigration } from './migration-executor.js';
4
4
  import { evaluateSchemaGate } from './migration-gate.js';
5
- /** Construct migration authority. Only a host calls this. */
5
+ import { isSqliteContentionError } from './sqlite-contention.js';
6
+ /** Process-private registry of genuine, host-minted migration capabilities. */
7
+ const MINTED_AUTHORITIES = new WeakSet();
8
+ /** Mint migration authority. Only a host calls this; the result cannot be reconstructed by shape. */
6
9
  export function migrationAuthority(grantedBy) {
7
- return { kind: 'axiom.migration-authority', grantedBy };
10
+ const principal = Object.freeze({
11
+ kind: 'axiom.migration-authority',
12
+ grantedBy: String(grantedBy),
13
+ });
14
+ MINTED_AUTHORITIES.add(principal);
15
+ return principal;
8
16
  }
17
+ /** True only for the exact object `migrationAuthority()` returned — provenance, not duck typing. */
9
18
  export function isMigrationPrincipal(value) {
10
- return (typeof value === 'object' &&
11
- value !== null &&
12
- value.kind === 'axiom.migration-authority' &&
13
- typeof value.grantedBy === 'string');
19
+ return typeof value === 'object' && value !== null && MINTED_AUTHORITIES.has(value);
14
20
  }
15
21
  export async function executeMigration(options) {
16
22
  if (!isMigrationPrincipal(options.principal)) {
@@ -21,7 +27,28 @@ export async function executeMigration(options) {
21
27
  };
22
28
  }
23
29
  const requiredVersion = options.ir.schemaVersion ?? 1;
24
- const record = await options.metadata.readSchema();
30
+ // `executeMigration` itself never holds the migration lease — `runMigration` does — so a
31
+ // SQLite lock this orchestrator hits is always another runner establishing/holding
32
+ // ownership (spec11.2 §6, §15). Reconcile it to a semantic outcome, never a leaked
33
+ // provider error.
34
+ let record;
35
+ try {
36
+ record = await options.metadata.readSchema();
37
+ }
38
+ catch (error) {
39
+ if (!isSqliteContentionError(error))
40
+ throw error;
41
+ const resolution = await classifyMigrationContention(options.metadata, requiredVersion, undefined, error);
42
+ if (resolution.kind === 'in-progress') {
43
+ return { ok: false, code: MIGRATION_DIAGNOSTIC_CODES.MIGRATION_IN_PROGRESS, message: resolution.message };
44
+ }
45
+ if (resolution.kind === 'failed') {
46
+ return { ok: false, code: MIGRATION_DIAGNOSTIC_CODES.MIGRATION_FAILED, message: resolution.message };
47
+ }
48
+ // already-at-target: a competing runner won the race. Re-read once — it succeeds now
49
+ // that the winner has released the writer lock — and fall through to the no-op path.
50
+ record = await options.metadata.readSchema();
51
+ }
25
52
  const fromVersion = options.fromVersion ?? record?.schemaVersion ?? 1;
26
53
  if (fromVersion === requiredVersion) {
27
54
  // Nothing to do — surface it as success with an empty plan.
@@ -56,9 +83,34 @@ export async function executeMigration(options) {
56
83
  return { ok: true, plan: planned.plan, run, gate };
57
84
  }
58
85
  export async function getMigrationStatus(metadata) {
59
- const record = await metadata.readSchema();
60
- const lock = await metadata.readLock();
61
- const checkpoint = await metadata.readCheckpoint();
86
+ // Host inspection must stay usable during a concurrent migration — a consumer may not be
87
+ // required to catch SQLite-native errors to read migration state (spec11.2 §12, §32).
88
+ // After the store's own bounded busy handling, residual contention on the
89
+ // `_axiom_migration_*` tables means a migration is active, so a read that still contends
90
+ // is reported as a coherent in-progress status rather than a thrown provider error.
91
+ let contended = false;
92
+ const read = async (fn) => {
93
+ try {
94
+ return await fn();
95
+ }
96
+ catch (error) {
97
+ if (isSqliteContentionError(error)) {
98
+ contended = true;
99
+ return null;
100
+ }
101
+ throw error;
102
+ }
103
+ };
104
+ const record = await read(() => metadata.readSchema());
105
+ const lock = await read(() => metadata.readLock());
106
+ const checkpoint = await read(() => metadata.readCheckpoint());
107
+ const phase = lock
108
+ ? 'in-progress'
109
+ : contended
110
+ ? 'in-progress'
111
+ : checkpoint
112
+ ? 'checkpointed'
113
+ : 'idle';
62
114
  return {
63
115
  schemaVersion: record?.schemaVersion ?? null,
64
116
  schemaFingerprint: record?.schemaFingerprint ?? null,
@@ -77,6 +129,6 @@ export async function getMigrationStatus(metadata) {
77
129
  batchCursor: checkpoint.batchCursor,
78
130
  }
79
131
  : null,
80
- phase: lock ? 'in-progress' : checkpoint ? 'checkpointed' : 'idle',
132
+ phase,
81
133
  };
82
134
  }
@@ -57,5 +57,29 @@ export type RunMigrationResult = {
57
57
  code: MigrationDiagnosticCode;
58
58
  message: string;
59
59
  };
60
+ /**
61
+ * A physical SQLite lock is not a semantic result (spec11.2 §5, §15). When a metadata or
62
+ * row-store call raises {@link SqliteContentionError} during a migration, this re-reads the
63
+ * migration metadata and answers in Axiom terms:
64
+ *
65
+ * - the persisted schema is already at (or past) the target → the race is already won →
66
+ * `already-at-target`;
67
+ * - a valid lease is held by *someone else* → `in-progress`;
68
+ * - this runner holds the lease and SQLite is still contended → an unrelated writer, not a
69
+ * migration owner → `failed`, with the physical cause retained (spec11.2 §35);
70
+ * - the metadata itself cannot be observed → only migration runners contend those tables, so
71
+ * without a lease of our own that means another migration is active → `in-progress`
72
+ * (spec11.2 §8, §11).
73
+ */
74
+ export type ContentionResolution = {
75
+ kind: 'already-at-target';
76
+ } | {
77
+ kind: 'in-progress';
78
+ message: string;
79
+ } | {
80
+ kind: 'failed';
81
+ message: string;
82
+ };
83
+ export declare function classifyMigrationContention(metadata: MigrationMetadataStore, targetVersion: number, ownToken: string | undefined, error: unknown): Promise<ContentionResolution>;
60
84
  export declare function runMigration(ir: ServerIR, plan: SemanticMigrationPlan, metadata: MigrationMetadataStore, target: MigrationRowStore | MigrationDataset, options: RunMigrationOptions): Promise<RunMigrationResult>;
61
85
  //# sourceMappingURL=migration-executor.d.ts.map