@cynodia/axiom-agent-api 0.4.1-alpha.1 → 0.5.2-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 +13 -2
- package/dist/api.d.ts +2 -2
- package/dist/api.js +2 -2
- package/dist/changes.d.ts +12 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/presentation-queries.d.ts +106 -0
- package/dist/presentation-queries.js +302 -0
- package/dist/transaction.d.ts +26 -3
- package/dist/transaction.js +91 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,8 +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.
|
|
11
|
+
|
|
12
|
+
Main exports: `AgentAPI`, `GraphQueries`, `PresentationQueries`, `Transaction`,
|
|
13
|
+
`ChangeSet`.
|
|
10
14
|
|
|
11
15
|
## Installation
|
|
12
16
|
|
|
@@ -20,6 +24,13 @@ Most applications should install the facade package instead, which re-exports th
|
|
|
20
24
|
npm install @cynodia/axiom@alpha
|
|
21
25
|
```
|
|
22
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
|
+
|
|
23
34
|
## License
|
|
24
35
|
|
|
25
36
|
MIT
|
package/dist/api.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ApplicationGraph, ValidationResult } from '@cynodia/axiom-core';
|
|
2
|
-
import {
|
|
2
|
+
import { PresentationQueries } from './presentation-queries.js';
|
|
3
3
|
import { Transaction } from './transaction.js';
|
|
4
4
|
import type { ChangeSet } from './changes.js';
|
|
5
5
|
/**
|
|
6
6
|
* The machine-facing interface to an application. Agents query semantics and apply
|
|
7
7
|
* structural transformations; they never edit generated code.
|
|
8
8
|
*/
|
|
9
|
-
export declare class AgentAPI extends
|
|
9
|
+
export declare class AgentAPI extends PresentationQueries {
|
|
10
10
|
private readonly changeLog;
|
|
11
11
|
constructor(graph: ApplicationGraph);
|
|
12
12
|
validate(): ValidationResult;
|
package/dist/api.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { validateGraph } from '@cynodia/axiom-core';
|
|
2
|
-
import {
|
|
2
|
+
import { PresentationQueries } from './presentation-queries.js';
|
|
3
3
|
import { Transaction } from './transaction.js';
|
|
4
4
|
/**
|
|
5
5
|
* The machine-facing interface to an application. Agents query semantics and apply
|
|
6
6
|
* structural transformations; they never edit generated code.
|
|
7
7
|
*/
|
|
8
|
-
export class AgentAPI extends
|
|
8
|
+
export class AgentAPI extends PresentationQueries {
|
|
9
9
|
changeLog = [];
|
|
10
10
|
constructor(graph) {
|
|
11
11
|
super(graph);
|
package/dist/changes.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AnyNode, FieldDef, FieldId, GraphEdge, NodeId } from '@cynodia/axiom-core';
|
|
2
|
-
export type GraphChange = AddNodeChange | RemoveNodeChange | UpdateNodeChange | AddFieldChange | RemoveFieldChange | AddEdgeChange | RemoveEdgeChange;
|
|
1
|
+
import type { AnyNode, FieldDef, FieldId, GraphEdge, NodeId, ThemeInput } from '@cynodia/axiom-core';
|
|
2
|
+
export type GraphChange = AddNodeChange | RemoveNodeChange | UpdateNodeChange | AddFieldChange | RemoveFieldChange | AddEdgeChange | RemoveEdgeChange | SetThemeChange;
|
|
3
3
|
export interface AddNodeChange {
|
|
4
4
|
kind: 'add-node';
|
|
5
5
|
nodeId: NodeId;
|
|
@@ -35,6 +35,16 @@ export interface RemoveEdgeChange {
|
|
|
35
35
|
kind: 'remove-edge';
|
|
36
36
|
edge: GraphEdge;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A change to the application's visual identity. It is recorded like any other change and
|
|
40
|
+
* can never alter behaviour, which is why an application-wide restyling is one operation
|
|
41
|
+
* rather than an edit to every node.
|
|
42
|
+
*/
|
|
43
|
+
export interface SetThemeChange {
|
|
44
|
+
kind: 'set-theme';
|
|
45
|
+
before?: ThemeInput;
|
|
46
|
+
after?: ThemeInput;
|
|
47
|
+
}
|
|
38
48
|
/** A semantic change record: graph operations and intent, never a textual diff. */
|
|
39
49
|
export interface ChangeSet {
|
|
40
50
|
id: string;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ActionDef, Density, DeviceClass, DiagnosticNode, FormNode, NodeId, Presentation, PresentationRole, ResolvedPresentation, ResolvedResponsive, StateDef, Theme, UINode, UxRole, ValidationIssue, ViewNode } from '@cynodia/axiom-core';
|
|
2
|
+
import { GraphQueries } from './queries.js';
|
|
3
|
+
/** One grouped part of a form, and what it contains. */
|
|
4
|
+
export interface FormSectionSummary {
|
|
5
|
+
nodeId: NodeId;
|
|
6
|
+
name?: string;
|
|
7
|
+
/** Text nodes inside the section that act as headings. */
|
|
8
|
+
headings: string[];
|
|
9
|
+
inputIds: NodeId[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The shape of a form as UX, rather than as a list of children: which parts are sections,
|
|
13
|
+
* which controls are required, where the actions are and which one is primary.
|
|
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
|
+
*/
|
|
21
|
+
export interface FormStructure {
|
|
22
|
+
formId: NodeId;
|
|
23
|
+
density: Density;
|
|
24
|
+
submitActionId?: NodeId;
|
|
25
|
+
/** Set when the form uses a declared `ButtonNode` as its submit control. */
|
|
26
|
+
submitButtonId?: NodeId;
|
|
27
|
+
sections: FormSectionSummary[];
|
|
28
|
+
/** Inputs that belong to no section. */
|
|
29
|
+
ungroupedInputIds: NodeId[];
|
|
30
|
+
actionGroupIds: NodeId[];
|
|
31
|
+
primaryActionIds: NodeId[];
|
|
32
|
+
destructiveActionIds: NodeId[];
|
|
33
|
+
requiredInputIds: NodeId[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Presentation and UX queries, §46 and §47.
|
|
37
|
+
*
|
|
38
|
+
* These are the questions that cannot be answered from a stylesheet: which control is the
|
|
39
|
+
* primary action, which regions are grouped and why, what happens on a narrow display,
|
|
40
|
+
* where the presentation contradicts the application's own semantics. They are answerable
|
|
41
|
+
* here only because UX intent is structured data.
|
|
42
|
+
*
|
|
43
|
+
* Resolution is recomputed per call rather than cached: a transaction mutates the graph
|
|
44
|
+
* underneath these queries, and a stale presentation answer would be worse than a slow one.
|
|
45
|
+
*/
|
|
46
|
+
export declare class PresentationQueries extends GraphQueries {
|
|
47
|
+
/** The application's visual identity, completed against the default theme. */
|
|
48
|
+
getTheme(): Theme;
|
|
49
|
+
/** Exactly what a node declares, before defaults, inheritance or inference. */
|
|
50
|
+
getPresentation(nodeId: NodeId): Presentation | undefined;
|
|
51
|
+
/** Presentation with every question answered, as a renderer receives it. */
|
|
52
|
+
resolvePresentation(nodeId: NodeId): ResolvedPresentation | undefined;
|
|
53
|
+
/** Resolved presentation for every UI node in the application. */
|
|
54
|
+
resolveAllPresentation(): Record<NodeId, ResolvedPresentation>;
|
|
55
|
+
getUxRole(nodeId: NodeId): UxRole | undefined;
|
|
56
|
+
/** What changes on a compact, regular or wide display. */
|
|
57
|
+
getResponsiveBehavior(nodeId: NodeId): Partial<Record<DeviceClass, ResolvedResponsive>>;
|
|
58
|
+
/** Every UI node whose resolved UX role is this one. */
|
|
59
|
+
findNodesByUxRole(uxRole: UxRole): UINode[];
|
|
60
|
+
/** Every UI node presented in this role, however the role was decided. */
|
|
61
|
+
findNodesByRole(role: PresentationRole): UINode[];
|
|
62
|
+
/** Which nodes end up at a given density, inheritance included. */
|
|
63
|
+
findNodesByDensity(density: Density): UINode[];
|
|
64
|
+
/** Views that present anything in this role — "which views use this theme role?". */
|
|
65
|
+
getViewsUsingRole(role: PresentationRole): ViewNode[];
|
|
66
|
+
/** Actions a view presents as primary. */
|
|
67
|
+
getPrimaryActions(viewId: NodeId): ActionDef[];
|
|
68
|
+
/** Actions a view presents as destructive, whether declared or inferred. */
|
|
69
|
+
getDestructiveActions(viewId: NodeId): ActionDef[];
|
|
70
|
+
/** Forms that offer no primary action, which is a hierarchy an agent can repair. */
|
|
71
|
+
getFormsWithoutPrimaryAction(): FormNode[];
|
|
72
|
+
/** Nodes carrying renderer-specific presentation that cannot be analyzed. */
|
|
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
|
+
/** States marked as ephemeral presentation state rather than domain facts. */
|
|
85
|
+
getEphemeralStates(): StateDef[];
|
|
86
|
+
/**
|
|
87
|
+
* Presentation and UX findings, optionally narrowed to one view. These are the answers
|
|
88
|
+
* to "which views contain presentation warnings?".
|
|
89
|
+
*/
|
|
90
|
+
getPresentationWarnings(viewId?: NodeId): ValidationIssue[];
|
|
91
|
+
/** A form described as UX: sections, controls, action groups and hierarchy. */
|
|
92
|
+
getFormStructure(formId: NodeId): FormStructure;
|
|
93
|
+
protected presentationMap(): Record<NodeId, ResolvedPresentation>;
|
|
94
|
+
protected uiNodes(): UINode[];
|
|
95
|
+
/**
|
|
96
|
+
* UI nodes beneath this one. `primaryPathOnly` restricts the walk to the arrangement that
|
|
97
|
+
* appears when every collection has members and every condition holds, which is what
|
|
98
|
+
* "structure that is on screen together" means.
|
|
99
|
+
*/
|
|
100
|
+
protected descendants(id: NodeId, options?: {
|
|
101
|
+
primaryPathOnly?: boolean;
|
|
102
|
+
}): UINode[];
|
|
103
|
+
protected descendantIds(id: NodeId): NodeId[];
|
|
104
|
+
private actionsWithUxRole;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=presentation-queries.d.ts.map
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { isUINode, primaryChildIds, resolvePresentationMap, uiChildIds, validateGraph, } from '@cynodia/axiom-core';
|
|
2
|
+
import { GraphQueries } from './queries.js';
|
|
3
|
+
/** Diagnostic codes produced by the presentation layer. */
|
|
4
|
+
const PRESENTATION_CODES = [
|
|
5
|
+
'UNKNOWN_PRESENTATION_TOKEN',
|
|
6
|
+
'PRESENTATION_SEMANTIC_CONFLICT',
|
|
7
|
+
'MULTIPLE_PRIMARY_ACTIONS',
|
|
8
|
+
'FORM_WITHOUT_PRIMARY_ACTION',
|
|
9
|
+
'DESTRUCTIVE_ACTION_PRESENTED_AS_SUCCESS',
|
|
10
|
+
'DESTRUCTIVE_ACTION_UNMARKED',
|
|
11
|
+
'EXCESSIVE_HORIZONTAL_ACTIONS',
|
|
12
|
+
'EMPTY_STATE_WITHOUT_RECOVERY_ACTION',
|
|
13
|
+
'RIGID_HORIZONTAL_LAYOUT',
|
|
14
|
+
'CONFLICTING_SIZING',
|
|
15
|
+
'INTERACTIVE_ELEMENT_MISSING_LABEL',
|
|
16
|
+
'FORM_INPUT_MISSING_LABEL',
|
|
17
|
+
'INVALID_HEADING_STRUCTURE',
|
|
18
|
+
'OPAQUE_PRESENTATION',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Presentation and UX queries, §46 and §47.
|
|
22
|
+
*
|
|
23
|
+
* These are the questions that cannot be answered from a stylesheet: which control is the
|
|
24
|
+
* primary action, which regions are grouped and why, what happens on a narrow display,
|
|
25
|
+
* where the presentation contradicts the application's own semantics. They are answerable
|
|
26
|
+
* here only because UX intent is structured data.
|
|
27
|
+
*
|
|
28
|
+
* Resolution is recomputed per call rather than cached: a transaction mutates the graph
|
|
29
|
+
* underneath these queries, and a stale presentation answer would be worse than a slow one.
|
|
30
|
+
*/
|
|
31
|
+
export class PresentationQueries extends GraphQueries {
|
|
32
|
+
/** The application's visual identity, completed against the default theme. */
|
|
33
|
+
getTheme() {
|
|
34
|
+
return this.graph.theme;
|
|
35
|
+
}
|
|
36
|
+
/** Exactly what a node declares, before defaults, inheritance or inference. */
|
|
37
|
+
getPresentation(nodeId) {
|
|
38
|
+
const node = this.graph.getNode(nodeId);
|
|
39
|
+
return node && isUINode(node) ? node.presentation : undefined;
|
|
40
|
+
}
|
|
41
|
+
/** Presentation with every question answered, as a renderer receives it. */
|
|
42
|
+
resolvePresentation(nodeId) {
|
|
43
|
+
return this.presentationMap()[nodeId];
|
|
44
|
+
}
|
|
45
|
+
/** Resolved presentation for every UI node in the application. */
|
|
46
|
+
resolveAllPresentation() {
|
|
47
|
+
return this.presentationMap();
|
|
48
|
+
}
|
|
49
|
+
getUxRole(nodeId) {
|
|
50
|
+
return this.resolvePresentation(nodeId)?.uxRole;
|
|
51
|
+
}
|
|
52
|
+
/** What changes on a compact, regular or wide display. */
|
|
53
|
+
getResponsiveBehavior(nodeId) {
|
|
54
|
+
return this.resolvePresentation(nodeId)?.responsive ?? {};
|
|
55
|
+
}
|
|
56
|
+
/** Every UI node whose resolved UX role is this one. */
|
|
57
|
+
findNodesByUxRole(uxRole) {
|
|
58
|
+
const resolved = this.presentationMap();
|
|
59
|
+
return this.uiNodes().filter((node) => resolved[node.id]?.uxRole === uxRole);
|
|
60
|
+
}
|
|
61
|
+
/** Every UI node presented in this role, however the role was decided. */
|
|
62
|
+
findNodesByRole(role) {
|
|
63
|
+
const resolved = this.presentationMap();
|
|
64
|
+
return this.uiNodes().filter((node) => resolved[node.id]?.role === role);
|
|
65
|
+
}
|
|
66
|
+
/** Which nodes end up at a given density, inheritance included. */
|
|
67
|
+
findNodesByDensity(density) {
|
|
68
|
+
const resolved = this.presentationMap();
|
|
69
|
+
return this.uiNodes().filter((node) => resolved[node.id]?.density === density);
|
|
70
|
+
}
|
|
71
|
+
/** Views that present anything in this role — "which views use this theme role?". */
|
|
72
|
+
getViewsUsingRole(role) {
|
|
73
|
+
const views = new Map();
|
|
74
|
+
for (const node of this.findNodesByRole(role)) {
|
|
75
|
+
for (const view of this.enclosingViews(node.id)) {
|
|
76
|
+
views.set(view.id, view);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return [...views.values()];
|
|
80
|
+
}
|
|
81
|
+
/** Actions a view presents as primary. */
|
|
82
|
+
getPrimaryActions(viewId) {
|
|
83
|
+
return this.actionsWithUxRole(viewId, 'primary-action');
|
|
84
|
+
}
|
|
85
|
+
/** Actions a view presents as destructive, whether declared or inferred. */
|
|
86
|
+
getDestructiveActions(viewId) {
|
|
87
|
+
return this.actionsWithUxRole(viewId, 'destructive-action');
|
|
88
|
+
}
|
|
89
|
+
/** Forms that offer no primary action, which is a hierarchy an agent can repair. */
|
|
90
|
+
getFormsWithoutPrimaryAction() {
|
|
91
|
+
return this.graph
|
|
92
|
+
.getNodesByKind('form')
|
|
93
|
+
.filter((form) => this.getFormStructure(form.id).primaryActionIds.length === 0);
|
|
94
|
+
}
|
|
95
|
+
/** Nodes carrying renderer-specific presentation that cannot be analyzed. */
|
|
96
|
+
getOpaquePresentationNodes() {
|
|
97
|
+
const resolved = this.presentationMap();
|
|
98
|
+
return this.uiNodes().filter((node) => resolved[node.id]?.opaque === true);
|
|
99
|
+
}
|
|
100
|
+
/** UI nodes that present failures from this action. */
|
|
101
|
+
getDiagnosticPresentations(actionId) {
|
|
102
|
+
return this.uiNodes().filter((node) => node.kind === 'diagnostic' && node.actionId === actionId);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Actions that can refuse but whose refusal no UI node presents.
|
|
106
|
+
*
|
|
107
|
+
* An action counts as able to refuse if it declares a guard, a precondition or a
|
|
108
|
+
* postcondition. Only actions a control actually invokes are reported, since an action
|
|
109
|
+
* nothing invokes has no refusal to explain.
|
|
110
|
+
*/
|
|
111
|
+
getActionsWithoutDiagnosticPresentation() {
|
|
112
|
+
const invoked = new Set();
|
|
113
|
+
for (const node of this.uiNodes()) {
|
|
114
|
+
if (node.kind === 'button') {
|
|
115
|
+
invoked.add(node.actionId);
|
|
116
|
+
}
|
|
117
|
+
if (node.kind === 'form' && node.submitActionId) {
|
|
118
|
+
invoked.add(node.submitActionId);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const presented = new Set(this.uiNodes()
|
|
122
|
+
.filter((node) => node.kind === 'diagnostic')
|
|
123
|
+
.map((node) => node.actionId));
|
|
124
|
+
return this.graph.getNodesByKind('action').filter((action) => {
|
|
125
|
+
if (!invoked.has(action.id) || presented.has(action.id)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const canRefuse = (action.guards ?? []).length > 0 ||
|
|
129
|
+
(action.preconditions ?? []).length > 0 ||
|
|
130
|
+
(action.postconditions ?? []).length > 0;
|
|
131
|
+
return canRefuse;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/** States marked as ephemeral presentation state rather than domain facts. */
|
|
135
|
+
getEphemeralStates() {
|
|
136
|
+
return this.graph.getNodesByKind('state').filter((state) => state.ephemeral === true);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Presentation and UX findings, optionally narrowed to one view. These are the answers
|
|
140
|
+
* to "which views contain presentation warnings?".
|
|
141
|
+
*/
|
|
142
|
+
getPresentationWarnings(viewId) {
|
|
143
|
+
const result = validateGraph(this.graph);
|
|
144
|
+
const findings = [...result.errors, ...result.warnings].filter((finding) => PRESENTATION_CODES.includes(finding.code));
|
|
145
|
+
if (!viewId) {
|
|
146
|
+
return findings;
|
|
147
|
+
}
|
|
148
|
+
const scope = new Set([viewId, ...this.descendantIds(viewId)]);
|
|
149
|
+
return findings.filter((finding) => finding.nodeId !== undefined && scope.has(finding.nodeId));
|
|
150
|
+
}
|
|
151
|
+
/** A form described as UX: sections, controls, action groups and hierarchy. */
|
|
152
|
+
getFormStructure(formId) {
|
|
153
|
+
const form = this.graph.getNode(formId);
|
|
154
|
+
if (!form || form.kind !== 'form') {
|
|
155
|
+
throw new Error(`${formId} is not a form`);
|
|
156
|
+
}
|
|
157
|
+
const resolved = this.presentationMap();
|
|
158
|
+
const sections = [];
|
|
159
|
+
const sectionInputs = new Set();
|
|
160
|
+
const actionGroupIds = [];
|
|
161
|
+
const primaryActionIds = new Set();
|
|
162
|
+
const destructiveActionIds = new Set();
|
|
163
|
+
const requiredInputIds = [];
|
|
164
|
+
const allInputIds = [];
|
|
165
|
+
const submitButton = form.submitButtonId
|
|
166
|
+
? this.graph.getNode(form.submitButtonId)
|
|
167
|
+
: undefined;
|
|
168
|
+
const submitActionId = form.submitActionId ??
|
|
169
|
+
(submitButton && isUINode(submitButton) && submitButton.kind === 'button'
|
|
170
|
+
? submitButton.actionId
|
|
171
|
+
: undefined);
|
|
172
|
+
if (submitActionId) {
|
|
173
|
+
primaryActionIds.add(submitActionId);
|
|
174
|
+
}
|
|
175
|
+
for (const node of this.descendants(formId, { primaryPathOnly: true })) {
|
|
176
|
+
const view = resolved[node.id];
|
|
177
|
+
if (view?.uxRole === 'form-section') {
|
|
178
|
+
const inner = this.descendants(node.id, { primaryPathOnly: true });
|
|
179
|
+
const inputIds = inner.filter((child) => child.kind === 'input').map((child) => child.id);
|
|
180
|
+
inputIds.forEach((id) => sectionInputs.add(id));
|
|
181
|
+
sections.push({
|
|
182
|
+
nodeId: node.id,
|
|
183
|
+
...(node.name ? { name: node.name } : {}),
|
|
184
|
+
headings: inner
|
|
185
|
+
.filter((child) => child.kind === 'text')
|
|
186
|
+
.filter((child) => isHeading(resolved[child.id]))
|
|
187
|
+
.map((child) => (typeof child.value === 'string' ? child.value : '')),
|
|
188
|
+
inputIds,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (view?.uxRole === 'action-group' || view?.uxRole === 'toolbar') {
|
|
192
|
+
actionGroupIds.push(node.id);
|
|
193
|
+
}
|
|
194
|
+
if (node.kind === 'input') {
|
|
195
|
+
allInputIds.push(node.id);
|
|
196
|
+
// Required is a fact about the model, not a presentation decision.
|
|
197
|
+
const addressed = requiredFieldOf(this, node);
|
|
198
|
+
if (addressed) {
|
|
199
|
+
requiredInputIds.push(node.id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (node.kind === 'button') {
|
|
203
|
+
if (view?.uxRole === 'primary-action') {
|
|
204
|
+
primaryActionIds.add(node.actionId);
|
|
205
|
+
}
|
|
206
|
+
if (view?.uxRole === 'destructive-action') {
|
|
207
|
+
destructiveActionIds.add(node.actionId);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
formId,
|
|
213
|
+
density: resolved[formId]?.density ?? 'comfortable',
|
|
214
|
+
...(submitActionId ? { submitActionId } : {}),
|
|
215
|
+
...(form.submitButtonId ? { submitButtonId: form.submitButtonId } : {}),
|
|
216
|
+
sections,
|
|
217
|
+
ungroupedInputIds: allInputIds.filter((id) => !sectionInputs.has(id)),
|
|
218
|
+
actionGroupIds,
|
|
219
|
+
primaryActionIds: [...primaryActionIds],
|
|
220
|
+
destructiveActionIds: [...destructiveActionIds],
|
|
221
|
+
requiredInputIds,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
// ---------------------------------------------------------------- internals
|
|
225
|
+
presentationMap() {
|
|
226
|
+
return resolvePresentationMap(this.graph.listNodes(), this.graph.theme);
|
|
227
|
+
}
|
|
228
|
+
uiNodes() {
|
|
229
|
+
return this.graph.listNodes().filter((node) => isUINode(node));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* UI nodes beneath this one. `primaryPathOnly` restricts the walk to the arrangement that
|
|
233
|
+
* appears when every collection has members and every condition holds, which is what
|
|
234
|
+
* "structure that is on screen together" means.
|
|
235
|
+
*/
|
|
236
|
+
descendants(id, options = {}) {
|
|
237
|
+
const children = options.primaryPathOnly ? primaryChildIds : uiChildIds;
|
|
238
|
+
const found = [];
|
|
239
|
+
const seen = new Set([id]);
|
|
240
|
+
const visit = (current) => {
|
|
241
|
+
const node = this.graph.getNode(current);
|
|
242
|
+
if (!node || !isUINode(node)) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
for (const childId of children(node)) {
|
|
246
|
+
if (seen.has(childId)) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
seen.add(childId);
|
|
250
|
+
const child = this.graph.getNode(childId);
|
|
251
|
+
if (child && isUINode(child)) {
|
|
252
|
+
found.push(child);
|
|
253
|
+
visit(childId);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
visit(id);
|
|
258
|
+
return found;
|
|
259
|
+
}
|
|
260
|
+
descendantIds(id) {
|
|
261
|
+
return this.descendants(id).map((node) => node.id);
|
|
262
|
+
}
|
|
263
|
+
actionsWithUxRole(viewId, uxRole) {
|
|
264
|
+
const resolved = this.presentationMap();
|
|
265
|
+
const found = new Map();
|
|
266
|
+
const candidates = [this.graph.getNode(viewId), ...this.descendants(viewId)];
|
|
267
|
+
for (const node of candidates) {
|
|
268
|
+
if (!node || !isUINode(node) || node.kind !== 'button') {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (resolved[node.id]?.uxRole !== uxRole) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
const action = this.graph.getNode(node.actionId);
|
|
275
|
+
if (action?.kind === 'action') {
|
|
276
|
+
found.set(action.id, action);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// A form's own submit is a primary action even without a button node of its own.
|
|
280
|
+
if (uxRole === 'primary-action') {
|
|
281
|
+
for (const node of candidates) {
|
|
282
|
+
if (node && isUINode(node) && node.kind === 'form' && node.submitActionId) {
|
|
283
|
+
const action = this.graph.getNode(node.submitActionId);
|
|
284
|
+
if (action?.kind === 'action') {
|
|
285
|
+
found.set(action.id, action);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return [...found.values()];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function isHeading(resolved) {
|
|
294
|
+
const role = resolved?.textRole;
|
|
295
|
+
return role === 'heading' || role === 'title' || role === 'display';
|
|
296
|
+
}
|
|
297
|
+
/** Whether the field an input addresses is declared required. */
|
|
298
|
+
function requiredFieldOf(queries, node) {
|
|
299
|
+
const location = node.binding.location;
|
|
300
|
+
const fieldId = location.kind === 'field' ? location.fieldId : undefined;
|
|
301
|
+
return fieldId ? queries.getField(fieldId)?.field.required === true : false;
|
|
302
|
+
}
|
package/dist/transaction.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
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 {
|
|
2
|
+
import type { ActionDef, AnyNode, ButtonNode, ConditionalNode, ConstraintDef, ContainerNode, Density, DeviceClass, EdgeId, EdgeKind, Expression, FieldDef, FieldDisplayNode, FieldId, FormNode, InputNode, Location, NodeId, Presentation, RepeatNode, ResponsiveOverride, RouteDef, StateDef, TextNode, ThemeInput, UINode, UxRole, ValidationResult, ValueFormat, ViewNode } from '@cynodia/axiom-core';
|
|
3
|
+
import { PresentationQueries } from './presentation-queries.js';
|
|
4
4
|
import type { ChangeSet, GraphChange } from './changes.js';
|
|
5
5
|
export declare class TransactionError extends Error {
|
|
6
6
|
readonly result?: ValidationResult;
|
|
@@ -13,7 +13,7 @@ type UIInput<T extends UINode> = Omit<T, 'id' | 'kind'> & {
|
|
|
13
13
|
* A staged set of graph transformations. Every change is applied to a private copy, so
|
|
14
14
|
* the graph an agent (or a runtime) can observe is unchanged until `commit()` succeeds.
|
|
15
15
|
*/
|
|
16
|
-
export declare class Transaction extends
|
|
16
|
+
export declare class Transaction extends PresentationQueries {
|
|
17
17
|
private readonly target;
|
|
18
18
|
private readonly onCommit;
|
|
19
19
|
private readonly operations;
|
|
@@ -81,6 +81,28 @@ export declare class Transaction extends GraphQueries {
|
|
|
81
81
|
* field added to the model is also populated by the actions that create records.
|
|
82
82
|
*/
|
|
83
83
|
addFieldToConstructors(entityId: NodeId, fieldId: FieldId, value: Expression): NodeId[];
|
|
84
|
+
/**
|
|
85
|
+
* Replaces a node's presentation intent outright.
|
|
86
|
+
*
|
|
87
|
+
* "Make this form more compact" is one of these calls, not seventeen edited style
|
|
88
|
+
* declarations — and because the result is still semantic data, the next agent can read
|
|
89
|
+
* back what was intended rather than infer it from CSS.
|
|
90
|
+
*/
|
|
91
|
+
setPresentation(nodeId: NodeId, presentation: Presentation | undefined): void;
|
|
92
|
+
/** Merges presentation intent, leaving whatever the node already said in place. */
|
|
93
|
+
mergePresentation(nodeId: NodeId, patch: Presentation): void;
|
|
94
|
+
setUxRole(nodeId: NodeId, uxRole: UxRole | undefined): void;
|
|
95
|
+
setDensity(nodeId: NodeId, density: Density): void;
|
|
96
|
+
setValueFormat(nodeId: NodeId, format: ValueFormat | undefined): void;
|
|
97
|
+
/** States what a node does on one class of display. */
|
|
98
|
+
setResponsiveBehavior(nodeId: NodeId, device: DeviceClass, override: ResponsiveOverride | undefined): void;
|
|
99
|
+
/**
|
|
100
|
+
* Replaces the theme. An application-wide visual change belongs here rather than in the
|
|
101
|
+
* UI nodes: a theme cannot alter an action, a constraint, a location or a route.
|
|
102
|
+
*/
|
|
103
|
+
setTheme(theme: ThemeInput | undefined): void;
|
|
104
|
+
/** Changes part of the theme, keeping the rest of what the application declared. */
|
|
105
|
+
mergeTheme(patch: ThemeInput): void;
|
|
84
106
|
appendChild(parentId: NodeId, childId: NodeId, position?: number): void;
|
|
85
107
|
addNode<T extends AnyNode>(node: T): NodeId;
|
|
86
108
|
updateNode(node: AnyNode): void;
|
|
@@ -97,6 +119,7 @@ export declare class Transaction extends GraphQueries {
|
|
|
97
119
|
rollback(): void;
|
|
98
120
|
private assertOpen;
|
|
99
121
|
private addUiNode;
|
|
122
|
+
private requireUiNode;
|
|
100
123
|
private requireNode;
|
|
101
124
|
}
|
|
102
125
|
export {};
|
package/dist/transaction.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ApplicationGraph, createFieldId, createNodeId, randomHex, synchronizeEdges, validateGraph, } from '@cynodia/axiom-core';
|
|
2
|
-
import {
|
|
1
|
+
import { ApplicationGraph, createFieldId, createNodeId, isUINode, randomHex, synchronizeEdges, validateGraph, } from '@cynodia/axiom-core';
|
|
2
|
+
import { PresentationQueries } from './presentation-queries.js';
|
|
3
3
|
export class TransactionError extends Error {
|
|
4
4
|
result;
|
|
5
5
|
constructor(message, result) {
|
|
@@ -12,7 +12,7 @@ export class TransactionError extends Error {
|
|
|
12
12
|
* A staged set of graph transformations. Every change is applied to a private copy, so
|
|
13
13
|
* the graph an agent (or a runtime) can observe is unchanged until `commit()` succeeds.
|
|
14
14
|
*/
|
|
15
|
-
export class Transaction extends
|
|
15
|
+
export class Transaction extends PresentationQueries {
|
|
16
16
|
target;
|
|
17
17
|
onCommit;
|
|
18
18
|
operations = [];
|
|
@@ -180,6 +180,70 @@ export class Transaction extends GraphQueries {
|
|
|
180
180
|
}
|
|
181
181
|
return updated;
|
|
182
182
|
}
|
|
183
|
+
// ------------------------------------------------------- presentation and theme
|
|
184
|
+
/**
|
|
185
|
+
* Replaces a node's presentation intent outright.
|
|
186
|
+
*
|
|
187
|
+
* "Make this form more compact" is one of these calls, not seventeen edited style
|
|
188
|
+
* declarations — and because the result is still semantic data, the next agent can read
|
|
189
|
+
* back what was intended rather than infer it from CSS.
|
|
190
|
+
*/
|
|
191
|
+
setPresentation(nodeId, presentation) {
|
|
192
|
+
const node = this.requireUiNode(nodeId);
|
|
193
|
+
if (presentation === undefined) {
|
|
194
|
+
delete node.presentation;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
node.presentation = structuredClone(presentation);
|
|
198
|
+
}
|
|
199
|
+
this.updateNode(node);
|
|
200
|
+
}
|
|
201
|
+
/** Merges presentation intent, leaving whatever the node already said in place. */
|
|
202
|
+
mergePresentation(nodeId, patch) {
|
|
203
|
+
const node = this.requireUiNode(nodeId);
|
|
204
|
+
node.presentation = { ...(node.presentation ?? {}), ...structuredClone(patch) };
|
|
205
|
+
this.updateNode(node);
|
|
206
|
+
}
|
|
207
|
+
setUxRole(nodeId, uxRole) {
|
|
208
|
+
this.mergePresentation(nodeId, { uxRole });
|
|
209
|
+
}
|
|
210
|
+
setDensity(nodeId, density) {
|
|
211
|
+
this.mergePresentation(nodeId, { density });
|
|
212
|
+
}
|
|
213
|
+
setValueFormat(nodeId, format) {
|
|
214
|
+
this.mergePresentation(nodeId, { format });
|
|
215
|
+
}
|
|
216
|
+
/** States what a node does on one class of display. */
|
|
217
|
+
setResponsiveBehavior(nodeId, device, override) {
|
|
218
|
+
const node = this.requireUiNode(nodeId);
|
|
219
|
+
const responsive = { ...(node.presentation?.responsive ?? {}) };
|
|
220
|
+
if (override === undefined) {
|
|
221
|
+
delete responsive[device];
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
responsive[device] = structuredClone(override);
|
|
225
|
+
}
|
|
226
|
+
node.presentation = { ...(node.presentation ?? {}), responsive };
|
|
227
|
+
this.updateNode(node);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Replaces the theme. An application-wide visual change belongs here rather than in the
|
|
231
|
+
* UI nodes: a theme cannot alter an action, a constraint, a location or a route.
|
|
232
|
+
*/
|
|
233
|
+
setTheme(theme) {
|
|
234
|
+
const before = this.graph.declaredTheme;
|
|
235
|
+
this.graph.setTheme(theme);
|
|
236
|
+
this.operations.push({
|
|
237
|
+
kind: 'set-theme',
|
|
238
|
+
...(before ? { before } : {}),
|
|
239
|
+
...(theme ? { after: structuredClone(theme) } : {}),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/** Changes part of the theme, keeping the rest of what the application declared. */
|
|
243
|
+
mergeTheme(patch) {
|
|
244
|
+
const before = this.graph.declaredTheme ?? {};
|
|
245
|
+
this.setTheme(mergeThemeInput(before, patch));
|
|
246
|
+
}
|
|
183
247
|
appendChild(parentId, childId, position) {
|
|
184
248
|
const parent = this.graph.getNode(parentId);
|
|
185
249
|
if (!parent || !('children' in parent) || !Array.isArray(parent.children)) {
|
|
@@ -280,6 +344,13 @@ export class Transaction extends GraphQueries {
|
|
|
280
344
|
const id = node.id ?? createNodeId(node.kind.replace('-', '_'));
|
|
281
345
|
return this.addNode({ ...node, id });
|
|
282
346
|
}
|
|
347
|
+
requireUiNode(id) {
|
|
348
|
+
const node = this.graph.getNode(id);
|
|
349
|
+
if (!node || !isUINode(node)) {
|
|
350
|
+
throw new TransactionError(`${id} is not a UI node`);
|
|
351
|
+
}
|
|
352
|
+
return node;
|
|
353
|
+
}
|
|
283
354
|
requireNode(id, kind) {
|
|
284
355
|
const node = this.graph.getNode(id);
|
|
285
356
|
if (!node || node.kind !== kind) {
|
|
@@ -288,3 +359,20 @@ export class Transaction extends GraphQueries {
|
|
|
288
359
|
return node;
|
|
289
360
|
}
|
|
290
361
|
}
|
|
362
|
+
/** Deep merge of two partial themes, so a patch never has to restate whole tables. */
|
|
363
|
+
function mergeThemeInput(base, patch) {
|
|
364
|
+
const isPlain = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
365
|
+
const merge = (left, right) => {
|
|
366
|
+
if (!isPlain(right)) {
|
|
367
|
+
return right === undefined ? left : right;
|
|
368
|
+
}
|
|
369
|
+
const result = isPlain(left) ? { ...left } : {};
|
|
370
|
+
for (const [key, value] of Object.entries(right)) {
|
|
371
|
+
if (value !== undefined) {
|
|
372
|
+
result[key] = merge(result[key], value);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
};
|
|
377
|
+
return merge(structuredClone(base), structuredClone(patch));
|
|
378
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom-agent-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2-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.5.2-alpha.1"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc -b tsconfig.json tsconfig.test.json",
|