@cynodia/axiom-agent-api 0.16.0-alpha.1 → 0.16.0-alpha.3

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.
@@ -1,3 +1,4 @@
1
+ import { actionOperations } from '@cynodia/axiom-core';
1
2
  import { analyzeLiveQuery } from './live-query.js';
2
3
  /**
3
4
  * Required-runtime-capability analysis (spec16 §30-31, §50-51). A graph declares no host or
@@ -65,7 +66,7 @@ export function analyzeCapabilities(graph) {
65
66
  }
66
67
  }
67
68
  for (const action of actions) {
68
- const effectOps = (action.operations ?? []).filter((op) => op.kind === 'integration-effect' || op.kind === 'blob-commit' || op.kind === 'blob-delete');
69
+ const effectOps = actionOperations(action).filter((op) => op.kind === 'integration-effect' || op.kind === 'blob-commit' || op.kind === 'blob-delete');
69
70
  if (effectOps.length > 0) {
70
71
  require('effect-execution', `ActionDef ${action.id} creates a logical effect (${effectOps.map((op) => op.kind).join(', ')})`);
71
72
  }
@@ -1,7 +1,7 @@
1
- import { authorityCompatibilityKey, schemaFingerprint, semanticFingerprint, } from '@cynodia/axiom-core';
1
+ import { actionOperations, authorityCompatibilityKey, schemaFingerprint, semanticFingerprint, } from '@cynodia/axiom-core';
2
2
  const EFFECT_OP_KINDS = new Set(['integration-effect', 'blob-commit', 'blob-delete']);
3
3
  function actionsWithEffects(graph) {
4
- return graph.getNodesByKind('action').filter((action) => (action.operations ?? []).some((op) => EFFECT_OP_KINDS.has(op.kind)));
4
+ return graph.getNodesByKind('action').filter((action) => actionOperations(action).some((op) => EFFECT_OP_KINDS.has(op.kind)));
5
5
  }
6
6
  function scheduledTriggers(graph) {
7
7
  return graph.getNodesByKind('trigger').filter((trigger) => trigger.when.kind === 'interval' || trigger.when.kind === 'delay');
package/dist/explain.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { actionOperations, } from '@cynodia/axiom-core';
1
2
  import { GraphQueries } from './queries.js';
2
3
  import { analyzeAuthorization } from './authorization.js';
3
4
  import { analyzeWorkflow } from './workflow.js';
@@ -36,12 +37,12 @@ export function explainAction(graph, actionId) {
36
37
  runsQueries.push(target.id);
37
38
  }
38
39
  }
39
- for (const operation of action.operations ?? []) {
40
+ for (const operation of actionOperations(action)) {
40
41
  if (operation.kind === 'blob-metadata' || operation.kind === 'blob-commit' || operation.kind === 'blob-delete') {
41
42
  storagesTouched.add(operation.storageId);
42
43
  }
43
44
  }
44
- const nativeOperations = (action.operations ?? [])
45
+ const nativeOperations = actionOperations(action)
45
46
  .filter((operation) => operation.kind === 'native')
46
47
  .map((operation) => ({ implementationId: operation.implementationId, declaredEffects: operation.declaredEffects ?? [] }));
47
48
  const analysisGaps = nativeOperations
@@ -92,7 +93,7 @@ export function explainAction(graph, actionId) {
92
93
  invokedBy: { triggers: triggers.sort(), workflowSteps: workflowSteps.sort() },
93
94
  clientInvocable: queries.isClientInvocable(actionId),
94
95
  systemOnly: queries.isSystemOnly(actionId),
95
- destructive: action.destructive === true || (action.operations ?? []).some((op) => op.kind === 'remove'),
96
+ destructive: action.destructive === true || actionOperations(action).some((op) => op.kind === 'remove'),
96
97
  analysisComplete: analysisGaps.length === 0,
97
98
  analysisGaps,
98
99
  };
@@ -169,7 +170,7 @@ export function explainGraph(graph) {
169
170
  const queryOps = authz.operations.filter((op) => op.nodeKind === 'query');
170
171
  let opaqueBoundaries = 0;
171
172
  for (const action of graph.getNodesByKind('action')) {
172
- opaqueBoundaries += (action.operations ?? []).filter((op) => op.kind === 'native').length;
173
+ opaqueBoundaries += actionOperations(action).filter((op) => op.kind === 'native').length;
173
174
  }
174
175
  return {
175
176
  nodeCountsByKind,
@@ -1,8 +1,9 @@
1
+ import { actionOperations, operationChildren } from '@cynodia/axiom-core';
1
2
  /** Every `NativeOperation` in the graph, with the action that contains it (spec16 §46). */
