@cynodia/axiom-agent-api 0.9.0-alpha.2 → 0.11.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { ApplicationGraph, ValidationResult } from '@cynodia/axiom-core';
1
+ import type { ApplicationGraph, SchemaDiff, ValidationResult } from '@cynodia/axiom-core';
2
2
  import { PresentationQueries } from './presentation-queries.js';
3
3
  import { Transaction } from './transaction.js';
4
4
  import type { ChangeSet } from './changes.js';
5
+ import type { MigrationImpact, SchemaInspection } from './migration.js';
5
6
  /**
6
7
  * The machine-facing interface to an application. Agents query semantics and apply
7
8
  * structural transformations; they never edit generated code.
@@ -21,5 +22,15 @@ export declare class AgentAPI extends PresentationQueries {
21
22
  result: ValidationResult;
22
23
  };
23
24
  history(): ChangeSet[];
25
+ /** A structural summary of the semantic schema this graph declares (spec11 §89). */
26
+ inspectSchema(): SchemaInspection;
27
+ /** The classified semantic diff between a previous schema and this one (spec11 §58). */
28
+ diffSchema(previous: ApplicationGraph): SchemaDiff;
29
+ /**
30
+ * Impact analysis for evolving `previous` into this graph (spec11 §57): the diff, whether
31
+ * the migration chain covers it, whether data loss is possible, and which queries,
32
+ * actions, read policies, constraints and UI nodes reference something a migration touches.
33
+ */
34
+ migrationImpact(previous: ApplicationGraph): MigrationImpact;
24
35
  }
25
36
  //# sourceMappingURL=api.d.ts.map
package/dist/api.js CHANGED
@@ -1,6 +1,7 @@
1
- import { validateGraph } from '@cynodia/axiom-core';
1
+ import { diffSchema, validateGraph } from '@cynodia/axiom-core';
2
2
  import { PresentationQueries } from './presentation-queries.js';
3
3
  import { Transaction } from './transaction.js';
4
+ import { inspectSchema, migrationImpact } from './migration.js';
4
5
  /**
5
6
  * The machine-facing interface to an application. Agents query semantics and apply
6
7
  * structural transformations; they never edit generated code.
@@ -32,4 +33,20 @@ export class AgentAPI extends PresentationQueries {
32
33
  history() {
33
34
  return this.changeLog.map((change) => ({ ...change, operations: [...change.operations] }));
34
35
  }
36
+ /** A structural summary of the semantic schema this graph declares (spec11 §89). */
37
+ inspectSchema() {
38
+ return inspectSchema(this.graph);
39
+ }
40
+ /** The classified semantic diff between a previous schema and this one (spec11 §58). */
41
+ diffSchema(previous) {
42
+ return diffSchema(previous, this.graph);
43
+ }
44
+ /**
45
+ * Impact analysis for evolving `previous` into this graph (spec11 §57): the diff, whether
46
+ * the migration chain covers it, whether data loss is possible, and which queries,
47
+ * actions, read policies, constraints and UI nodes reference something a migration touches.
48
+ */
49
+ migrationImpact(previous) {
50
+ return migrationImpact(previous, this.graph);
51
+ }
35
52
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './changes.js';
2
2
  export * from './queries.js';
3
+ export * from './migration.js';
3
4
  export * from './presentation-queries.js';
4
5
  export * from './transaction.js';
5
6
  export * from './api.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './changes.js';
2
2
  export * from './queries.js';
3
+ export * from './migration.js';
3
4
  export * from './presentation-queries.js';
4
5
  export * from './transaction.js';
5
6
  export * from './api.js';
