@cynodia/axiom-ui 0.7.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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/catalog.d.ts +32 -0
- package/dist/catalog.js +29 -0
- package/dist/example/app.d.ts +6 -0
- package/dist/example/app.js +365 -0
- package/dist/example/domain.d.ts +71 -0
- package/dist/example/domain.js +469 -0
- package/dist/example/index.d.ts +3 -0
- package/dist/example/index.js +2 -0
- package/dist/expand.d.ts +119 -0
- package/dist/expand.js +227 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +11 -0
- package/dist/inference.d.ts +55 -0
- package/dist/inference.js +113 -0
- package/dist/pattern.d.ts +192 -0
- package/dist/pattern.js +39 -0
- package/dist/patterns/action-bar.d.ts +24 -0
- package/dist/patterns/action-bar.js +94 -0
- package/dist/patterns/entity-form.d.ts +93 -0
- package/dist/patterns/entity-form.js +340 -0
- package/dist/patterns/entity-list.d.ts +45 -0
- package/dist/patterns/entity-list.js +236 -0
- package/dist/patterns/metric-grid.d.ts +31 -0
- package/dist/patterns/metric-grid.js +107 -0
- package/dist/patterns/page.d.ts +28 -0
- package/dist/patterns/page.js +104 -0
- package/dist/queries.d.ts +60 -0
- package/dist/queries.js +53 -0
- package/dist/toolkit.d.ts +8 -0
- package/dist/toolkit.js +19 -0
- package/docs/OWNERSHIP.md +75 -0
- package/docs/PATTERN_AUTHORING.md +71 -0
- package/docs/PATTERN_CATALOG.json +504 -0
- package/docs/PROVENANCE.md +55 -0
- package/docs/TOOLKIT_AGENT_REFERENCE.md +205 -0
- package/package.json +47 -0
package/dist/expand.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { stripAuthoringMetadata, withAuthoringMetadata } from '@cynodia/axiom-core';
|
|
2
|
+
import { PROVENANCE_KEY, TOOLKIT_NAME, TOOLKIT_VERSION, partId, provenanceOf } from './pattern.js';
|
|
3
|
+
export class PatternExpansionError extends Error {
|
|
4
|
+
findings;
|
|
5
|
+
constructor(findings) {
|
|
6
|
+
super(`The pattern declaration was rejected before expansion:\n${findings
|
|
7
|
+
.map((finding) => ` [${finding.code}] ${finding.path}: ${finding.message}`)
|
|
8
|
+
.join('\n')}`);
|
|
9
|
+
this.findings = findings;
|
|
10
|
+
this.name = 'PatternExpansionError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Expansions are recorded per graph, outside it: an expansion record is not application data. */
|
|
14
|
+
const records = new WeakMap();
|
|
15
|
+
export function createToolkit(definitions) {
|
|
16
|
+
const patterns = new Map(definitions.map((definition) => [definition.name, definition]));
|
|
17
|
+
function expandInto(graph, declaration, model, ownership, parent, ancestry) {
|
|
18
|
+
const definition = patterns.get(declaration.pattern);
|
|
19
|
+
if (!definition) {
|
|
20
|
+
throw new PatternExpansionError([
|
|
21
|
+
{
|
|
22
|
+
code: 'UNKNOWN_PATTERN',
|
|
23
|
+
message: `No pattern named "${declaration.pattern}". Available: ${[...patterns.keys()].join(', ')}.`,
|
|
24
|
+
severity: 'error',
|
|
25
|
+
path: declaration.instance,
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
// Checked against the graph before a single node is created, so a mistake is reported
|
|
30
|
+
// against the declaration the author wrote rather than against generated output.
|
|
31
|
+
const findings = definition.check?.(declaration, { graph, instance: declaration.instance }) ?? [];
|
|
32
|
+
if (findings.some((finding) => finding.severity === 'error')) {
|
|
33
|
+
throw new PatternExpansionError(findings);
|
|
34
|
+
}
|
|
35
|
+
const record = {
|
|
36
|
+
instance: declaration.instance,
|
|
37
|
+
pattern: declaration.pattern,
|
|
38
|
+
declaration,
|
|
39
|
+
ownership,
|
|
40
|
+
patternVersion: definition.version ?? TOOLKIT_VERSION,
|
|
41
|
+
rootId: partId(declaration.instance, 'root'),
|
|
42
|
+
nodeIds: [],
|
|
43
|
+
generated: {},
|
|
44
|
+
explanations: [],
|
|
45
|
+
findings,
|
|
46
|
+
...(parent ? { parent } : {}),
|
|
47
|
+
};
|
|
48
|
+
const context = {
|
|
49
|
+
graph,
|
|
50
|
+
instance: declaration.instance,
|
|
51
|
+
id: (part, index) => partId(declaration.instance, part, index),
|
|
52
|
+
add(node, part) {
|
|
53
|
+
const provenance = {
|
|
54
|
+
toolkit: TOOLKIT_NAME,
|
|
55
|
+
pattern: declaration.pattern,
|
|
56
|
+
patternVersion: record.patternVersion,
|
|
57
|
+
instance: declaration.instance,
|
|
58
|
+
part,
|
|
59
|
+
ownership,
|
|
60
|
+
...(parent ? { parent } : {}),
|
|
61
|
+
...(ancestry.length > 0 ? { ancestry: [...ancestry] } : {}),
|
|
62
|
+
};
|
|
63
|
+
const stamped = model === 'macro' ? node : withAuthoringMetadata(node, { [PROVENANCE_KEY]: provenance });
|
|
64
|
+
graph.addNode(stamped);
|
|
65
|
+
record.nodeIds.push(node.id);
|
|
66
|
+
// The node as generated, without provenance, is the baseline drift compares against.
|
|
67
|
+
record.generated[String(node.id)] = stripAuthoringMetadata(node);
|
|
68
|
+
return node.id;
|
|
69
|
+
},
|
|
70
|
+
explain: (message) => record.explanations.push(message),
|
|
71
|
+
child: (nested) => expandInto(graph, nested, model, ownership, declaration.instance, [...ancestry, declaration.instance]),
|
|
72
|
+
slot(name) {
|
|
73
|
+
const content = declaration[name];
|
|
74
|
+
if (content === undefined) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const items = Array.isArray(content) ? content : [content];
|
|
78
|
+
return items.map((item) => typeof item === 'string'
|
|
79
|
+
? item
|
|
80
|
+
: expandInto(graph, item, model, ownership, declaration.instance, [...ancestry, declaration.instance]));
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
record.rootId = definition.expand(declaration, context);
|
|
84
|
+
const existing = records.get(graph) ?? [];
|
|
85
|
+
existing.push(record);
|
|
86
|
+
records.set(graph, existing);
|
|
87
|
+
return record.rootId;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
patterns,
|
|
91
|
+
expand(graph, declaration, options = {}) {
|
|
92
|
+
return expandInto(graph, declaration, options.model ?? 'provenance', options.ownership ?? 'declaration', undefined, []);
|
|
93
|
+
},
|
|
94
|
+
expansions: (graph) => [...(records.get(graph) ?? [])],
|
|
95
|
+
inspect: (graph, instance) => (records.get(graph) ?? []).find((record) => record.instance === instance),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Which pattern instance owns a node, read from the graph alone.
|
|
100
|
+
*
|
|
101
|
+
* This is the Model B claim under test: an agent holding only the expanded graph — no
|
|
102
|
+
* toolkit, no expansion record, no build step — can still recover the grouping.
|
|
103
|
+
*/
|
|
104
|
+
export function nodesOfInstance(graph, instance) {
|
|
105
|
+
return graph
|
|
106
|
+
.listNodes()
|
|
107
|
+
.filter((node) => provenanceOf(node)?.instance === instance)
|
|
108
|
+
.map((node) => node.id);
|
|
109
|
+
}
|
|
110
|
+
export function instancesOfPattern(graph, pattern) {
|
|
111
|
+
const found = new Set();
|
|
112
|
+
for (const node of graph.listNodes()) {
|
|
113
|
+
const provenance = provenanceOf(node);
|
|
114
|
+
if (provenance?.pattern === pattern) {
|
|
115
|
+
found.add(provenance.instance);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...found];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Compares the graph against what expansion produced, property by property.
|
|
122
|
+
*
|
|
123
|
+
* Under `declaration` ownership this is the safety net: the declaration is the source of
|
|
124
|
+
* truth, so a hand-edited generated node will be silently overwritten on the next build
|
|
125
|
+
* unless something says so first. Under `graph` ownership drift is expected and this is
|
|
126
|
+
* merely a record of how far the graph has moved from its origin.
|
|
127
|
+
*
|
|
128
|
+
* It reports what changed rather than only that something did, because "your edit will be
|
|
129
|
+
* lost" is only actionable if it names the edit.
|
|
130
|
+
*/
|
|
131
|
+
export function detectDrift(graph, expansion) {
|
|
132
|
+
const drifts = [];
|
|
133
|
+
const report = (nodeId, property, expected, actual, message) => {
|
|
134
|
+
drifts.push({
|
|
135
|
+
code: 'TOOLKIT_EXPANSION_DRIFT',
|
|
136
|
+
instance: expansion.instance,
|
|
137
|
+
pattern: expansion.pattern,
|
|
138
|
+
nodeId,
|
|
139
|
+
property,
|
|
140
|
+
expected,
|
|
141
|
+
actual,
|
|
142
|
+
message,
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
for (const id of expansion.nodeIds) {
|
|
146
|
+
const current = graph.getNode(id);
|
|
147
|
+
const generated = expansion.generated[String(id)];
|
|
148
|
+
if (!current) {
|
|
149
|
+
report(id, 'removed', generated, undefined, `${String(id)} was generated by ${expansion.instance} and has been removed`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (!provenanceOf(current)) {
|
|
153
|
+
report(id, 'provenance-lost', expansion.instance, undefined, `${String(id)} no longer records which pattern generated it`);
|
|
154
|
+
}
|
|
155
|
+
const now = stripAuthoringMetadata(current);
|
|
156
|
+
const then = generated;
|
|
157
|
+
for (const key of new Set([...Object.keys(then), ...Object.keys(now)])) {
|
|
158
|
+
if (JSON.stringify(now[key]) !== JSON.stringify(then[key])) {
|
|
159
|
+
report(id, key, then[key], now[key], `${String(id)}.${key} differs from what ${expansion.pattern} generated`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return drifts;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Hands ownership of a pattern's generated nodes to the graph.
|
|
167
|
+
*
|
|
168
|
+
* The alternative to accidental drift. After this the declaration is history: the nodes stay
|
|
169
|
+
* exactly as they are, edits to them are legitimate, and re-expanding the declaration would
|
|
170
|
+
* be a mistake rather than a refresh. Provenance is kept — it still answers "where did this
|
|
171
|
+
* come from" — but re-marked so nothing treats the declaration as authoritative again.
|
|
172
|
+
*
|
|
173
|
+
* There is no un-detach. Recovering a declaration from an expanded graph is a different
|
|
174
|
+
* problem and this prototype does not attempt it.
|
|
175
|
+
*/
|
|
176
|
+
export function materializePattern(graph, expansion) {
|
|
177
|
+
for (const id of expansion.nodeIds) {
|
|
178
|
+
const node = graph.getNode(id);
|
|
179
|
+
if (!node) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const provenance = provenanceOf(node);
|
|
183
|
+
if (!provenance) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
graph.updateNode(withAuthoringMetadata(node, {
|
|
187
|
+
[PROVENANCE_KEY]: { ...provenance, ownership: 'graph' },
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
expansion.ownership = 'graph';
|
|
191
|
+
return expansion;
|
|
192
|
+
}
|
|
193
|
+
export function diffPatternExpansion(expansion, target, graphForTarget) {
|
|
194
|
+
target.expand(graphForTarget, expansion.declaration, { model: 'provenance', ownership: 'declaration' });
|
|
195
|
+
const after = target.inspect(graphForTarget, expansion.instance);
|
|
196
|
+
const before = expansion.generated;
|
|
197
|
+
const now = after?.generated ?? {};
|
|
198
|
+
const added = Object.keys(now).filter((id) => !(id in before));
|
|
199
|
+
const removed = Object.keys(before).filter((id) => !(id in now));
|
|
200
|
+
const changed = [];
|
|
201
|
+
for (const id of Object.keys(before).filter((entry) => entry in now)) {
|
|
202
|
+
const from = before[id];
|
|
203
|
+
const to = now[id];
|
|
204
|
+
for (const key of new Set([...Object.keys(from), ...Object.keys(to)])) {
|
|
205
|
+
if (JSON.stringify(from[key]) !== JSON.stringify(to[key])) {
|
|
206
|
+
changed.push({ nodeId: id, property: key, from: from[key], to: to[key] });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { added, removed, changed };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Strips every trace of the toolkit from an expanded graph.
|
|
214
|
+
*
|
|
215
|
+
* §37 requires that doing this changes nothing but toolkit-aware introspection, and §66
|
|
216
|
+
* requires the result still validate, compile, execute and render. A function that performs
|
|
217
|
+
* the removal is how both are tested rather than asserted.
|
|
218
|
+
*/
|
|
219
|
+
export function stripProvenance(graph) {
|
|
220
|
+
for (const node of graph.listNodes()) {
|
|
221
|
+
const stripped = stripAuthoringMetadata(node);
|
|
222
|
+
if (stripped !== node) {
|
|
223
|
+
graph.updateNode(stripped);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return graph;
|
|
227
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './pattern.js';
|
|
2
|
+
export * from './inference.js';
|
|
3
|
+
export * from './expand.js';
|
|
4
|
+
export * from './catalog.js';
|
|
5
|
+
export * from './queries.js';
|
|
6
|
+
export { page } from './patterns/page.js';
|
|
7
|
+
export { metricGrid } from './patterns/metric-grid.js';
|
|
8
|
+
export { entityList, rowRef, rowField } from './patterns/entity-list.js';
|
|
9
|
+
export { entityForm } from './patterns/entity-form.js';
|
|
10
|
+
export { actionBar } from './patterns/action-bar.js';
|
|
11
|
+
export { axiomUi } from './toolkit.js';
|
|
12
|
+
export type { PageDeclaration } from './patterns/page.js';
|
|
13
|
+
export type { MetricGridDeclaration, MetricDeclaration } from './patterns/metric-grid.js';
|
|
14
|
+
export type { EntityListDeclaration } from './patterns/entity-list.js';
|
|
15
|
+
export type { EntityFormDeclaration, EntityFormTarget } from './patterns/entity-form.js';
|
|
16
|
+
export type { ActionBarDeclaration } from './patterns/action-bar.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './pattern.js';
|
|
2
|
+
export * from './inference.js';
|
|
3
|
+
export * from './expand.js';
|
|
4
|
+
export * from './catalog.js';
|
|
5
|
+
export * from './queries.js';
|
|
6
|
+
export { page } from './patterns/page.js';
|
|
7
|
+
export { metricGrid } from './patterns/metric-grid.js';
|
|
8
|
+
export { entityList, rowRef, rowField } from './patterns/entity-list.js';
|
|
9
|
+
export { entityForm } from './patterns/entity-form.js';
|
|
10
|
+
export { actionBar } from './patterns/action-bar.js';
|
|
11
|
+
export { axiomUi } from './toolkit.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ActionDef, ApplicationGraph, EntityDef, FieldDef, FieldId, NodeId, StateDef, TypeRef, ValueFormat, ControlVariant } from '@cynodia/axiom-core';
|
|
2
|
+
/**
|
|
3
|
+
* What the toolkit reads out of the graph instead of asking the author to restate it.
|
|
4
|
+
*
|
|
5
|
+
* The rule (§34): **infer what Axiom already knows; require only application-specific UX
|
|
6
|
+
* choices.** Everything here is a read of an existing declaration — an entity's identity
|
|
7
|
+
* field, a field's type, an action's `destructive` flag. Nothing here guesses from a *name*,
|
|
8
|
+
* because a heuristic over names is precisely the kind of hidden rule that makes an agent's
|
|
9
|
+
* output unpredictable.
|
|
10
|
+
*/
|
|
11
|
+
export declare function stateOf(graph: ApplicationGraph, id: NodeId): StateDef | undefined;
|
|
12
|
+
export declare function entityOf(graph: ApplicationGraph, id: NodeId): EntityDef | undefined;
|
|
13
|
+
export declare function actionOf(graph: ApplicationGraph, id: NodeId): ActionDef | undefined;
|
|
14
|
+
/** The entity a collection state holds, if it holds one. */
|
|
15
|
+
export declare function memberEntityId(valueType: TypeRef): NodeId | undefined;
|
|
16
|
+
export declare function isCollection(valueType: TypeRef): boolean;
|
|
17
|
+
export declare function fieldOf(entity: EntityDef, fieldId: FieldId): FieldDef | undefined;
|
|
18
|
+
/** Unwraps `optional` so a nullable number is still a number for presentation purposes. */
|
|
19
|
+
export declare function baseType(valueType: TypeRef): TypeRef;
|
|
20
|
+
/**
|
|
21
|
+
* A value format from a declared type.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately conservative: it never infers `currency` or `percentage`, because nothing in
|
|
24
|
+
* a `number` says which — that is an application-specific UX choice and stays explicit.
|
|
25
|
+
* Guessing from a field named `price` is exactly the hidden heuristic §35 warns about.
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatFor(valueType: TypeRef): ValueFormat | undefined;
|
|
28
|
+
/** A control from a declared type. Absent means "let the runtime decide", which it can. */
|
|
29
|
+
export declare function controlFor(valueType: TypeRef): ControlVariant | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* A human label for a field.
|
|
32
|
+
*
|
|
33
|
+
* `name` is metadata authors already write, so the toolkit uses it and falls back to the id
|
|
34
|
+
* only when there is nothing else. It does **not** prettify an id into title case: a label
|
|
35
|
+
* invented from an identifier is a guess presented as a fact.
|
|
36
|
+
*/
|
|
37
|
+
export declare function labelFor(field: FieldDef): string | undefined;
|
|
38
|
+
/** Fields worth showing in a list when the author names none: every field but the identity. */
|
|
39
|
+
export declare function defaultListFields(entity: EntityDef): FieldId[];
|
|
40
|
+
/**
|
|
41
|
+
* Fields worth editing when the author names none.
|
|
42
|
+
*
|
|
43
|
+
* A create form offers **every** field, identity included; an edit form omits the identity,
|
|
44
|
+
* because an instance's identity is what addresses it and is not a thing to retype.
|
|
45
|
+
*
|
|
46
|
+
* Phase 1 had one rule for both and chose "omit the identity", by analogy with the list. That
|
|
47
|
+
* was wrong for creation, and wrong in the dangerous direction: the form rendered, validated,
|
|
48
|
+
* and then refused every submission for a value the author could not see was missing. Which
|
|
49
|
+
* mode a form is in cannot be inferred from a draft state, so it is declared — and the
|
|
50
|
+
* default is the one whose mistake is visible.
|
|
51
|
+
*/
|
|
52
|
+
export declare function defaultFormFields(entity: EntityDef, mode?: 'create' | 'edit'): FieldId[];
|
|
53
|
+
/** The UX role an action's own semantics imply. */
|
|
54
|
+
export declare function roleForAction(action: ActionDef, isPrimary: boolean): 'primary-action' | 'secondary-action' | 'destructive-action';
|
|
55
|
+
//# sourceMappingURL=inference.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the toolkit reads out of the graph instead of asking the author to restate it.
|
|
3
|
+
*
|
|
4
|
+
* The rule (§34): **infer what Axiom already knows; require only application-specific UX
|
|
5
|
+
* choices.** Everything here is a read of an existing declaration — an entity's identity
|
|
6
|
+
* field, a field's type, an action's `destructive` flag. Nothing here guesses from a *name*,
|
|
7
|
+
* because a heuristic over names is precisely the kind of hidden rule that makes an agent's
|
|
8
|
+
* output unpredictable.
|
|
9
|
+
*/
|
|
10
|
+
export function stateOf(graph, id) {
|
|
11
|
+
const node = graph.getNode(id);
|
|
12
|
+
return node?.kind === 'state' ? node : undefined;
|
|
13
|
+
}
|
|
14
|
+
export function entityOf(graph, id) {
|
|
15
|
+
const node = graph.getNode(id);
|
|
16
|
+
return node?.kind === 'entity' ? node : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function actionOf(graph, id) {
|
|
19
|
+
const node = graph.getNode(id);
|
|
20
|
+
return node?.kind === 'action' ? node : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** The entity a collection state holds, if it holds one. */
|
|
23
|
+
export function memberEntityId(valueType) {
|
|
24
|
+
if (valueType.kind === 'collection') {
|
|
25
|
+
return memberEntityId(valueType.itemType) ?? undefined;
|
|
26
|
+
}
|
|
27
|
+
if (valueType.kind === 'optional') {
|
|
28
|
+
return memberEntityId(valueType.valueType);
|
|
29
|
+
}
|
|
30
|
+
return valueType.kind === 'entity' ? valueType.entityId : undefined;
|
|
31
|
+
}
|
|
32
|
+
export function isCollection(valueType) {
|
|
33
|
+
return valueType.kind === 'collection' || (valueType.kind === 'optional' && isCollection(valueType.valueType));
|
|
34
|
+
}
|
|
35
|
+
export function fieldOf(entity, fieldId) {
|
|
36
|
+
return entity.fields.find((field) => field.id === fieldId);
|
|
37
|
+
}
|
|
38
|
+
/** Unwraps `optional` so a nullable number is still a number for presentation purposes. */
|
|
39
|
+
export function baseType(valueType) {
|
|
40
|
+
return valueType.kind === 'optional' ? baseType(valueType.valueType) : valueType;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A value format from a declared type.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately conservative: it never infers `currency` or `percentage`, because nothing in
|
|
46
|
+
* a `number` says which — that is an application-specific UX choice and stays explicit.
|
|
47
|
+
* Guessing from a field named `price` is exactly the hidden heuristic §35 warns about.
|
|
48
|
+
*/
|
|
49
|
+
export function formatFor(valueType) {
|
|
50
|
+
const base = baseType(valueType);
|
|
51
|
+
if (base.kind !== 'primitive') {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
switch (base.primitive) {
|
|
55
|
+
case 'number':
|
|
56
|
+
return { kind: 'number' };
|
|
57
|
+
case 'boolean':
|
|
58
|
+
return { kind: 'boolean' };
|
|
59
|
+
case 'date':
|
|
60
|
+
return { kind: 'date' };
|
|
61
|
+
case 'datetime':
|
|
62
|
+
return { kind: 'datetime' };
|
|
63
|
+
default:
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** A control from a declared type. Absent means "let the runtime decide", which it can. */
|
|
68
|
+
export function controlFor(valueType) {
|
|
69
|
+
const base = baseType(valueType);
|
|
70
|
+
if (base.kind === 'enum') {
|
|
71
|
+
return 'select';
|
|
72
|
+
}
|
|
73
|
+
if (base.kind !== 'primitive') {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return base.primitive === 'boolean' ? 'checkbox' : base.primitive === 'number' ? 'stepper' : undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* A human label for a field.
|
|
80
|
+
*
|
|
81
|
+
* `name` is metadata authors already write, so the toolkit uses it and falls back to the id
|
|
82
|
+
* only when there is nothing else. It does **not** prettify an id into title case: a label
|
|
83
|
+
* invented from an identifier is a guess presented as a fact.
|
|
84
|
+
*/
|
|
85
|
+
export function labelFor(field) {
|
|
86
|
+
return field.name;
|
|
87
|
+
}
|
|
88
|
+
/** Fields worth showing in a list when the author names none: every field but the identity. */
|
|
89
|
+
export function defaultListFields(entity) {
|
|
90
|
+
return entity.fields.filter((field) => field.id !== entity.identityFieldId).map((field) => field.id);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Fields worth editing when the author names none.
|
|
94
|
+
*
|
|
95
|
+
* A create form offers **every** field, identity included; an edit form omits the identity,
|
|
96
|
+
* because an instance's identity is what addresses it and is not a thing to retype.
|
|
97
|
+
*
|
|
98
|
+
* Phase 1 had one rule for both and chose "omit the identity", by analogy with the list. That
|
|
99
|
+
* was wrong for creation, and wrong in the dangerous direction: the form rendered, validated,
|
|
100
|
+
* and then refused every submission for a value the author could not see was missing. Which
|
|
101
|
+
* mode a form is in cannot be inferred from a draft state, so it is declared — and the
|
|
102
|
+
* default is the one whose mistake is visible.
|
|
103
|
+
*/
|
|
104
|
+
export function defaultFormFields(entity, mode = 'create') {
|
|
105
|
+
return mode === 'edit' ? defaultListFields(entity) : entity.fields.map((field) => field.id);
|
|
106
|
+
}
|
|
107
|
+
/** The UX role an action's own semantics imply. */
|
|
108
|
+
export function roleForAction(action, isPrimary) {
|
|
109
|
+
if (action.destructive) {
|
|
110
|
+
return 'destructive-action';
|
|
111
|
+
}
|
|
112
|
+
return isPrimary ? 'primary-action' : 'secondary-action';
|
|
113
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { ApplicationGraph, Expression, NodeId, Presentation, UINode } from '@cynodia/axiom-core';
|
|
2
|
+
/**
|
|
3
|
+
* The toolkit's contract with a pattern.
|
|
4
|
+
*
|
|
5
|
+
* A pattern is **not** a component. It has no runtime existence, renders nothing, and owns
|
|
6
|
+
* no state. It is a function from a declaration to canonical Axiom UI nodes, plus enough
|
|
7
|
+
* machine-readable description for an agent to use it without reading its implementation.
|
|
8
|
+
*
|
|
9
|
+
* The split matters: `inputs`, `slots` and `purpose` are **data** and are queryable through
|
|
10
|
+
* the catalog; `expand` is authoring-time TypeScript and never reaches a graph, an IR or a
|
|
11
|
+
* runtime. That is the hybrid answer to "are pattern definitions data or code" — the part an
|
|
12
|
+
* agent must discover is data, the part that only runs during authoring is code.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* User-visible text a pattern places in the graph.
|
|
16
|
+
*
|
|
17
|
+
* The rule 0.7 adopts (spec7 §29): **a pattern input carrying user-visible value text takes
|
|
18
|
+
* `string | Expression`** unless there is a concrete semantic reason it cannot. Phase 2 found
|
|
19
|
+
* `PageDeclaration.title: string` produced a detail page titled "Edit product" rather than
|
|
20
|
+
* the product's name — and the restriction was the pattern's, not Axiom's, because
|
|
21
|
+
* `TextNode.value` accepted an expression all along.
|
|
22
|
+
*/
|
|
23
|
+
export type PatternText = string | Expression;
|
|
24
|
+
/**
|
|
25
|
+
* A node's `name` is metadata for people and resolves nothing, so it only makes sense when
|
|
26
|
+
* the text is literal. An expression has no name to give.
|
|
27
|
+
*/
|
|
28
|
+
export declare function nameOf(text: PatternText | undefined): string | undefined;
|
|
29
|
+
/** How a pattern input is described to an agent that has never seen the pattern. */
|
|
30
|
+
export interface PatternInput {
|
|
31
|
+
/**
|
|
32
|
+
* `state` — a state id whose value the pattern reads.
|
|
33
|
+
* `entity` — an entity id.
|
|
34
|
+
* `field-list` — field ids of the entity in play.
|
|
35
|
+
* `action` / `action-list` — action ids the pattern binds controls to.
|
|
36
|
+
* `text` — a literal caption.
|
|
37
|
+
* `slot` — semantic UI content supplied by the caller (never markup).
|
|
38
|
+
* `nodes` — existing UI node ids to place.
|
|
39
|
+
* `flag` / `token` — a boolean or a presentation token.
|
|
40
|
+
*/
|
|
41
|
+
kind: 'state' | 'entity' | 'field-list' | 'action' | 'action-list' | 'text' | 'slot' | 'nodes' | 'flag' | 'token';
|
|
42
|
+
required: boolean;
|
|
43
|
+
/** What the pattern does with it. One sentence, for an agent choosing inputs. */
|
|
44
|
+
purpose: string;
|
|
45
|
+
/** What the pattern derives when the input is absent, if anything. */
|
|
46
|
+
inferredWhenAbsent?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface PatternDefinition<Declaration = Record<string, unknown>> {
|
|
49
|
+
name: string;
|
|
50
|
+
/**
|
|
51
|
+
* The pattern's own version, recorded in provenance.
|
|
52
|
+
*
|
|
53
|
+
* It is what makes a stored expansion reproducible and a toolkit upgrade diffable: without
|
|
54
|
+
* it, "expand this declaration again" means "expand it under whatever semantics are
|
|
55
|
+
* installed today", and an application changes shape on an unrelated `npm install`.
|
|
56
|
+
*/
|
|
57
|
+
version?: string;
|
|
58
|
+
/** What UX concept this compresses. */
|
|
59
|
+
purpose: string;
|
|
60
|
+
inputs: Record<string, PatternInput>;
|
|
61
|
+
/** Slot names, in the order a renderer would encounter them. */
|
|
62
|
+
slots: readonly string[];
|
|
63
|
+
/** What the expansion is guaranteed to produce, as canonical node kinds. */
|
|
64
|
+
produces: readonly UINode['kind'][];
|
|
65
|
+
/**
|
|
66
|
+
* The shape of the generated tree, part by part.
|
|
67
|
+
*
|
|
68
|
+
* A blind agent using the prototype read the pattern implementations despite the docs
|
|
69
|
+
* saying it should not need to — because the catalogue said what a pattern *takes* and not
|
|
70
|
+
* what it *builds*, and composing against a generated tree requires knowing its shape.
|
|
71
|
+
* Each entry is a `part` name (the same one provenance records), the node kind, and where
|
|
72
|
+
* it sits. Generated ids are `ui_<instance>_<part>`, so this doubles as the id an author
|
|
73
|
+
* can address before expansion has happened.
|
|
74
|
+
*/
|
|
75
|
+
expansion: readonly {
|
|
76
|
+
part: string;
|
|
77
|
+
kind: UINode['kind'];
|
|
78
|
+
role: string;
|
|
79
|
+
}[];
|
|
80
|
+
/**
|
|
81
|
+
* Checked before expansion, against the graph. Returning findings here is what makes a
|
|
82
|
+
* mistake point at `ProductList.fields[2]` rather than at a generated node id.
|
|
83
|
+
*/
|
|
84
|
+
check?(declaration: Declaration, context: CheckContext): PatternFinding[];
|
|
85
|
+
expand(declaration: Declaration, context: ExpansionContext): NodeId;
|
|
86
|
+
}
|
|
87
|
+
export interface PatternFinding {
|
|
88
|
+
code: string;
|
|
89
|
+
message: string;
|
|
90
|
+
severity: 'error' | 'warning';
|
|
91
|
+
/** Where in the declaration, e.g. `ProductList.fields[2]`. */
|
|
92
|
+
path: string;
|
|
93
|
+
}
|
|
94
|
+
export interface CheckContext {
|
|
95
|
+
graph: ApplicationGraph;
|
|
96
|
+
/** The declaration's own instance id, for building a `path`. */
|
|
97
|
+
instance: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* What a pattern is given while it expands.
|
|
101
|
+
*
|
|
102
|
+
* Everything a pattern adds to the graph goes through `add`, which is what makes provenance,
|
|
103
|
+
* deterministic identity and the expansion explanation possible without each pattern
|
|
104
|
+
* remembering to cooperate.
|
|
105
|
+
*/
|
|
106
|
+
export interface ExpansionContext extends CheckContext {
|
|
107
|
+
/** A deterministic id: same declaration and same part always yield the same id. */
|
|
108
|
+
id(part: string, index?: number): NodeId;
|
|
109
|
+
/** Adds a canonical UI node and returns its id. */
|
|
110
|
+
add<T extends UINode>(node: T, part: string): NodeId;
|
|
111
|
+
/** Records why the expansion chose something, for `inspectPattern`. */
|
|
112
|
+
explain(message: string): void;
|
|
113
|
+
/** Expands a nested pattern declaration and returns its root node id. */
|
|
114
|
+
child(declaration: PatternDeclaration): NodeId;
|
|
115
|
+
/** Slot content the caller supplied, already expanded to node ids. */
|
|
116
|
+
slot(name: string): NodeId[];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A pattern instance as the author writes it: **plain data**, no closures.
|
|
120
|
+
*
|
|
121
|
+
* Slots hold node ids or nested declarations — semantic content, never markup — which is
|
|
122
|
+
* what keeps a declaration serializable and analyzable.
|
|
123
|
+
*/
|
|
124
|
+
export interface PatternDeclaration {
|
|
125
|
+
pattern: string;
|
|
126
|
+
/** Stable, author-chosen, and the root of every generated id. */
|
|
127
|
+
instance: string;
|
|
128
|
+
[input: string]: unknown;
|
|
129
|
+
}
|
|
130
|
+
export type SlotContent = NodeId | PatternDeclaration;
|
|
131
|
+
/** Which of the three researched architectures an expansion runs under. */
|
|
132
|
+
export type ExpansionModel = 'macro' | 'provenance' | 'pattern-node';
|
|
133
|
+
/**
|
|
134
|
+
* Provenance: how a node was authored, never how it executes.
|
|
135
|
+
*
|
|
136
|
+
* It lives under core's reserved **authoring metadata** key, which means the compiler strips
|
|
137
|
+
* it from every artifact by default — client IR, server IR, generated page — and a tool that
|
|
138
|
+
* wants it asks for it. Phase 1 put it in plain `metadata` and it shipped to the browser;
|
|
139
|
+
* that was the leak, and this is the fix. Nothing about the fix is toolkit-specific.
|
|
140
|
+
*
|
|
141
|
+
* Everything in it is a stable, serializable string. Nothing here is a source location, an
|
|
142
|
+
* AST pointer or anything else that changes when a file is reformatted.
|
|
143
|
+
*/
|
|
144
|
+
export interface ToolkitProvenance {
|
|
145
|
+
toolkit: string;
|
|
146
|
+
pattern: string;
|
|
147
|
+
/** The pattern's own version, so a stored expansion can be reproduced or diffed. */
|
|
148
|
+
patternVersion: string;
|
|
149
|
+
instance: string;
|
|
150
|
+
/** Which part of the pattern this node is — `row`, `field`, `empty-state`. */
|
|
151
|
+
part: string;
|
|
152
|
+
index?: number;
|
|
153
|
+
/** The enclosing pattern instance, for nested patterns. */
|
|
154
|
+
parent?: string;
|
|
155
|
+
/** The full chain from outermost to nearest, so an ancestor is reachable in one read. */
|
|
156
|
+
ancestry?: string[];
|
|
157
|
+
/** Who owns this node now. See `Ownership`. */
|
|
158
|
+
ownership: Ownership;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Who decides what a generated node contains.
|
|
162
|
+
*
|
|
163
|
+
* - `declaration` — the pattern declaration is the source of truth. The graph is a build artifact, re-expansion is authoritative, and an edit to a generated node is drift.
|
|
164
|
+
* - `graph` — the expanded graph is the source of truth. The declaration is history, provenance is informational, and edits are legitimate.
|
|
165
|
+
*
|
|
166
|
+
* The choice is explicit at every entry point, because leaving it implicit is what makes
|
|
167
|
+
* "may I edit this node?" unanswerable.
|
|
168
|
+
*/
|
|
169
|
+
export type Ownership = 'declaration' | 'graph';
|
|
170
|
+
export declare const PROVENANCE_KEY = "toolkit";
|
|
171
|
+
export declare const TOOLKIT_NAME = "@cynodia/axiom-ui";
|
|
172
|
+
/**
|
|
173
|
+
* The semantics an expansion was produced under.
|
|
174
|
+
*
|
|
175
|
+
* Bumped whenever a pattern's expansion changes, which is what makes a stored expansion
|
|
176
|
+
* reproducible and an upgrade diffable: 0.7 changed three patterns — an edit mode, a label
|
|
177
|
+
* inferred from a state, a title that may be an expression — so an expansion recorded as
|
|
178
|
+
* `0.2.0` is not one this toolkit would produce. `diffPatternExpansion` is how an author sees
|
|
179
|
+
* the difference before adopting it.
|
|
180
|
+
*/
|
|
181
|
+
export declare const TOOLKIT_VERSION = "0.7.0";
|
|
182
|
+
export declare function provenanceOf(node: {
|
|
183
|
+
metadata?: Record<string, unknown>;
|
|
184
|
+
}): ToolkitProvenance | undefined;
|
|
185
|
+
/** The reserved key provenance is nested under, for tests that assert placement. */
|
|
186
|
+
export declare const AUTHORING_KEY = "axiomAuthoring";
|
|
187
|
+
/** Ids are derived from the instance and the part, never from a counter. */
|
|
188
|
+
export declare function partId(instance: string, part: string, index?: number): NodeId;
|
|
189
|
+
export declare function definePattern<Declaration>(definition: PatternDefinition<Declaration>): PatternDefinition<Declaration>;
|
|
190
|
+
/** Presentation helper: drops absent keys so a node carries no empty declaration. */
|
|
191
|
+
export declare function presentation(value: Presentation): Presentation | undefined;
|
|
192
|
+
//# sourceMappingURL=pattern.d.ts.map
|
package/dist/pattern.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AUTHORING_METADATA_KEY, authoringMetadata, nodeId } from '@cynodia/axiom-core';
|
|
2
|
+
/**
|
|
3
|
+
* A node's `name` is metadata for people and resolves nothing, so it only makes sense when
|
|
4
|
+
* the text is literal. An expression has no name to give.
|
|
5
|
+
*/
|
|
6
|
+
export function nameOf(text) {
|
|
7
|
+
return typeof text === 'string' ? text : undefined;
|
|
8
|
+
}
|
|
9
|
+
export const PROVENANCE_KEY = 'toolkit';
|
|
10
|
+
export const TOOLKIT_NAME = '@cynodia/axiom-ui';
|
|
11
|
+
/**
|
|
12
|
+
* The semantics an expansion was produced under.
|
|
13
|
+
*
|
|
14
|
+
* Bumped whenever a pattern's expansion changes, which is what makes a stored expansion
|
|
15
|
+
* reproducible and an upgrade diffable: 0.7 changed three patterns — an edit mode, a label
|
|
16
|
+
* inferred from a state, a title that may be an expression — so an expansion recorded as
|
|
17
|
+
* `0.2.0` is not one this toolkit would produce. `diffPatternExpansion` is how an author sees
|
|
18
|
+
* the difference before adopting it.
|
|
19
|
+
*/
|
|
20
|
+
export const TOOLKIT_VERSION = '0.7.0';
|
|
21
|
+
export function provenanceOf(node) {
|
|
22
|
+
const found = authoringMetadata(node)?.[PROVENANCE_KEY];
|
|
23
|
+
return found === undefined ? undefined : found;
|
|
24
|
+
}
|
|
25
|
+
/** The reserved key provenance is nested under, for tests that assert placement. */
|
|
26
|
+
export const AUTHORING_KEY = AUTHORING_METADATA_KEY;
|
|
27
|
+
/** Ids are derived from the instance and the part, never from a counter. */
|
|
28
|
+
export function partId(instance, part, index) {
|
|
29
|
+
const suffix = index === undefined ? '' : `_${index}`;
|
|
30
|
+
return nodeId(`ui_${instance}_${part}${suffix}`.replace(/[^a-zA-Z0-9_]/g, '_'));
|
|
31
|
+
}
|
|
32
|
+
export function definePattern(definition) {
|
|
33
|
+
return definition;
|
|
34
|
+
}
|
|
35
|
+
/** Presentation helper: drops absent keys so a node carries no empty declaration. */
|
|
36
|
+
export function presentation(value) {
|
|
37
|
+
const entries = Object.entries(value).filter(([, entry]) => entry !== undefined);
|
|
38
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
39
|
+
}
|