2
3
  export function listNativeOperations(graph) {
3
4
  const found = [];
4
5
  for (const action of graph.getNodesByKind('action')) {
5
- for (const operation of collectNative(action.operations ?? [])) {
6
+ for (const operation of collectNative(actionOperations(action))) {
6
7
  found.push({
7
8
  actionId: action.id,
8
9
  implementationId: operation.implementationId,
@@ -22,7 +23,9 @@ function collectNative(operations) {
22
23
  found.push(operation);
23
24
  }
24
25
  else if (operation.kind === 'for-each') {
25
- found.push(...collectNative(operation.operations ?? []));
26
+ // A valid for-each may only contain set/insert/remove (validateGraph enforces this),
27
+ // so this recursion only matters for an already-invalid graph — still total, never a crash.
28
+ found.push(...collectNative(operationChildren(operation)));
26
29
  }
27
30
  }
28
31
  return found;
package/dist/queries.js CHANGED
@@ -1,4 +1,4 @@
1
- import { allowedInvocationSources, isClientInvocable as actionIsClientInvocable, isSystemOnlyAction, isUINode, locationFieldIds, locationRootStateId, locationSelectorFieldIds, referencedIds, } from '@cynodia/axiom-core';
1
+ import { actionOperations, allowedInvocationSources, isClientInvocable as actionIsClientInvocable, isSystemOnlyAction, isUINode, locationFieldIds, locationRootStateId, locationSelectorFieldIds, referencedIds, } from '@cynodia/axiom-core';
2
2
  import { expressionFieldIds, queryExpressions, queryIsAggregate, queryRowEntityId, sortKeyDirection, } from '@cynodia/axiom-core';
3
3
  const READ_KINDS = ['reads', 'binds', 'derives-from'];
4
4
  const WRITE_KINDS = ['writes'];
@@ -64,7 +64,7 @@ export class GraphQueries {
64
64
  analysisGaps() {
65
65
  const gaps = [];
66
66
  for (const action of this.graph.getNodesByKind('action')) {
67
- for (const operation of action.operations ?? []) {
67
+ for (const operation of actionOperations(action)) {
68
68
  if (operation.kind === 'native' && (operation.declaredEffects ?? []).length === 0) {
69
69
  gaps.push(`${action.name ?? action.id} runs the native operation "${operation.implementationId}" without declaring its effects`);
70
70
  }
@@ -82,7 +82,7 @@ export class GraphQueries {
82
82
  if (touchesState) {
83
83
  return true;
84
84
  }
85
- return (action.operations ?? []).some((operation) => 'value' in operation && operation.value.kind === 'object'
85
+ return actionOperations(action).some((operation) => 'value' in operation && operation.value.kind === 'object'
86
86
  ? operation.value.entityId === entityId
87
87
  : false);
88
88
  });
@@ -133,7 +133,7 @@ export class GraphQueries {
133
133
  return this.graph
134
134
  .getNodesByKind('action')
135
135
  .filter((action) => action.destructive === true ||
136
- (action.operations ?? []).some((operation) => operation.kind === 'remove'));
136
+ actionOperations(action).some((operation) => operation.kind === 'remove'));
137
137
  }
138
138
  /**
139
139
  * The neighbourhood of a node, optionally restricted to particular relationships.
@@ -657,7 +657,7 @@ export class GraphQueries {
657
657
  getActionsUsingStorage(storageId) {
658
658
  return this.graph
659
659
  .getNodesByKind('action')
660
- .filter((action) => (action.operations ?? []).some((operation) => (operation.kind === 'blob-metadata' ||
660
+ .filter((action) => actionOperations(action).some((operation) => (operation.kind === 'blob-metadata' ||
661
661
  operation.kind === 'blob-commit' ||
662
662
  operation.kind === 'blob-delete') &&
663
663
  operation.storageId === storageId));
@@ -73,6 +73,41 @@ function fixtureGraph() {
73
73
  });
74
74
  return g;
75
75
  }
76
+ /**
77
+ * A self-contained entity + query + (optionally referenced) read policies, for the F3
78
+ * attach/detach/replace fixtures. `declarePolicies` names every `ReadPolicyDef` node to
79
+ * declare (both, for a "replace" scenario), independent of which one `readPolicyId` points
80
+ * at, so the diff reflects only the reference change, not a policy node appearing/vanishing.
81
+ */
82
+ function readPolicyFixtureGraph(readPolicyId, ...declarePolicies) {
83
+ const g = new ApplicationGraph('ct-read-policy-graph', 'Read Policy Fixture');
84
+ const entityId = nodeId('read_policy_entity');
85
+ const identityField = fieldId('read_policy_entity_id');
86
+ g.addNode({
87
+ id: entityId,
88
+ kind: 'entity',
89
+ identityFieldId: identityField,
90
+ fields: [{ id: identityField, valueType: primitiveType('string'), required: true }],
91
+ });
92
+ for (const policyId of new Set([...(readPolicyId ? [readPolicyId] : []), ...declarePolicies])) {
93
+ g.addNode({
94
+ id: nodeId(policyId),
95
+ kind: 'read-policy',
96
+ entityId,
97
+ rowScopeId: nodeId('read_policy_row'),
98
+ predicate: literal(true),
99
+ });
100
+ }
101
+ g.addNode({
102
+ id: nodeId('read_policy_query'),
103
+ kind: 'query',
104
+ source: entityId,
105
+ rowScopeId: nodeId('read_policy_query_row'),
106
+ pagination: { strategy: 'offset', maxPageSize: 50 },
107
+ ...(readPolicyId ? { readPolicyId: nodeId(readPolicyId) } : {}),
108
+ });
109
+ return g;
110
+ }
76
111
  export function toolingConformanceFixtures() {
77
112
  return [
78
113
  {
@@ -145,7 +180,7 @@ export function toolingConformanceFixtures() {
145
180
  {
146
181
  id: 'semantic-diff-authorization-change-is-tagged',
147
182
  category: 'semantic-diff',
148
- description: 'detaching a policy from an action is classified as an authorization change',
183
+ description: 'detaching a policy from an action is classified as an authorization change, additive with its own kind category (spec16pt2 §40)',
149
184
  run: () => {
150
185
  const before = fixtureGraph();
151
186
  const after = fixtureGraph();
@@ -155,7 +190,43 @@ export function toolingConformanceFixtures() {
155
190
  const diff = new AgentAPI(after).semanticDiff(before);
156
191
  return diff.entries.find((e) => e.nodeId === String(A_BUMP))?.categories;
157
192
  },
158
- expected: ['authorization'],
193
+ expected: ['semantic', 'authorization'],
194
+ },
195
+ {
196
+ id: 'semantic-diff-read-policy-attach-is-authorization',
197
+ category: 'semantic-diff',
198
+ description: 'spec16pt2 F3/§34-35: attaching a QueryDef.readPolicyId is classified query + authorization',
199
+ run: () => {
200
+ const before = readPolicyFixtureGraph(undefined);
201
+ const after = readPolicyFixtureGraph('read_policy_owner');
202
+ const diff = new AgentAPI(after).semanticDiff(before);
203
+ return diff.entries.find((e) => e.nodeId === 'read_policy_query')?.categories;
204
+ },
205
+ expected: ['query', 'authorization'],
206
+ },
207
+ {
208
+ id: 'semantic-diff-read-policy-detach-is-authorization',
209
+ category: 'semantic-diff',
210
+ description: 'spec16pt2 F3 exact reproduction: detaching a QueryDef.readPolicyId is classified query + authorization',
211
+ run: () => {
212
+ const before = readPolicyFixtureGraph('read_policy_owner');
213
+ const after = readPolicyFixtureGraph(undefined);
214
+ const diff = new AgentAPI(after).semanticDiff(before);
215
+ return diff.entries.find((e) => e.nodeId === 'read_policy_query')?.categories;
216
+ },
217
+ expected: ['query', 'authorization'],
218
+ },
219
+ {
220
+ id: 'semantic-diff-read-policy-replace-is-authorization',
221
+ category: 'semantic-diff',
222
+ description: 'spec16pt2 §36: replacing one read policy reference for another is classified authorization',
223
+ run: () => {
224
+ const before = readPolicyFixtureGraph('read_policy_owner', 'read_policy_other');
225
+ const after = readPolicyFixtureGraph('read_policy_other', 'read_policy_other');
226
+ const diff = new AgentAPI(after).semanticDiff(before);
227
+ return diff.entries.find((e) => e.nodeId === 'read_policy_query')?.categories.includes('authorization');
228
+ },
229
+ expected: true,
159
230
  },
160
231
  {
161
232
  id: 'diagnostics-unresolved-reference-is-structured',
@@ -1,4 +1,4 @@
1
- import { ApplicationGraph, createFieldId, createNodeId, isUINode, randomHex, synchronizeEdges, validateGraph, } from '@cynodia/axiom-core';
1
+ import { ApplicationGraph, actionOperations, createFieldId, createNodeId, isUINode, randomHex, synchronizeEdges, validateGraph, } from '@cynodia/axiom-core';
2
2
  import { PresentationQueries } from './presentation-queries.js';
3
3
  export class TransactionError extends Error {
4
4
  result;
@@ -157,7 +157,10 @@ export class Transaction extends PresentationQueries {
157
157
  const updated = [];
158
158
  for (const action of this.graph.getNodesByKind('action')) {
159
159
  let changed = false;
160
- const operations = (action.operations ?? []).map((operation) => {
160
+ // spec16pt2 §12-20: a hand-tampered or AI-generated ActionDef.operations can be
161
+ // malformed (non-array, or hold a malformed entry). `actionOperations` establishes
162
+ // both before this ever iterates, rather than assuming the declared type holds.
163
+ const operations = actionOperations(action).map((operation) => {
161
164
  if (!('value' in operation) || operation.value.kind !== 'object') {
162
165
  return operation;
163
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cynodia/axiom-agent-api",
3
- "version": "0.16.0-alpha.1",
3
+ "version": "0.16.0-alpha.3",
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.16.0-alpha.1"
34
+ "@cynodia/axiom-core": "0.16.0-alpha.3"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsc -b tsconfig.json tsconfig.test.json",