@@ -0,0 +1,85 @@
1
+ import { DEFAULT_SCHEMA_VERSION } from '@cynodia/axiom-core';
2
+ import type { ApplicationGraph, SchemaChangeClass, SchemaDiff, SchemaDiffEntry } from '@cynodia/axiom-core';
3
+ /**
4
+ * Authoring-time schema-evolution inspection for an agent (spec11 §56-58, §93).
5
+ *
6
+ * These functions answer, from the graph alone, what an agent needs before proposing or
7
+ * approving an upgrade: what the schema currently is, what a diff between two versions
8
+ * changes, whether the migration chain covers it, and which application semantics — queries,
9
+ * actions, read policies, constraints, UI — reference something a migration touches. The
10
+ * runtime planner (`planMigration` / `explainMigration` in `@cynodia/axiom-server`) answers
11
+ * the execution-side questions.
12
+ */
13
+ export interface SchemaInspection {
14
+ schemaVersion: number;
15
+ schemaFingerprint: string;
16
+ entities: Array<{
17
+ id: string;
18
+ identityFieldId: string | null;
19
+ fieldCount: number;
20
+ requiredFieldCount: number;
21
+ }>;
22
+ persistedStates: Array<{
23
+ id: string;
24
+ derived: boolean;
25
+ authority: 'client' | 'server';
26
+ }>;
27
+ relationships: Array<{
28
+ id: string;
29
+ from: {
30
+ entityId: string;
31
+ fieldId: string;
32
+ };
33
+ to: {
34
+ entityId: string;
35
+ fieldId: string;
36
+ };
37
+ cardinality: 'to-one' | 'to-many';
38
+ }>;
39
+ readPolicies: Array<{
40
+ id: string;
41
+ entityId: string;
42
+ }>;
43
+ migrations: Array<{
44
+ id: string;
45
+ fromSchema: number;
46
+ toSchema: number;
47
+ operationCount: number;
48
+ destructiveOperationCount: number;
49
+ }>;
50
+ /** Whether a contiguous migration chain connects schema 1 to `schemaVersion`. */
51
+ chainComplete: boolean;
52
+ }
53
+ /** A structural summary of the schema a graph declares (spec11 §89 `inspectSchema`). */
54
+ export declare function inspectSchema(graph: ApplicationGraph): SchemaInspection;
55
+ /**
56
+ * The terse semantic diff render of spec11 §58 — `+` added, `-` removed, `~` changed,
57
+ * never a JSON text diff.
58
+ */
59
+ export declare function explainSchemaDiff(diff: SchemaDiff): string;
60
+ export interface MigrationImpact {
61
+ fromVersion: number;
62
+ toVersion: number;
63
+ diff: SchemaDiff;
64
+ verdict: SchemaChangeClass;
65
+ /** Whether the migration chain in `next` accounts for every data-affecting diff entry. */
66
+ covered: boolean;
67
+ uncovered: SchemaDiffEntry[];
68
+ dataLossPossible: boolean;
69
+ affectedEntities: string[];
70
+ affectedFields: string[];
71
+ affectedQueries: string[];
72
+ affectedActions: string[];
73
+ affectedReadPolicies: string[];
74
+ affectedConstraints: string[];
75
+ affectedUiNodes: string[];
76
+ /** Read-policy diff entries — an authorization-semantic change, distinct from a data change (spec11 §42). */
77
+ authorizationChanges: string[];
78
+ }
79
+ /**
80
+ * Impact analysis for a proposed upgrade (spec11 §57). Diffs `previous` against `next` and
81
+ * reports which application semantics reference a field or entity the migration changes.
82
+ */
83
+ export declare function migrationImpact(previous: ApplicationGraph, next: ApplicationGraph): MigrationImpact;
84
+ export { DEFAULT_SCHEMA_VERSION };
85
+ //# sourceMappingURL=migration.d.ts.map
@@ -0,0 +1,154 @@
1
+ import { DEFAULT_SCHEMA_VERSION, diffSchema, migrationCoversDiff, schemaFingerprint, schemaProjection, } from '@cynodia/axiom-core';
2
+ function destructiveOps(migration) {
3
+ return migration.operations.filter((operation) => operation.destructive === true ||
4
+ operation.kind === 'remove-field' ||
5
+ operation.kind === 'remove-entity' ||
6
+ (operation.kind === 'transform-record' && (operation.removesFields?.length ?? 0) > 0)).length;
7
+ }
8
+ /** A structural summary of the schema a graph declares (spec11 §89 `inspectSchema`). */
9
+ export function inspectSchema(graph) {
10
+ const projection = schemaProjection(graph);
11
+ const migrations = graph.getNodesByKind('migration');
12
+ const byFrom = new Map(migrations.map((migration) => [migration.fromSchema, migration]));
13
+ let chainComplete = true;
14
+ for (let version = 1; version < projection.schemaVersion; version += 1) {
15
+ const step = byFrom.get(version);
16
+ if (!step || step.toSchema !== version + 1) {
17
+ chainComplete = false;
18
+ break;
19
+ }
20
+ }
21
+ return {
22
+ chainComplete,
23
+ schemaVersion: projection.schemaVersion,
24
+ schemaFingerprint: schemaFingerprint(graph),
25
+ entities: projection.entities.map((entity) => ({
26
+ id: entity.id,
27
+ identityFieldId: entity.identityFieldId,
28
+ fieldCount: entity.fields.length,
29
+ requiredFieldCount: entity.fields.filter((field) => field.required).length,
30
+ })),
31
+ persistedStates: projection.states.map((state) => ({
32
+ id: state.id,
33
+ derived: state.derived,
34
+ authority: state.authority,
35
+ })),
36
+ relationships: projection.relationships.map((relationship) => ({
37
+ id: relationship.id,
38
+ from: relationship.from,
39
+ to: relationship.to,
40
+ cardinality: relationship.cardinality,
41
+ })),
42
+ readPolicies: projection.readPolicies.map((policy) => ({ id: policy.id, entityId: policy.entityId })),
43
+ migrations: [...migrations]
44
+ .sort((a, b) => a.fromSchema - b.fromSchema)
45
+ .map((migration) => ({
46
+ id: String(migration.id),
47
+ fromSchema: migration.fromSchema,
48
+ toSchema: migration.toSchema,
49
+ operationCount: migration.operations.length,
50
+ destructiveOperationCount: destructiveOps(migration),
51
+ })),
52
+ };
53
+ }
54
+ /**
55
+ * The terse semantic diff render of spec11 §58 — `+` added, `-` removed, `~` changed,
56
+ * never a JSON text diff.
57
+ */
58
+ export function explainSchemaDiff(diff) {
59
+ if (diff.entries.length === 0) {
60
+ return `schema ${diff.fromVersion} → ${diff.toVersion}: no persistence-relevant change`;
61
+ }
62
+ const lines = [`schema ${diff.fromVersion} → ${diff.toVersion} (verdict: ${diff.verdict})`];
63
+ for (const entry of diff.entries) {
64
+ const subject = entry.fieldId
65
+ ? `${entry.entityId ?? '?'}.${entry.fieldId}`
66
+ : entry.entityId ?? entry.stateId ?? entry.relationshipId ?? entry.readPolicyId ?? '?';
67
+ lines.push(` ${entry.mark} ${subject} [${entry.class}] ${entry.message}`);
68
+ }
69
+ return lines.join('\n');
70
+ }
71
+ const UI_KINDS = new Set([
72
+ 'view',
73
+ 'container',
74
+ 'text',
75
+ 'repeat',
76
+ 'field-display',
77
+ 'form',
78
+ 'input',
79
+ 'button',
80
+ 'conditional',
81
+ ]);
82
+ /**
83
+ * Impact analysis for a proposed upgrade (spec11 §57). Diffs `previous` against `next` and
84
+ * reports which application semantics reference a field or entity the migration changes.
85
+ */
86
+ export function migrationImpact(previous, next) {
87
+ const diff = diffSchema(previous, next);
88
+ const changedFields = new Set(diff.entries.map((entry) => entry.fieldId).filter(Boolean));
89
+ const changedEntities = new Set(diff.entries.map((entry) => entry.entityId).filter(Boolean));
90
+ const migrations = next.getNodesByKind('migration');
91
+ const operations = migrations.flatMap((migration) => migration.operations);
92
+ const coverage = migrationCoversDiff(diff, operations);
93
+ const touchesChange = (nodeId) => {
94
+ for (const edge of next.getEdges(nodeId, { kinds: ['reads', 'writes', 'references'] })) {
95
+ const fieldIds = edge.metadata?.fieldIds ?? [];
96
+ if (fieldIds.some((id) => changedFields.has(id)))
97
+ return true;
98
+ if (changedEntities.has(String(edge.to)))
99
+ return true;
100
+ }
101
+ return false;
102
+ };
103
+ const affectedQueries = [];
104
+ const affectedActions = [];
105
+ const affectedReadPolicies = [];
106
+ const affectedConstraints = [];
107
+ const affectedUiNodes = [];
108
+ for (const node of next.listNodes()) {
109
+ const id = String(node.id);
110
+ if (node.kind === 'query') {
111
+ if (changedEntities.has(String(node.source)) || touchesChange(id)) {
112
+ affectedQueries.push(id);
113
+ }
114
+ }
115
+ else if (node.kind === 'action') {
116
+ if (touchesChange(id))
117
+ affectedActions.push(id);
118
+ }
119
+ else if (node.kind === 'read-policy') {
120
+ if (changedEntities.has(String(node.entityId)) || touchesChange(id)) {
121
+ affectedReadPolicies.push(id);
122
+ }
123
+ }
124
+ else if (node.kind === 'constraint' || node.kind === 'transition-constraint') {
125
+ if (changedEntities.has(String(node.entityId)) || touchesChange(id)) {
126
+ affectedConstraints.push(id);
127
+ }
128
+ }
129
+ else if (UI_KINDS.has(node.kind)) {
130
+ if (touchesChange(id))
131
+ affectedUiNodes.push(id);
132
+ }
133
+ }
134
+ return {
135
+ fromVersion: diff.fromVersion,
136
+ toVersion: diff.toVersion,
137
+ diff,
138
+ verdict: diff.verdict,
139
+ covered: coverage.covered,
140
+ uncovered: coverage.uncovered,
141
+ dataLossPossible: diff.destructive.length > 0,
142
+ affectedEntities: [...changedEntities].sort(),
143
+ affectedFields: [...changedFields].sort(),
144
+ affectedQueries: affectedQueries.sort(),
145
+ affectedActions: affectedActions.sort(),
146
+ affectedReadPolicies: affectedReadPolicies.sort(),
147
+ affectedConstraints: affectedConstraints.sort(),
148
+ affectedUiNodes: affectedUiNodes.sort(),
149
+ authorizationChanges: diff.entries
150
+ .filter((entry) => entry.authorizationChange)
151
+ .map((entry) => entry.readPolicyId ?? '?'),
152
+ };
153
+ }
154
+ export { DEFAULT_SCHEMA_VERSION };
package/dist/queries.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ActionDef, AnyNode, ApplicationGraph, ConstraintDef, EdgeKind, EventDef, Expression, ExpressionDef, FieldId, FormNode, GraphEdge, IntegrationDef, IntegrationOperationDef, Location, NodeId, StateDef, StorageDef, SubscriptionDef, TransitionConstraintDef, TriggerDef, UINode, ViewNode } from '@cynodia/axiom-core';
1
+ import type { ActionDef, AnyNode, ApplicationGraph, ConstraintDef, EdgeKind, EntityDef, EventDef, Expression, ExpressionDef, FieldId, FormNode, GraphEdge, IntegrationDef, IntegrationOperationDef, Location, NodeId, QueryAggregate, QueryDef, QueryParameter, ReadPolicyDef, RelationshipDef, StateDef, StorageDef, SubscriptionDef, TransitionConstraintDef, TriggerDef, UINode, ViewNode } from '@cynodia/axiom-core';
2
2
  export interface SubgraphRequest {
3
3
  root: NodeId;
4
4
  depth?: number;
@@ -19,6 +19,11 @@ export interface MutationImpact {
19
19
  /** Rules that govern how this state may change, whatever writes it. */
20
20
  affectedTransitionConstraints: TransitionConstraintDef[];
21
21
  affectedViews: ViewNode[];
22
+ /**
23
+ * Registered queries a write to this location may invalidate — conservatively, every
24
+ * query that reads an entity stored in the affected state (spec 0.10 §72-74).
25
+ */
26
+ affectedQueries: QueryDef[];
22
27
  /**
23
28
  * False when something in the graph cannot be analyzed — a native operation that does
24
29
  * not declare its effects, for instance. An incomplete answer says so rather than
@@ -28,6 +33,47 @@ export interface MutationImpact {
28
33
  /** Why the analysis is incomplete, when it is. */
29
34
  analysisGaps: string[];
30
35
  }
36
+ /** A structured, agent-readable account of what a query does (spec 0.10 §86). */
37
+ export interface QueryExplanation {
38
+ queryId: NodeId;
39
+ /** The authoritative source entity. */
40
+ source: NodeId;
41
+ parameters: QueryParameter[];
42
+ /** The requested predicate, in prose-free structural form. Absent means "every row". */
43
+ filter?: Expression;
44
+ /** The read policy predicate AND-ed into the effective filter, if one governs the source. */
45
+ readPolicyPredicate?: Expression;
46
+ /** Sort keys, most significant first, plus the appended canonical-identity tie-breaker. */
47
+ sort: Array<{
48
+ key: Expression;
49
+ direction: 'asc' | 'desc';
50
+ nulls: 'first' | 'last';
51
+ }>;
52
+ identityTieBreaker?: FieldId;
53
+ /** Relationship traversals: relationship id, cardinality, bound alias. */
54
+ relationships: Array<{
55
+ relationshipId: NodeId;
56
+ cardinality: 'to-one' | 'to-many';
57
+ bindAs: NodeId;
58
+ }>;
59
+ /** Projected result fields, or `undefined` for a query that returns whole source rows. */
60
+ projection?: {
61
+ entityId: NodeId;
62
+ fields: FieldId[];
63
+ };
64
+ aggregates: QueryAggregate[];
65
+ groupBy: Expression[];
66
+ pagination: {
67
+ strategy: 'cursor' | 'offset';
68
+ maxPageSize: number;
69
+ };
70
+ /** Entities the query reads, transitively through its relationships. */
71
+ entities: NodeId[];
72
+ /** Fields the query reads across all its clauses. */
73
+ fields: FieldId[];
74
+ /** Actions that may invalidate this query when they commit. */
75
+ invalidatingActions: NodeId[];
76
+ }
31
77
  export declare class GraphQueries {
32
78
  protected graph: ApplicationGraph;
33
79
  constructor(graph: ApplicationGraph);
@@ -95,6 +141,47 @@ export declare class GraphQueries {
95
141
  * graph relationships, so an agent never has to read application source to find out.
96
142
  */
97
143
  getMutationImpact(location: Location): MutationImpact;
144
+ /** Every registered query the application declares (spec 0.10 §85). */
145
+ listQueries(): QueryDef[];
146
+ getQuery(id: NodeId): QueryDef | undefined;
147
+ /** Every explicit entity-to-entity relationship. */
148
+ listRelationships(): RelationshipDef[];
149
+ getRelationship(id: NodeId): RelationshipDef | undefined;
150
+ /** Relationships that start from or reach an entity. */
151
+ getRelationshipsForEntity(entityId: NodeId): RelationshipDef[];
152
+ /** Every row-level read policy. */
153
+ listReadPolicies(): ReadPolicyDef[];
154
+ /** The read policy governing an entity's rows, if one is declared. */
155
+ getReadPolicyForEntity(entityId: NodeId): ReadPolicyDef | undefined;
156
+ /** The read policy a query's rows are filtered by — named on the query, or over its source. */
157
+ getReadPolicyForQuery(id: NodeId): ReadPolicyDef | undefined;
158
+ getQueryParameters(id: NodeId): QueryParameter[];
159
+ /** Whether this query reduces rows to aggregate scalars rather than returning them. */
160
+ isAggregateQuery(id: NodeId): boolean;
161
+ /** The entity a non-aggregate result row conforms to (the projection entity, else the source). */
162
+ getQueryResultEntity(id: NodeId): NodeId | undefined;
163
+ /** Every entity a query reads, including relationship targets it traverses. */
164
+ getQueryEntities(id: NodeId): EntityDef[];
165
+ private queryReadEntities;
166
+ /** Relationships a query traverses. */
167
+ getQueryRelationships(id: NodeId): RelationshipDef[];
168
+ /** Every field a query reads across its filter, sort, projection, group and aggregate clauses. */
169
+ getQueryFields(id: NodeId): FieldId[];
170
+ /**
171
+ * Actions that may invalidate a query's results when they commit — conservatively, every
172
+ * action that writes a state holding an entity the query reads, or that mutates a
173
+ * provider-backed row of one (spec 0.10 §73-74). Over-inclusion is acceptable; a
174
+ * known-stale result is not (spec §72).
175
+ */
176
+ getActionsInvalidatingQuery(id: NodeId): ActionDef[];
177
+ /** The inverse: every query an action's commit may invalidate. */
178
+ getQueriesInvalidatedByAction(actionId: NodeId): QueryDef[];
179
+ /**
180
+ * A structured explanation of a query — source, effective filter (with the read-policy
181
+ * conjunct called out), ordering with its identity tie-breaker, projection, pagination,
182
+ * the entities and fields it reads, and the actions that can invalidate it (spec §86).
183
+ */
184
+ explainQuery(id: NodeId): QueryExplanation | undefined;
98
185
  private constraintTouches;
99
186
  /** Derived states that depend on a state, directly or through other derived states. */
100
187
  private derivedStatesFrom;
package/dist/queries.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { allowedInvocationSources, isClientInvocable as actionIsClientInvocable, isSystemOnlyAction, isUINode, locationFieldIds, locationRootStateId, locationSelectorFieldIds, referencedIds, } from '@cynodia/axiom-core';
2
+ import { expressionFieldIds, queryExpressions, queryIsAggregate, queryRowEntityId, sortKeyDirection, } from '@cynodia/axiom-core';
2
3
  const READ_KINDS = ['reads', 'binds', 'derives-from'];
3
4
  const WRITE_KINDS = ['writes'];
4
5
  const CONTAINMENT_KINDS = ['contains', 'renders'];
@@ -265,6 +266,7 @@ export class GraphQueries {
265
266
  .getNodesByKind('transition-constraint')
266
267
  .filter((constraint) => entityIds.has(constraint.entityId));
267
268
  const gaps = this.analysisGaps();
269
+ const affectedQueries = this.listQueries().filter((query) => this.queryReadEntities(query).some((entityId) => entityIds.has(entityId)));
268
270
  return {
269
271
  location,
270
272
  rootStateId,
@@ -274,10 +276,190 @@ export class GraphQueries {
274
276
  affectedConstraints,
275
277
  affectedTransitionConstraints,
276
278
  affectedViews: [...views.values()],
279
+ affectedQueries,
277
280
  analysisComplete: gaps.length === 0,
278
281
  analysisGaps: gaps,
279
282
  };
280
283
  }
284
+ // ------------------------------------------------- queries, relationships, read policies
285
+ /** Every registered query the application declares (spec 0.10 §85). */
286
+ listQueries() {
287
+ return this.graph.getNodesByKind('query');
288
+ }
289
+ getQuery(id) {
290
+ const node = this.graph.getNode(id);
291
+ return node?.kind === 'query' ? node : undefined;
292
+ }
293
+ /** Every explicit entity-to-entity relationship. */
294
+ listRelationships() {
295
+ return this.graph.getNodesByKind('relationship');
296
+ }
297
+ getRelationship(id) {
298
+ const node = this.graph.getNode(id);
299
+ return node?.kind === 'relationship' ? node : undefined;
300
+ }
301
+ /** Relationships that start from or reach an entity. */
302
+ getRelationshipsForEntity(entityId) {
303
+ return this.listRelationships().filter((relationship) => relationship.from.entityId === entityId || relationship.to.entityId === entityId);
304
+ }
305
+ /** Every row-level read policy. */
306
+ listReadPolicies() {
307
+ return this.graph.getNodesByKind('read-policy');
308
+ }
309
+ /** The read policy governing an entity's rows, if one is declared. */
310
+ getReadPolicyForEntity(entityId) {
311
+ return this.listReadPolicies().find((policy) => policy.entityId === entityId);
312
+ }
313
+ /** The read policy a query's rows are filtered by — named on the query, or over its source. */
314
+ getReadPolicyForQuery(id) {
315
+ const query = this.getQuery(id);
316
+ if (!query) {
317
+ return undefined;
318
+ }
319
+ if (query.readPolicyId) {
320
+ const policy = this.graph.getNode(query.readPolicyId);
321
+ return policy?.kind === 'read-policy' ? policy : undefined;
322
+ }
323
+ return this.getReadPolicyForEntity(query.source);
324
+ }
325
+ getQueryParameters(id) {
326
+ return this.getQuery(id)?.parameters ?? [];
327
+ }
328
+ /** Whether this query reduces rows to aggregate scalars rather than returning them. */
329
+ isAggregateQuery(id) {
330
+ const query = this.getQuery(id);
331
+ return query ? queryIsAggregate(query) : false;
332
+ }
333
+ /** The entity a non-aggregate result row conforms to (the projection entity, else the source). */
334
+ getQueryResultEntity(id) {
335
+ const query = this.getQuery(id);
336
+ return query ? queryRowEntityId(query) : undefined;
337
+ }
338
+ /** Every entity a query reads, including relationship targets it traverses. */
339
+ getQueryEntities(id) {
340
+ const query = this.getQuery(id);
341
+ if (!query) {
342
+ return [];
343
+ }
344
+ return this.queryReadEntities(query)
345
+ .map((entityId) => this.graph.getNode(entityId))
346
+ .filter((node) => node?.kind === 'entity');
347
+ }
348
+ queryReadEntities(query) {
349
+ const entities = new Set([query.source]);
350
+ for (const use of query.relationships ?? []) {
351
+ const relationship = this.getRelationship(use.relationshipId);
352
+ if (relationship) {
353
+ entities.add(relationship.to.entityId);
354
+ }
355
+ }
356
+ return [...entities];
357
+ }
358
+ /** Relationships a query traverses. */
359
+ getQueryRelationships(id) {
360
+ const query = this.getQuery(id);
361
+ if (!query) {
362
+ return [];
363
+ }
364
+ return (query.relationships ?? [])
365
+ .map((use) => this.getRelationship(use.relationshipId))
366
+ .filter((relationship) => Boolean(relationship));
367
+ }
368
+ /** Every field a query reads across its filter, sort, projection, group and aggregate clauses. */
369
+ getQueryFields(id) {
370
+ const query = this.getQuery(id);
371
+ if (!query) {
372
+ return [];
373
+ }
374
+ const fields = new Set();
375
+ for (const expression of queryExpressions(query)) {
376
+ for (const fieldId of expressionFieldIds(expression)) {
377
+ fields.add(fieldId);
378
+ }
379
+ }
380
+ return [...fields];
381
+ }
382
+ /**
383
+ * Actions that may invalidate a query's results when they commit — conservatively, every
384
+ * action that writes a state holding an entity the query reads, or that mutates a
385
+ * provider-backed row of one (spec 0.10 §73-74). Over-inclusion is acceptable; a
386
+ * known-stale result is not (spec §72).
387
+ */
388
+ getActionsInvalidatingQuery(id) {
389
+ const query = this.getQuery(id);
390
+ if (!query) {
391
+ return [];
392
+ }
393
+ const readEntities = new Set(this.queryReadEntities(query));
394
+ return this.graph.getNodesByKind('action').filter((action) => {
395
+ for (const edge of this.graph.getOutgoingEdges(action.id, { kinds: WRITE_KINDS })) {
396
+ if (readEntities.has(edge.to)) {
397
+ return true; // a provider-record write links the action straight to the entity
398
+ }
399
+ const target = this.graph.getNode(edge.to);
400
+ if (target?.kind !== 'state') {
401
+ continue;
402
+ }
403
+ const holds = this.graph
404
+ .getOutgoingEdges(target.id, { kinds: ['references'] })
405
+ .some((reference) => readEntities.has(reference.to));
406
+ if (holds) {
407
+ return true;
408
+ }
409
+ }
410
+ return false;
411
+ });
412
+ }
413
+ /** The inverse: every query an action's commit may invalidate. */
414
+ getQueriesInvalidatedByAction(actionId) {
415
+ return this.listQueries().filter((query) => this.getActionsInvalidatingQuery(query.id).some((action) => action.id === actionId));
416
+ }
417
+ /**
418
+ * A structured explanation of a query — source, effective filter (with the read-policy
419
+ * conjunct called out), ordering with its identity tie-breaker, projection, pagination,
420
+ * the entities and fields it reads, and the actions that can invalidate it (spec §86).
421
+ */
422
+ explainQuery(id) {
423
+ const query = this.getQuery(id);
424
+ if (!query) {
425
+ return undefined;
426
+ }
427
+ const source = this.graph.getNode(query.source);
428
+ const identityTieBreaker = source?.kind === 'entity' ? source.identityFieldId : undefined;
429
+ return {
430
+ queryId: query.id,
431
+ source: query.source,
432
+ parameters: query.parameters ?? [],
433
+ ...(query.filter ? { filter: query.filter } : {}),
434
+ ...(this.getReadPolicyForQuery(id)
435
+ ? { readPolicyPredicate: this.getReadPolicyForQuery(id).predicate }
436
+ : {}),
437
+ sort: (query.sort ?? []).map((key) => ({
438
+ key: key.key,
439
+ direction: sortKeyDirection(key),
440
+ nulls: key.nulls ?? (sortKeyDirection(key) === 'asc' ? 'last' : 'first'),
441
+ })),
442
+ ...(identityTieBreaker ? { identityTieBreaker } : {}),
443
+ relationships: (query.relationships ?? []).flatMap((use) => {
444
+ const relationship = this.getRelationship(use.relationshipId);
445
+ return relationship
446
+ ? [{ relationshipId: use.relationshipId, cardinality: relationship.cardinality, bindAs: use.bindAs }]
447
+ : [];
448
+ }),
449
+ ...(query.projection
450
+ ? { projection: { entityId: query.projection.entityId, fields: query.projection.fields.map((field) => field.id) } }
451
+ : {}),
452
+ aggregates: query.aggregate ?? [],
453
+ groupBy: query.groupBy ?? [],
454
+ pagination: {
455
+ strategy: query.pagination?.strategy ?? 'cursor',
456
+ maxPageSize: query.pagination?.maxPageSize ?? 100,
457
+ },
458
+ entities: this.queryReadEntities(query),
459
+ fields: this.getQueryFields(id),
460
+ invalidatingActions: this.getActionsInvalidatingQuery(id).map((action) => action.id),
461
+ };
462
+ }
281
463
  constraintTouches(constraint, fieldIds) {
282
464
  const declared = this.graph
283
465
  .getOutgoingEdges(constraint.id, { kinds: ['constrains'] })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-agent-api",
3
- "version": "0.9.0-alpha.2",
3
+ "version": "0.11.0-alpha.1",
4
4
  "description": "Semantic queries and transactional graph transformations for AI agents.",
5
5
  "license": "MIT",
6
6
  "author": "AskTech AS",
@@ -31,7 +31,7 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "@cynodia/axiom-core": "0.9.0-alpha.2"
34
+ "@cynodia/axiom-core": "0.11.0-alpha.1"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc -b tsconfig.json tsconfig.test.json",