@cynodia/axiom-agent-api 0.5.0-alpha.1 → 0.6.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/README.md +12 -6
- package/dist/presentation-queries.d.ts +57 -2
- package/dist/presentation-queries.js +121 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,13 +5,12 @@ framework.
|
|
|
5
5
|
|
|
6
6
|
**Status: experimental / alpha.** The API may change between alpha releases.
|
|
7
7
|
|
|
8
|
-
The machine-facing interface: semantic queries over the graph, field-level dependency
|
|
9
|
-
|
|
8
|
+
The machine-facing interface: semantic queries over the graph, field-level dependency and
|
|
9
|
+
mutation-impact analysis, presentation and UX queries, and transactional graph
|
|
10
|
+
transformations.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
screens carry presentation warnings — and transformable: "make this form compact" is one
|
|
14
|
-
semantic change, and an application-wide restyling is a single theme change.
|
|
12
|
+
Main exports: `AgentAPI`, `GraphQueries`, `PresentationQueries`, `Transaction`,
|
|
13
|
+
`ChangeSet`.
|
|
15
14
|
|
|
16
15
|
## Installation
|
|
17
16
|
|
|
@@ -25,6 +24,13 @@ Most applications should install the facade package instead, which re-exports th
|
|
|
25
24
|
npm install @cynodia/axiom@alpha
|
|
26
25
|
```
|
|
27
26
|
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
|
|
30
|
+
The canonical operational contract lives in the `docs/` directory of the
|
|
31
|
+
[`@cynodia/axiom`](https://www.npmjs.com/package/@cynodia/axiom) package, and in
|
|
32
|
+
[the repository](https://github.com/cynodia/axiom). Start with `docs/AGENT_REFERENCE.md`.
|
|
33
|
+
|
|
28
34
|
## License
|
|
29
35
|
|
|
30
36
|
MIT
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActionDef, Density, DeviceClass, FormNode, NodeId, Presentation, PresentationRole, ResolvedPresentation, ResolvedResponsive, StateDef, Theme, UINode, UxRole, ValidationIssue, ViewNode } from '@cynodia/axiom-core';
|
|
1
|
+
import type { ActionDef, Authority, AuthorityContext, Density, DeviceClass, DiagnosticNode, FormNode, NodeId, Expression, Presentation, PresentationRole, ResolvedPresentation, ResolvedResponsive, StateDef, Theme, UINode, UxRole, ValidationIssue, ViewNode } from '@cynodia/axiom-core';
|
|
2
2
|
import { GraphQueries } from './queries.js';
|
|
3
3
|
/** One grouped part of a form, and what it contains. */
|
|
4
4
|
export interface FormSectionSummary {
|
|
@@ -12,10 +12,18 @@ export interface FormSectionSummary {
|
|
|
12
12
|
* The shape of a form as UX, rather than as a list of children: which parts are sections,
|
|
13
13
|
* which controls are required, where the actions are and which one is primary.
|
|
14
14
|
*/
|
|
15
|
+
/**
|
|
16
|
+
* The **declared** structure of a form: what it contains, not what is on screen right now.
|
|
17
|
+
*
|
|
18
|
+
* It is read along the primary render path, so an alternative branch — an empty template,
|
|
19
|
+
* a conditional's false branch — is not described as part of the form's structure.
|
|
20
|
+
*/
|
|
15
21
|
export interface FormStructure {
|
|
16
22
|
formId: NodeId;
|
|
17
23
|
density: Density;
|
|
18
24
|
submitActionId?: NodeId;
|
|
25
|
+
/** Set when the form uses a declared `ButtonNode` as its submit control. */
|
|
26
|
+
submitButtonId?: NodeId;
|
|
19
27
|
sections: FormSectionSummary[];
|
|
20
28
|
/** Inputs that belong to no section. */
|
|
21
29
|
ungroupedInputIds: NodeId[];
|
|
@@ -63,6 +71,46 @@ export declare class PresentationQueries extends GraphQueries {
|
|
|
63
71
|
getFormsWithoutPrimaryAction(): FormNode[];
|
|
64
72
|
/** Nodes carrying renderer-specific presentation that cannot be analyzed. */
|
|
65
73
|
getOpaquePresentationNodes(): UINode[];
|
|
74
|
+
/** UI nodes that present failures from this action. */
|
|
75
|
+
getDiagnosticPresentations(actionId: NodeId): DiagnosticNode[];
|
|
76
|
+
/**
|
|
77
|
+
* Actions that can refuse but whose refusal no UI node presents.
|
|
78
|
+
*
|
|
79
|
+
* An action counts as able to refuse if it declares a guard, a precondition or a
|
|
80
|
+
* postcondition. Only actions a control actually invokes are reported, since an action
|
|
81
|
+
* nothing invokes has no refusal to explain.
|
|
82
|
+
*/
|
|
83
|
+
getActionsWithoutDiagnosticPresentation(): ActionDef[];
|
|
84
|
+
/**
|
|
85
|
+
* Who may commit this state. Absent metadata means `'client'`, so a 0.5.x graph answers
|
|
86
|
+
* `'client'` for everything.
|
|
87
|
+
*/
|
|
88
|
+
getAuthority(stateId: NodeId): Authority | undefined;
|
|
89
|
+
/** Where this action executes. Derived from what it writes, never declared. */
|
|
90
|
+
getActionAuthority(actionId: NodeId): Authority | undefined;
|
|
91
|
+
/** Actions the client must send to the authority rather than execute itself. */
|
|
92
|
+
getServerActions(): ActionDef[];
|
|
93
|
+
/** States a client may commit directly. */
|
|
94
|
+
getClientWritableStates(): StateDef[];
|
|
95
|
+
/** States only the authority may commit. */
|
|
96
|
+
getServerWritableStates(): StateDef[];
|
|
97
|
+
/** States the client never receives at all. */
|
|
98
|
+
getServerOnlyStates(): StateDef[];
|
|
99
|
+
/** Actions that write any server-authoritative state, with the states each touches. */
|
|
100
|
+
getActionsAffectingServerState(): Array<{
|
|
101
|
+
action: ActionDef;
|
|
102
|
+
stateIds: NodeId[];
|
|
103
|
+
}>;
|
|
104
|
+
/**
|
|
105
|
+
* The rule deciding whether a caller may invoke this action, or `undefined` when there is
|
|
106
|
+
* none — in which case every caller may.
|
|
107
|
+
*/
|
|
108
|
+
getAuthorizationForAction(actionId: NodeId): Expression | undefined;
|
|
109
|
+
/** Server actions whose invocation no authorization rule restricts. */
|
|
110
|
+
getUnauthorizedServerActions(): ActionDef[];
|
|
111
|
+
/** Where a state's committed value survives. */
|
|
112
|
+
getPersistenceForState(stateId: NodeId): StateDef['persistence'];
|
|
113
|
+
protected authority(): AuthorityContext;
|
|
66
114
|
/** States marked as ephemeral presentation state rather than domain facts. */
|
|
67
115
|
getEphemeralStates(): StateDef[];
|
|
68
116
|
/**
|
|
@@ -74,7 +122,14 @@ export declare class PresentationQueries extends GraphQueries {
|
|
|
74
122
|
getFormStructure(formId: NodeId): FormStructure;
|
|
75
123
|
protected presentationMap(): Record<NodeId, ResolvedPresentation>;
|
|
76
124
|
protected uiNodes(): UINode[];
|
|
77
|
-
|
|
125
|
+
/**
|
|
126
|
+
* UI nodes beneath this one. `primaryPathOnly` restricts the walk to the arrangement that
|
|
127
|
+
* appears when every collection has members and every condition holds, which is what
|
|
128
|
+
* "structure that is on screen together" means.
|
|
129
|
+
*/
|
|
130
|
+
protected descendants(id: NodeId, options?: {
|
|
131
|
+
primaryPathOnly?: boolean;
|
|
132
|
+
}): UINode[];
|
|
78
133
|
protected descendantIds(id: NodeId): NodeId[];
|
|
79
134
|
private actionsWithUxRole;
|
|
80
135
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isUINode, resolvePresentationMap, uiChildIds, validateGraph, } from '@cynodia/axiom-core';
|
|
1
|
+
import { actionAuthority, authorityContext, isUINode, primaryChildIds, resolvePresentationMap, stateAuthority, uiChildIds, validateGraph, } from '@cynodia/axiom-core';
|
|
2
|
+
import { statesWrittenBy } from '@cynodia/axiom-core';
|
|
2
3
|
import { GraphQueries } from './queries.js';
|
|
3
4
|
/** Diagnostic codes produced by the presentation layer. */
|
|
4
5
|
const PRESENTATION_CODES = [
|
|
@@ -97,6 +98,104 @@ export class PresentationQueries extends GraphQueries {
|
|
|
97
98
|
const resolved = this.presentationMap();
|
|
98
99
|
return this.uiNodes().filter((node) => resolved[node.id]?.opaque === true);
|
|
99
100
|
}
|
|
101
|
+
/** UI nodes that present failures from this action. */
|
|
102
|
+
getDiagnosticPresentations(actionId) {
|
|
103
|
+
return this.uiNodes().filter((node) => node.kind === 'diagnostic' && node.actionId === actionId);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Actions that can refuse but whose refusal no UI node presents.
|
|
107
|
+
*
|
|
108
|
+
* An action counts as able to refuse if it declares a guard, a precondition or a
|
|
109
|
+
* postcondition. Only actions a control actually invokes are reported, since an action
|
|
110
|
+
* nothing invokes has no refusal to explain.
|
|
111
|
+
*/
|
|
112
|
+
getActionsWithoutDiagnosticPresentation() {
|
|
113
|
+
const invoked = new Set();
|
|
114
|
+
for (const node of this.uiNodes()) {
|
|
115
|
+
if (node.kind === 'button') {
|
|
116
|
+
invoked.add(node.actionId);
|
|
117
|
+
}
|
|
118
|
+
if (node.kind === 'form' && node.submitActionId) {
|
|
119
|
+
invoked.add(node.submitActionId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const presented = new Set(this.uiNodes()
|
|
123
|
+
.filter((node) => node.kind === 'diagnostic')
|
|
124
|
+
.map((node) => node.actionId));
|
|
125
|
+
return this.graph.getNodesByKind('action').filter((action) => {
|
|
126
|
+
if (!invoked.has(action.id) || presented.has(action.id)) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const canRefuse = (action.guards ?? []).length > 0 ||
|
|
130
|
+
(action.preconditions ?? []).length > 0 ||
|
|
131
|
+
(action.postconditions ?? []).length > 0;
|
|
132
|
+
return canRefuse;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// -------------------------------------------------------------- authority
|
|
136
|
+
/**
|
|
137
|
+
* Who may commit this state. Absent metadata means `'client'`, so a 0.5.x graph answers
|
|
138
|
+
* `'client'` for everything.
|
|
139
|
+
*/
|
|
140
|
+
getAuthority(stateId) {
|
|
141
|
+
const state = this.graph.getNode(stateId);
|
|
142
|
+
return state?.kind === 'state' ? stateAuthority(state) : undefined;
|
|
143
|
+
}
|
|
144
|
+
/** Where this action executes. Derived from what it writes, never declared. */
|
|
145
|
+
getActionAuthority(actionId) {
|
|
146
|
+
const action = this.graph.getNode(actionId);
|
|
147
|
+
return action?.kind === 'action' ? actionAuthority(action, this.authority()) : undefined;
|
|
148
|
+
}
|
|
149
|
+
/** Actions the client must send to the authority rather than execute itself. */
|
|
150
|
+
getServerActions() {
|
|
151
|
+
const context = this.authority();
|
|
152
|
+
return this.graph
|
|
153
|
+
.getNodesByKind('action')
|
|
154
|
+
.filter((action) => actionAuthority(action, context) === 'server');
|
|
155
|
+
}
|
|
156
|
+
/** States a client may commit directly. */
|
|
157
|
+
getClientWritableStates() {
|
|
158
|
+
return this.graph
|
|
159
|
+
.getNodesByKind('state')
|
|
160
|
+
.filter((state) => !state.derivation && stateAuthority(state) === 'client');
|
|
161
|
+
}
|
|
162
|
+
/** States only the authority may commit. */
|
|
163
|
+
getServerWritableStates() {
|
|
164
|
+
return this.graph
|
|
165
|
+
.getNodesByKind('state')
|
|
166
|
+
.filter((state) => !state.derivation && stateAuthority(state) === 'server');
|
|
167
|
+
}
|
|
168
|
+
/** States the client never receives at all. */
|
|
169
|
+
getServerOnlyStates() {
|
|
170
|
+
return this.graph.getNodesByKind('state').filter((state) => state.serverOnly === true);
|
|
171
|
+
}
|
|
172
|
+
/** Actions that write any server-authoritative state, with the states each touches. */
|
|
173
|
+
getActionsAffectingServerState() {
|
|
174
|
+
const context = this.authority();
|
|
175
|
+
const server = new Set(this.getServerWritableStates().map((state) => state.id));
|
|
176
|
+
return this.getServerActions().map((action) => ({
|
|
177
|
+
action,
|
|
178
|
+
stateIds: [...statesWrittenBy(action, context)].filter((id) => server.has(id)),
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The rule deciding whether a caller may invoke this action, or `undefined` when there is
|
|
183
|
+
* none — in which case every caller may.
|
|
184
|
+
*/
|
|
185
|
+
getAuthorizationForAction(actionId) {
|
|
186
|
+
return this.graph.getNode(actionId)?.authorization;
|
|
187
|
+
}
|
|
188
|
+
/** Server actions whose invocation no authorization rule restricts. */
|
|
189
|
+
getUnauthorizedServerActions() {
|
|
190
|
+
return this.getServerActions().filter((action) => !action.authorization);
|
|
191
|
+
}
|
|
192
|
+
/** Where a state's committed value survives. */
|
|
193
|
+
getPersistenceForState(stateId) {
|
|
194
|
+
return this.graph.getNode(stateId)?.persistence;
|
|
195
|
+
}
|
|
196
|
+
authority() {
|
|
197
|
+
return authorityContext(this.graph.listNodes(), this.graph.principalEntityId);
|
|
198
|
+
}
|
|
100
199
|
/** States marked as ephemeral presentation state rather than domain facts. */
|
|
101
200
|
getEphemeralStates() {
|
|
102
201
|
return this.graph.getNodesByKind('state').filter((state) => state.ephemeral === true);
|
|
@@ -128,13 +227,20 @@ export class PresentationQueries extends GraphQueries {
|
|
|
128
227
|
const destructiveActionIds = new Set();
|
|
129
228
|
const requiredInputIds = [];
|
|
130
229
|
const allInputIds = [];
|
|
131
|
-
|
|
132
|
-
|
|
230
|
+
const submitButton = form.submitButtonId
|
|
231
|
+
? this.graph.getNode(form.submitButtonId)
|
|
232
|
+
: undefined;
|
|
233
|
+
const submitActionId = form.submitActionId ??
|
|
234
|
+
(submitButton && isUINode(submitButton) && submitButton.kind === 'button'
|
|
235
|
+
? submitButton.actionId
|
|
236
|
+
: undefined);
|
|
237
|
+
if (submitActionId) {
|
|
238
|
+
primaryActionIds.add(submitActionId);
|
|
133
239
|
}
|
|
134
|
-
for (const node of this.descendants(formId)) {
|
|
240
|
+
for (const node of this.descendants(formId, { primaryPathOnly: true })) {
|
|
135
241
|
const view = resolved[node.id];
|
|
136
242
|
if (view?.uxRole === 'form-section') {
|
|
137
|
-
const inner = this.descendants(node.id);
|
|
243
|
+
const inner = this.descendants(node.id, { primaryPathOnly: true });
|
|
138
244
|
const inputIds = inner.filter((child) => child.kind === 'input').map((child) => child.id);
|
|
139
245
|
inputIds.forEach((id) => sectionInputs.add(id));
|
|
140
246
|
sections.push({
|
|
@@ -170,7 +276,8 @@ export class PresentationQueries extends GraphQueries {
|
|
|
170
276
|
return {
|
|
171
277
|
formId,
|
|
172
278
|
density: resolved[formId]?.density ?? 'comfortable',
|
|
173
|
-
...(
|
|
279
|
+
...(submitActionId ? { submitActionId } : {}),
|
|
280
|
+
...(form.submitButtonId ? { submitButtonId: form.submitButtonId } : {}),
|
|
174
281
|
sections,
|
|
175
282
|
ungroupedInputIds: allInputIds.filter((id) => !sectionInputs.has(id)),
|
|
176
283
|
actionGroupIds,
|
|
@@ -186,7 +293,13 @@ export class PresentationQueries extends GraphQueries {
|
|
|
186
293
|
uiNodes() {
|
|
187
294
|
return this.graph.listNodes().filter((node) => isUINode(node));
|
|
188
295
|
}
|
|
189
|
-
|
|
296
|
+
/**
|
|
297
|
+
* UI nodes beneath this one. `primaryPathOnly` restricts the walk to the arrangement that
|
|
298
|
+
* appears when every collection has members and every condition holds, which is what
|
|
299
|
+
* "structure that is on screen together" means.
|
|
300
|
+
*/
|
|
301
|
+
descendants(id, options = {}) {
|
|
302
|
+
const children = options.primaryPathOnly ? primaryChildIds : uiChildIds;
|
|
190
303
|
const found = [];
|
|
191
304
|
const seen = new Set([id]);
|
|
192
305
|
const visit = (current) => {
|
|
@@ -194,7 +307,7 @@ export class PresentationQueries extends GraphQueries {
|
|
|
194
307
|
if (!node || !isUINode(node)) {
|
|
195
308
|
return;
|
|
196
309
|
}
|
|
197
|
-
for (const childId of
|
|
310
|
+
for (const childId of children(node)) {
|
|
198
311
|
if (seen.has(childId)) {
|
|
199
312
|
continue;
|
|
200
313
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom-agent-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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.
|
|
34
|
+
"@cynodia/axiom-core": "0.6.0-alpha.1"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc -b tsconfig.json tsconfig.test.json",
|