@cynodia/axiom-core 0.15.0-alpha.3 → 0.16.0-alpha.2

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
+ }
package/dist/authority.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { referencedIds } from './derive-edges.js';
2
- import { actionGuards, isMutationOperation } from './nodes.js';
2
+ import { actionGuards, actionOperations, isMutationOperation, operationChildren } from './nodes.js';
3
3
  import { locationExpressions, locationProviderEntityId, locationRootStateId } from './location.js';
4
4
  import { queryExpressions } from './query.js';
5
5
  export const AUTHORITIES = ['client', 'server'];
@@ -121,7 +121,7 @@ function operationExpressions(operation) {
121
121
  switch (operation.kind) {
122
122
  case 'for-each':
123
123
  found.push(operation.collection);
124
- for (const nested of operation.operations ?? []) {
124
+ for (const nested of operationChildren(operation)) {
125
125
  found.push(...operationExpressions(nested));
126
126
  }
127
127
  break;
@@ -157,11 +157,11 @@ function operationExpressions(operation) {
157
157
  }
158
158
  /** Whether an action calls out to an integration anywhere in its top-level operations. */
159
159
  export function actionUsesIntegration(action) {
160
- return (action.operations ?? []).some((operation) => operation.kind === 'integration-query' || operation.kind === 'integration-effect');
160
+ return actionOperations(action).some((operation) => operation.kind === 'integration-query' || operation.kind === 'integration-effect');
161
161
  }
162
162
  /** Whether an action reaches an object store anywhere in its top-level operations. */
163
163
  export function actionUsesStorage(action) {
164
- return (action.operations ?? []).some((operation) => operation.kind === 'blob-metadata' ||
164
+ return actionOperations(action).some((operation) => operation.kind === 'blob-metadata' ||
165
165
  operation.kind === 'blob-commit' ||
166
166
  operation.kind === 'blob-delete');
167
167
  }
@@ -170,7 +170,7 @@ export function actionUsesStorage(action) {
170
170
  * data provider, so an action that reads authoritative data through a query executes there.
171
171
  */
172
172
  export function actionUsesQuery(action) {
173
- return (action.operations ?? []).some((operation) => operation.kind === 'query');
173
+ return actionOperations(action).some((operation) => operation.kind === 'query');
174
174
  }
175
175
  /**
176
176
  * Whether an action writes a `provider-record` location anywhere in its operations
@@ -183,11 +183,11 @@ export function actionWritesProviderRecord(action) {
183
183
  return locationProviderEntityId(operation.target) !== undefined;
184
184
  }
185
185
  if (operation.kind === 'for-each') {
186
- return targets(operation.operations);
186
+ return targets(operationChildren(operation));
187
187
  }
188
188
  return false;
189
189
  });
190
- return targets(action.operations ?? []);
190
+ return targets(actionOperations(action));
191
191
  }
192
192
  /** The states an action writes, following `for-each`, `invoke` and declared native effects. */
193
193
  export function statesWrittenBy(action, context, visited = new Set()) {
@@ -204,7 +204,7 @@ export function statesWrittenBy(action, context, visited = new Set()) {
204
204
  }
205
205
  switch (operation.kind) {
206
206
  case 'for-each':
207
- walk(operation.operations ?? []);
207
+ walk(operationChildren(operation));
208
208
  break;
209
209
  case 'invoke': {
210
210
  const target = context.actions.get(operation.actionId);
@@ -238,7 +238,7 @@ export function statesWrittenBy(action, context, visited = new Set()) {
238
238
  }
239
239
  }
240
240
  };
241
- walk(action.operations ?? []);
241
+ walk(actionOperations(action));
242
242
  return found;
243
243
  }
244
244
  /** The states an action reads: its guards, its values, its selectors, its authorization. */
@@ -252,11 +252,11 @@ export function statesReadByAction(action, context, visited = new Set()) {
252
252
  ...(action.postconditions ?? []),
253
253
  ...(action.authorization ? [action.authorization] : []),
254
254
  ];
255
- for (const operation of action.operations ?? []) {
255
+ for (const operation of actionOperations(action)) {
256
256
  expressions.push(...operationExpressions(operation));
257
257
  }
258
258
  const found = statesReadBy(expressions, context);
259
- for (const operation of action.operations ?? []) {
259
+ for (const operation of actionOperations(action)) {
260
260
  if (operation.kind === 'invoke') {
261
261
  const target = context.actions.get(operation.actionId);
262
262
  if (target) {
@@ -1,8 +1,11 @@
1
1
  import { constructedFieldIds, expressionDefsIn, expressionFieldIds, walkExpression } from './expressions.js';
2
+ import { actionOperations, operationChildren } from './nodes.js';
2
3
  import { locationExpressions, locationFieldIds, locationRootStateId, locationSelectorFieldIds, } from './location.js';
3
4
  import { isGroupFieldId } from './group.js';
4
5
  import { isUINode } from './ui.js';
5
6
  import { queryExpressions } from './query.js';
7
+ import { workflowActionIds, workflowEventIds } from './workflows.js';
8
+ import { nodeAuthorizationPolicyRefs } from './authorization.js';
6
9
  /**
7
10
  * Ids a `ref` expression mentions anywhere in the tree.
8
11
  *
@@ -210,12 +213,34 @@ export function deriveEdges(nodes) {
210
213
  link(node.id, node.entityId, 'constrains');
211
214
  reads(node.id, node.predicate, new Map([...rootScope, [node.rowScopeId, statesByEntity.get(node.entityId) ?? []]]));
212
215
  break;
216
+ case 'workflow':
217
+ // Workflow expressions are closed-scope (inputs / bindings / EVENT / PRINCIPAL —
218
+ // spec14 §—, never StateDef), so there is nothing to attribute as a state read here.
219
+ // What the graph *can* say is which actions a step may invoke and which events it
220
+ // waits on, which is exactly what dependency/impact analysis needs (spec16 §12).
221
+ for (const actionId of workflowActionIds(node)) {
222
+ link(node.id, actionId, 'invokes');
223
+ }
224
+ for (const eventId of workflowEventIds(node)) {
225
+ link(node.id, eventId, 'references');
226
+ }
227
+ break;
228
+ case 'authorization-policy':
213
229
  case 'integration':
214
230
  case 'event':
215
231
  break;
216
232
  default:
217
233
  }
218
234
  }
235
+ // Any node that references an `AuthorizationPolicyDef` (an action's or query's
236
+ // `authorizationPolicy`, a workflow's `startPolicy` / `instanceAccessPolicy`) depends on
237
+ // it — one generic pass over the closed set of policy-reference fields (spec15,
238
+ // spec16 §12), rather than a hand-maintained case per node kind that references one.
239
+ for (const node of nodes) {
240
+ for (const policyId of nodeAuthorizationPolicyRefs(node)) {
241
+ link(node.id, policyId, 'references');
242
+ }
243
+ }
219
244
  return [...pending.values()].map((entry) => ({
220
245
  id: `${entry.from}:${entry.kind}:${entry.to}`,
221
246
  from: entry.from,
@@ -227,12 +252,22 @@ export function deriveEdges(nodes) {
227
252
  },
228
253
  }));
229
254
  }
255
+ function isPlainObject(value) {
256
+ return !!value && typeof value === 'object' && !Array.isArray(value);
257
+ }
230
258
  /**
231
259
  * The states an expression ultimately draws its members from. Following `field` and calls
232
260
  * matters: a collection reached as `coalesce(field(ref(state), lines), [])` still comes
233
261
  * from that state, and its members' fields are still reads of it.
262
+ *
263
+ * Total over malformed input (spec16pt2 §12-24): a candidate expression can be
264
+ * AI-generated, deserialized or hand-tampered.
234
265
  */
235
- function statesOf(expression, scope, states, defs = new Map()) {
266
+ function statesOf(expressionInput, scope, states, defs = new Map()) {
267
+ if (!isPlainObject(expressionInput) || typeof expressionInput.kind !== 'string') {
268
+ return [];
269
+ }
270
+ const expression = expressionInput;
236
271
  switch (expression.kind) {
237
272
  case 'ref': {
238
273
  const bound = scope.get(expression.targetId);
@@ -340,7 +375,7 @@ function bind(scope, id, targets) {
340
375
  * projecting a field of each member is recorded as a read of that field of the state the
341
376
  * members came from.
342
377
  */
343
- function collectReads(expression, scope, states, found = new Map(), defs = new Map()) {
378
+ function collectReads(expressionInput, scope, states, found = new Map(), defs = new Map()) {
344
379
  const record = (stateId, fieldId) => {
345
380
  const entry = found.get(stateId) ?? new Set();
346
381
  if (fieldId) {
@@ -348,6 +383,10 @@ function collectReads(expression, scope, states, found = new Map(), defs = new M
348
383
  }
349
384
  found.set(stateId, entry);
350
385
  };
386
+ if (!isPlainObject(expressionInput) || typeof expressionInput.kind !== 'string') {
387
+ return found;
388
+ }
389
+ const expression = expressionInput;
351
390
  switch (expression.kind) {
352
391
  case 'ref':
353
392
  for (const stateId of statesOf(expression, scope, states, defs)) {
@@ -431,7 +470,7 @@ function linkAction(action, linker) {
431
470
  for (const expression of [...(action.preconditions ?? []), ...(action.postconditions ?? [])]) {
432
471
  linker.reads(action.id, expression, linker.scope);
433
472
  }
434
- linkOperations(action.id, action.operations ?? [], linker, linker.scope);
473
+ linkOperations(action.id, actionOperations(action), linker, linker.scope);
435
474
  }
436
475
  function linkOperations(actionId, operations, linker, scope) {
437
476
  for (const operation of operations) {
@@ -452,7 +491,7 @@ function linkOperations(actionId, operations, linker, scope) {
452
491
  case 'for-each': {
453
492
  linker.reads(actionId, operation.collection, scope);
454
493
  const inner = bind(scope, operation.scopeId, statesOf(operation.collection, scope, linker.states));
455
- linkOperations(actionId, operation.operations, linker, inner);
494
+ linkOperations(actionId, operationChildren(operation), linker, inner);
456
495
  break;
457
496
  }
458
497
  case 'invoke':
@@ -212,5 +212,9 @@ export declare const VALIDATION_CODES: {
212
212
  readonly authorizationUnknownPolicy: "AUTHORIZATION_UNKNOWN_POLICY";
213
213
  /** An `AuthorizationPolicyDef.allow` expression calls `now` / `uuid` / `random` — authorization must be deterministic (spec15 §34). */
214
214
  readonly authorizationNondeterministic: "AUTHORIZATION_NONDETERMINISTIC";
215
+ /** `ActionDef.operations`, or a `for-each`'s nested `operations`, is present but not an array. */
216
+ readonly invalidOperationCollection: "INVALID_OPERATION_COLLECTION";
217
+ /** An operation array entry that is not a plain object with a recognized `kind`. */
218
+ readonly invalidOperation: "INVALID_OPERATION";
215
219
  };
216
220
  //# sourceMappingURL=diagnostics.d.ts.map
@@ -211,4 +211,11 @@ export const VALIDATION_CODES = {
211
211
  authorizationUnknownPolicy: 'AUTHORIZATION_UNKNOWN_POLICY',
212
212
  /** An `AuthorizationPolicyDef.allow` expression calls `now` / `uuid` / `random` — authorization must be deterministic (spec15 §34). */
213
213
  authorizationNondeterministic: 'AUTHORIZATION_NONDETERMINISTIC',
214
+ // Validation totality (0.16pt2, spec16pt2 §12-24). A candidate graph can be AI-generated,
215
+ // deserialized or hand-tampered, so `validateGraph` must reject a malformed runtime
216
+ // *shape* structurally — never assume TypeScript's compile-time types already hold.
217
+ /** `ActionDef.operations`, or a `for-each`'s nested `operations`, is present but not an array. */
218
+ invalidOperationCollection: 'INVALID_OPERATION_COLLECTION',
219
+ /** An operation array entry that is not a plain object with a recognized `kind`. */
220
+ invalidOperation: 'INVALID_OPERATION',
214
221
  };
@@ -203,15 +203,21 @@ export declare function groupItems(source: Expression): FieldExpression;
203
203
  */
204
204
  export declare function expressionRef(expressionId: NodeId, args?: Record<string, Expression>): ExpressionRefExpression;
205
205
  export declare function conditional(condition: Expression, whenTrue: Expression, whenFalse: Expression): ConditionalExpression;
206
- /** Visits every sub-expression, parents before children. */
207
- export declare function walkExpression(expression: Expression, visit: (node: Expression) => void): void;
206
+ /**
207
+ * Visits every sub-expression, parents before children. Total over malformed input
208
+ * (spec16pt2 §12-24): a candidate expression tree can arrive from AI generation,
209
+ * deserialization or hand-tampering (a deleted field, a tampered array element), and this
210
+ * is the shared visitor most expression-derived analysis in the codebase is built on — a
211
+ * malformed node here simply is not visited, rather than throwing while reading `.kind`.
212
+ */
213
+ export declare function walkExpression(expressionInput: unknown, visit: (node: Expression) => void): void;
208
214
  /**
209
215
  * Field ids a constructed record assigns. Only the record's own entries count: the
210
216
  * expressions that compute those values are reads, not writes.
211
217
  */
212
- export declare function constructedFieldIds(expression: Expression): FieldId[];
218
+ export declare function constructedFieldIds(expression: unknown): FieldId[];
213
219
  /** Expression definitions an expression reaches directly, in tree order. */
214
- export declare function expressionDefsIn(expression: Expression): NodeId[];
220
+ export declare function expressionDefsIn(expression: unknown): NodeId[];
215
221
  /** Field ids an expression reads, including nested sources and constructed records. */
216
- export declare function expressionFieldIds(expression: Expression): FieldId[];
222
+ export declare function expressionFieldIds(expression: unknown): FieldId[];
217
223
  //# sourceMappingURL=expressions.d.ts.map
@@ -128,8 +128,21 @@ export function expressionRef(expressionId, args) {
128
128
  export function conditional(condition, whenTrue, whenFalse) {
129
129
  return { kind: 'conditional', condition, whenTrue, whenFalse };
130
130
  }
131
- /** Visits every sub-expression, parents before children. */
132
- export function walkExpression(expression, visit) {
131
+ function isPlainObject(value) {
132
+ return !!value && typeof value === 'object' && !Array.isArray(value);
133
+ }
134
+ /**
135
+ * Visits every sub-expression, parents before children. Total over malformed input
136
+ * (spec16pt2 §12-24): a candidate expression tree can arrive from AI generation,
137
+ * deserialization or hand-tampering (a deleted field, a tampered array element), and this
138
+ * is the shared visitor most expression-derived analysis in the codebase is built on — a
139
+ * malformed node here simply is not visited, rather than throwing while reading `.kind`.
140
+ */
141
+ export function walkExpression(expressionInput, visit) {
142
+ if (!isPlainObject(expressionInput) || typeof expressionInput.kind !== 'string') {
143
+ return;
144
+ }
145
+ const expression = expressionInput;
133
146
  visit(expression);
134
147
  switch (expression.kind) {
135
148
  case 'field':
@@ -195,7 +208,12 @@ export function walkExpression(expression, visit) {
195
208
  * expressions that compute those values are reads, not writes.
196
209
  */
197
210
  export function constructedFieldIds(expression) {
198
- return expression.kind === 'object' ? expression.entries.map((entry) => entry.fieldId) : [];
211
+ if (!isPlainObject(expression) || expression.kind !== 'object' || !Array.isArray(expression.entries)) {
212
+ return [];
213
+ }
214
+ return expression.entries
215
+ .filter((entry) => isPlainObject(entry) && typeof entry.fieldId === 'string')
216
+ .map((entry) => entry.fieldId);
199
217
  }
200
218
  /** Expression definitions an expression reaches directly, in tree order. */
201
219
  export function expressionDefsIn(expression) {