@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.
- package/dist/authoring-schema.d.ts +54 -0
- package/dist/authoring-schema.js +293 -0
- package/dist/authority.js +11 -11
- package/dist/derive-edges.js +43 -4
- package/dist/diagnostics.d.ts +4 -0
- package/dist/diagnostics.js +7 -0
- package/dist/expressions.d.ts +11 -5
- package/dist/expressions.js +21 -3
- package/dist/graph.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/infer.d.ts +9 -6
- package/dist/infer.js +39 -7
- package/dist/location.d.ts +15 -5
- package/dist/location.js +42 -2
- package/dist/nodes.d.ts +28 -1
- package/dist/nodes.js +42 -0
- package/dist/resolve-presentation.js +2 -1
- package/dist/semantic-diff.d.ts +61 -0
- package/dist/semantic-diff.js +184 -0
- package/dist/semantic-identity.d.ts +7 -0
- package/dist/semantic-identity.js +9 -0
- package/dist/server-ir.d.ts +9 -0
- package/dist/server-ir.js +54 -4
- package/dist/validate-location.d.ts +6 -2
- package/dist/validate-location.js +29 -1
- package/dist/validate-presentation.js +2 -1
- package/dist/validate.js +59 -5
- 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/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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
186
|
+
return targets(operationChildren(operation));
|
|
187
187
|
}
|
|
188
188
|
return false;
|
|
189
189
|
});
|
|
190
|
-
return targets(action
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) {
|
package/dist/derive-edges.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
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
|
|
494
|
+
linkOperations(actionId, operationChildren(operation), linker, inner);
|
|
456
495
|
break;
|
|
457
496
|
}
|
|
458
497
|
case 'invoke':
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -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
|
package/dist/diagnostics.js
CHANGED
|
@@ -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
|
};
|
package/dist/expressions.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
207
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
222
|
+
export declare function expressionFieldIds(expression: unknown): FieldId[];
|
|
217
223
|
//# sourceMappingURL=expressions.d.ts.map
|
package/dist/expressions.js
CHANGED
|
@@ -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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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) {
|