@cynodia/axiom-core 0.15.0-alpha.2 → 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.
- package/dist/authoring-schema.d.ts +54 -0
- package/dist/authoring-schema.js +293 -0
- package/dist/authorization.d.ts +63 -12
- package/dist/authorization.js +52 -14
- package/dist/derive-edges.js +24 -0
- package/dist/graph.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/semantic-diff.d.ts +61 -0
- package/dist/semantic-diff.js +164 -0
- package/dist/semantic-identity.d.ts +16 -6
- package/dist/semantic-identity.js +9 -0
- package/dist/server-ir.d.ts +21 -0
- package/dist/server-ir.js +64 -0
- package/package.json +1 -1
|
@@ -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/authorization.d.ts
CHANGED
|
@@ -21,6 +21,17 @@
|
|
|
21
21
|
* equivalent to an inline `allow` over `PRINCIPAL`; `ReadPolicyDef` (spec10) remains the
|
|
22
22
|
* row-level read mechanism, unified into query authorization by spec15 Phase D.
|
|
23
23
|
*
|
|
24
|
+
* **spec15pt3 — one security-absence-aware evaluator for *every* authorization expression.**
|
|
25
|
+
* `AuthorizationPolicyDef.allow` and the legacy `ActionDef.authorization` expression share
|
|
26
|
+
* {@link evaluateAuthorizationExpression}: a `field(ref(PRINCIPAL), …)` / `field(ref(RESOURCE),
|
|
27
|
+
* …)` read of a key the scope object does not carry is **security-scope absence**, not an
|
|
28
|
+
* ordinary `undefined`, and `neq` / `not` / `or` / any comparison whose truth would depend on
|
|
29
|
+
* that absence is *not satisfied* — so a missing principal attribute can never manufacture
|
|
30
|
+
* authority through either surface (spec15pt3 §5, §7, §32). The two modes differ only in
|
|
31
|
+
* their *final interpretation* (a policy allows on exactly `true`; legacy keeps its historical
|
|
32
|
+
* truthiness), which `decideAuthorization` applies downstream — never in how absence, boolean
|
|
33
|
+
* composition or errors propagate.
|
|
34
|
+
*
|
|
24
35
|
* This module is portable plain data + `Expression` trees; it carries no enforcement (that
|
|
25
36
|
* is Phases C–F). The accessors are **total over any input** so a hand-tampered policy fails
|
|
26
37
|
* closed with a structured diagnostic, never a native exception (spec15 §37).
|
|
@@ -122,6 +133,10 @@ export interface AuthorizationCheckInput {
|
|
|
122
133
|
/**
|
|
123
134
|
* The legacy `ActionDef.authorization` expression outcome, when that expression is present.
|
|
124
135
|
* Its historical truthiness rule is kept: a non-empty array or any truthy value allows.
|
|
136
|
+
* spec15pt3 — this part MUST be produced by {@link evaluateAuthorizationExpression} (mode
|
|
137
|
+
* `'legacy-action'`), so `{ ok: true, value: false }` for a decision that depended on a
|
|
138
|
+
* missing security-scope field: absence reaches this combiner as a plain DENY, never as a
|
|
139
|
+
* truthy value.
|
|
125
140
|
*/
|
|
126
141
|
legacy?: AuthorizationCheckPart;
|
|
127
142
|
}
|
|
@@ -152,21 +167,57 @@ export interface AuthorizationPolicyScope {
|
|
|
152
167
|
operation: string;
|
|
153
168
|
}
|
|
154
169
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* `decideAuthorization`
|
|
170
|
+
* Which authorization surface an expression is evaluated for (spec15pt3 §32). Both modes
|
|
171
|
+
* share {@link evaluateAuthorizationExpression}'s security-absence, boolean-composition and
|
|
172
|
+
* error propagation; they differ only downstream, in `decideAuthorization` — a `'policy'`
|
|
173
|
+
* result allows on exactly `true`, a `'legacy-action'` result keeps the historical
|
|
174
|
+
* `ActionDef.authorization` truthiness rule.
|
|
175
|
+
*/
|
|
176
|
+
export type AuthorizationEvaluationMode = 'policy' | 'legacy-action';
|
|
177
|
+
/**
|
|
178
|
+
* The evaluation context for {@link evaluateAuthorizationExpression}. `'policy'` mode uses the
|
|
179
|
+
* closed `{ principal, resource, operation }` scope only. `'legacy-action'` mode additionally
|
|
180
|
+
* honours the historical `ActionDef.authorization` scope, which could also read an ordinary
|
|
181
|
+
* `StateDef`: {@link resolveExternalRef} resolves such a `ref` through the caller's ordinary
|
|
182
|
+
* evaluator, and an id it cannot resolve fails **closed** (never silently allows), exactly as
|
|
183
|
+
* a throwing `runtime.evaluate` did pre-pt3.
|
|
184
|
+
*/
|
|
185
|
+
export interface AuthorizationExpressionContext {
|
|
186
|
+
principal: Record<string, unknown> | null | undefined;
|
|
187
|
+
resource?: Record<string, unknown> | null | undefined;
|
|
188
|
+
operation?: string;
|
|
189
|
+
resolveExternalRef?: (refId: string) => {
|
|
190
|
+
found: boolean;
|
|
191
|
+
value?: unknown;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* spec15pt3 §32 — the **one** canonical security-absence-aware authorization-expression
|
|
196
|
+
* evaluator, shared by `AuthorizationPolicyDef.allow` (`mode: 'policy'`) and the legacy
|
|
197
|
+
* `ActionDef.authorization` expression (`mode: 'legacy-action'`). Returns the `{ ok, value }`
|
|
198
|
+
* shape `decideAuthorization` consumes:
|
|
158
199
|
*
|
|
159
|
-
* -
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* -
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* - an evaluation error (malformed node reaching the runtime, unsupported builtin
|
|
166
|
-
* `{ ok: false }
|
|
200
|
+
* - the expression reduces to a concrete value ⇒ `{ ok: true, value }` — for `'policy'` only
|
|
201
|
+
* an exact `true` is ALLOW; for `'legacy-action'` the historical truthiness rule (a truthy
|
|
202
|
+
* value / non-empty array) applies, both in `decideAuthorization`;
|
|
203
|
+
* - its truth depends on a **missing PRINCIPAL / RESOURCE field** ⇒ `{ ok: true, value: false }`
|
|
204
|
+
* — absence never creates authority through `eq` / `neq` / `not` / `lt` / `contains` /
|
|
205
|
+
* `or` / any composition (spec15pt2 §8-§12, spec15pt3 §11-§16, §79-§82);
|
|
206
|
+
* - an evaluation error (malformed node reaching the runtime, unsupported builtin, an
|
|
207
|
+
* unresolvable legacy `ref`) ⇒ `{ ok: false }` — fail closed, and the error keeps its
|
|
208
|
+
* provenance through `not` / `or` so it cannot be negated back to ALLOW (spec15pt3 §84).
|
|
167
209
|
*
|
|
168
210
|
* A constant `literal(true)` is `{ ok: true, value: true }` regardless of principal — an
|
|
169
|
-
* explicitly public
|
|
211
|
+
* explicitly public rule still admits an anonymous caller; the invariant is "missing
|
|
212
|
+
* referenced security fields cannot create authority", not "anonymous is forbidden"
|
|
213
|
+
* (spec15pt2 §13, §14; spec15pt3 §17). A `literal` nullish value stays a concrete value,
|
|
214
|
+
* never security absence (spec15pt3 §58).
|
|
215
|
+
*/
|
|
216
|
+
export declare function evaluateAuthorizationExpression(expression: unknown, context: AuthorizationExpressionContext, mode: AuthorizationEvaluationMode): AuthorizationCheckPart;
|
|
217
|
+
/**
|
|
218
|
+
* spec15pt2 F1 — the three-valued `AuthorizationPolicyDef.allow` evaluator. A thin, stable
|
|
219
|
+
* wrapper over {@link evaluateAuthorizationExpression} in `'policy'` mode, kept for every
|
|
220
|
+
* existing policy-bearing call site (spec15pt2 §33).
|
|
170
221
|
*/
|
|
171
222
|
export declare function evaluateAuthorizationPolicyAllow(allow: unknown, scope: AuthorizationPolicyScope): AuthorizationCheckPart;
|
|
172
223
|
/**
|
package/dist/authorization.js
CHANGED
|
@@ -21,6 +21,17 @@
|
|
|
21
21
|
* equivalent to an inline `allow` over `PRINCIPAL`; `ReadPolicyDef` (spec10) remains the
|
|
22
22
|
* row-level read mechanism, unified into query authorization by spec15 Phase D.
|
|
23
23
|
*
|
|
24
|
+
* **spec15pt3 — one security-absence-aware evaluator for *every* authorization expression.**
|
|
25
|
+
* `AuthorizationPolicyDef.allow` and the legacy `ActionDef.authorization` expression share
|
|
26
|
+
* {@link evaluateAuthorizationExpression}: a `field(ref(PRINCIPAL), …)` / `field(ref(RESOURCE),
|
|
27
|
+
* …)` read of a key the scope object does not carry is **security-scope absence**, not an
|
|
28
|
+
* ordinary `undefined`, and `neq` / `not` / `or` / any comparison whose truth would depend on
|
|
29
|
+
* that absence is *not satisfied* — so a missing principal attribute can never manufacture
|
|
30
|
+
* authority through either surface (spec15pt3 §5, §7, §32). The two modes differ only in
|
|
31
|
+
* their *final interpretation* (a policy allows on exactly `true`; legacy keeps its historical
|
|
32
|
+
* truthiness), which `decideAuthorization` applies downstream — never in how absence, boolean
|
|
33
|
+
* composition or errors propagate.
|
|
34
|
+
*
|
|
24
35
|
* This module is portable plain data + `Expression` trees; it carries no enforcement (that
|
|
25
36
|
* is Phases C–F). The accessors are **total over any input** so a hand-tampered policy fails
|
|
26
37
|
* closed with a structured diagnostic, never a native exception (spec15 §37).
|
|
@@ -377,6 +388,14 @@ function evalAuthz(expression, scope, seen) {
|
|
|
377
388
|
return C(scope.resource ?? null);
|
|
378
389
|
if (t === AUTHZ_OPERATION_SCOPE)
|
|
379
390
|
return C(scope.operation);
|
|
391
|
+
// spec15pt3 — a legacy `ActionDef.authorization` expression's historical scope also
|
|
392
|
+
// reaches ordinary `StateDef` refs. Resolve them through the caller's ordinary
|
|
393
|
+
// evaluator; an unresolved ref fails closed (`E` ⇒ DENY), never silently allows. A
|
|
394
|
+
// policy expression's scope is closed, so this branch is unreachable in `'policy'` mode.
|
|
395
|
+
if (scope.mode === 'legacy-action' && scope.resolveExternalRef) {
|
|
396
|
+
const resolved = scope.resolveExternalRef(t);
|
|
397
|
+
return resolved.found ? C(resolved.value) : E;
|
|
398
|
+
}
|
|
380
399
|
return E; // out of the closed scope — validation rejects it; be total
|
|
381
400
|
}
|
|
382
401
|
case 'field': {
|
|
@@ -541,26 +560,37 @@ function applyPolicyBuiltin(fn, args) {
|
|
|
541
560
|
}
|
|
542
561
|
}
|
|
543
562
|
/**
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
* `
|
|
563
|
+
* spec15pt3 §32 — the **one** canonical security-absence-aware authorization-expression
|
|
564
|
+
* evaluator, shared by `AuthorizationPolicyDef.allow` (`mode: 'policy'`) and the legacy
|
|
565
|
+
* `ActionDef.authorization` expression (`mode: 'legacy-action'`). Returns the `{ ok, value }`
|
|
566
|
+
* shape `decideAuthorization` consumes:
|
|
547
567
|
*
|
|
548
|
-
* -
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
* -
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
* - an evaluation error (malformed node reaching the runtime, unsupported builtin
|
|
555
|
-
* `{ ok: false }
|
|
568
|
+
* - the expression reduces to a concrete value ⇒ `{ ok: true, value }` — for `'policy'` only
|
|
569
|
+
* an exact `true` is ALLOW; for `'legacy-action'` the historical truthiness rule (a truthy
|
|
570
|
+
* value / non-empty array) applies, both in `decideAuthorization`;
|
|
571
|
+
* - its truth depends on a **missing PRINCIPAL / RESOURCE field** ⇒ `{ ok: true, value: false }`
|
|
572
|
+
* — absence never creates authority through `eq` / `neq` / `not` / `lt` / `contains` /
|
|
573
|
+
* `or` / any composition (spec15pt2 §8-§12, spec15pt3 §11-§16, §79-§82);
|
|
574
|
+
* - an evaluation error (malformed node reaching the runtime, unsupported builtin, an
|
|
575
|
+
* unresolvable legacy `ref`) ⇒ `{ ok: false }` — fail closed, and the error keeps its
|
|
576
|
+
* provenance through `not` / `or` so it cannot be negated back to ALLOW (spec15pt3 §84).
|
|
556
577
|
*
|
|
557
578
|
* A constant `literal(true)` is `{ ok: true, value: true }` regardless of principal — an
|
|
558
|
-
* explicitly public
|
|
579
|
+
* explicitly public rule still admits an anonymous caller; the invariant is "missing
|
|
580
|
+
* referenced security fields cannot create authority", not "anonymous is forbidden"
|
|
581
|
+
* (spec15pt2 §13, §14; spec15pt3 §17). A `literal` nullish value stays a concrete value,
|
|
582
|
+
* never security absence (spec15pt3 §58).
|
|
559
583
|
*/
|
|
560
|
-
export function
|
|
584
|
+
export function evaluateAuthorizationExpression(expression, context, mode) {
|
|
561
585
|
let result;
|
|
562
586
|
try {
|
|
563
|
-
result = evalAuthz(
|
|
587
|
+
result = evalAuthz(expression, {
|
|
588
|
+
principal: context.principal ?? null,
|
|
589
|
+
resource: context.resource ?? null,
|
|
590
|
+
operation: context.operation ?? 'action.invoke',
|
|
591
|
+
mode,
|
|
592
|
+
...(context.resolveExternalRef ? { resolveExternalRef: context.resolveExternalRef } : {}),
|
|
593
|
+
}, new Set());
|
|
564
594
|
}
|
|
565
595
|
catch {
|
|
566
596
|
return { ok: false };
|
|
@@ -571,6 +601,14 @@ export function evaluateAuthorizationPolicyAllow(allow, scope) {
|
|
|
571
601
|
return { ok: true, value: false };
|
|
572
602
|
return { ok: true, value: result.v };
|
|
573
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* spec15pt2 F1 — the three-valued `AuthorizationPolicyDef.allow` evaluator. A thin, stable
|
|
606
|
+
* wrapper over {@link evaluateAuthorizationExpression} in `'policy'` mode, kept for every
|
|
607
|
+
* existing policy-bearing call site (spec15pt2 §33).
|
|
608
|
+
*/
|
|
609
|
+
export function evaluateAuthorizationPolicyAllow(allow, scope) {
|
|
610
|
+
return evaluateAuthorizationExpression(allow, { principal: scope.principal, resource: scope.resource, operation: scope.operation }, 'policy');
|
|
611
|
+
}
|
|
574
612
|
/**
|
|
575
613
|
* The distinct policy ids a graph node references for authorization — for dependency
|
|
576
614
|
* analysis and `validateGraph` reference resolution. Total over malformed input.
|
package/dist/derive-edges.js
CHANGED
|
@@ -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.
|
|
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
package/dist/index.js
CHANGED
|
@@ -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;
|
|
@@ -101,12 +108,15 @@ export interface AuthorityCompatibilityKey {
|
|
|
101
108
|
serverContract: string;
|
|
102
109
|
semanticFingerprint: string;
|
|
103
110
|
/**
|
|
104
|
-
* spec15pt2 §35 — the runtime authorization-evaluator semantics
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
111
|
+
* spec15pt2 §35, spec15pt3 §37-§39 — the runtime authorization-evaluator semantics
|
|
112
|
+
* version. Successive builds evaluate the *same* Server IR authorization expression
|
|
113
|
+
* differently — `alpha.1` → `alpha.2` for `AuthorizationPolicyDef.allow` (absent-value
|
|
114
|
+
* safety, F1), `alpha.2` → `alpha.3` for the legacy `ActionDef.authorization` expression
|
|
115
|
+
* (F1-legacy) — yet the graph, and therefore `semanticFingerprint`, is identical. This
|
|
116
|
+
* discriminator, present whenever the IR carries an authorization *decision* (a policy
|
|
117
|
+
* reference or a legacy `authorization` expression), keeps builds that disagree from
|
|
118
|
+
* silently co-participating in one authority domain. Absent on a graph with no
|
|
119
|
+
* authorization decision (its evaluation is unchanged across every build).
|
|
110
120
|
*/
|
|
111
121
|
authorizationRuntime?: string;
|
|
112
122
|
}
|
|
@@ -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);
|
package/dist/server-ir.d.ts
CHANGED
|
@@ -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';
|
|
@@ -141,6 +142,18 @@ export declare function usesAuthorizationVocabulary(ir: {
|
|
|
141
142
|
instanceAccessPolicy?: unknown;
|
|
142
143
|
}[];
|
|
143
144
|
}): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Whether a document carries any legacy `ActionDef.authorization` expression (spec15pt3
|
|
147
|
+
* §39). This is not 0.15 *vocabulary* — it predates the milestone — but its runtime meaning
|
|
148
|
+
* changed in `0.15.0-alpha.3` (security-absence-aware evaluation, F1-legacy), so a mixed
|
|
149
|
+
* `alpha.2` / `alpha.3` cluster over a graph that can exercise it must fail closed. Total
|
|
150
|
+
* over a tampered IR.
|
|
151
|
+
*/
|
|
152
|
+
export declare function usesLegacyActionAuthorization(ir: {
|
|
153
|
+
actions?: Record<string, {
|
|
154
|
+
authorization?: unknown;
|
|
155
|
+
}>;
|
|
156
|
+
}): boolean;
|
|
144
157
|
/**
|
|
145
158
|
* Authorization vocabulary whose *enforcement* has not shipped yet — the admission gate a
|
|
146
159
|
* build uses to fail closed rather than run a declared policy as a silent no-op (spec4 §4,
|
|
@@ -169,6 +182,14 @@ export declare function usesQueryVocabulary(ir: {
|
|
|
169
182
|
}): boolean;
|
|
170
183
|
/** The higher of two contracts, ordered by `SERVER_IR_CONTRACTS`. */
|
|
171
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;
|
|
172
193
|
/** Every expression a Server IR document contains, in no particular order. */
|
|
173
194
|
export declare function serverIRExpressions(ir: {
|
|
174
195
|
states: readonly StateDef[];
|
package/dist/server-ir.js
CHANGED
|
@@ -177,6 +177,21 @@ export function usesAuthorizationVocabulary(ir) {
|
|
|
177
177
|
}
|
|
178
178
|
return false;
|
|
179
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Whether a document carries any legacy `ActionDef.authorization` expression (spec15pt3
|
|
182
|
+
* §39). This is not 0.15 *vocabulary* — it predates the milestone — but its runtime meaning
|
|
183
|
+
* changed in `0.15.0-alpha.3` (security-absence-aware evaluation, F1-legacy), so a mixed
|
|
184
|
+
* `alpha.2` / `alpha.3` cluster over a graph that can exercise it must fail closed. Total
|
|
185
|
+
* over a tampered IR.
|
|
186
|
+
*/
|
|
187
|
+
export function usesLegacyActionAuthorization(ir) {
|
|
188
|
+
for (const action of Object.values(ir.actions ?? {})) {
|
|
189
|
+
if (action && typeof action === 'object' && action.authorization !== undefined) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
180
195
|
/**
|
|
181
196
|
* Authorization vocabulary whose *enforcement* has not shipped yet — the admission gate a
|
|
182
197
|
* build uses to fail closed rather than run a declared policy as a silent no-op (spec4 §4,
|
|
@@ -212,6 +227,55 @@ export function usesQueryVocabulary(ir) {
|
|
|
212
227
|
export function maxContract(a, b) {
|
|
213
228
|
return SERVER_IR_CONTRACTS.indexOf(b) > SERVER_IR_CONTRACTS.indexOf(a) ? b : a;
|
|
214
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
|
+
}
|
|
215
279
|
/** Every expression a Server IR document contains, in no particular order. */
|
|
216
280
|
export function serverIRExpressions(ir) {
|
|
217
281
|
const found = [];
|