@cynodia/axiom-agent-api 0.3.1-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/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/api.d.ts +25 -0
- package/dist/api.js +35 -0
- package/dist/changes.d.ts +46 -0
- package/dist/changes.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/queries.d.ts +70 -0
- package/dist/queries.js +292 -0
- package/dist/transaction.d.ts +103 -0
- package/dist/transaction.js +290 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AskTech AS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Axiom Agent API
|
|
2
|
+
|
|
3
|
+
Part of [Axiom](https://github.com/cynodia/axiom), an AI-native semantic web application
|
|
4
|
+
framework.
|
|
5
|
+
|
|
6
|
+
**Status: experimental / alpha.** The API may change between alpha releases.
|
|
7
|
+
|
|
8
|
+
The machine-facing interface: semantic queries over the graph, field-level dependency
|
|
9
|
+
and mutation-impact analysis, and transactional graph transformations.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @cynodia/axiom-agent-api@alpha
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Most applications should install the facade package instead, which re-exports this one:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @cynodia/axiom@alpha
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## License
|
|
24
|
+
|
|
25
|
+
MIT
|
|
26
|
+
|
|
27
|
+
Copyright (c) 2026 AskTech AS.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ApplicationGraph, ValidationResult } from '@cynodia/axiom-core';
|
|
2
|
+
import { GraphQueries } from './queries.js';
|
|
3
|
+
import { Transaction } from './transaction.js';
|
|
4
|
+
import type { ChangeSet } from './changes.js';
|
|
5
|
+
/**
|
|
6
|
+
* The machine-facing interface to an application. Agents query semantics and apply
|
|
7
|
+
* structural transformations; they never edit generated code.
|
|
8
|
+
*/
|
|
9
|
+
export declare class AgentAPI extends GraphQueries {
|
|
10
|
+
private readonly changeLog;
|
|
11
|
+
constructor(graph: ApplicationGraph);
|
|
12
|
+
validate(): ValidationResult;
|
|
13
|
+
beginTransaction(): Transaction;
|
|
14
|
+
/** Runs a set of transformations, committing only if the result validates. */
|
|
15
|
+
transact(apply: (transaction: Transaction) => void, options?: {
|
|
16
|
+
reason?: string;
|
|
17
|
+
actor?: string;
|
|
18
|
+
}): {
|
|
19
|
+
committed: boolean;
|
|
20
|
+
change?: ChangeSet;
|
|
21
|
+
result: ValidationResult;
|
|
22
|
+
};
|
|
23
|
+
history(): ChangeSet[];
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=api.d.ts.map
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { validateGraph } from '@cynodia/axiom-core';
|
|
2
|
+
import { GraphQueries } from './queries.js';
|
|
3
|
+
import { Transaction } from './transaction.js';
|
|
4
|
+
/**
|
|
5
|
+
* The machine-facing interface to an application. Agents query semantics and apply
|
|
6
|
+
* structural transformations; they never edit generated code.
|
|
7
|
+
*/
|
|
8
|
+
export class AgentAPI extends GraphQueries {
|
|
9
|
+
changeLog = [];
|
|
10
|
+
constructor(graph) {
|
|
11
|
+
super(graph);
|
|
12
|
+
}
|
|
13
|
+
validate() {
|
|
14
|
+
return validateGraph(this.graph);
|
|
15
|
+
}
|
|
16
|
+
beginTransaction() {
|
|
17
|
+
return new Transaction(this.graph, (change) => {
|
|
18
|
+
this.changeLog.push(change);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** Runs a set of transformations, committing only if the result validates. */
|
|
22
|
+
transact(apply, options = {}) {
|
|
23
|
+
const transaction = this.beginTransaction();
|
|
24
|
+
apply(transaction);
|
|
25
|
+
const result = transaction.validate();
|
|
26
|
+
if (!result.valid) {
|
|
27
|
+
transaction.rollback();
|
|
28
|
+
return { committed: false, result };
|
|
29
|
+
}
|
|
30
|
+
return { committed: true, change: transaction.commit(options), result };
|
|
31
|
+
}
|
|
32
|
+
history() {
|
|
33
|
+
return this.changeLog.map((change) => ({ ...change, operations: [...change.operations] }));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AnyNode, FieldDef, FieldId, GraphEdge, NodeId } from '@cynodia/axiom-core';
|
|
2
|
+
export type GraphChange = AddNodeChange | RemoveNodeChange | UpdateNodeChange | AddFieldChange | RemoveFieldChange | AddEdgeChange | RemoveEdgeChange;
|
|
3
|
+
export interface AddNodeChange {
|
|
4
|
+
kind: 'add-node';
|
|
5
|
+
nodeId: NodeId;
|
|
6
|
+
node: AnyNode;
|
|
7
|
+
}
|
|
8
|
+
export interface RemoveNodeChange {
|
|
9
|
+
kind: 'remove-node';
|
|
10
|
+
nodeId: NodeId;
|
|
11
|
+
node: AnyNode;
|
|
12
|
+
}
|
|
13
|
+
export interface UpdateNodeChange {
|
|
14
|
+
kind: 'update-node';
|
|
15
|
+
nodeId: NodeId;
|
|
16
|
+
before: AnyNode;
|
|
17
|
+
after: AnyNode;
|
|
18
|
+
}
|
|
19
|
+
export interface AddFieldChange {
|
|
20
|
+
kind: 'add-field';
|
|
21
|
+
entityId: NodeId;
|
|
22
|
+
field: FieldDef;
|
|
23
|
+
}
|
|
24
|
+
export interface RemoveFieldChange {
|
|
25
|
+
kind: 'remove-field';
|
|
26
|
+
entityId: NodeId;
|
|
27
|
+
fieldId: FieldId;
|
|
28
|
+
field: FieldDef;
|
|
29
|
+
}
|
|
30
|
+
export interface AddEdgeChange {
|
|
31
|
+
kind: 'add-edge';
|
|
32
|
+
edge: GraphEdge;
|
|
33
|
+
}
|
|
34
|
+
export interface RemoveEdgeChange {
|
|
35
|
+
kind: 'remove-edge';
|
|
36
|
+
edge: GraphEdge;
|
|
37
|
+
}
|
|
38
|
+
/** A semantic change record: graph operations and intent, never a textual diff. */
|
|
39
|
+
export interface ChangeSet {
|
|
40
|
+
id: string;
|
|
41
|
+
timestamp: number;
|
|
42
|
+
operations: GraphChange[];
|
|
43
|
+
reason?: string;
|
|
44
|
+
actor?: string;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=changes.d.ts.map
|
package/dist/changes.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ActionDef, AnyNode, ApplicationGraph, ConstraintDef, EdgeKind, Expression, FieldId, FormNode, GraphEdge, Location, NodeId, StateDef, UINode, ViewNode } from '@cynodia/axiom-core';
|
|
2
|
+
export interface SubgraphRequest {
|
|
3
|
+
root: NodeId;
|
|
4
|
+
depth?: number;
|
|
5
|
+
edgeKinds?: readonly EdgeKind[];
|
|
6
|
+
}
|
|
7
|
+
export interface Subgraph {
|
|
8
|
+
nodes: AnyNode[];
|
|
9
|
+
edges: GraphEdge[];
|
|
10
|
+
}
|
|
11
|
+
/** What a change to a location can reach. */
|
|
12
|
+
export interface MutationImpact {
|
|
13
|
+
location: Location;
|
|
14
|
+
rootStateId: NodeId;
|
|
15
|
+
fieldIds: FieldId[];
|
|
16
|
+
directWriters: AnyNode[];
|
|
17
|
+
dependentDerivedStates: StateDef[];
|
|
18
|
+
affectedConstraints: ConstraintDef[];
|
|
19
|
+
affectedViews: ViewNode[];
|
|
20
|
+
}
|
|
21
|
+
export declare class GraphQueries {
|
|
22
|
+
protected graph: ApplicationGraph;
|
|
23
|
+
constructor(graph: ApplicationGraph);
|
|
24
|
+
getNode(id: NodeId): AnyNode | undefined;
|
|
25
|
+
getField(id: FieldId): import("@cynodia/axiom-core").FieldIndexEntry | undefined;
|
|
26
|
+
getEdges(id: NodeId, kinds?: readonly EdgeKind[]): GraphEdge[];
|
|
27
|
+
getDependencies(id: NodeId, kinds?: readonly EdgeKind[]): AnyNode[];
|
|
28
|
+
getDependents(id: NodeId, kinds?: readonly EdgeKind[]): AnyNode[];
|
|
29
|
+
/** Nodes that read a state: views, derived state and action conditions. */
|
|
30
|
+
getReaders(stateId: NodeId): AnyNode[];
|
|
31
|
+
/** Actions that mutate a state. */
|
|
32
|
+
getWriters(stateId: NodeId): ActionDef[];
|
|
33
|
+
/** States whose value type mentions the entity. */
|
|
34
|
+
getStatesForEntity(entityId: NodeId): StateDef[];
|
|
35
|
+
getConstraintsForEntity(entityId: NodeId): ConstraintDef[];
|
|
36
|
+
/** Actions that write a state holding the entity, or that construct instances of it. */
|
|
37
|
+
getActionsForEntity(entityId: NodeId): ActionDef[];
|
|
38
|
+
/** Every UI node that binds or displays one of the entity's fields. */
|
|
39
|
+
getUiNodesForEntity(entityId: NodeId): UINode[];
|
|
40
|
+
/** Views that render any UI node touching the entity. */
|
|
41
|
+
getViewsForEntity(entityId: NodeId): ViewNode[];
|
|
42
|
+
/** Forms that edit the entity — where a new field usually needs an input. */
|
|
43
|
+
getFormsForEntity(entityId: NodeId): FormNode[];
|
|
44
|
+
/** Actions an agent should treat as destructive, declared or inferred. */
|
|
45
|
+
findDestructiveActions(): ActionDef[];
|
|
46
|
+
/**
|
|
47
|
+
* The neighbourhood of a node, optionally restricted to particular relationships.
|
|
48
|
+
* This is the query an agent uses instead of reading files.
|
|
49
|
+
*/
|
|
50
|
+
getSubgraph(request: SubgraphRequest): Subgraph;
|
|
51
|
+
/** Ids a node's expressions reference, for dependency reporting. */
|
|
52
|
+
referencedBy(expression: Expression): NodeId[];
|
|
53
|
+
/** Nodes that read a specific field, from the field metadata on their read edges. */
|
|
54
|
+
getFieldReaders(fieldId: FieldId): AnyNode[];
|
|
55
|
+
/** Nodes that write a specific field — actions and the inputs bound to it. */
|
|
56
|
+
getFieldWriters(fieldId: FieldId): AnyNode[];
|
|
57
|
+
/**
|
|
58
|
+
* What can change this location, and what observes it. The answer comes entirely from
|
|
59
|
+
* graph relationships, so an agent never has to read application source to find out.
|
|
60
|
+
*/
|
|
61
|
+
getMutationImpact(location: Location): MutationImpact;
|
|
62
|
+
private constraintTouches;
|
|
63
|
+
/** Derived states that depend on a state, directly or through other derived states. */
|
|
64
|
+
private derivedStatesFrom;
|
|
65
|
+
private nodesWithFieldEdge;
|
|
66
|
+
protected ancestors(id: NodeId): UINode[];
|
|
67
|
+
protected enclosingViews(id: NodeId): ViewNode[];
|
|
68
|
+
private resolve;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=queries.d.ts.map
|
package/dist/queries.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { isUINode, locationFieldIds, locationRootStateId, locationSelectorFieldIds, referencedIds, } from '@cynodia/axiom-core';
|
|
2
|
+
const READ_KINDS = ['reads', 'binds', 'derives-from'];
|
|
3
|
+
const WRITE_KINDS = ['writes'];
|
|
4
|
+
const CONTAINMENT_KINDS = ['contains', 'renders'];
|
|
5
|
+
function edgeFieldIds(edge) {
|
|
6
|
+
const fieldIds = edge.metadata?.fieldIds;
|
|
7
|
+
return Array.isArray(fieldIds) ? fieldIds : [];
|
|
8
|
+
}
|
|
9
|
+
export class GraphQueries {
|
|
10
|
+
graph;
|
|
11
|
+
constructor(graph) {
|
|
12
|
+
this.graph = graph;
|
|
13
|
+
}
|
|
14
|
+
getNode(id) {
|
|
15
|
+
return this.graph.getNode(id);
|
|
16
|
+
}
|
|
17
|
+
getField(id) {
|
|
18
|
+
return this.graph.getField(id);
|
|
19
|
+
}
|
|
20
|
+
getEdges(id, kinds) {
|
|
21
|
+
return this.graph.getEdges(id, kinds ? { kinds } : {});
|
|
22
|
+
}
|
|
23
|
+
getDependencies(id, kinds) {
|
|
24
|
+
return this.resolve(this.graph.getOutgoingEdges(id, kinds ? { kinds } : {}).map((edge) => edge.to));
|
|
25
|
+
}
|
|
26
|
+
getDependents(id, kinds) {
|
|
27
|
+
return this.resolve(this.graph.getIncomingEdges(id, kinds ? { kinds } : {}).map((edge) => edge.from));
|
|
28
|
+
}
|
|
29
|
+
/** Nodes that read a state: views, derived state and action conditions. */
|
|
30
|
+
getReaders(stateId) {
|
|
31
|
+
return this.getDependents(stateId, READ_KINDS);
|
|
32
|
+
}
|
|
33
|
+
/** Actions that mutate a state. */
|
|
34
|
+
getWriters(stateId) {
|
|
35
|
+
return this.getDependents(stateId, WRITE_KINDS).filter((node) => node.kind === 'action');
|
|
36
|
+
}
|
|
37
|
+
/** States whose value type mentions the entity. */
|
|
38
|
+
getStatesForEntity(entityId) {
|
|
39
|
+
return this.graph
|
|
40
|
+
.getNodesByKind('state')
|
|
41
|
+
.filter((state) => this.graph
|
|
42
|
+
.getOutgoingEdges(state.id, { kinds: ['references'] })
|
|
43
|
+
.some((edge) => edge.to === entityId));
|
|
44
|
+
}
|
|
45
|
+
getConstraintsForEntity(entityId) {
|
|
46
|
+
return this.graph.getNodesByKind('constraint').filter((constraint) => constraint.entityId === entityId);
|
|
47
|
+
}
|
|
48
|
+
/** Actions that write a state holding the entity, or that construct instances of it. */
|
|
49
|
+
getActionsForEntity(entityId) {
|
|
50
|
+
const stateIds = new Set(this.getStatesForEntity(entityId).map((state) => state.id));
|
|
51
|
+
return this.graph.getNodesByKind('action').filter((action) => {
|
|
52
|
+
const touchesState = this.graph
|
|
53
|
+
.getOutgoingEdges(action.id, { kinds: ['writes'] })
|
|
54
|
+
.some((edge) => stateIds.has(edge.to));
|
|
55
|
+
if (touchesState) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return (action.operations ?? []).some((operation) => 'value' in operation && operation.value.kind === 'object'
|
|
59
|
+
? operation.value.entityId === entityId
|
|
60
|
+
: false);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Every UI node that binds or displays one of the entity's fields. */
|
|
64
|
+
getUiNodesForEntity(entityId) {
|
|
65
|
+
const fieldIds = new Set((this.graph.getNode(entityId)?.fields ?? []).map((field) => field.id));
|
|
66
|
+
return this.graph
|
|
67
|
+
.listNodes()
|
|
68
|
+
.filter((node) => isUINode(node))
|
|
69
|
+
.filter((node) => {
|
|
70
|
+
if (node.kind === 'input') {
|
|
71
|
+
return locationFieldIds(node.binding.location).some((id) => fieldIds.has(id));
|
|
72
|
+
}
|
|
73
|
+
if (node.kind === 'field-display') {
|
|
74
|
+
return fieldIds.has(node.fieldId);
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Views that render any UI node touching the entity. */
|
|
80
|
+
getViewsForEntity(entityId) {
|
|
81
|
+
const views = new Map();
|
|
82
|
+
for (const node of this.getUiNodesForEntity(entityId)) {
|
|
83
|
+
for (const view of this.enclosingViews(node.id)) {
|
|
84
|
+
views.set(view.id, view);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...views.values()];
|
|
88
|
+
}
|
|
89
|
+
/** Forms that edit the entity — where a new field usually needs an input. */
|
|
90
|
+
getFormsForEntity(entityId) {
|
|
91
|
+
const forms = new Map();
|
|
92
|
+
for (const node of this.getUiNodesForEntity(entityId)) {
|
|
93
|
+
if (node.kind !== 'input') {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
for (const ancestor of this.ancestors(node.id)) {
|
|
97
|
+
if (ancestor.kind === 'form') {
|
|
98
|
+
forms.set(ancestor.id, ancestor);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [...forms.values()];
|
|
103
|
+
}
|
|
104
|
+
/** Actions an agent should treat as destructive, declared or inferred. */
|
|
105
|
+
findDestructiveActions() {
|
|
106
|
+
return this.graph
|
|
107
|
+
.getNodesByKind('action')
|
|
108
|
+
.filter((action) => action.destructive === true ||
|
|
109
|
+
(action.operations ?? []).some((operation) => operation.kind === 'remove'));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The neighbourhood of a node, optionally restricted to particular relationships.
|
|
113
|
+
* This is the query an agent uses instead of reading files.
|
|
114
|
+
*/
|
|
115
|
+
getSubgraph(request) {
|
|
116
|
+
const depth = request.depth ?? 1;
|
|
117
|
+
const query = request.edgeKinds ? { kinds: request.edgeKinds } : {};
|
|
118
|
+
const visited = new Set([request.root]);
|
|
119
|
+
const edges = new Map();
|
|
120
|
+
let frontier = [request.root];
|
|
121
|
+
for (let level = 0; level < depth; level += 1) {
|
|
122
|
+
const next = [];
|
|
123
|
+
for (const id of frontier) {
|
|
124
|
+
for (const edge of this.graph.getEdges(id, query)) {
|
|
125
|
+
edges.set(edge.id, edge);
|
|
126
|
+
const neighbour = edge.from === id ? edge.to : edge.from;
|
|
127
|
+
if (!visited.has(neighbour)) {
|
|
128
|
+
visited.add(neighbour);
|
|
129
|
+
next.push(neighbour);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
frontier = next;
|
|
134
|
+
if (frontier.length === 0) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { nodes: this.resolve([...visited]), edges: [...edges.values()] };
|
|
139
|
+
}
|
|
140
|
+
/** Ids a node's expressions reference, for dependency reporting. */
|
|
141
|
+
referencedBy(expression) {
|
|
142
|
+
return referencedIds(expression);
|
|
143
|
+
}
|
|
144
|
+
/** Nodes that read a specific field, from the field metadata on their read edges. */
|
|
145
|
+
getFieldReaders(fieldId) {
|
|
146
|
+
return this.nodesWithFieldEdge(fieldId, READ_KINDS);
|
|
147
|
+
}
|
|
148
|
+
/** Nodes that write a specific field — actions and the inputs bound to it. */
|
|
149
|
+
getFieldWriters(fieldId) {
|
|
150
|
+
return this.nodesWithFieldEdge(fieldId, WRITE_KINDS);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* What can change this location, and what observes it. The answer comes entirely from
|
|
154
|
+
* graph relationships, so an agent never has to read application source to find out.
|
|
155
|
+
*/
|
|
156
|
+
getMutationImpact(location) {
|
|
157
|
+
const rootStateId = locationRootStateId(location);
|
|
158
|
+
const fieldIds = locationFieldIds(location);
|
|
159
|
+
const selectorFieldIds = locationSelectorFieldIds(location);
|
|
160
|
+
const directWriters = this.graph
|
|
161
|
+
.getIncomingEdges(rootStateId, { kinds: WRITE_KINDS })
|
|
162
|
+
.filter((edge) => {
|
|
163
|
+
if (fieldIds.length === 0) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
const edgeFields = edgeFieldIds(edge);
|
|
167
|
+
// An edge with no field metadata replaces the whole value.
|
|
168
|
+
return edgeFields.length === 0 || edgeFields.some((id) => fieldIds.includes(id));
|
|
169
|
+
})
|
|
170
|
+
.map((edge) => this.graph.getNode(edge.from))
|
|
171
|
+
.filter((node) => Boolean(node));
|
|
172
|
+
const dependentDerivedStates = this.derivedStatesFrom(rootStateId);
|
|
173
|
+
const entityIds = new Set([...fieldIds, ...selectorFieldIds]
|
|
174
|
+
.map((id) => this.graph.getField(id)?.entityId)
|
|
175
|
+
.filter((id) => Boolean(id)));
|
|
176
|
+
if (entityIds.size === 0) {
|
|
177
|
+
// Replacing a whole state affects every entity stored in it.
|
|
178
|
+
for (const edge of this.graph.getOutgoingEdges(rootStateId, { kinds: ['references'] })) {
|
|
179
|
+
entityIds.add(edge.to);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const affectedConstraints = this.graph.getNodesByKind('constraint').filter((constraint) => {
|
|
183
|
+
if (constraint.entityId && entityIds.has(constraint.entityId)) {
|
|
184
|
+
return fieldIds.length === 0 || this.constraintTouches(constraint, fieldIds);
|
|
185
|
+
}
|
|
186
|
+
return this.graph
|
|
187
|
+
.getOutgoingEdges(constraint.id, { kinds: READ_KINDS })
|
|
188
|
+
.some((edge) => edge.to === rootStateId);
|
|
189
|
+
});
|
|
190
|
+
const observers = new Set([rootStateId, ...dependentDerivedStates.map((state) => state.id)]);
|
|
191
|
+
const views = new Map();
|
|
192
|
+
for (const node of this.graph.listNodes()) {
|
|
193
|
+
if (!isUINode(node)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const touches = this.graph
|
|
197
|
+
.getOutgoingEdges(node.id, { kinds: [...READ_KINDS, ...WRITE_KINDS] })
|
|
198
|
+
.some((edge) => observers.has(edge.to));
|
|
199
|
+
if (!touches) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
for (const view of this.enclosingViews(node.id)) {
|
|
203
|
+
views.set(view.id, view);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
location,
|
|
208
|
+
rootStateId,
|
|
209
|
+
fieldIds,
|
|
210
|
+
directWriters,
|
|
211
|
+
dependentDerivedStates,
|
|
212
|
+
affectedConstraints,
|
|
213
|
+
affectedViews: [...views.values()],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
constraintTouches(constraint, fieldIds) {
|
|
217
|
+
const declared = this.graph
|
|
218
|
+
.getOutgoingEdges(constraint.id, { kinds: ['constrains'] })
|
|
219
|
+
.flatMap((edge) => edgeFieldIds(edge));
|
|
220
|
+
return declared.length === 0 || declared.some((id) => fieldIds.includes(id));
|
|
221
|
+
}
|
|
222
|
+
/** Derived states that depend on a state, directly or through other derived states. */
|
|
223
|
+
derivedStatesFrom(stateId) {
|
|
224
|
+
const found = new Map();
|
|
225
|
+
let frontier = [stateId];
|
|
226
|
+
const seen = new Set([stateId]);
|
|
227
|
+
while (frontier.length > 0) {
|
|
228
|
+
const next = [];
|
|
229
|
+
for (const current of frontier) {
|
|
230
|
+
for (const edge of this.graph.getIncomingEdges(current, { kinds: ['derives-from'] })) {
|
|
231
|
+
if (seen.has(edge.from)) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
seen.add(edge.from);
|
|
235
|
+
const node = this.graph.getNode(edge.from);
|
|
236
|
+
if (node?.kind === 'state') {
|
|
237
|
+
found.set(node.id, node);
|
|
238
|
+
next.push(node.id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
frontier = next;
|
|
243
|
+
}
|
|
244
|
+
return [...found.values()];
|
|
245
|
+
}
|
|
246
|
+
nodesWithFieldEdge(fieldId, kinds) {
|
|
247
|
+
const found = new Map();
|
|
248
|
+
for (const edge of this.graph.listEdges()) {
|
|
249
|
+
if (!kinds.includes(edge.kind) || !edgeFieldIds(edge).includes(fieldId)) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const node = this.graph.getNode(edge.from);
|
|
253
|
+
if (node) {
|
|
254
|
+
found.set(node.id, node);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return [...found.values()];
|
|
258
|
+
}
|
|
259
|
+
ancestors(id) {
|
|
260
|
+
const found = [];
|
|
261
|
+
const seen = new Set([id]);
|
|
262
|
+
let frontier = [id];
|
|
263
|
+
while (frontier.length > 0) {
|
|
264
|
+
const next = [];
|
|
265
|
+
for (const current of frontier) {
|
|
266
|
+
for (const edge of this.graph.getIncomingEdges(current, { kinds: CONTAINMENT_KINDS })) {
|
|
267
|
+
if (seen.has(edge.from)) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
seen.add(edge.from);
|
|
271
|
+
const parent = this.graph.getNode(edge.from);
|
|
272
|
+
if (parent && isUINode(parent)) {
|
|
273
|
+
found.push(parent);
|
|
274
|
+
next.push(parent.id);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
frontier = next;
|
|
279
|
+
}
|
|
280
|
+
return found;
|
|
281
|
+
}
|
|
282
|
+
enclosingViews(id) {
|
|
283
|
+
const node = this.graph.getNode(id);
|
|
284
|
+
const self = node && node.kind === 'view' ? [node] : [];
|
|
285
|
+
return [...self, ...this.ancestors(id).filter((ancestor) => ancestor.kind === 'view')];
|
|
286
|
+
}
|
|
287
|
+
resolve(ids) {
|
|
288
|
+
return ids
|
|
289
|
+
.map((id) => this.graph.getNode(id))
|
|
290
|
+
.filter((node) => Boolean(node));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ApplicationGraph } from '@cynodia/axiom-core';
|
|
2
|
+
import type { ActionDef, AnyNode, ButtonNode, ConditionalNode, ConstraintDef, ContainerNode, EdgeId, EdgeKind, Expression, FieldDef, FieldDisplayNode, FieldId, FormNode, InputNode, Location, NodeId, RepeatNode, RouteDef, StateDef, TextNode, UINode, ValidationResult, ViewNode } from '@cynodia/axiom-core';
|
|
3
|
+
import { GraphQueries } from './queries.js';
|
|
4
|
+
import type { ChangeSet, GraphChange } from './changes.js';
|
|
5
|
+
export declare class TransactionError extends Error {
|
|
6
|
+
readonly result?: ValidationResult;
|
|
7
|
+
constructor(message: string, result?: ValidationResult);
|
|
8
|
+
}
|
|
9
|
+
type UIInput<T extends UINode> = Omit<T, 'id' | 'kind'> & {
|
|
10
|
+
id?: NodeId;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A staged set of graph transformations. Every change is applied to a private copy, so
|
|
14
|
+
* the graph an agent (or a runtime) can observe is unchanged until `commit()` succeeds.
|
|
15
|
+
*/
|
|
16
|
+
export declare class Transaction extends GraphQueries {
|
|
17
|
+
private readonly target;
|
|
18
|
+
private readonly onCommit;
|
|
19
|
+
private readonly operations;
|
|
20
|
+
private settled;
|
|
21
|
+
constructor(target: ApplicationGraph, onCommit: (change: ChangeSet) => void);
|
|
22
|
+
/** The staged graph. Reading it shows uncommitted changes. */
|
|
23
|
+
get staged(): ApplicationGraph;
|
|
24
|
+
get changes(): GraphChange[];
|
|
25
|
+
addEntity(entity: {
|
|
26
|
+
id?: NodeId;
|
|
27
|
+
name?: string;
|
|
28
|
+
fields?: FieldDef[];
|
|
29
|
+
identityFieldId?: FieldId;
|
|
30
|
+
}): NodeId;
|
|
31
|
+
/** Adds a field to an existing entity without the caller rebuilding the entity. */
|
|
32
|
+
addField(entityId: NodeId, field: Omit<FieldDef, 'id'> & {
|
|
33
|
+
id?: FieldId;
|
|
34
|
+
}): FieldId;
|
|
35
|
+
removeField(fieldId: FieldId): void;
|
|
36
|
+
addState(state: Omit<StateDef, 'id' | 'kind'> & {
|
|
37
|
+
id?: NodeId;
|
|
38
|
+
}): NodeId;
|
|
39
|
+
addAction(action: Omit<ActionDef, 'id' | 'kind'> & {
|
|
40
|
+
id?: NodeId;
|
|
41
|
+
}): NodeId;
|
|
42
|
+
addConstraint(constraint: Omit<ConstraintDef, 'id' | 'kind'> & {
|
|
43
|
+
id?: NodeId;
|
|
44
|
+
}): NodeId;
|
|
45
|
+
addRoute(route: Omit<RouteDef, 'id' | 'kind'> & {
|
|
46
|
+
id?: NodeId;
|
|
47
|
+
}): NodeId;
|
|
48
|
+
addView(view: UIInput<ViewNode>): NodeId;
|
|
49
|
+
addContainer(container: UIInput<ContainerNode>): NodeId;
|
|
50
|
+
addText(text: UIInput<TextNode>): NodeId;
|
|
51
|
+
addRepeat(repeat: UIInput<RepeatNode>): NodeId;
|
|
52
|
+
addForm(form: UIInput<FormNode>): NodeId;
|
|
53
|
+
addConditional(conditional: UIInput<ConditionalNode>): NodeId;
|
|
54
|
+
addInput(input: UIInput<InputNode>): NodeId;
|
|
55
|
+
addButton(button: UIInput<ButtonNode>): NodeId;
|
|
56
|
+
addFieldDisplay(display: UIInput<FieldDisplayNode>): NodeId;
|
|
57
|
+
/**
|
|
58
|
+
* Creates an input for a field and attaches it to a parent, which is the whole of
|
|
59
|
+
* "make this field editable here".
|
|
60
|
+
*/
|
|
61
|
+
bindField(request: {
|
|
62
|
+
parentId: NodeId;
|
|
63
|
+
/** The addressable position the input writes to. */
|
|
64
|
+
location: Location;
|
|
65
|
+
label?: string;
|
|
66
|
+
inputHint?: InputNode['inputHint'];
|
|
67
|
+
id?: NodeId;
|
|
68
|
+
position?: number;
|
|
69
|
+
}): NodeId;
|
|
70
|
+
/** Creates a read-only display for a field and attaches it to a parent. */
|
|
71
|
+
displayField(request: {
|
|
72
|
+
parentId: NodeId;
|
|
73
|
+
source: Expression;
|
|
74
|
+
fieldId: FieldId;
|
|
75
|
+
label?: string;
|
|
76
|
+
id?: NodeId;
|
|
77
|
+
position?: number;
|
|
78
|
+
}): NodeId;
|
|
79
|
+
/**
|
|
80
|
+
* Teaches every action that constructs instances of an entity about a new field, so a
|
|
81
|
+
* field added to the model is also populated by the actions that create records.
|
|
82
|
+
*/
|
|
83
|
+
addFieldToConstructors(entityId: NodeId, fieldId: FieldId, value: Expression): NodeId[];
|
|
84
|
+
appendChild(parentId: NodeId, childId: NodeId, position?: number): void;
|
|
85
|
+
addNode<T extends AnyNode>(node: T): NodeId;
|
|
86
|
+
updateNode(node: AnyNode): void;
|
|
87
|
+
removeNode(id: NodeId): void;
|
|
88
|
+
addEdge(from: NodeId, to: NodeId, kind: EdgeKind): EdgeId;
|
|
89
|
+
removeEdge(id: EdgeId): void;
|
|
90
|
+
/** Re-derives structural edges and checks referential integrity of the staged graph. */
|
|
91
|
+
validate(): ValidationResult;
|
|
92
|
+
commit(options?: {
|
|
93
|
+
reason?: string;
|
|
94
|
+
actor?: string;
|
|
95
|
+
timestamp?: number;
|
|
96
|
+
}): ChangeSet;
|
|
97
|
+
rollback(): void;
|
|
98
|
+
private assertOpen;
|
|
99
|
+
private addUiNode;
|
|
100
|
+
private requireNode;
|
|
101
|
+
}
|
|
102
|
+
export {};
|
|
103
|
+
//# sourceMappingURL=transaction.d.ts.map
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { ApplicationGraph, createFieldId, createNodeId, randomHex, synchronizeEdges, validateGraph, } from '@cynodia/axiom-core';
|
|
2
|
+
import { GraphQueries } from './queries.js';
|
|
3
|
+
export class TransactionError extends Error {
|
|
4
|
+
result;
|
|
5
|
+
constructor(message, result) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'TransactionError';
|
|
8
|
+
this.result = result;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A staged set of graph transformations. Every change is applied to a private copy, so
|
|
13
|
+
* the graph an agent (or a runtime) can observe is unchanged until `commit()` succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export class Transaction extends GraphQueries {
|
|
16
|
+
target;
|
|
17
|
+
onCommit;
|
|
18
|
+
operations = [];
|
|
19
|
+
settled = false;
|
|
20
|
+
constructor(target, onCommit) {
|
|
21
|
+
super(ApplicationGraph.deserialize(target.toJSON()));
|
|
22
|
+
this.target = target;
|
|
23
|
+
this.onCommit = onCommit;
|
|
24
|
+
}
|
|
25
|
+
/** The staged graph. Reading it shows uncommitted changes. */
|
|
26
|
+
get staged() {
|
|
27
|
+
return this.graph;
|
|
28
|
+
}
|
|
29
|
+
get changes() {
|
|
30
|
+
return this.operations.map((operation) => ({ ...operation }));
|
|
31
|
+
}
|
|
32
|
+
// ------------------------------------------------------------ transformations
|
|
33
|
+
addEntity(entity) {
|
|
34
|
+
const id = entity.id ?? createNodeId('entity');
|
|
35
|
+
this.addNode({
|
|
36
|
+
id,
|
|
37
|
+
kind: 'entity',
|
|
38
|
+
name: entity.name,
|
|
39
|
+
fields: entity.fields ?? [],
|
|
40
|
+
identityFieldId: entity.identityFieldId,
|
|
41
|
+
});
|
|
42
|
+
return id;
|
|
43
|
+
}
|
|
44
|
+
/** Adds a field to an existing entity without the caller rebuilding the entity. */
|
|
45
|
+
addField(entityId, field) {
|
|
46
|
+
const entity = this.requireNode(entityId, 'entity');
|
|
47
|
+
const id = field.id ?? createFieldId('field');
|
|
48
|
+
const before = structuredClone(entity);
|
|
49
|
+
entity.fields = [...entity.fields, { ...field, id }];
|
|
50
|
+
this.graph.updateNode(entity);
|
|
51
|
+
this.operations.push({ kind: 'add-field', entityId, field: { ...field, id } });
|
|
52
|
+
this.operations.push({ kind: 'update-node', nodeId: entityId, before, after: structuredClone(entity) });
|
|
53
|
+
return id;
|
|
54
|
+
}
|
|
55
|
+
removeField(fieldId) {
|
|
56
|
+
const location = this.graph.getField(fieldId);
|
|
57
|
+
if (!location) {
|
|
58
|
+
throw new TransactionError(`Field ${fieldId} does not exist`);
|
|
59
|
+
}
|
|
60
|
+
const entity = this.requireNode(location.entityId, 'entity');
|
|
61
|
+
const before = structuredClone(entity);
|
|
62
|
+
entity.fields = entity.fields.filter((field) => field.id !== fieldId);
|
|
63
|
+
if (entity.identityFieldId === fieldId) {
|
|
64
|
+
delete entity.identityFieldId;
|
|
65
|
+
}
|
|
66
|
+
this.graph.updateNode(entity);
|
|
67
|
+
this.operations.push({
|
|
68
|
+
kind: 'remove-field',
|
|
69
|
+
entityId: location.entityId,
|
|
70
|
+
fieldId,
|
|
71
|
+
field: location.field,
|
|
72
|
+
});
|
|
73
|
+
this.operations.push({
|
|
74
|
+
kind: 'update-node',
|
|
75
|
+
nodeId: location.entityId,
|
|
76
|
+
before,
|
|
77
|
+
after: structuredClone(entity),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
addState(state) {
|
|
81
|
+
const id = state.id ?? createNodeId('state');
|
|
82
|
+
this.addNode({ ...state, id, kind: 'state' });
|
|
83
|
+
return id;
|
|
84
|
+
}
|
|
85
|
+
addAction(action) {
|
|
86
|
+
const id = action.id ?? createNodeId('action');
|
|
87
|
+
this.addNode({ ...action, id, kind: 'action' });
|
|
88
|
+
return id;
|
|
89
|
+
}
|
|
90
|
+
addConstraint(constraint) {
|
|
91
|
+
const id = constraint.id ?? createNodeId('constraint');
|
|
92
|
+
this.addNode({ ...constraint, id, kind: 'constraint' });
|
|
93
|
+
return id;
|
|
94
|
+
}
|
|
95
|
+
addRoute(route) {
|
|
96
|
+
const id = route.id ?? createNodeId('route');
|
|
97
|
+
this.addNode({ ...route, id, kind: 'route' });
|
|
98
|
+
return id;
|
|
99
|
+
}
|
|
100
|
+
addView(view) {
|
|
101
|
+
return this.addUiNode({ ...view, kind: 'view' });
|
|
102
|
+
}
|
|
103
|
+
addContainer(container) {
|
|
104
|
+
return this.addUiNode({ ...container, kind: 'container' });
|
|
105
|
+
}
|
|
106
|
+
addText(text) {
|
|
107
|
+
return this.addUiNode({ ...text, kind: 'text' });
|
|
108
|
+
}
|
|
109
|
+
addRepeat(repeat) {
|
|
110
|
+
return this.addUiNode({ ...repeat, kind: 'repeat' });
|
|
111
|
+
}
|
|
112
|
+
addForm(form) {
|
|
113
|
+
return this.addUiNode({ ...form, kind: 'form' });
|
|
114
|
+
}
|
|
115
|
+
addConditional(conditional) {
|
|
116
|
+
return this.addUiNode({ ...conditional, kind: 'conditional' });
|
|
117
|
+
}
|
|
118
|
+
addInput(input) {
|
|
119
|
+
return this.addUiNode({ ...input, kind: 'input' });
|
|
120
|
+
}
|
|
121
|
+
addButton(button) {
|
|
122
|
+
return this.addUiNode({ ...button, kind: 'button' });
|
|
123
|
+
}
|
|
124
|
+
addFieldDisplay(display) {
|
|
125
|
+
return this.addUiNode({ ...display, kind: 'field-display' });
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Creates an input for a field and attaches it to a parent, which is the whole of
|
|
129
|
+
* "make this field editable here".
|
|
130
|
+
*/
|
|
131
|
+
bindField(request) {
|
|
132
|
+
const inputId = this.addInput({
|
|
133
|
+
id: request.id,
|
|
134
|
+
label: request.label,
|
|
135
|
+
inputHint: request.inputHint,
|
|
136
|
+
binding: { location: request.location },
|
|
137
|
+
});
|
|
138
|
+
this.appendChild(request.parentId, inputId, request.position);
|
|
139
|
+
return inputId;
|
|
140
|
+
}
|
|
141
|
+
/** Creates a read-only display for a field and attaches it to a parent. */
|
|
142
|
+
displayField(request) {
|
|
143
|
+
const displayId = this.addFieldDisplay({
|
|
144
|
+
id: request.id,
|
|
145
|
+
source: request.source,
|
|
146
|
+
fieldId: request.fieldId,
|
|
147
|
+
label: request.label,
|
|
148
|
+
});
|
|
149
|
+
this.appendChild(request.parentId, displayId, request.position);
|
|
150
|
+
return displayId;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Teaches every action that constructs instances of an entity about a new field, so a
|
|
154
|
+
* field added to the model is also populated by the actions that create records.
|
|
155
|
+
*/
|
|
156
|
+
addFieldToConstructors(entityId, fieldId, value) {
|
|
157
|
+
const updated = [];
|
|
158
|
+
for (const action of this.graph.getNodesByKind('action')) {
|
|
159
|
+
let changed = false;
|
|
160
|
+
const operations = (action.operations ?? []).map((operation) => {
|
|
161
|
+
if (!('value' in operation) || operation.value.kind !== 'object') {
|
|
162
|
+
return operation;
|
|
163
|
+
}
|
|
164
|
+
if (operation.value.entityId !== entityId) {
|
|
165
|
+
return operation;
|
|
166
|
+
}
|
|
167
|
+
if (operation.value.entries.some((entry) => entry.fieldId === fieldId)) {
|
|
168
|
+
return operation;
|
|
169
|
+
}
|
|
170
|
+
changed = true;
|
|
171
|
+
return {
|
|
172
|
+
...operation,
|
|
173
|
+
value: { ...operation.value, entries: [...operation.value.entries, { fieldId, value }] },
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
if (changed) {
|
|
177
|
+
this.updateNode({ ...action, operations });
|
|
178
|
+
updated.push(action.id);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return updated;
|
|
182
|
+
}
|
|
183
|
+
appendChild(parentId, childId, position) {
|
|
184
|
+
const parent = this.graph.getNode(parentId);
|
|
185
|
+
if (!parent || !('children' in parent) || !Array.isArray(parent.children)) {
|
|
186
|
+
throw new TransactionError(`Node ${parentId} cannot contain children`);
|
|
187
|
+
}
|
|
188
|
+
const before = structuredClone(parent);
|
|
189
|
+
const container = parent;
|
|
190
|
+
const children = [...container.children];
|
|
191
|
+
children.splice(position ?? children.length, 0, childId);
|
|
192
|
+
container.children = children;
|
|
193
|
+
this.graph.updateNode(parent);
|
|
194
|
+
this.operations.push({
|
|
195
|
+
kind: 'update-node',
|
|
196
|
+
nodeId: parentId,
|
|
197
|
+
before,
|
|
198
|
+
after: structuredClone(parent),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
addNode(node) {
|
|
202
|
+
const id = this.graph.addNode(node);
|
|
203
|
+
const stored = this.graph.getNode(id);
|
|
204
|
+
if (stored) {
|
|
205
|
+
this.operations.push({ kind: 'add-node', nodeId: id, node: stored });
|
|
206
|
+
}
|
|
207
|
+
return id;
|
|
208
|
+
}
|
|
209
|
+
updateNode(node) {
|
|
210
|
+
const before = this.graph.getNode(node.id);
|
|
211
|
+
if (!before) {
|
|
212
|
+
throw new TransactionError(`Node ${node.id} does not exist`);
|
|
213
|
+
}
|
|
214
|
+
this.graph.updateNode(node);
|
|
215
|
+
this.operations.push({ kind: 'update-node', nodeId: node.id, before, after: structuredClone(node) });
|
|
216
|
+
}
|
|
217
|
+
removeNode(id) {
|
|
218
|
+
const node = this.graph.getNode(id);
|
|
219
|
+
if (!node) {
|
|
220
|
+
throw new TransactionError(`Node ${id} does not exist`);
|
|
221
|
+
}
|
|
222
|
+
for (const edge of this.graph.getEdges(id)) {
|
|
223
|
+
this.operations.push({ kind: 'remove-edge', edge });
|
|
224
|
+
}
|
|
225
|
+
this.graph.removeNode(id);
|
|
226
|
+
this.operations.push({ kind: 'remove-node', nodeId: id, node });
|
|
227
|
+
}
|
|
228
|
+
addEdge(from, to, kind) {
|
|
229
|
+
const id = this.graph.addEdge(from, to, kind);
|
|
230
|
+
const edge = this.graph.getEdge(id);
|
|
231
|
+
if (edge) {
|
|
232
|
+
this.operations.push({ kind: 'add-edge', edge });
|
|
233
|
+
}
|
|
234
|
+
return id;
|
|
235
|
+
}
|
|
236
|
+
removeEdge(id) {
|
|
237
|
+
const edge = this.graph.getEdge(id);
|
|
238
|
+
if (!edge) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.graph.removeEdge(id);
|
|
242
|
+
this.operations.push({ kind: 'remove-edge', edge });
|
|
243
|
+
}
|
|
244
|
+
// ------------------------------------------------------------------ lifecycle
|
|
245
|
+
/** Re-derives structural edges and checks referential integrity of the staged graph. */
|
|
246
|
+
validate() {
|
|
247
|
+
synchronizeEdges(this.graph);
|
|
248
|
+
return validateGraph(this.graph);
|
|
249
|
+
}
|
|
250
|
+
commit(options = {}) {
|
|
251
|
+
this.assertOpen();
|
|
252
|
+
const result = this.validate();
|
|
253
|
+
if (!result.valid) {
|
|
254
|
+
throw new TransactionError(`Refusing to commit an invalid graph:\n${result.errors
|
|
255
|
+
.map((problem) => ` [${problem.code}] ${problem.message}`)
|
|
256
|
+
.join('\n')}`, result);
|
|
257
|
+
}
|
|
258
|
+
this.target.restore(this.graph.toJSON());
|
|
259
|
+
this.settled = true;
|
|
260
|
+
const change = {
|
|
261
|
+
id: `change_${randomHex(6)}`,
|
|
262
|
+
timestamp: options.timestamp ?? Date.now(),
|
|
263
|
+
operations: this.changes,
|
|
264
|
+
...(options.reason ? { reason: options.reason } : {}),
|
|
265
|
+
...(options.actor ? { actor: options.actor } : {}),
|
|
266
|
+
};
|
|
267
|
+
this.onCommit(change);
|
|
268
|
+
return change;
|
|
269
|
+
}
|
|
270
|
+
rollback() {
|
|
271
|
+
this.assertOpen();
|
|
272
|
+
this.settled = true;
|
|
273
|
+
}
|
|
274
|
+
assertOpen() {
|
|
275
|
+
if (this.settled) {
|
|
276
|
+
throw new TransactionError('This transaction has already been committed or rolled back');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
addUiNode(node) {
|
|
280
|
+
const id = node.id ?? createNodeId(node.kind.replace('-', '_'));
|
|
281
|
+
return this.addNode({ ...node, id });
|
|
282
|
+
}
|
|
283
|
+
requireNode(id, kind) {
|
|
284
|
+
const node = this.graph.getNode(id);
|
|
285
|
+
if (!node || node.kind !== kind) {
|
|
286
|
+
throw new TransactionError(`${id} is not a ${String(kind)} node`);
|
|
287
|
+
}
|
|
288
|
+
return node;
|
|
289
|
+
}
|
|
290
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cynodia/axiom-agent-api",
|
|
3
|
+
"version": "0.3.1-alpha.1",
|
|
4
|
+
"description": "Semantic queries and transactional graph transformations for AI agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "AskTech AS",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/cynodia/axiom.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/cynodia/axiom",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/cynodia/axiom/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist/**/*.js",
|
|
21
|
+
"dist/**/*.d.ts",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@cynodia/axiom-core": "0.3.1-alpha.1"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -b tsconfig.json tsconfig.test.json",
|
|
38
|
+
"test": "node --test dist-test/**/*.test.js"
|
|
39
|
+
}
|
|
40
|
+
}
|