@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 +4 -2
- package/docs/ACTIONS_TRANSACTIONS.md +1 -1
- package/docs/AGENT_API.md +1 -1
- package/docs/AGENT_REFERENCE.md +145 -5
- package/docs/ANTI_PATTERNS.md +184 -1
- package/docs/AUTHORITY.md +49 -4
- package/docs/CONSTRAINTS.md +1 -1
- package/docs/EFFECTS.md +1 -1
- package/docs/EVENTS.md +1 -1
- package/docs/EXPRESSIONS.md +6 -3
- package/docs/GRAPH_MODEL.md +1 -1
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/LOCATIONS.md +1 -1
- package/docs/MIGRATIONS.md +271 -0
- package/docs/PRESENTATION.md +1 -1
- package/docs/QUERIES.md +307 -0
- package/docs/RUNTIME.md +3 -1
- package/docs/SEMANTIC_CONTRACT.md +1 -1
- package/docs/STATE.md +1 -1
- package/docs/STORAGE.md +1 -1
- package/docs/SUBSCRIPTIONS.md +1 -1
- package/docs/TRIGGERS.md +1 -1
- package/docs/UI.md +1 -1
- package/docs/VALIDATION.md +45 -2
- package/llms.txt +1 -1
- package/package.json +5 -5
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.
|
|
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.
|
|
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
|
package/docs/AGENT_API.md
CHANGED
package/docs/AGENT_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent reference
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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.
|
|
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 (
|
|
144
|
-
`one-of` `count` `sum` `lowercase` `to-string` `
|
|
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
|
|
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
|
package/docs/ANTI_PATTERNS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Anti-patterns
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
package/docs/CONSTRAINTS.md
CHANGED
package/docs/EFFECTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Effects
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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.
|
|
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
|
package/docs/EXPRESSIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Expressions
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
-
|
|
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
|
|
package/docs/GRAPH_MODEL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Graph model
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Integrations
|
|
2
2
|
|
|
3
|
-
Axiom 0.
|
|
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
|
|