@cynodia/axiom 0.9.0-alpha.2 → 0.11.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/README.md CHANGED
@@ -33,7 +33,7 @@ Shorter forms of the same routing: [`AGENTS.md`](AGENTS.md) and [`llms.txt`](llm
33
33
  at this package's root.
34
34
 
35
35
  **Status: experimental / alpha.** The API may change between alpha releases. The
36
- documentation in `docs/` describes this exact version, `0.9.0-alpha.2`.
36
+ documentation in `docs/` describes this exact version, `0.11.0-alpha.1`.
37
37
 
38
38
  ## Installation
39
39
 
@@ -47,7 +47,7 @@ Every release of this project is a pre-release and npm's `latest` tag points at
47
47
  plain command above installs the current version. **There is no `alpha` dist-tag** — the tag
48
48
  was removed once it stopped tracking releases, and `npm install @cynodia/axiom@alpha` now
49
49
  fails with a 404. Pin the exact version instead when one is needed:
50
- `npm install @cynodia/axiom@0.9.0-alpha.2`.
50
+ `npm install @cynodia/axiom@0.11.0-alpha.1`.
51
51
 
52
52
  These are ES modules compiled to ES2022; import them with `import`, not `require`. There is
53
53
  no published Axiom CLI. `@cynodia/axiom-server`'s SQLite persistence adapter additionally
@@ -189,6 +189,8 @@ focused document when the reference is not specific enough for the question at h
189
189
  | Timed and lifecycle execution, event-invoked actions | [`docs/TRIGGERS.md`](docs/TRIGGERS.md) |
190
190
  | Live inbound streams: lifecycle, delivery, deduplication, backpressure | [`docs/SUBSCRIPTIONS.md`](docs/SUBSCRIPTIONS.md) |
191
191
  | Binary data: `BlobRef`, upload, download, authorization, orphans | [`docs/STORAGE.md`](docs/STORAGE.md) |
192
+ | Large authoritative datasets: `QueryDef`, relationships, read policy, providers, cursors, cache | [`docs/QUERIES.md`](docs/QUERIES.md) |
193
+ | Evolving a deployed schema: `MigrationDef`, fingerprint, the startup gate, `executeMigration`, providers | [`docs/MIGRATIONS.md`](docs/MIGRATIONS.md) |
192
194
  | Machine queries, mutation impact and graph transformations | [`docs/AGENT_API.md`](docs/AGENT_API.md) |
193
195
 
194
196
  `docs/AGENT_REFERENCE.md` plus the `.d.ts` declarations are intended to be sufficient on
@@ -1,6 +1,6 @@
1
1
  # Actions and transactions
2
2
 
3
- Axiom 0.9.0-alpha.2. An action is behavior expressed as data, executed as a transaction.
3
+ Axiom 0.11.0-alpha.1. An action is behavior expressed as data, executed as a transaction.
4
4
 
5
5
  ```ts
6
6
  {
package/docs/AGENT_API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agent API
2
2
 
3
- Axiom 0.9.0-alpha.2. The machine-facing interface. Agents query semantics and apply
3
+ Axiom 0.11.0-alpha.1. The machine-facing interface. Agents query semantics and apply
4
4
  structural transformations; they never edit generated code.
5
5
 
6
6
  ```ts
@@ -1,6 +1,6 @@
1
1
  # Agent reference
2
2
 
3
- Axiom 0.9.0-alpha.2. Compressed operational contract. Read this plus the `.d.ts`
3
+ Axiom 0.11.0-alpha.1. Compressed operational contract. Read this plus the `.d.ts`
4
4
  declarations before authoring or modifying an Axiom application.
5
5
 
6
6
  Formal guarantees: [`SEMANTIC_CONTRACT.md`](SEMANTIC_CONTRACT.md). Mistakes that compile:
@@ -65,7 +65,7 @@ One canonical term per concept. These are not interchangeable.
65
65
  ## Graph construction
66
66
 
67
67
  ```ts
68
- const graph = new ApplicationGraph(id, name); // version defaults to '0.9.0'
68
+ const graph = new ApplicationGraph(id, name); // version defaults to '0.11.0'
69
69
  graph.addNode<StateDef>({ id, kind: 'state', ... }); // returns NodeId; throws if id exists
70
70
  graph.getNode<StateDef>(id); // deep clone, or undefined
71
71
  graph.updateNode(node); // write a modified node back
@@ -140,8 +140,10 @@ group(src, scopeId, by) expressionRef(expressionId, args?)
140
140
  - `group` partitions a collection: `Collection<A>` → `Collection<Group<K, A>>`, read with `groupKey(g)` and `groupItems(g)`. Groups appear in **first-seen key order**, members keep source order, keys compare structurally. Nothing is sorted — use `sort` for that.
141
141
  - `expressionRef` evaluates a named `ExpressionDef` node: the calculation exists **once** in the graph and every consumer references it. Arguments are evaluated in the calling scope; **the body is evaluated in an isolated scope** that sees its parameters and application state and nothing else, so a definition means the same thing everywhere and its scope ids can never collide with a caller's.
142
142
 
143
- Builtins (14): `required` `is-empty` `non-empty` `length` `contains` `concat` `coalesce`
144
- `one-of` `count` `sum` `lowercase` `to-string` `now` `uuid`.
143
+ Builtins (17): `required` `is-empty` `non-empty` `length` `contains` `concat` `coalesce`
144
+ `one-of` `count` `sum` `lowercase` `to-string` `trim` `substring-before` `substring-after`
145
+ `now` `uuid`. `trim` / `substring-before` / `substring-after` are `axiom.server.v7`
146
+ vocabulary.
145
147
 
146
148
  Binary operators: `eq` `neq` `gt` `gte` `lt` `lte` `and` `or` `add` `subtract` `multiply`
147
149
  `divide`. Unary: `not` `negate`.
@@ -726,8 +728,12 @@ Portable artifacts, for a runtime written in another language:
726
728
  @cynodia/axiom-server/schema/server-ir.v2.schema.json JSON Schema for axiom.server.v2
727
729
  @cynodia/axiom-server/schema/server-ir.v3.schema.json JSON Schema for axiom.server.v3
728
730
  @cynodia/axiom-server/schema/server-ir.v4.schema.json JSON Schema for axiom.server.v4
729
- @cynodia/axiom-server/schema/server-ir.v5.schema.json JSON Schema for axiom.server.v5 (latest)
731
+ @cynodia/axiom-server/schema/server-ir.v5.schema.json JSON Schema for axiom.server.v5
732
+ @cynodia/axiom-server/schema/server-ir.v6.schema.json JSON Schema for axiom.server.v6
733
+ @cynodia/axiom-server/schema/server-ir.v7.schema.json JSON Schema for axiom.server.v7 (latest)
730
734
  @cynodia/axiom-server/schema/protocol.v1.schema.json JSON Schema for the protocol
735
+ @cynodia/axiom-server/conformance/queries/<name>.json one query conformance fixture (axiom.conformance.v4)
736
+ @cynodia/axiom-server/conformance/migrations/<name>.json one migration conformance fixture (axiom.conformance.v5)
731
737
  ```
732
738
 
733
739
  This list is generated/tested content, not hand-maintained prose: `packages/demo/test
@@ -820,6 +826,140 @@ Diagnostics: `SUBSCRIPTION_ADAPTER_MISSING` `SUBSCRIPTION_START_FAILED`
820
826
  `BLOB_NOT_FOUND` `BLOB_ACCESS_DENIED` `BLOB_TOO_LARGE` `BLOB_MEDIA_TYPE_REJECTED`
821
827
  `BLOB_OPERATION_FAILED` `BLOB_STORAGE_UNAVAILABLE` `BLOB_METADATA_FAILED`.
822
828
 
829
+ ## SEMANTIC DATA ACCESS & QUERY LAYER
830
+
831
+ Full model: [`QUERIES.md`](QUERIES.md). For authoritative data too large to materialize as a
832
+ `StateDef` — 500,000 orders, years of audit rows. The graph owns the meaning; a
833
+ `DataProvider` owns execution. `axiom.server.v6`.
834
+
835
+ ```ts
836
+ // A registered query: one node, fixed named clauses, every leaf an ordinary Expression.
837
+ graph.addNode<QueryDef>({
838
+ id: QUERY_RECENT_ORDERS, kind: 'query',
839
+ source: ENTITY_ORDER, rowScopeId: SC_ROW,
840
+ parameters: [{ id: P_STATUS, valueType: enumType([...]) }],
841
+ filter: binary('eq', field(ref(SC_ROW), F_STATUS), ref(P_STATUS)), // boolean; PRINCIPAL, params in scope
842
+ sort: [{ key: field(ref(SC_ROW), F_CREATED_AT), direction: 'desc' }], // canonical identity appended as tie-breaker
843
+ relationships: [{ relationshipId: REL_ORDER_ACCOUNT, bindAs: SC_ACCOUNT }],
844
+ projection: { entityId: ENTITY_ORDER_SUMMARY, fields: [{ id: F_S_NAME, value: field(ref(SC_ACCOUNT), F_ACCOUNT_NAME) }, ...] },
845
+ aggregate: [{ function: 'count' | 'sum' | 'min' | 'max' | 'average', key?: Expression, as: FieldId }],
846
+ groupBy: [Expression], // first-seen key order; present only with aggregate
847
+ pagination: { strategy: 'cursor' | 'offset', maxPageSize, defaultPageSize },
848
+ readPolicyId: POLICY_ORDER,
849
+ });
850
+
851
+ graph.addNode<RelationshipDef>({ id, kind: 'relationship', cardinality: 'to-one' | 'to-many',
852
+ from: { entityId, fieldId }, to: { entityId, fieldId } }); // explicit, never inferred
853
+
854
+ graph.addNode<ReadPolicyDef>({ id, kind: 'read-policy', entityId, rowScopeId,
855
+ predicate }); // boolean over row + PRINCIPAL; AND-ed into every query's filter, server-side
856
+
857
+ // Inside an action, before the transaction opens; makes the action server-authority:
858
+ { kind: 'query', queryId, arguments?, bindAs }
859
+
860
+ // Mutate a provider-backed row by identity, no collection materialized:
861
+ { kind: 'set', target: providerRecordFieldLocation(ENTITY_ORDER, F_ID, ref(P_ID), F_STATUS), value: literal('confirmed') }
862
+ ```
863
+
864
+ Client protocol: `{ kind: 'query', queryId, arguments?, cursor?, pageSize?, offset? }` →
865
+ `{ kind: 'query-result', ok, diagnostics, page? | aggregate?, revision }`. The client
866
+ invokes a query **by id**, never a query language. `page.nextCursor` is opaque — store it
867
+ and hand it back; never parse it.
868
+
869
+ Client lifecycle: `createQueryStore(fetcher)` — a `QueryView` per `{queryId, arguments}` key
870
+ with `load` / `refresh` / `loadMore` / `reset` / `subscribe`. States:
871
+ `idle | loading | ready | refreshing | error` (`QUERY_LIFECYCLE_STATES`). A failed first
872
+ load → `error` with no data; a failed refresh → `error` with the last good data still
873
+ visible.
874
+
875
+ Server: `createAxiomServer({ ir, dataProvider | dataProviders, cursorSecret?, queryCache? })`.
876
+ `createMemoryDataProvider({ rows })` and `createSqliteDataProvider({ location, entities, seed })`
877
+ are the reference providers, semantically identical. Cache identity includes a principal and
878
+ read-policy fingerprint; any committed mutation clears it. `server.clearQueryCache()` /
879
+ `server.queryCacheStats()`.
880
+
881
+ AgentAPI: `listQueries()` `getQuery(id)` `explainQuery(id)` `getQueryEntities(id)`
882
+ `getQueryFields(id)` `getQueryRelationships(id)` `getReadPolicyForQuery(id)`
883
+ `getActionsInvalidatingQuery(id)` `getQueriesInvalidatedByAction(actionId)` `listRelationships()`
884
+ `listReadPolicies()`; `getMutationImpact(location).affectedQueries`.
885
+
886
+ Portable: `runQueryConformanceFixture` / `runQueryConformanceSuite` over the
887
+ `axiom.conformance.v4` fixtures in `conformance/queries/`.
888
+
889
+ Diagnostics: `QUERY_NOT_FOUND` `QUERY_ARGUMENT_TYPE_MISMATCH` `QUERY_CAPABILITY_UNSUPPORTED`
890
+ `QUERY_CURSOR_INVALID` `QUERY_PAGE_SIZE_EXCEEDED` `QUERY_PROVIDER_FAILURE`
891
+ `QUERY_RESULT_TYPE_MISMATCH` `QUERY_PROVIDER_MISSING` (boundary); `QUERY_RESOLVER_UNAVAILABLE`
892
+ `QUERY_OPERATION_FAILED` (a `query` operation in the runtime). Validation:
893
+ `UNKNOWN_QUERY_ENTITY` `UNKNOWN_RELATIONSHIP` `INVALID_RELATIONSHIP` `UNKNOWN_READ_POLICY`
894
+ `INVALID_READ_POLICY` `DUPLICATE_READ_POLICY` `INVALID_QUERY_PREDICATE` `INVALID_QUERY_SORT`
895
+ `INVALID_QUERY_PROJECTION` `INVALID_QUERY_AGGREGATE` `INVALID_QUERY_GROUPING`
896
+ `INVALID_QUERY_PARAMETER` `UNSTABLE_PAGINATION` `INVALID_QUERY_OPERATION`
897
+ `INVALID_PROVIDER_RECORD_LOCATION`.
898
+
899
+ ## SCHEMA EVOLUTION & SEMANTIC MIGRATIONS
900
+
901
+ Full model: [`MIGRATIONS.md`](MIGRATIONS.md).
902
+
903
+ A deployed application evolves its semantic model and persisted data through
904
+ **`MigrationDef`** nodes — never application SQL, an ORM migration, a callback or a manual
905
+ schema-version check. `graph.schemaVersion` is a monotonic integer (default `1`), distinct
906
+ from the npm version and the Server IR contract. `schemaFingerprint(graph)` is a
907
+ deterministic hash of every persistence-relevant fact and nothing else — renaming a `label`
908
+ does not change it.
909
+
910
+ A `MigrationDef` has `fromSchema` / `toSchema` (differ by one), and `operations` from a
911
+ closed set of ten: `add-entity` `remove-entity` `add-field` `remove-field` `change-field`
912
+ `populate-field` `transform-field` `transform-record` `add-relationship`
913
+ `remove-relationship` (`MIGRATION_OPERATION_KINDS`). The set of migrations must form a
914
+ contiguous chain `1 → … → schemaVersion`. `transform-record` is the split/merge primitive.
915
+
916
+ Transform expressions (`populate` / `value` / `expression` / `produce`) are ordinary
917
+ `Expression` trees read in an isolated scope: `field(ref(MIGRATION_OLD_SCOPE), fieldId)`,
918
+ declared constants, and nested iteration scopes only. They MUST be pure — `now` / `uuid` /
919
+ any other scope read throw. The string builtins `trim` `substring-before` `substring-after`
920
+ (v7 vocabulary) exist for record transforms like splitting a name.
921
+
922
+ Any `MigrationDef`, or a `schemaVersion > 1`, makes the Server IR **`axiom.server.v7`**,
923
+ carrying `schemaVersion`, `schemaFingerprint` and `migrations`.
924
+
925
+ Authoring-time: `validateGraph` rejects a broken chain, an unmarked destructive op, an
926
+ impure or mistyped transform, an add-required-without-populate. `diffSchema(prev, next)`
927
+ (core) classifies each change; `migrationCoversDiff` proves the chain covers the diff.
928
+ AgentAPI: `inspectSchema()` `diffSchema(previous)` `migrationImpact(previous)`
929
+ `explainSchemaDiff(diff)`.
930
+
931
+ Runtime (`@cynodia/axiom-server`): `planMigration(ir, { fromVersion })` (pure) →
932
+ `SemanticMigrationPlan`; `explainMigration(plan)` (the §56 account); `executeMigration({ ir,
933
+ metadata, rows, principal: migrationAuthority('id'), approveDestructive })` — **host-only**,
934
+ no `ServerRequest` branch; `getMigrationStatus(metadata)`. Row transforms are keyset-batched
935
+ and checkpointed — a crash resumes to the identical result, a re-run is a no-op, a 2M-row
936
+ migration is bounded memory. Destructive operations need explicit `approveDestructive` or
937
+ zero writes occur. The target version commits only after post-migration validation.
938
+
939
+ Startup gate: `createAxiomServer({ ir, migrationMetadata })` → `start()` refuses on anything
940
+ but `compatible` / `fresh` — `SCHEMA_MIGRATION_REQUIRED` / `SCHEMA_INCOMPATIBLE` /
941
+ `MIGRATION_IN_PROGRESS` / `MIGRATION_FINGERPRINT_MISMATCH`. `server.schemaGate()` reports
942
+ without starting. While a migration runs, all requests get `MIGRATION_IN_PROGRESS`.
943
+
944
+ Providers: `createMemoryRowStore` + `createMemoryMigrationStore` (deterministic reference);
945
+ `createSqliteRowStore` + `createSqliteMigrationStore` (real `ALTER TABLE`, batched keyset
946
+ `UPDATE`, `_axiom_migration_*` tables). Both derive equivalent target data. Portable:
947
+ `runMigrationConformanceFixture` over the `axiom.conformance.v5` fixtures in
948
+ `conformance/migrations/`.
949
+
950
+ CLI (private): `axiom schema status` / `schema diff --against=<prev>` / `migrate plan
951
+ --from=N` / `migrate --sqlite=<path> --approve=op,op` / `migrate status --sqlite=<path>`.
952
+
953
+ Diagnostics: `SCHEMA_MIGRATION_REQUIRED` `SCHEMA_INCOMPATIBLE` `MIGRATION_IN_PROGRESS`
954
+ `MIGRATION_STATE_CORRUPTED` `MIGRATION_PATH_NOT_FOUND` `MIGRATION_APPROVAL_REQUIRED`
955
+ `MIGRATION_DESTRUCTIVE` `MIGRATION_PROVIDER_UNSUPPORTED` `MIGRATION_TRANSFORM_FAILED`
956
+ `MIGRATION_VALIDATION_FAILED` `MIGRATION_CHECKPOINT_INVALID` `MIGRATION_FINGERPRINT_MISMATCH`
957
+ `MIGRATION_NOT_AUTHORIZED` `MIGRATION_FAILED` (boundary). Validation:
958
+ `INVALID_MIGRATION_VERSION` `MIGRATION_PATH_NOT_FOUND` `MIGRATION_CHAIN_FORK`
959
+ `DUPLICATE_MIGRATION_OPERATION_ID` `MIGRATION_REQUIRED_FIELD_WITHOUT_DEFAULT`
960
+ `MIGRATION_DESTRUCTIVE_UNMARKED` `INVALID_MIGRATION_OPERATION` `MIGRATION_TRANSFORM_IMPURE`
961
+ `MIGRATION_TRANSFORM_TYPE_MISMATCH`.
962
+
823
963
  ## Metadata classes
824
964
 
825
965
  ```ts
@@ -1,6 +1,6 @@
1
1
  # Anti-patterns
2
2
 
3
- Axiom 0.9.0-alpha.2. Each of these compiles. Each is wrong. Each is followed by the correct
3
+ Axiom 0.11.0-alpha.1. Each of these compiles. Each is wrong. Each is followed by the correct
4
4
  alternative.
5
5
 
6
6
  ## 1. Field names as entity runtime keys
@@ -499,3 +499,186 @@ The second is also a path traversal waiting to happen, and neither can be reache
499
499
  and `GET /axiom/blob/<storageId>/<key>` already exist, for every Axiom application, with
500
500
  `StorageDef.uploadAuthorization` and `StorageDef.readAuthorization` enforced by the
501
501
  authority. Application-authored upload/download routes: zero.
502
+
503
+ ## 37. Bulk-loading a large collection into `StateDef`
504
+
505
+ ```ts
506
+ // WRONG — every order in the browser, so the app can filter and sort them itself.
507
+ graph.addNode<StateDef>({ id: STATE_ALL_ORDERS, kind: 'state',
508
+ valueType: collectionType(entityType(ENTITY_ORDER)), authority: 'server' });
509
+ ```
510
+
511
+ At any real scale this is a non-starter, and it makes the client the enforcement point for
512
+ filtering, sorting, pagination and visibility — all four of which then have to be re-done,
513
+ correctly, on the authority anyway. A `QueryDef` says *what data is required*; the provider
514
+ decides how to retrieve it. `StateDef` is for values that can reasonably be held as a whole.
515
+
516
+ ## 38. `SELECT`, a repository call, or `fetch('/api/orders')` in application code
517
+
518
+ ```ts
519
+ // WRONG — the semantic boundary has leaked.
520
+ const rows = await db.query('SELECT * FROM orders WHERE status = $1 ORDER BY created_at DESC LIMIT 50', [status]);
521
+ const orders = await orderRepository.findMany({ where: { status }, take: 50 });
522
+ const page = await fetch(`/api/orders?status=${status}&cursor=${cursor}`).then((r) => r.json());
523
+ ```
524
+
525
+ The target is: the application says *"I need these orders"*; Axiom understands what that
526
+ means, who may see them, and which mutations affect them; the provider decides how to
527
+ retrieve them efficiently. Handwritten SQL, ORM calls, repository classes, application data
528
+ endpoints and canonical-data `fetch()` in a reference application: zero. If the reference
529
+ provider generates SQL, that SQL is provider infrastructure — values are parameters, and raw
530
+ input never becomes a table name, a column name or a fragment.
531
+
532
+ ## 39. A second comparison or arithmetic language inside the query system
533
+
534
+ ```ts
535
+ // WRONG — `{ op: 'eq', field, value }` is almost, but not exactly, what `binary('eq', …)` means.
536
+ filter: { op: 'and', operands: [{ op: 'eq', field: F_STATUS, value: { param: 'status' } }] }
537
+ ```
538
+
539
+ Every leaf of every query clause is an ordinary Axiom `Expression`. One `eq`, one null
540
+ truth table, one arithmetic, one dependency walker. A parallel predicate language is two
541
+ truth tables that must be kept identical by hand forever — which is `INVALID_QUERY_PREDICATE`
542
+ waiting to be a subtle divergence between two runtimes.
543
+
544
+ ## 40. `visibleWhen`, a hidden column, or client-side filtering as read authorization
545
+
546
+ ```ts
547
+ // WRONG — the rows still crossed the wire; a hostile client just asked for them raw.
548
+ const visible = allOrders.filter((order) => order.customerId === session.customerId);
549
+ ```
550
+
551
+ Presentation does not authorize behaviour (spec 5 §75), and the query layer does not change
552
+ that. Row visibility is a `ReadPolicyDef` whose predicate is AND-ed into the effective
553
+ filter on the authority — it scopes rows, aggregates and relationship traversals uniformly,
554
+ and no client argument can remove it. `filterUnauthorizedRows(...)` in application code:
555
+ zero.
556
+
557
+ ## 41. Parsing, forging, or hand-building a cursor string
558
+
559
+ ```ts
560
+ // WRONG — a cursor is opaque application data, not a structure to construct.
561
+ const next = btoa(JSON.stringify({ afterId: lastRow.id, page: page + 1 }));
562
+ ```
563
+
564
+ A cursor carries a signed fingerprint of the query, the arguments, the principal and the
565
+ read policy. Parsing it, or building your own, breaks continuation the moment the fingerprint
566
+ does not match — `QUERY_CURSOR_INVALID`. Store `page.nextCursor` and hand it back
567
+ unmodified; the client store's `loadMore` does exactly that. Manual cursor manipulation in
568
+ application code: zero.
569
+
570
+ ## 42. A hand-written SQL migration, an ORM migration, or a repository script
571
+
572
+ ```ts
573
+ // WRONG — the migration lives outside the graph, drifts from it, and is not portable.
574
+ await db.exec('ALTER TABLE t_order ADD COLUMN status TEXT NOT NULL DEFAULT "draft"');
575
+ ```
576
+
577
+ Express the *semantic* change as a `MigrationDef` operation:
578
+ `{ kind: 'add-field', entityId: E_ORDER, field: F_STATUS, populate: literal('draft') }`.
579
+ The provider turns it into `ALTER TABLE` (SQLite), a record transform (memory), or its own
580
+ plan (a future Postgres). Handwritten migration SQL in application code: zero.
581
+
582
+ ## 43. An arbitrary migration callback
583
+
584
+ ```ts
585
+ // WRONG — a stored closure is not serializable, not portable, and not inspectable.
586
+ { kind: 'transform-record', run: (old) => ({ given: old.name.split(' ')[0] }) }
587
+ ```
588
+
589
+ A transform is an `Expression` tree read in an isolated scope — `object([{ fieldId: F_GIVEN,
590
+ value: call('substring-before', field(ref(MIGRATION_OLD_SCOPE), F_NAME), literal(' ')) }])`.
591
+ It is typed, deterministic and analyzable. `run: fn` does not exist and will not be added.
592
+
593
+ ## 44. Changing a `FieldId` to perform a rename
594
+
595
+ ```ts
596
+ // WRONG — the diff sees a removed field and an added one, not a rename.
597
+ fields: [{ id: fieldId('field_account_name'), /* was field_customer_name */ ... }]
598
+ ```
599
+
600
+ `FieldId` is identity. Changing it makes the migration planner classify `-customer_name`
601
+ `+account_name` as `incompatible-ambiguous` — and if you had wired a `remove-field`, it
602
+ would drop the data. Keep the `FieldId`; change the `label`. That is a presentation-only
603
+ change and needs no migration.
604
+
605
+ ## 45. Silently adding a required field
606
+
607
+ ```ts
608
+ // WRONG — existing rows have no value for it, and none is invented.
609
+ { kind: 'add-field', entityId: E_ORDER, field: { id: F_STATUS, required: true } }
610
+ ```
611
+
612
+ `validateGraph` rejects this (`MIGRATION_REQUIRED_FIELD_WITHOUT_DEFAULT`). Supply a
613
+ `populate` expression, an explicit `populate-field`, or provider proof that every row
614
+ already satisfies it. Axiom does not invent a zero / empty / null value.
615
+
616
+ ## 46. Deleting a populated field without approval
617
+
618
+ ```ts
619
+ // WRONG — the migration exists, but nobody approved data loss.
620
+ await executeMigration({ ir, metadata, rows, principal }); // no approveDestructive
621
+ ```
622
+
623
+ A destructive operation is refused (`MIGRATION_APPROVAL_REQUIRED`) with **zero writes**
624
+ until every destructive operation id appears in `approveDestructive`. "A migration exists"
625
+ is never "the operator approved data loss."
626
+
627
+ ## 47. Assuming the package version is the schema version
628
+
629
+ ```ts
630
+ // WRONG — three independent version concepts, conflated.
631
+ if (pkg.version === persisted.axiomVersion) startNormally();
632
+ ```
633
+
634
+ `@cynodia/axiom` `0.11.0`, `axiom.server.v7`, and `graph.schemaVersion` `14` are unrelated.
635
+ Compare `schemaFingerprint(graph)` and `graph.schemaVersion` against what the provider
636
+ durably recorded — which is exactly what the startup gate does.
637
+
638
+ ## 48. Starting an application against a mismatched persisted schema
639
+
640
+ ```ts
641
+ // WRONG — the server starts, then queries and actions fail in arbitrary ways.
642
+ const server = createAxiomServer({ ir }); // no migrationMetadata, schema has moved on
643
+ await server.start();
644
+ ```
645
+
646
+ Pass `migrationMetadata`. `start()` then refuses on anything but `compatible` / `fresh`
647
+ with a specific diagnostic (`SCHEMA_MIGRATION_REQUIRED`, `SCHEMA_INCOMPATIBLE`, …). There is
648
+ no hopeful startup.
649
+
650
+ ## 49. Loading the whole provider dataset into JS to migrate it
651
+
652
+ ```ts
653
+ // WRONG — a 2,000,000-row table does not fit in a JS array.
654
+ const all = await provider.loadAll(E_ORDER_LINE);
655
+ for (const row of all) row.gross = row.total * 1.25;
656
+ await provider.writeAll(E_ORDER_LINE, all);
657
+ ```
658
+
659
+ `executeMigration` reads a **keyset-ordered batch**, transforms it, writes it back,
660
+ checkpoints, and moves on. Peak memory is one batch, whatever the table size. Unbounded
661
+ load-all transformations: zero.
662
+
663
+ ## 50. Wall-clock time or randomness inside a transform
664
+
665
+ ```ts
666
+ // WRONG — the migration result depends on when it ran.
667
+ { kind: 'populate-field', value: call('now') }
668
+ ```
669
+
670
+ `now` and `uuid` throw inside a migration transform (`MIGRATION_TRANSFORM_IMPURE`). A
671
+ migration must be reproducible: the same source record and the same operation produce the
672
+ same target record on every run, on every provider, in every language.
673
+
674
+ ## 51. Editing the provider's stored migration metadata by hand
675
+
676
+ ```sql
677
+ -- WRONG — the schema version and the data no longer agree.
678
+ UPDATE _axiom_migration_schema SET version = 7;
679
+ ```
680
+
681
+ The stored version, fingerprint and step history are written only by the migration
682
+ executor, atomically with the work they describe. Hand-editing them produces
683
+ `MIGRATION_FINGERPRINT_MISMATCH` or `MIGRATION_STATE_CORRUPTED` at startup — the gate's
684
+ whole purpose is to catch exactly this.
package/docs/AUTHORITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Authority
2
2
 
3
- Axiom 0.9.0-alpha.2. How an application crosses the trust boundary.
3
+ Axiom 0.11.0-alpha.1. How an application crosses the trust boundary.
4
4
 
5
5
  Until 0.5.x an Axiom application executed locally. 0.6 adds an **authority**: a generic
6
6
  runtime that owns state, decides mutations and persists them. The same semantic graph
@@ -488,6 +488,48 @@ does for a local failure.
488
488
  | `BLOB_MEDIA_TYPE_REJECTED` | An upload's media type is not in the store's declared `acceptedMediaTypes`. |
489
489
  | `BLOB_OPERATION_FAILED` | A `blob-commit` or `blob-delete` failed at the store, after its retry policy. |
490
490
  | `INVOCATION_SOURCE_NOT_ALLOWED` | The action's `invocation.allowedSources` does not include this invocation's source (spec 8.1 §3-9) — refused before `authorization` is even evaluated, because no caller reaching the authority this way may invoke the action at all. |
491
+ | `QUERY_NOT_FOUND` | The `QueryRequest` named a `QueryDef` this authority does not execute. |
492
+ | `QUERY_ARGUMENT_TYPE_MISMATCH` | A query argument was missing, unknown, or did not conform to its declared `QueryParameter` type. Rejected before the provider is touched. |
493
+ | `QUERY_UNAUTHORIZED` | The caller may not run this query — reserved for a future explicit query-authorization rule; row visibility today is enforced by the read policy AND-ed into the filter, not by refusal. |
494
+ | `QUERY_CAPABILITY_UNSUPPORTED` | The configured `DataProvider` cannot push down a semantic this query requires, or the query's expression subset contains a leaf the provider cannot translate. Never approximated in memory. |
495
+ | `QUERY_CURSOR_INVALID` | The cursor was tampered with, truncated, or minted for a different query / principal / read policy / contract. Continuing from it is refused; nothing is disclosed. |
496
+ | `QUERY_PAGE_SIZE_EXCEEDED` | The requested page size exceeds the ceiling for this query (`min(QueryDef.pagination.maxPageSize, provider.maxPageSize)`). Refused, never silently truncated. |
497
+ | `QUERY_PROVIDER_FAILURE` | The provider reported a failure executing the query or applying a provider-record mutation. Carries no state value. |
498
+ | `QUERY_RESULT_TYPE_MISMATCH` | The provider returned rows that do not conform to the query's declared result shape. |
499
+ | `QUERY_PROVIDER_MISSING` | No `DataProvider` is registered for this query's source entity (`AxiomServerOptions.dataProvider` / `dataProviders`). |
500
+ | `SCHEMA_MIGRATION_REQUIRED` | Persisted canonical data is at an older semantic schema than the graph requires; a migration must run before the authority will serve traffic (spec11 §12). |
501
+ | `SCHEMA_INCOMPATIBLE` | Persisted data cannot be reconciled with the graph — a stored schema version ahead of the graph's, or no migration path to it. |
502
+ | `MIGRATION_IN_PROGRESS` | A migration is already running: a migration lock is held with a valid lease, by this instance or another (spec11 §66). |
503
+ | `MIGRATION_STATE_CORRUPTED` | The provider's stored migration metadata is internally inconsistent — a fingerprint or completed-step history that does not add up. |
504
+ | `MIGRATION_PATH_NOT_FOUND` | No contiguous `MigrationDef` chain connects the persisted schema version to the required one (spec11 §13). |
505
+ | `MIGRATION_APPROVAL_REQUIRED` | The plan contains destructive operations and the `executeMigration` call did not name them in `approveDestructive` (spec11 §21). |
506
+ | `MIGRATION_DESTRUCTIVE` | Reported by planning for each operation that discards persisted information — surfaced before execution (spec11 §20). |
507
+ | `MIGRATION_PROVIDER_UNSUPPORTED` | The configured provider cannot execute a capability the plan requires. Refused before any write (spec11 §79). |
508
+ | `MIGRATION_TRANSFORM_FAILED` | A migration transform expression threw, or produced a value that does not satisfy the target field. The target schema version was not committed. |
509
+ | `MIGRATION_VALIDATION_FAILED` | Post-migration validation found persisted data that does not satisfy the target semantic schema (spec11 §37). |
510
+ | `MIGRATION_CHECKPOINT_INVALID` | A resume was attempted from a checkpoint that does not match the current plan or schema fingerprint. |
511
+ | `MIGRATION_FINGERPRINT_MISMATCH` | The persisted schema fingerprint does not match the origin of the resolved migration path — the stored data is not the shape the chain expects. |
512
+ | `MIGRATION_NOT_AUTHORIZED` | The caller is not the host-controlled migration principal. Naming a migration id over the client protocol does nothing (spec11 §73, §74). |
513
+ | `MIGRATION_FAILED` | A migration failed for a reason with no more specific code; the target schema version was not committed and the recovery state is defined. |
514
+
515
+ **Migration execution is host-controlled.** `executeMigration()` is a standalone function,
516
+ not a `ServerRequest` branch — there is no path from a client through the semantic protocol
517
+ that runs a migration (spec11 §73). It requires a `MigrationPrincipal` minted by the host
518
+ with `migrationAuthority(grantedBy)`; a call without one is `MIGRATION_NOT_AUTHORIZED`.
519
+ Destructive operations additionally require each operation id in `approveDestructive`
520
+ (spec11 §21) — "a migration exists" is never "the operator approved data loss", and an
521
+ unapproved destructive migration performs **zero** writes (spec11 §106).
522
+
523
+ **Which correctness layers apply during a migration** (spec11 §37-40). Only **schema
524
+ conformance** — required fields present, identity present, declared field types — is checked,
525
+ at the target-record boundary, before the new schema version is committed
526
+ (`MIGRATION_VALIDATION_FAILED`). Entity `ConstraintDef`s are **not** evaluated during a
527
+ migration: a valid migration may pass through representations that are not valid application
528
+ states (spec11 §38), and the target record is what must be valid, expressed by the transform
529
+ itself. `TransitionConstraintDef`s are **never** applied to historical-data migration — a
530
+ migration is not a user edit and must not pretend to be one (spec11 §40). A host that needs
531
+ a business invariant re-checked after a migration does so by starting the authority and
532
+ running its own audit query.
491
533
 
492
534
  Two client-side codes belong to the boundary as well:
493
535
 
@@ -637,8 +679,10 @@ page plus the conformance fixtures.
637
679
  | `axiom.server.v3` | integrations, integration operations, events, triggers, and the `integration-query`/`integration-effect` operation kinds | 0.8.0 |
638
680
  | `axiom.server.v4` | `ActionDef.invocation.allowedSources` invocation-source restriction, and the structured effect-outcome envelope (`effectOutcomeEntity`, `EFFECT_ID_FIELD` and its sibling reserved fields) that every effect dispatch uses from 8.1 onward | 0.8.1 |
639
681
  | `axiom.server.v5` | `SubscriptionDef` and `StorageDef`, and the `blob-metadata`/`blob-commit`/`blob-delete` operation kinds — the inbound external-I/O direction and binary object storage | 0.9.0 |
682
+ | `axiom.server.v6` | `QueryDef`, `RelationshipDef` and `ReadPolicyDef`, the `query` operation kind, and the `provider-record` location — the semantic data-access & query layer over large authoritative datasets | 0.10.0 |
683
+ | `axiom.server.v7` | `MigrationDef` and the closed migration-operation vocabulary, plus the top-level `schemaVersion` and `schemaFingerprint` fields — semantic schema evolution over persisted canonical data | 0.11.0 |
640
684
 
641
- `SERVER_IR_CONTRACTS` enumerates all five, and is the single source of truth this table is
685
+ `SERVER_IR_CONTRACTS` enumerates all seven, and is the single source of truth this table is
642
686
  tested against — `packages/demo/test/documentation.test.ts` fails if a contract in
643
687
  `SERVER_IR_CONTRACTS` has no row here, or a row here names a contract the code does not
644
688
  declare (spec 8.2 §7-8). The rules:
@@ -646,11 +690,12 @@ declare (spec 8.2 §7-8). The rules:
646
690
  - **A document declares the oldest contract that can carry it.** `compileToServerIR` computes the label from the vocabulary the document actually uses, so an application that uses nothing from 0.7 or 0.8 produces a byte-identical `axiom.server.v1` document, and the committed v1 conformance fixtures are unchanged. `usesV4Semantics` computes the v4 case specifically: an action's `invocation.allowedSources` genuinely restricting the default two-source set, or any `integration-operation` with `mode: 'effect'` (since every effect dispatch uses the structured v4 envelope) — a document that merely mentions `invocation` without restricting it only needs `axiom.server.v2`, the same tier `group`/`expression-ref` occupy.
647
691
  - **A runtime MUST refuse a contract it does not implement**, and MUST refuse a document whose vocabulary exceeds its declared contract. A v2 runtime executing a v1-labelled document that uses `group` would accept what a conforming v1 runtime elsewhere refuses, and the two would then disagree about the same file. `createAxiomServer` raises rather than executing one — including refusing a document that **understates** its own contract (`understatedContract`).
648
692
  - **A frozen contract gains nothing.** `axiom.server.v1` does not contain `group`, `expression-ref`, `expressionDefs`, an integration, a trigger, an event, the `integration-query`/`integration-effect` operation kinds, `invocation`, or the structured effect-outcome envelope, and `server-ir.v1.schema.json` is byte-frozen. Vocabulary arrives under a new identifier or not at all.
649
- - **`axiom.server.v5` is the latest contract as of 0.9.0** (`SERVER_IR_LATEST_CONTRACT`). `usesExternalIOVocabulary` computes it from the document: any `subscriptions`, any `storages`, or any of the three blob operation kinds. The reason it is an incompatible change rather than an additive one is exact — a v4 runtime that ignored `subscriptions` would start an application whose declared live event source never activates, and one that ignored a `blob-commit` would leave state referencing an object that stays staged forever. Both are silent divergence, which is what a contract label exists to prevent.
693
+ - **`axiom.server.v6` is the latest contract as of 0.10.0** (`SERVER_IR_LATEST_CONTRACT`). `usesQueryVocabulary` computes it from the document: any `queries`, any `relationships`, any `readPolicies`, or any `query` operation. It is incompatible rather than additive for the same reason every prior tier is — a v5 runtime that ignored `queries` would accept an application that promises demand-driven, read-authorized, paginated access to a large dataset and silently execute none of it, and one that ignored a `provider-record` mutation would silently skip a canonical write. Both are the divergence a label exists to prevent.
650
694
 
651
695
  There is one JSON Schema per contract, each generated from the runtime's own vocabulary and
652
696
  each shipped: `server-ir.v1.schema.json`, `server-ir.v2.schema.json`, `server-ir.v3.schema.json`,
653
- `server-ir.v4.schema.json`, `server-ir.v5.schema.json`.
697
+ `server-ir.v4.schema.json`, `server-ir.v5.schema.json`, `server-ir.v6.schema.json`,
698
+ `server-ir.v7.schema.json`.
654
699
 
655
700
  **`SubscriptionDef`, `StorageDef` and the blob operations.** Their normative semantics —
656
701
  lifecycle states and transitions, at-least-once delivery, per-subscription ordering and the
@@ -1,6 +1,6 @@
1
1
  # Constraints
2
2
 
3
- Axiom 0.9.0-alpha.2. Two constructs, answering different questions. They are not
3
+ Axiom 0.11.0-alpha.1. Two constructs, answering different questions. They are not
4
4
  interchangeable.
5
5
 
6
6
  | | Question | Sees |
package/docs/EFFECTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Effects
2
2
 
3
- Axiom 0.9.0-alpha.2. External effects are not rollback-capable state mutations. This file
3
+ Axiom 0.11.0-alpha.1. External effects are not rollback-capable state mutations. This file
4
4
  is the delivery model; [`AUTHORITY.md`](AUTHORITY.md#external-effects) is the load-bearing
5
5
  statement of why, and [`INTEGRATIONS.md`](INTEGRATIONS.md) is the operation vocabulary this
6
6
  builds on.
package/docs/EVENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Events
2
2
 
3
- Axiom 0.9.0-alpha.2. An event is a typed fact — something that happened — never work
3
+ Axiom 0.11.0-alpha.1. An event is a typed fact — something that happened — never work
4
4
  itself. [`AUTHORITY.md`](AUTHORITY.md#external-events) is the load-bearing statement;
5
5
  this file is the vocabulary and the webhook delivery mechanism. A **subscription** is the
6
6
  other way an external fact becomes an `EventDef` payload — see
@@ -1,6 +1,6 @@
1
1
  # Expressions
2
2
 
3
- Axiom 0.9.0-alpha.2. An expression describes **what value is computed**. It is a tree of
3
+ Axiom 0.11.0-alpha.1. An expression describes **what value is computed**. It is a tree of
4
4
  plain data, never source text and never a callback. Evaluation is pure: an expression MUST
5
5
  NOT change state.
6
6
 
@@ -31,7 +31,7 @@ Three coercions decide most edge cases. They are shared by every kind.
31
31
  A collection is truthy only when non-empty. This is the one coercion most likely to
32
32
  surprise: use `count(...) > 0` when you mean "has members" and want it to read that way.
33
33
 
34
- **Text** (`concat`, `to-string`, `lowercase`, comparison of non-numbers, rendering):
34
+ **Text** (`concat`, `to-string`, `lowercase`, `trim`, `substring-before`, `substring-after`, comparison of non-numbers, rendering):
35
35
  `null`/`undefined` → `''`; string → itself; number/boolean → `String(v)`; anything else →
36
36
  `JSON.stringify(v)`.
37
37
 
@@ -210,7 +210,7 @@ Validation: `UNKNOWN_EXPRESSION_DEF` for a reference to something that is not on
210
210
 
211
211
  ## Built-in functions
212
212
 
213
- 14 names, enumerated by `BUILTIN_FUNCTIONS`. `AGGREGATE_FUNCTIONS` lists those that reduce
213
+ 17 names, enumerated by `BUILTIN_FUNCTIONS`. `AGGREGATE_FUNCTIONS` lists those that reduce
214
214
  a collection of numbers (`sum`).
215
215
 
216
216
  | Function | Arity | Input | Output | Notes |
@@ -227,6 +227,9 @@ a collection of numbers (`sum`).
227
227
  | `sum` | 1 | `Collection<number>` | `number` | **Fails on `null`, and on any non-finite or non-numeric member.** `[]` → `0`. |
228
228
  | `lowercase` | 1 | any | `string` | Text form, lower-cased. |
229
229
  | `to-string` | 1 | any | `string` | Text form. |
230
+ | `trim` | 1 | any | `string` | Text form with leading/trailing whitespace removed. |
231
+ | `substring-before` | 2 | text, separator | `string` | Text before the first occurrence of `separator`; the whole string if it does not occur (empty separator → whole string). |
232
+ | `substring-after` | 2 | text, separator | `string` | Text after the first occurrence of `separator`; `''` if it does not occur. |
230
233
  | `now` | 0 | — | `string` | ISO timestamp from the host. |
231
234
  | `uuid` | 0 | — | `string` | Identifier from the host. |
232
235
 
@@ -1,6 +1,6 @@
1
1
  # Graph model
2
2
 
3
- Axiom 0.9.0-alpha.2. The `ApplicationGraph` is the authoritative representation of an
3
+ Axiom 0.11.0-alpha.1. The `ApplicationGraph` is the authoritative representation of an
4
4
  application. Everything else — the IR, the page, the DOM — is derived from it and is never
5
5
  edited.
6
6
 
@@ -1,6 +1,6 @@
1
1
  # Integrations
2
2
 
3
- Axiom 0.9.0-alpha.2. How an application declares and calls an external system, without
3
+ Axiom 0.11.0-alpha.1. How an application declares and calls an external system, without
4
4
  embedding a transport, an SDK or a secret in the graph. The authority boundary this
5
5
  depends on is [`AUTHORITY.md`](AUTHORITY.md#external-systems); this file is the vocabulary.
6
6
 
package/docs/LOCATIONS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Locations
2
2
 
3
- Axiom 0.9.0-alpha.2.
3
+ Axiom 0.11.0-alpha.1.
4
4
 
5
5
  ```text
6
6
  Expression = a value