@cynodia/axiom-core 0.15.0-alpha.3 → 0.16.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.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Canonical machine-readable authoring metadata (spec16 §67-78).
3
+ *
4
+ * An AI agent constructing or modifying an Axiom graph must not have to guess valid node
5
+ * shapes, valid references, closed vocabularies or required fields from source code. This
6
+ * module is the **single** structured description of every publicly authorable semantic
7
+ * node kind — the same eighteen kinds `SEMANTIC_NODE_KINDS` enumerates — derived from the
8
+ * same closed vocabularies validation and the runtime already use (`OPERATION_KINDS`,
9
+ * `EDGE_KINDS`, `EXPRESSION_KINDS`, `WORKFLOW_STEP_TYPES`, `AUTHORIZATION_OPERATIONS`, …)
10
+ * rather than duplicating them by hand (spec16 §71).
11
+ *
12
+ * It is deliberately scoped to the **graph model** — `EntityDef` through
13
+ * `AuthorizationPolicyDef` — not the nine UI node kinds. UI authoring already has its own
14
+ * canonical, mechanically-checked machine catalogue (`@cynodia/axiom-ui`'s
15
+ * `packages/ui-toolkit/docs/PATTERN_CATALOG.json`, generated by `npm run toolkit:catalog`);
16
+ * a second description of the same nodes here would be exactly the "four unrelated sources
17
+ * of truth" spec16 §71 warns against, so `docs/UI.md` and the toolkit catalogue remain that
18
+ * authority.
19
+ *
20
+ * A template is a **minimal valid shape**, not a set of defaults: it never assigns
21
+ * security-sensitive semantics an author did not ask for (spec16 §78) — an `ActionDef`
22
+ * template carries no `authorizationPolicy`, which is exactly its pre-0.15 public contract,
23
+ * not a silently-inserted permission.
24
+ */
25
+ import type { SemanticNodeKind } from './types.js';
26
+ /** One field an authorable node kind carries. */
27
+ export interface AuthoringFieldDescriptor {
28
+ name: string;
29
+ required: boolean;
30
+ /** A short, stable, human/machine readable type description — not a TypeScript type. */
31
+ type: string;
32
+ /** Other semantic node kinds this field's value (a `NodeId` or `NodeId[]`) may reference. */
33
+ referenceTargets?: readonly SemanticNodeKind[];
34
+ /** The closed vocabulary this field's value is drawn from, when it has one. */
35
+ closedEnum?: readonly string[];
36
+ description: string;
37
+ }
38
+ /** Every publicly authorable graph-model semantic node kind, structurally described. */
39
+ export interface AuthoringKindDescriptor {
40
+ kind: SemanticNodeKind;
41
+ purpose: string;
42
+ fields: AuthoringFieldDescriptor[];
43
+ /** A minimal valid shape. `NodeId`/`FieldId` values are placeholders (`"<id>"`). */
44
+ template: Record<string, unknown>;
45
+ }
46
+ /** Every graph-model semantic node kind an agent may author (spec16 §72). */
47
+ export declare function listAuthorableKinds(): readonly SemanticNodeKind[];
48
+ /** The structural authoring contract for one node kind, or `undefined` for an unknown kind. */
49
+ export declare function describeAuthoringKind(kind: string): AuthoringKindDescriptor | undefined;
50
+ /** The complete authoring schema — one descriptor per authorable graph-model kind (spec16 §69, §70). */
51
+ export declare function authoringSchema(): readonly AuthoringKindDescriptor[];
52
+ /** Every edge kind the graph can derive, for tooling that renders `getEdges` results. */
53
+ export declare function authoringEdgeKinds(): readonly string[];
54
+ //# sourceMappingURL=authoring-schema.d.ts.map
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Canonical machine-readable authoring metadata (spec16 §67-78).
3
+ *
4
+ * An AI agent constructing or modifying an Axiom graph must not have to guess valid node
5
+ * shapes, valid references, closed vocabularies or required fields from source code. This
6
+ * module is the **single** structured description of every publicly authorable semantic
7
+ * node kind — the same eighteen kinds `SEMANTIC_NODE_KINDS` enumerates — derived from the
8
+ * same closed vocabularies validation and the runtime already use (`OPERATION_KINDS`,
9
+ * `EDGE_KINDS`, `EXPRESSION_KINDS`, `WORKFLOW_STEP_TYPES`, `AUTHORIZATION_OPERATIONS`, …)
10
+ * rather than duplicating them by hand (spec16 §71).
11
+ *
12
+ * It is deliberately scoped to the **graph model** — `EntityDef` through
13
+ * `AuthorizationPolicyDef` — not the nine UI node kinds. UI authoring already has its own
14
+ * canonical, mechanically-checked machine catalogue (`@cynodia/axiom-ui`'s
15
+ * `packages/ui-toolkit/docs/PATTERN_CATALOG.json`, generated by `npm run toolkit:catalog`);
16
+ * a second description of the same nodes here would be exactly the "four unrelated sources
17
+ * of truth" spec16 §71 warns against, so `docs/UI.md` and the toolkit catalogue remain that
18
+ * authority.
19
+ *
20
+ * A template is a **minimal valid shape**, not a set of defaults: it never assigns
21
+ * security-sensitive semantics an author did not ask for (spec16 §78) — an `ActionDef`
22
+ * template carries no `authorizationPolicy`, which is exactly its pre-0.15 public contract,
23
+ * not a silently-inserted permission.
24
+ */
25
+ import { EDGE_KINDS, OPERATION_KINDS, INVOCATION_SOURCES } from './nodes.js';
26
+ import { EXPRESSION_KINDS } from './expressions.js';
27
+ import { TYPE_REF_KINDS } from './type-ref.js';
28
+ import { AUTHORIZATION_OPERATIONS } from './authorization.js';
29
+ import { SEMANTIC_NODE_KINDS } from './types.js';
30
+ import { WORKFLOW_STEP_TYPES } from './workflows.js';
31
+ import { RELATIONSHIP_CARDINALITIES } from './relationships.js';
32
+ import { MIGRATION_OPERATION_KINDS } from './migration.js';
33
+ import { INTEGRATION_OPERATION_MODES } from './integrations.js';
34
+ const f = (name, required, type, description, extra = {}) => ({ name, required, type, description, ...extra });
35
+ const EXPR = 'Expression';
36
+ const NODE_ID = 'NodeId';
37
+ const KINDS = [
38
+ {
39
+ kind: 'entity',
40
+ purpose: 'A record type. Instances live wherever a StateDef says they do, including nested inside collections and other entities.',
41
+ fields: [
42
+ f('fields', true, 'FieldDef[]', 'The entity\'s fields: id, valueType, required, defaultValue.'),
43
+ f('identityFieldId', false, 'FieldId', 'The field distinguishing instances. Required to address one by identity, and required by every TransitionConstraintDef on this entity — without it such a rule is silently skipped.'),
44
+ ],
45
+ template: { kind: 'entity', id: '<id>', fields: [{ id: '<field-id>', valueType: { kind: 'primitive', primitive: 'string' } }] },
46
+ },
47
+ {
48
+ kind: 'state',
49
+ purpose: 'A named application value: stored, or computed from other state via `derivation`.',
50
+ fields: [
51
+ f('valueType', true, 'TypeRef', 'The value\'s structural type.', { closedEnum: TYPE_REF_KINDS }),
52
+ f('initialValue', false, 'LiteralValue', 'Seed data, keyed by FieldId wherever it contains a record. Absent starts the state at its type default.'),
53
+ f('derivation', false, EXPR, 'When present, the state is computed rather than stored, and is read-only — a write is rejected.'),
54
+ f('draft', false, 'boolean', 'Marks work in progress. Skipped by instance validation until an action commits it.'),
55
+ f('ephemeral', false, 'boolean', 'Marks a UI fact rather than a domain fact. Skipped by instance validation; may not be persisted.'),
56
+ f('authority', false, 'Authority', 'Who may commit a canonical mutation: \'client\' (default) or \'server\'.', { closedEnum: ['client', 'server'] }),
57
+ f('serverOnly', false, 'boolean', 'Excludes the state from the client IR entirely.'),
58
+ f('persistence', false, 'StatePersistence', 'Where a stored value survives: memory, local-storage, or remote.', { closedEnum: ['memory', 'local-storage', 'remote'] }),
59
+ ],
60
+ template: { kind: 'state', id: '<id>', valueType: { kind: 'primitive', primitive: 'string' } },
61
+ },
62
+ {
63
+ kind: 'action',
64
+ purpose: 'Behaviour expressed as data, executed as a transaction: bind parameters, evaluate guards, run operations, check constraints, commit or roll back everything.',
65
+ fields: [
66
+ f('parameters', false, 'ActionParameter[]', 'Named, typed inputs.'),
67
+ f('guards', false, 'ActionGuard[]', 'Condition + failure pairs, checked in order before any transaction opens. Preferred over the parallel preconditions/failureModes arrays.'),
68
+ f('operations', true, 'Operation[]', 'The mutations and effects this action performs, run sequentially against provisional state.', { closedEnum: OPERATION_KINDS }),
69
+ f('postconditions', false, `${EXPR}[]`, 'Checked against proposed state after operations run, before commit.'),
70
+ f('destructive', false, 'boolean', 'Declared destructive intent (also inferred from a `remove` operation).'),
71
+ f('requiresConfirmation', false, 'boolean', 'UX only — never an authorization mechanism.'),
72
+ f('authorization', false, EXPR, 'Legacy boolean expression, evaluated on the authority with the caller bound to PRINCIPAL. Never reaches the client.'),
73
+ f('authorizationPolicy', false, NODE_ID, 'The AuthorizationPolicyDef governing action.invoke (spec15). If both this and `authorization` are present, the effective decision is their conjunction.', { referenceTargets: ['authorization-policy'] }),
74
+ f('invocation', false, 'ActionInvocationPolicy', 'Restricts which invocation sources (client / system) may reach this action at all.', { closedEnum: INVOCATION_SOURCES }),
75
+ ],
76
+ template: { kind: 'action', id: '<id>', operations: [] },
77
+ },
78
+ {
79
+ kind: 'constraint',
80
+ purpose: 'An invariant over proposed state, evaluated after every governed mutation. A constraint that cannot be evaluated counts as violated, never satisfied.',
81
+ fields: [
82
+ f('expression', true, EXPR, 'The boolean invariant.'),
83
+ f('entityId', false, NODE_ID, 'When set, evaluated once per canonical instance of this entity, bound to ref(entityId). Without it, evaluated once in the root scope.', { referenceTargets: ['entity'] }),
84
+ f('severity', false, `'error' | 'warning'`, 'Defaults to error. A warning is advice and never blocks a write.', { closedEnum: ['error', 'warning'] }),
85
+ f('message', false, 'string', 'Human-readable explanation of a violation.'),
86
+ ],
87
+ template: { kind: 'constraint', id: '<id>', expression: { kind: 'literal', value: true } },
88
+ },
89
+ {
90
+ kind: 'transition-constraint',
91
+ purpose: 'A rule about how state may change, seeing the instance as it was at transaction entry and as proposed. Holds on every governed write path, not just one.',
92
+ fields: [
93
+ f('entityId', true, NODE_ID, 'The entity whose transitions are governed. Must declare identityFieldId, or this rule is silently skipped.', { referenceTargets: ['entity'] }),
94
+ f('previousScopeId', true, NODE_ID, 'Binds the instance as it was at the outermost transaction\'s start.'),
95
+ f('proposedScopeId', true, NODE_ID, 'Binds the instance as proposed. Bound to nothing when the instance is being removed.'),
96
+ f('expression', true, EXPR, 'Must hold for every governed transition.'),
97
+ f('severity', false, `'error' | 'warning'`, 'Defaults to error.', { closedEnum: ['error', 'warning'] }),
98
+ f('message', false, 'string', 'Human-readable explanation of a violation.'),
99
+ ],
100
+ template: {
101
+ kind: 'transition-constraint',
102
+ id: '<id>',
103
+ entityId: '<entity-id>',
104
+ previousScopeId: '<previous-scope>',
105
+ proposedScopeId: '<proposed-scope>',
106
+ expression: { kind: 'literal', value: true },
107
+ },
108
+ },
109
+ {
110
+ kind: 'route',
111
+ purpose: 'A URL path mapped to a view, with typed path parameters.',
112
+ fields: [
113
+ f('path', true, 'string', 'The route path, with `:name` placeholders.'),
114
+ f('viewId', true, NODE_ID, 'The view this route renders.'),
115
+ f('parameters', false, 'RouteParameter[]', 'Matches each `:name` placeholder to an id and type.'),
116
+ ],
117
+ template: { kind: 'route', id: '<id>', path: '/', viewId: '<view-id>' },
118
+ },
119
+ {
120
+ kind: 'expression',
121
+ purpose: 'A named, reusable calculation: this semantic calculation exists once. Its body is evaluated in an isolated scope — its own parameters and application state, nothing else.',
122
+ fields: [
123
+ f('parameters', false, 'ExpressionParameter[]', 'Bound only inside this definition\'s body.'),
124
+ f('expression', true, EXPR, 'The calculation.', { closedEnum: EXPRESSION_KINDS }),
125
+ f('valueType', false, 'TypeRef', 'The declared result type. Absent, inferred from the body.'),
126
+ f('description', false, 'string', 'Human documentation.'),
127
+ ],
128
+ template: { kind: 'expression', id: '<id>', expression: { kind: 'literal', value: null } },
129
+ },
130
+ {
131
+ kind: 'integration',
132
+ purpose: 'An external capability domain — a name for "the systems outside Axiom that do X" — with no host, credential or SDK reference.',
133
+ fields: [],
134
+ template: { kind: 'integration', id: '<id>' },
135
+ },
136
+ {
137
+ kind: 'integration-operation',
138
+ purpose: 'A typed operation an integration exposes: a query (ask, wait for a finite answer) or an effect (tell, no answer joins the transaction).',
139
+ fields: [
140
+ f('integrationId', true, NODE_ID, 'The integration this operation belongs to.', { referenceTargets: ['integration'] }),
141
+ f('mode', true, `'query' | 'effect'`, 'Query or effect.', { closedEnum: INTEGRATION_OPERATION_MODES }),
142
+ f('parameters', false, 'IntegrationOperationParameter[]', 'Named, typed inputs.'),
143
+ f('resultType', true, 'TypeRef', 'The result shape.'),
144
+ f('clientSafe', false, 'boolean', 'Whether client-executed code may invoke it. Absent means server-only.'),
145
+ f('idempotent', false, 'boolean', 'Whether invoking this effect twice with the same idempotency key has one effect.'),
146
+ f('retry', false, 'RetryPolicy', 'Retry policy for a failed effect. Meaningless for a query.'),
147
+ ],
148
+ template: { kind: 'integration-operation', id: '<id>', integrationId: '<integration-id>', mode: 'query', resultType: { kind: 'primitive', primitive: 'string' } },
149
+ },
150
+ {
151
+ kind: 'event',
152
+ purpose: 'A typed semantic fact this application can dispatch and react to, through the one inbound event pipeline.',
153
+ fields: [f('payloadType', true, 'TypeRef', 'The shape of the event\'s payload.')],
154
+ template: { kind: 'event', id: '<id>', payloadType: { kind: 'primitive', primitive: 'string' } },
155
+ },
156
+ {
157
+ kind: 'trigger',
158
+ purpose: 'Invokes an ordinary action under a system principal, on an interval, after a delay, on a lifecycle moment, or on an event.',
159
+ fields: [
160
+ f('actionId', true, NODE_ID, 'The action this trigger invokes.', { referenceTargets: ['action'] }),
161
+ f('when', true, 'TriggerSpec', 'interval | delay | lifecycle | event.', { closedEnum: ['interval', 'delay', 'lifecycle', 'event'] }),
162
+ f('arguments', false, 'Record<paramId, Expression>', 'Keyed by the target action\'s parameter id.'),
163
+ f('enabledWhen', false, EXPR, 'Dynamic enablement, evaluated each time the trigger would otherwise fire.'),
164
+ ],
165
+ template: { kind: 'trigger', id: '<id>', actionId: '<action-id>', when: { kind: 'interval', everyMs: 60000 } },
166
+ },
167
+ {
168
+ kind: 'subscription',
169
+ purpose: 'A long-lived external event source: the world tells Axiom, while Axiom is listening. Delivers into the ordinary EventDef -> TriggerDef -> ActionDef pipeline.',
170
+ fields: [
171
+ f('integrationId', true, NODE_ID, 'The capability domain whose adapter maintains the source.', { referenceTargets: ['integration'] }),
172
+ f('source', false, 'string', 'Which semantic source within that integration. Absent, the subscription\'s own id is the name.'),
173
+ f('arguments', false, 'Record<string, Expression>', 'Configuration evaluated once, at activation.'),
174
+ f('eventId', true, NODE_ID, 'The EventDef a delivery becomes.', { referenceTargets: ['event'] }),
175
+ f('lifecycle', false, 'SubscriptionLifecyclePolicy', 'autoStart, required, reconnect.'),
176
+ f('delivery', false, 'SubscriptionDeliveryPolicy', 'Queue depth, backpressure policy, deduplication, retry.', { closedEnum: ['block', 'drop-oldest', 'drop-newest'] }),
177
+ ],
178
+ template: { kind: 'subscription', id: '<id>', integrationId: '<integration-id>', eventId: '<event-id>' },
179
+ },
180
+ {
181
+ kind: 'storage',
182
+ purpose: 'A portable object store for binary attachments. Bytes never enter the graph; state holds a BlobRef of five scalars.',
183
+ fields: [
184
+ f('blobEntityId', true, NODE_ID, 'The entity every BlobRef of this store conforms to.', { referenceTargets: ['entity'] }),
185
+ f('readAuthorization', false, EXPR, 'Who may read bytes/metadata. Absent means no one.'),
186
+ f('uploadAuthorization', false, EXPR, 'Who may upload. Absent means no one.'),
187
+ f('acceptedMediaTypes', false, 'string[]', 'Absent accepts any.'),
188
+ f('maxSizeBytes', false, 'number', 'The largest accepted upload.'),
189
+ f('retry', false, 'RetryPolicy', 'For a failed blob-commit/blob-delete.'),
190
+ ],
191
+ template: { kind: 'storage', id: '<id>', blobEntityId: '<entity-id>' },
192
+ },
193
+ {
194
+ kind: 'query',
195
+ purpose: 'A demand-driven read over authoritative data too large to materialize: fixed named clauses, every leaf an ordinary Expression.',
196
+ fields: [
197
+ f('parameters', false, 'QueryParameter[]', 'Named, typed inputs.'),
198
+ f('source', true, NODE_ID, 'The authoritative row entity.', { referenceTargets: ['entity'] }),
199
+ f('rowScopeId', true, NODE_ID, 'Binds one source row for every clause expression.'),
200
+ f('filter', false, EXPR, 'Boolean predicate. AND-ed with the entity\'s ReadPolicyDef on the authority.'),
201
+ f('sort', false, 'QuerySortKey[]', 'Ordering keys, most significant first; canonical identity is appended as a tie-breaker.'),
202
+ f('relationships', false, 'QueryRelationshipUse[]', 'Explicit RelationshipDef traversals.', { referenceTargets: ['relationship'] }),
203
+ f('projection', false, 'QueryProjection', 'Projected result fields. Absent returns whole source rows.'),
204
+ f('groupBy', false, `${EXPR}[]`, 'Group keys, present only with aggregate.'),
205
+ f('aggregate', false, 'QueryAggregate[]', 'Reductions over groups or the whole result.'),
206
+ f('pagination', false, 'QueryPagination', 'Strategy and page-size ceiling.', { closedEnum: ['cursor', 'offset'] }),
207
+ f('readPolicyId', false, NODE_ID, 'The ReadPolicyDef AND-ed into filter before execution.', { referenceTargets: ['read-policy'] }),
208
+ f('authorizationPolicy', false, NODE_ID, 'The AuthorizationPolicyDef governing query.read (spec15) — whether the principal may run the query at all.', { referenceTargets: ['authorization-policy'] }),
209
+ ],
210
+ template: { kind: 'query', id: '<id>', source: '<entity-id>', rowScopeId: '<row-scope>' },
211
+ },
212
+ {
213
+ kind: 'relationship',
214
+ purpose: 'An explicit, never-inferred entity-to-entity link a query may traverse.',
215
+ fields: [
216
+ f('from', true, 'RelationshipEndpoint', 'The entity + field a traversal starts from.'),
217
+ f('to', true, 'RelationshipEndpoint', 'The entity + field a traversal reaches. For to-one this MUST be the target\'s identityFieldId.'),
218
+ f('cardinality', true, `'to-one' | 'to-many'`, 'Whether one `from` row reaches one or many `to` rows.', { closedEnum: RELATIONSHIP_CARDINALITIES }),
219
+ ],
220
+ template: {
221
+ kind: 'relationship',
222
+ id: '<id>',
223
+ from: { entityId: '<entity-id>', fieldId: '<field-id>' },
224
+ to: { entityId: '<entity-id>', fieldId: '<field-id>' },
225
+ cardinality: 'to-one',
226
+ },
227
+ },
228
+ {
229
+ kind: 'read-policy',
230
+ purpose: 'A row-level filter AND-ed into every query\'s effective filter on the authority. A failed predicate makes a row invisible, never disclosed.',
231
+ fields: [
232
+ f('entityId', true, NODE_ID, 'The entity whose rows this policy governs. At most one per entity.', { referenceTargets: ['entity'] }),
233
+ f('rowScopeId', true, NODE_ID, 'Binds one candidate row while predicate is evaluated.'),
234
+ f('predicate', true, EXPR, 'Boolean. A row is visible when this is true for it, with the caller bound to PRINCIPAL.'),
235
+ ],
236
+ template: { kind: 'read-policy', id: '<id>', entityId: '<entity-id>', rowScopeId: '<row-scope>', predicate: { kind: 'literal', value: true } },
237
+ },
238
+ {
239
+ kind: 'migration',
240
+ purpose: 'A semantic migration between two consecutive schema versions: a closed operation vocabulary and pure Expression transforms.',
241
+ fields: [
242
+ f('fromSchema', true, 'number', 'The schema version this migration upgrades from.'),
243
+ f('toSchema', true, 'number', 'Must equal fromSchema + 1.'),
244
+ f('operations', true, 'MigrationOperation[]', 'The transformations this migration applies.', { closedEnum: MIGRATION_OPERATION_KINDS }),
245
+ f('reversibility', false, `'irreversible' | 'reverse-supplied'`, 'Absent means irreversible — the honest default for anything that may discard information.', { closedEnum: ['irreversible', 'reverse-supplied'] }),
246
+ f('reverseOperations', false, 'MigrationOperation[]', 'An explicit down-migration, present only with reversibility \'reverse-supplied\'.'),
247
+ ],
248
+ template: { kind: 'migration', id: '<id>', fromSchema: 1, toSchema: 2, operations: [] },
249
+ },
250
+ {
251
+ kind: 'workflow',
252
+ purpose: 'A durable, portable multi-step process: six step kinds, no script body, typed single-assignment bindings, closed expression scope (inputs / bindings / EVENT / PRINCIPAL — never StateDef, QueryDef, now/uuid/random).',
253
+ fields: [
254
+ f('inputs', false, 'WorkflowInput[]', 'Typed inputs bound when the workflow starts.'),
255
+ f('bindings', false, 'WorkflowBinding[]', 'Single-assignment values, each produced by exactly one wait-event step.'),
256
+ f('entry', true, NODE_ID, 'The first step id.'),
257
+ f('steps', true, 'WorkflowStep[]', 'action | wait-event | timer | branch | complete | fail.', { closedEnum: WORKFLOW_STEP_TYPES }),
258
+ f('startPolicy', false, NODE_ID, 'The AuthorizationPolicyDef governing workflow.start. Absent means any principal may start it.', { referenceTargets: ['authorization-policy'] }),
259
+ f('instanceAccessPolicy', false, NODE_ID, 'The AuthorizationPolicyDef governing inspect/history/cancel on a running instance. Absent means the 0.14 owner-fingerprint rule.', { referenceTargets: ['authorization-policy'] }),
260
+ ],
261
+ template: {
262
+ kind: 'workflow',
263
+ id: '<id>',
264
+ entry: '<step-id>',
265
+ steps: [{ type: 'complete', id: '<step-id>' }],
266
+ },
267
+ },
268
+ {
269
+ kind: 'authorization-policy',
270
+ purpose: 'The one authorization language: a single boolean `allow` expression over the closed scope PRINCIPAL / RESOURCE / OPERATION. Exactly true is ALLOW; false, absence or any evaluation error is DENY.',
271
+ fields: [
272
+ f('allow', true, EXPR, 'Deterministic — no now/uuid/random, no StateDef/QueryDef reference.', { closedEnum: [...AUTHORIZATION_OPERATIONS] }),
273
+ ],
274
+ template: { kind: 'authorization-policy', id: '<id>', allow: { kind: 'literal', value: false } },
275
+ },
276
+ ];
277
+ const BY_KIND = new Map(KINDS.map((d) => [d.kind, d]));
278
+ /** Every graph-model semantic node kind an agent may author (spec16 §72). */
279
+ export function listAuthorableKinds() {
280
+ return SEMANTIC_NODE_KINDS;
281
+ }
282
+ /** The structural authoring contract for one node kind, or `undefined` for an unknown kind. */
283
+ export function describeAuthoringKind(kind) {
284
+ return BY_KIND.get(kind);
285
+ }
286
+ /** The complete authoring schema — one descriptor per authorable graph-model kind (spec16 §69, §70). */
287
+ export function authoringSchema() {
288
+ return KINDS;
289
+ }
290
+ /** Every edge kind the graph can derive, for tooling that renders `getEdges` results. */
291
+ export function authoringEdgeKinds() {
292
+ return EDGE_KINDS;
293
+ }
@@ -3,6 +3,8 @@ import { locationExpressions, locationFieldIds, locationRootStateId, locationSel
3
3
  import { isGroupFieldId } from './group.js';
4
4
  import { isUINode } from './ui.js';
5
5
  import { queryExpressions } from './query.js';
6
+ import { workflowActionIds, workflowEventIds } from './workflows.js';
7
+ import { nodeAuthorizationPolicyRefs } from './authorization.js';
6
8
  /**
7
9
  * Ids a `ref` expression mentions anywhere in the tree.
8
10
  *
@@ -210,12 +212,34 @@ export function deriveEdges(nodes) {
210
212
  link(node.id, node.entityId, 'constrains');
211
213
  reads(node.id, node.predicate, new Map([...rootScope, [node.rowScopeId, statesByEntity.get(node.entityId) ?? []]]));
212
214
  break;
215
+ case 'workflow':
216
+ // Workflow expressions are closed-scope (inputs / bindings / EVENT / PRINCIPAL —
217
+ // spec14 §—, never StateDef), so there is nothing to attribute as a state read here.
218
+ // What the graph *can* say is which actions a step may invoke and which events it
219
+ // waits on, which is exactly what dependency/impact analysis needs (spec16 §12).
220
+ for (const actionId of workflowActionIds(node)) {
221
+ link(node.id, actionId, 'invokes');
222
+ }
223
+ for (const eventId of workflowEventIds(node)) {
224
+ link(node.id, eventId, 'references');
225
+ }
226
+ break;
227
+ case 'authorization-policy':
213
228
  case 'integration':
214
229
  case 'event':
215
230
  break;
216
231
  default:
217
232
  }
218
233
  }
234
+ // Any node that references an `AuthorizationPolicyDef` (an action's or query's
235
+ // `authorizationPolicy`, a workflow's `startPolicy` / `instanceAccessPolicy`) depends on
236
+ // it — one generic pass over the closed set of policy-reference fields (spec15,
237
+ // spec16 §12), rather than a hand-maintained case per node kind that references one.
238
+ for (const node of nodes) {
239
+ for (const policyId of nodeAuthorizationPolicyRefs(node)) {
240
+ link(node.id, policyId, 'references');
241
+ }
242
+ }
219
243
  return [...pending.values()].map((entry) => ({
220
244
  id: `${entry.from}:${entry.kind}:${entry.to}`,
221
245
  from: entry.from,
package/dist/graph.js CHANGED
@@ -23,7 +23,7 @@ export class ApplicationGraph {
23
23
  /** Bumped by every change, so the derived edge index can never serve stale data. */
24
24
  revision = 0;
25
25
  semanticIndex;
26
- constructor(id, name, version = '0.15.0') {
26
+ constructor(id, name, version = '0.16.0') {
27
27
  this.data = { id, name, version, nodes: {}, edges: {} };
28
28
  }
29
29
  get id() {
package/dist/index.d.ts CHANGED
@@ -42,4 +42,6 @@ export * from './derive-edges.js';
42
42
  export * from './authority.js';
43
43
  export * from './ir.js';
44
44
  export * from './server-ir.js';
45
+ export * from './authoring-schema.js';
46
+ export * from './semantic-diff.js';
45
47
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -42,3 +42,5 @@ export * from './derive-edges.js';
42
42
  export * from './authority.js';
43
43
  export * from './ir.js';
44
44
  export * from './server-ir.js';
45
+ export * from './authoring-schema.js';
46
+ export * from './semantic-diff.js';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Canonical semantic graph diffing (spec16 §34-38, §150-160).
3
+ *
4
+ * `diffSchema` (spec11) already classifies changes to entities, fields, states,
5
+ * relationships and read policies — the *persistence-relevant* structure. `semanticDiff`
6
+ * is the superset spec16 asks for: every other executable and presentation node kind,
7
+ * classified into categories an agent or a reviewer can reason about (`semantic`,
8
+ * `authorization`, `provider`, `workflow`, `query`, `presentation`, `metadata`), plus the
9
+ * compatibility impact a change carries — does it move `semanticFingerprint`, does it move
10
+ * `schemaFingerprint`, does it raise the required Server IR contract. It embeds
11
+ * `diffSchema`'s result rather than re-deriving it, so there remains exactly one place that
12
+ * classifies a schema change (spec5.1 §26 "one canonical location per semantic rule").
13
+ *
14
+ * A node compared by full JSON identity **after** stripping human metadata is exactly the
15
+ * notion of "meaning" `semanticFingerprint` uses ({@link stripNonSemanticMetadata}), so a
16
+ * rename or a `metadata` edit is reported as `metadata`, never as `semantic` — and a
17
+ * presentation-only edit to a UI node or a route is `presentation`, never `authorization` or
18
+ * `schema` (spec16 §155).
19
+ */
20
+ import type { SchemaDiff } from './schema-diff.js';
21
+ import type { ServerIRContract } from './server-ir.js';
22
+ import type { ApplicationGraph } from './graph.js';
23
+ /** The classification vocabulary a semantic diff entry may belong to. A change may belong to more than one. */
24
+ export declare const SEMANTIC_DIFF_CATEGORIES: readonly ["semantic", "authorization", "schema", "provider", "workflow", "query", "presentation", "metadata"];
25
+ export type SemanticDiffCategory = (typeof SEMANTIC_DIFF_CATEGORIES)[number];
26
+ export interface SemanticDiffEntry {
27
+ changeKind: 'added' | 'removed' | 'changed';
28
+ nodeId: string;
29
+ nodeKind: string;
30
+ /** One or more classifications this change belongs to (spec16 §36). */
31
+ categories: SemanticDiffCategory[];
32
+ message: string;
33
+ }
34
+ export interface SemanticDiffCompatibilityImpact {
35
+ semanticFingerprintChanged: boolean;
36
+ schemaFingerprintChanged: boolean;
37
+ /** Whether a mixed-build cluster could now disagree — either fingerprint moved. */
38
+ authorityCompatibilityAffected: boolean;
39
+ serverContractBefore: ServerIRContract;
40
+ serverContractAfter: ServerIRContract;
41
+ serverContractChanged: boolean;
42
+ /** Whether the embedded schema diff has any `migration-required` / `destructive` entry. */
43
+ migrationRequired: boolean;
44
+ }
45
+ export interface SemanticDiff {
46
+ /** Every non-schema-owned node kind's add/remove/change, classified. Canonically ordered by id. */
47
+ entries: SemanticDiffEntry[];
48
+ /** The detailed field-level diff for entities, fields, states, relationships and read policies. */
49
+ schema: SchemaDiff;
50
+ compatibility: SemanticDiffCompatibilityImpact;
51
+ /** Entry counts per category, `entries` only (the schema diff has its own `byClass`). */
52
+ byCategory: Record<SemanticDiffCategory, number>;
53
+ /** True when neither `entries` nor the schema diff found anything — a genuine no-op. */
54
+ isNoOp: boolean;
55
+ }
56
+ /**
57
+ * The canonical semantic difference between two graphs (spec16 §34-38). Pure and
58
+ * side-effect-free: it reads both graphs and computes, never mutates either (spec16 §133).
59
+ */
60
+ export declare function semanticDiff(before: ApplicationGraph, after: ApplicationGraph): SemanticDiff;
61
+ //# sourceMappingURL=semantic-diff.d.ts.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Canonical semantic graph diffing (spec16 §34-38, §150-160).
3
+ *
4
+ * `diffSchema` (spec11) already classifies changes to entities, fields, states,
5
+ * relationships and read policies — the *persistence-relevant* structure. `semanticDiff`
6
+ * is the superset spec16 asks for: every other executable and presentation node kind,
7
+ * classified into categories an agent or a reviewer can reason about (`semantic`,
8
+ * `authorization`, `provider`, `workflow`, `query`, `presentation`, `metadata`), plus the
9
+ * compatibility impact a change carries — does it move `semanticFingerprint`, does it move
10
+ * `schemaFingerprint`, does it raise the required Server IR contract. It embeds
11
+ * `diffSchema`'s result rather than re-deriving it, so there remains exactly one place that
12
+ * classifies a schema change (spec5.1 §26 "one canonical location per semantic rule").
13
+ *
14
+ * A node compared by full JSON identity **after** stripping human metadata is exactly the
15
+ * notion of "meaning" `semanticFingerprint` uses ({@link stripNonSemanticMetadata}), so a
16
+ * rename or a `metadata` edit is reported as `metadata`, never as `semantic` — and a
17
+ * presentation-only edit to a UI node or a route is `presentation`, never `authorization` or
18
+ * `schema` (spec16 §155).
19
+ */
20
+ import { diffSchema } from './schema-diff.js';
21
+ import { schemaFingerprint, canonicalJSON } from './schema-identity.js';
22
+ import { semanticFingerprint, stripNonSemanticMetadata } from './semantic-identity.js';
23
+ import { requiredServerContractForGraph } from './server-ir.js';
24
+ import { SEMANTIC_NODE_KINDS } from './types.js';
25
+ import { UI_NODE_KINDS } from './ui.js';
26
+ /** The classification vocabulary a semantic diff entry may belong to. A change may belong to more than one. */
27
+ export const SEMANTIC_DIFF_CATEGORIES = [
28
+ 'semantic',
29
+ 'authorization',
30
+ 'schema',
31
+ 'provider',
32
+ 'workflow',
33
+ 'query',
34
+ 'presentation',
35
+ 'metadata',
36
+ ];
37
+ /** Kinds `diffSchema` already classifies in full field-level detail; excluded from the generic node loop. */
38
+ const SCHEMA_OWNED_KINDS = new Set(['entity', 'state', 'relationship', 'read-policy']);
39
+ const PROVIDER_KINDS = new Set(['integration', 'integration-operation', 'subscription', 'storage']);
40
+ const PRESENTATION_KINDS = new Set([...UI_NODE_KINDS, 'route']);
41
+ function kindCategory(kind) {
42
+ if (kind === 'authorization-policy')
43
+ return 'authorization';
44
+ if (kind === 'query')
45
+ return 'query';
46
+ if (kind === 'workflow')
47
+ return 'workflow';
48
+ if (kind === 'migration')
49
+ return 'schema';
50
+ if (PROVIDER_KINDS.has(kind))
51
+ return 'provider';
52
+ if (PRESENTATION_KINDS.has(kind))
53
+ return 'presentation';
54
+ return 'semantic';
55
+ }
56
+ /** Field names whose change on a node makes the change authorization-semantic too (spec16 §156). */
57
+ const AUTHZ_FIELDS = ['authorizationPolicy', 'authorization', 'startPolicy', 'instanceAccessPolicy'];
58
+ function isPlainObject(value) {
59
+ return !!value && typeof value === 'object' && !Array.isArray(value);
60
+ }
61
+ function equalJSON(a, b) {
62
+ return canonicalJSON(a) === canonicalJSON(b);
63
+ }
64
+ function describeChange(kind, id, changeKind) {
65
+ const verb = changeKind === 'added' ? 'added' : changeKind === 'removed' ? 'removed' : 'changed';
66
+ return `${kind} ${id} ${verb}`;
67
+ }
68
+ /**
69
+ * The canonical semantic difference between two graphs (spec16 §34-38). Pure and
70
+ * side-effect-free: it reads both graphs and computes, never mutates either (spec16 §133).
71
+ */
72
+ export function semanticDiff(before, after) {
73
+ const schema = diffSchema(before, after);
74
+ const entries = [];
75
+ const kinds = [...SEMANTIC_NODE_KINDS, ...UI_NODE_KINDS, 'route'].filter((kind, index, all) => all.indexOf(kind) === index);
76
+ for (const kind of kinds) {
77
+ if (SCHEMA_OWNED_KINDS.has(kind)) {
78
+ continue;
79
+ }
80
+ const beforeById = new Map(before.getNodesByKind(kind).map((node) => [String(node.id), node]));
81
+ const afterById = new Map(after.getNodesByKind(kind).map((node) => [String(node.id), node]));
82
+ for (const [id, node] of afterById) {
83
+ if (!beforeById.has(id)) {
84
+ entries.push({
85
+ changeKind: 'added',
86
+ nodeId: id,
87
+ nodeKind: kind,
88
+ categories: [kindCategory(kind)],
89
+ message: describeChange(kind, id, 'added'),
90
+ });
91
+ continue;
92
+ }
93
+ const previous = beforeById.get(id);
94
+ if (equalJSON(previous, node)) {
95
+ continue;
96
+ }
97
+ const strippedEqual = equalJSON(stripNonSemanticMetadata(previous), stripNonSemanticMetadata(node));
98
+ const categories = new Set();
99
+ if (strippedEqual) {
100
+ categories.add('metadata');
101
+ }
102
+ else if (isPlainObject(previous) && isPlainObject(node)) {
103
+ const changedFields = new Set([...Object.keys(previous), ...Object.keys(node)].filter((key) => !equalJSON(previous[key], node[key])));
104
+ const onlyAuthzFieldsChanged = changedFields.size > 0 && [...changedFields].every((key) => AUTHZ_FIELDS.includes(key));
105
+ if (onlyAuthzFieldsChanged) {
106
+ categories.add('authorization');
107
+ }
108
+ else {
109
+ categories.add(kindCategory(kind));
110
+ if ([...changedFields].some((key) => AUTHZ_FIELDS.includes(key))) {
111
+ categories.add('authorization');
112
+ }
113
+ }
114
+ }
115
+ else {
116
+ categories.add(kindCategory(kind));
117
+ }
118
+ entries.push({
119
+ changeKind: 'changed',
120
+ nodeId: id,
121
+ nodeKind: kind,
122
+ categories: [...categories],
123
+ message: describeChange(kind, id, 'changed'),
124
+ });
125
+ }
126
+ for (const id of beforeById.keys()) {
127
+ if (!afterById.has(id)) {
128
+ entries.push({
129
+ changeKind: 'removed',
130
+ nodeId: id,
131
+ nodeKind: kind,
132
+ categories: [kindCategory(kind)],
133
+ message: describeChange(kind, id, 'removed'),
134
+ });
135
+ }
136
+ }
137
+ }
138
+ entries.sort((a, b) => (a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : a.changeKind.localeCompare(b.changeKind)));
139
+ const semanticFingerprintChanged = semanticFingerprint(before) !== semanticFingerprint(after);
140
+ const schemaFingerprintChanged = schemaFingerprint(before) !== schemaFingerprint(after);
141
+ const serverContractBefore = requiredServerContractForGraph(before);
142
+ const serverContractAfter = requiredServerContractForGraph(after);
143
+ const byCategory = Object.fromEntries(SEMANTIC_DIFF_CATEGORIES.map((c) => [c, 0]));
144
+ for (const entry of entries) {
145
+ for (const category of entry.categories) {
146
+ byCategory[category] += 1;
147
+ }
148
+ }
149
+ return {
150
+ entries,
151
+ schema,
152
+ compatibility: {
153
+ semanticFingerprintChanged,
154
+ schemaFingerprintChanged,
155
+ authorityCompatibilityAffected: semanticFingerprintChanged || schemaFingerprintChanged,
156
+ serverContractBefore,
157
+ serverContractAfter,
158
+ serverContractChanged: serverContractBefore !== serverContractAfter,
159
+ migrationRequired: schema.needsMigration.length > 0,
160
+ },
161
+ byCategory,
162
+ isNoOp: entries.length === 0 && schema.entries.length === 0,
163
+ };
164
+ }
@@ -73,6 +73,13 @@ export declare const SEMANTIC_FINGERPRINT_VERSION = 1;
73
73
  */
74
74
  export declare const EXECUTABLE_KINDS: readonly ["action", "integration", "integration-operation", "trigger", "event", "subscription", "read-policy", "query", "expression", "constraint", "transition-constraint", "storage", "relationship", "workflow", "authorization-policy"];
75
75
  export type ExecutableKind = (typeof EXECUTABLE_KINDS)[number];
76
+ /**
77
+ * Removes human metadata (`name` / `description` / `label` / `metadata` / authoring
78
+ * provenance) from a value, recursively. Exported so tooling that needs "does this node's
79
+ * *meaning* differ" — `semanticDiff`, for one — shares the exact same notion of "meaning"
80
+ * the fingerprint uses, rather than reimplementing it (spec16 §35, §154).
81
+ */
82
+ export declare function stripNonSemanticMetadata(value: unknown): unknown;
76
83
  export interface SemanticProjection {
77
84
  fingerprintVersion: number;
78
85
  schemaVersion: number;
@@ -93,6 +93,15 @@ export const EXECUTABLE_KINDS = [
93
93
  ];
94
94
  /** Keys removed everywhere in the tree — human metadata, never executable meaning. */
95
95
  const NON_SEMANTIC_KEYS = new Set(['name', 'description', 'label', 'metadata', AUTHORING_METADATA_KEY]);
96
+ /**
97
+ * Removes human metadata (`name` / `description` / `label` / `metadata` / authoring
98
+ * provenance) from a value, recursively. Exported so tooling that needs "does this node's
99
+ * *meaning* differ" — `semanticDiff`, for one — shares the exact same notion of "meaning"
100
+ * the fingerprint uses, rather than reimplementing it (spec16 §35, §154).
101
+ */
102
+ export function stripNonSemanticMetadata(value) {
103
+ return stripNonSemantic(value);
104
+ }
96
105
  function stripNonSemantic(value) {
97
106
  if (Array.isArray(value)) {
98
107
  return value.map(stripNonSemantic);
@@ -2,6 +2,7 @@ import type { FieldId, NodeId } from './ids.js';
2
2
  import type { ActionDef, ConstraintDef, EntityDef, ExpressionDef, StateDef, TransitionConstraintDef } from './nodes.js';
3
3
  import type { Expression } from './expressions.js';
4
4
  import type { FieldIndexEntry } from './graph.js';
5
+ import type { ApplicationGraph } from './graph.js';
5
6
  import type { EventDef } from './events.js';
6
7
  import type { IntegrationDef, IntegrationOperationDef } from './integrations.js';
7
8
  import type { TriggerDef } from './triggers.js';
@@ -181,6 +182,14 @@ export declare function usesQueryVocabulary(ir: {
181
182
  }): boolean;
182
183
  /** The higher of two contracts, ordered by `SERVER_IR_CONTRACTS`. */
183
184
  export declare function maxContract(a: ServerIRContract, b: ServerIRContract): ServerIRContract;
185
+ /**
186
+ * The Server IR contract this graph requires, computed straight from its nodes rather than
187
+ * from a compiled `ServerIR` (spec16 §32-33). This is the same "computed from the document,
188
+ * never asserted by hand" rule `compileToServerIR` applies, made available to static
189
+ * tooling that has only an `ApplicationGraph` — a semantic diff or a compatibility
190
+ * explanation should never need to run the compiler to answer "does this graph need v9".
191
+ */
192
+ export declare function requiredServerContractForGraph(graph: ApplicationGraph): ServerIRContract;
184
193
  /** Every expression a Server IR document contains, in no particular order. */
185
194
  export declare function serverIRExpressions(ir: {
186
195
  states: readonly StateDef[];
package/dist/server-ir.js CHANGED
@@ -227,6 +227,55 @@ export function usesQueryVocabulary(ir) {
227
227
  export function maxContract(a, b) {
228
228
  return SERVER_IR_CONTRACTS.indexOf(b) > SERVER_IR_CONTRACTS.indexOf(a) ? b : a;
229
229
  }
230
+ /**
231
+ * The Server IR contract this graph requires, computed straight from its nodes rather than
232
+ * from a compiled `ServerIR` (spec16 §32-33). This is the same "computed from the document,
233
+ * never asserted by hand" rule `compileToServerIR` applies, made available to static
234
+ * tooling that has only an `ApplicationGraph` — a semantic diff or a compatibility
235
+ * explanation should never need to run the compiler to answer "does this graph need v9".
236
+ */
237
+ export function requiredServerContractForGraph(graph) {
238
+ const actionsList = graph.getNodesByKind('action');
239
+ const actions = Object.fromEntries(actionsList.map((a) => [a.id, a]));
240
+ const ir = {
241
+ states: graph.getNodesByKind('state'),
242
+ actions,
243
+ constraints: graph.getNodesByKind('constraint'),
244
+ transitionConstraints: graph.getNodesByKind('transition-constraint'),
245
+ expressionDefs: Object.fromEntries(graph.getNodesByKind('expression').map((e) => [e.id, e])),
246
+ integrations: graph.getNodesByKind('integration'),
247
+ integrationOperations: Object.fromEntries(graph.getNodesByKind('integration-operation').map((o) => [o.id, o])),
248
+ events: graph.getNodesByKind('event'),
249
+ triggers: graph.getNodesByKind('trigger'),
250
+ subscriptions: graph.getNodesByKind('subscription'),
251
+ storages: graph.getNodesByKind('storage'),
252
+ queries: graph.getNodesByKind('query'),
253
+ readPolicies: graph.getNodesByKind('read-policy'),
254
+ authorizationPolicies: graph.getNodesByKind('authorization-policy'),
255
+ migrations: graph.getNodesByKind('migration'),
256
+ workflows: graph.getNodesByKind('workflow'),
257
+ schemaVersion: 1,
258
+ };
259
+ let required = requiredServerContract(serverIRExpressions(ir));
260
+ if (usesIntegrationVocabulary(ir))
261
+ required = maxContract(required, 'axiom.server.v3');
262
+ if (usesInvocationVocabulary(ir))
263
+ required = maxContract(required, 'axiom.server.v2');
264
+ if (usesV4Semantics(ir))
265
+ required = maxContract(required, 'axiom.server.v4');
266
+ if (usesExternalIOVocabulary(ir))
267
+ required = maxContract(required, 'axiom.server.v5');
268
+ if (usesQueryVocabulary(ir))
269
+ required = maxContract(required, 'axiom.server.v6');
270
+ if (usesMigrationVocabulary(ir))
271
+ required = maxContract(required, 'axiom.server.v7');
272
+ if (usesWorkflowVocabulary(ir))
273
+ required = maxContract(required, 'axiom.server.v8');
274
+ if (usesAuthorizationVocabulary(ir) || usesLegacyActionAuthorization(ir)) {
275
+ required = maxContract(required, 'axiom.server.v9');
276
+ }
277
+ return required;
278
+ }
230
279
  /** Every expression a Server IR document contains, in no particular order. */
231
280
  export function serverIRExpressions(ir) {
232
281
  const found = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-core",
3
- "version": "0.15.0-alpha.3",
3
+ "version": "0.16.0-alpha.1",
4
4
  "description": "Application Graph, semantic types, locations and validation for Axiom.",
5
5
  "license": "MIT",
6
6
  "author": "AskTech AS",