@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.
@@ -0,0 +1,469 @@
1
+ import { ApplicationGraph, binary, call, collectionType, entityType, expressionRef, field, fieldId, fieldLocation, filter, find, forEach, group, groupType, identitySelector, itemLocation, literal, map, nodeId, optionalType, primitiveType, ref, stateLocation, } from '@cynodia/axiom-core';
2
+ /**
3
+ * The order-management domain used by both research applications.
4
+ *
5
+ * **Business semantics only** — entities, state, actions, constraints. No UI, no
6
+ * presentation, no routes. Both the baseline application and the toolkit application build
7
+ * their UI on top of this identical graph, which is what makes the comparison a comparison of
8
+ * UI authoring rather than of two different applications.
9
+ */
10
+ export const ENTITY_PRODUCT = nodeId('entity_product');
11
+ export const F_PRODUCT_ID = fieldId('field_product_id');
12
+ export const F_PRODUCT_NAME = fieldId('field_product_name');
13
+ export const F_PRODUCT_PRICE = fieldId('field_product_price');
14
+ export const F_PRODUCT_STOCK = fieldId('field_product_stock');
15
+ export const F_PRODUCT_ACTIVE = fieldId('field_product_active');
16
+ export const ENTITY_CUSTOMER = nodeId('entity_customer');
17
+ export const F_CUSTOMER_ID = fieldId('field_customer_id');
18
+ export const F_CUSTOMER_NAME = fieldId('field_customer_name');
19
+ export const F_CUSTOMER_EMAIL = fieldId('field_customer_email');
20
+ export const F_CUSTOMER_SINCE = fieldId('field_customer_since');
21
+ export const ENTITY_ORDER = nodeId('entity_order');
22
+ export const F_ORDER_ID = fieldId('field_order_id');
23
+ export const F_ORDER_CUSTOMER = fieldId('field_order_customer');
24
+ export const F_ORDER_PRODUCT = fieldId('field_order_product');
25
+ export const F_ORDER_QUANTITY = fieldId('field_order_quantity');
26
+ export const F_ORDER_TOTAL = fieldId('field_order_total');
27
+ export const F_ORDER_STATUS = fieldId('field_order_status');
28
+ export const STATE_PRODUCTS = nodeId('state_products');
29
+ export const STATE_CUSTOMERS = nodeId('state_customers');
30
+ export const STATE_ORDERS = nodeId('state_orders');
31
+ export const STATE_DRAFT_PRODUCT = nodeId('state_draft_product');
32
+ export const STATE_DRAFT_ORDER = nodeId('state_draft_order');
33
+ export const STATE_DRAFT_CUSTOMER = nodeId('state_draft_customer');
34
+ export const STATE_ORDER_COUNT = nodeId('state_order_count');
35
+ export const STATE_REVENUE = nodeId('state_revenue');
36
+ export const STATE_LOW_STOCK = nodeId('state_low_stock');
37
+ export const STATE_LOW_STOCK_PRODUCTS = nodeId('state_low_stock_products');
38
+ /** Orders partitioned by status: a `group` expression, not four filters. */
39
+ export const STATE_ORDERS_BY_STATUS = nodeId('state_orders_by_status');
40
+ /** Which order the person has asked to cancel. Ephemeral: a UI fact, not a domain one. */
41
+ export const STATE_CANCELLING = nodeId('state_cancelling');
42
+ /** The reorder threshold, so the rule below reads it rather than hard-coding it. */
43
+ export const STATE_THRESHOLD = nodeId('state_threshold');
44
+ /**
45
+ * "the products at or below the reorder threshold", named once.
46
+ *
47
+ * Three consumers read it: the dashboard figure, the restock warning's visibility, and the
48
+ * guard on the bulk-restock action. Written out, that is the same filter three times with
49
+ * three scope ids — which is where the Phase 2 agent collided with itself.
50
+ */
51
+ export const EXPRESSION_LOW_STOCK = nodeId('expression_low_stock');
52
+ export const PARAM_STOCK_SOURCE = nodeId('param_stock_source');
53
+ export const ACTION_ADD_PRODUCT = nodeId('action_add_product');
54
+ export const ACTION_DELETE_PRODUCT = nodeId('action_delete_product');
55
+ export const ACTION_RESTOCK = nodeId('action_restock');
56
+ export const ACTION_ADD_CUSTOMER = nodeId('action_add_customer');
57
+ export const ACTION_PLACE_ORDER = nodeId('action_place_order');
58
+ export const ACTION_CONFIRM_ORDER = nodeId('action_confirm_order');
59
+ export const ACTION_CANCEL_ORDER = nodeId('action_cancel_order');
60
+ export const ACTION_ASK_CANCEL = nodeId('action_ask_cancel');
61
+ export const ACTION_DISMISS_CANCEL = nodeId('action_dismiss_cancel');
62
+ export const ACTION_RESTOCK_ALL = nodeId('action_restock_all');
63
+ export const PARAM_PRODUCT = nodeId('param_product');
64
+ export const PARAM_ORDER = nodeId('param_order');
65
+ export const PARAM_AMOUNT = nodeId('param_amount');
66
+ const SCOPE_PRODUCT = nodeId('scope_product');
67
+ const SCOPE_LOW_STOCK = nodeId('scope_low_stock');
68
+ const SCOPE_STATUS = nodeId('scope_status');
69
+ const SCOPE_RESTOCK = nodeId('scope_restock');
70
+ const SCOPE_ORDER = nodeId('scope_order');
71
+ const SCOPE_LOW = nodeId('scope_low');
72
+ const SCOPE_TOTAL = nodeId('scope_total');
73
+ const CONSTRAINT_STOCK = nodeId('constraint_stock');
74
+ const CONSTRAINT_QUANTITY = nodeId('constraint_quantity');
75
+ const TRANSITION_CONFIRMED = nodeId('transition_confirmed');
76
+ const SCOPE_PREVIOUS = nodeId('scope_previous');
77
+ const SCOPE_PROPOSED = nodeId('scope_proposed');
78
+ const productById = (id) => find(ref(STATE_PRODUCTS), SCOPE_PRODUCT, binary('eq', field(ref(SCOPE_PRODUCT), F_PRODUCT_ID), id));
79
+ /** The one calculation, referenced. Every consumer says this and nothing more. */
80
+ export const lowStock = () => expressionRef(EXPRESSION_LOW_STOCK, { [PARAM_STOCK_SOURCE]: ref(STATE_PRODUCTS) });
81
+ /** Entities, state, behaviour and rules. Nothing here knows a UI exists. */
82
+ export function createOrderDomain() {
83
+ const graph = new ApplicationGraph('order-desk', 'Order Desk');
84
+ graph.addNode({
85
+ id: ENTITY_PRODUCT,
86
+ kind: 'entity',
87
+ name: 'Product',
88
+ identityFieldId: F_PRODUCT_ID,
89
+ fields: [
90
+ { id: F_PRODUCT_ID, name: 'Code', valueType: primitiveType('string'), required: true },
91
+ { id: F_PRODUCT_NAME, name: 'Name', valueType: primitiveType('string'), required: true },
92
+ { id: F_PRODUCT_PRICE, name: 'Unit price', valueType: primitiveType('number'), required: true },
93
+ { id: F_PRODUCT_STOCK, name: 'On hand', valueType: primitiveType('number'), required: true },
94
+ { id: F_PRODUCT_ACTIVE, name: 'Active', valueType: primitiveType('boolean') },
95
+ ],
96
+ });
97
+ graph.addNode({
98
+ id: ENTITY_CUSTOMER,
99
+ kind: 'entity',
100
+ name: 'Customer',
101
+ identityFieldId: F_CUSTOMER_ID,
102
+ fields: [
103
+ { id: F_CUSTOMER_ID, name: 'Reference', valueType: primitiveType('string'), required: true },
104
+ { id: F_CUSTOMER_NAME, name: 'Name', valueType: primitiveType('string'), required: true },
105
+ { id: F_CUSTOMER_EMAIL, name: 'Email', valueType: primitiveType('string'), required: true },
106
+ { id: F_CUSTOMER_SINCE, name: 'Customer since', valueType: primitiveType('date') },
107
+ ],
108
+ });
109
+ graph.addNode({
110
+ id: ENTITY_ORDER,
111
+ kind: 'entity',
112
+ name: 'Order',
113
+ identityFieldId: F_ORDER_ID,
114
+ fields: [
115
+ { id: F_ORDER_ID, name: 'Number', valueType: primitiveType('string'), required: true },
116
+ { id: F_ORDER_CUSTOMER, name: 'Customer', valueType: primitiveType('string'), required: true },
117
+ { id: F_ORDER_PRODUCT, name: 'Product', valueType: primitiveType('string'), required: true },
118
+ { id: F_ORDER_QUANTITY, name: 'Quantity', valueType: primitiveType('number'), required: true },
119
+ { id: F_ORDER_TOTAL, name: 'Total', valueType: primitiveType('number'), required: true },
120
+ { id: F_ORDER_STATUS, name: 'Status', valueType: primitiveType('string'), required: true },
121
+ ],
122
+ });
123
+ graph.addNode({
124
+ id: STATE_PRODUCTS,
125
+ kind: 'state',
126
+ name: 'Products',
127
+ valueType: collectionType(entityType(ENTITY_PRODUCT)),
128
+ initialValue: [
129
+ { [F_PRODUCT_ID]: 'bolt', [F_PRODUCT_NAME]: 'Hex bolt', [F_PRODUCT_PRICE]: 12.5, [F_PRODUCT_STOCK]: 40, [F_PRODUCT_ACTIVE]: true },
130
+ { [F_PRODUCT_ID]: 'nut', [F_PRODUCT_NAME]: 'Hex nut', [F_PRODUCT_PRICE]: 4.25, [F_PRODUCT_STOCK]: 2, [F_PRODUCT_ACTIVE]: true },
131
+ { [F_PRODUCT_ID]: 'washer', [F_PRODUCT_NAME]: 'Flat washer', [F_PRODUCT_PRICE]: 1.75, [F_PRODUCT_STOCK]: 500, [F_PRODUCT_ACTIVE]: false },
132
+ ],
133
+ });
134
+ graph.addNode({
135
+ id: STATE_CUSTOMERS,
136
+ kind: 'state',
137
+ name: 'Customers',
138
+ valueType: collectionType(entityType(ENTITY_CUSTOMER)),
139
+ initialValue: [
140
+ { [F_CUSTOMER_ID]: 'c1', [F_CUSTOMER_NAME]: 'Nordvik AS', [F_CUSTOMER_EMAIL]: 'post@nordvik.example', [F_CUSTOMER_SINCE]: '2024-03-01' },
141
+ { [F_CUSTOMER_ID]: 'c2', [F_CUSTOMER_NAME]: 'Fjell Bygg', [F_CUSTOMER_EMAIL]: 'kontakt@fjell.example', [F_CUSTOMER_SINCE]: '2025-11-14' },
142
+ ],
143
+ });
144
+ graph.addNode({
145
+ id: STATE_ORDERS,
146
+ kind: 'state',
147
+ name: 'Orders',
148
+ valueType: collectionType(entityType(ENTITY_ORDER)),
149
+ initialValue: [],
150
+ });
151
+ graph.addNode({
152
+ id: STATE_DRAFT_PRODUCT,
153
+ kind: 'state',
154
+ name: 'New product',
155
+ draft: true,
156
+ valueType: entityType(ENTITY_PRODUCT),
157
+ initialValue: { [F_PRODUCT_ID]: '', [F_PRODUCT_NAME]: '', [F_PRODUCT_PRICE]: 0, [F_PRODUCT_STOCK]: 0, [F_PRODUCT_ACTIVE]: true },
158
+ });
159
+ graph.addNode({
160
+ id: STATE_DRAFT_ORDER,
161
+ kind: 'state',
162
+ name: 'New order',
163
+ draft: true,
164
+ valueType: entityType(ENTITY_ORDER),
165
+ initialValue: {
166
+ [F_ORDER_ID]: '',
167
+ [F_ORDER_CUSTOMER]: '',
168
+ [F_ORDER_PRODUCT]: '',
169
+ [F_ORDER_QUANTITY]: 1,
170
+ [F_ORDER_TOTAL]: 0,
171
+ [F_ORDER_STATUS]: 'draft',
172
+ },
173
+ });
174
+ graph.addNode({
175
+ id: STATE_DRAFT_CUSTOMER,
176
+ kind: 'state',
177
+ name: 'New customer',
178
+ draft: true,
179
+ valueType: entityType(ENTITY_CUSTOMER),
180
+ initialValue: {
181
+ [F_CUSTOMER_ID]: '',
182
+ [F_CUSTOMER_NAME]: '',
183
+ [F_CUSTOMER_EMAIL]: '',
184
+ [F_CUSTOMER_SINCE]: '2026-01-01',
185
+ },
186
+ });
187
+ graph.addNode({
188
+ id: STATE_ORDER_COUNT,
189
+ kind: 'state',
190
+ name: 'Order count',
191
+ valueType: primitiveType('number'),
192
+ derivation: call('count', ref(STATE_ORDERS)),
193
+ });
194
+ graph.addNode({
195
+ id: STATE_REVENUE,
196
+ kind: 'state',
197
+ name: 'Revenue',
198
+ valueType: primitiveType('number'),
199
+ derivation: call('sum', map(ref(STATE_ORDERS), SCOPE_TOTAL, field(ref(SCOPE_TOTAL), F_ORDER_TOTAL))),
200
+ });
201
+ graph.addNode({
202
+ id: STATE_THRESHOLD,
203
+ kind: 'state',
204
+ name: 'Reorder threshold',
205
+ valueType: primitiveType('number'),
206
+ initialValue: 5,
207
+ });
208
+ /**
209
+ * The calculation exists **once**, as a node.
210
+ *
211
+ * Its parameter is the collection to look in, so the definition says nothing about where
212
+ * the products live; its body reads the threshold from state. Isolated scope: `SCOPE_LOW`
213
+ * belongs to this definition and can never meet a caller's iteration scope.
214
+ */
215
+ graph.addNode({
216
+ id: EXPRESSION_LOW_STOCK,
217
+ kind: 'expression',
218
+ name: 'Low stock',
219
+ description: 'Products at or below the reorder threshold.',
220
+ parameters: [
221
+ { id: PARAM_STOCK_SOURCE, name: 'products', valueType: collectionType(entityType(ENTITY_PRODUCT)) },
222
+ ],
223
+ expression: filter(ref(PARAM_STOCK_SOURCE), SCOPE_LOW, binary('lte', field(ref(SCOPE_LOW), F_PRODUCT_STOCK), ref(STATE_THRESHOLD))),
224
+ });
225
+ graph.addNode({
226
+ id: STATE_LOW_STOCK,
227
+ kind: 'state',
228
+ name: 'Low stock',
229
+ valueType: primitiveType('number'),
230
+ derivation: call('count', lowStock()),
231
+ });
232
+ // The same calculation again, as a collection this time. Two consumers, one definition.
233
+ graph.addNode({
234
+ id: STATE_LOW_STOCK_PRODUCTS,
235
+ kind: 'state',
236
+ name: 'Needs restocking',
237
+ valueType: collectionType(entityType(ENTITY_PRODUCT)),
238
+ derivation: lowStock(),
239
+ });
240
+ /**
241
+ * Orders by status, as a semantic partition.
242
+ *
243
+ * Before `group`, this was four filters over four statuses known at authoring time — which
244
+ * could not have handled a status the domain gained later, and could not have grouped by
245
+ * customer at all.
246
+ */
247
+ graph.addNode({
248
+ id: STATE_ORDERS_BY_STATUS,
249
+ kind: 'state',
250
+ name: 'Orders by status',
251
+ valueType: collectionType(groupType(primitiveType('string'), entityType(ENTITY_ORDER))),
252
+ derivation: group(ref(STATE_ORDERS), SCOPE_STATUS, field(ref(SCOPE_STATUS), F_ORDER_STATUS)),
253
+ });
254
+ graph.addNode({
255
+ id: STATE_CANCELLING,
256
+ kind: 'state',
257
+ name: 'Order awaiting cancellation',
258
+ // A UI fact, not a domain fact: instance validation skips it and it may not be persisted.
259
+ ephemeral: true,
260
+ valueType: primitiveType('string'),
261
+ initialValue: '',
262
+ });
263
+ graph.addNode({
264
+ id: ACTION_ADD_PRODUCT,
265
+ kind: 'action',
266
+ name: 'Add product',
267
+ guards: [
268
+ {
269
+ condition: call('non-empty', field(ref(STATE_DRAFT_PRODUCT), F_PRODUCT_NAME)),
270
+ failureMode: { code: 'name-required', message: 'A product needs a name.' },
271
+ },
272
+ {
273
+ condition: binary('gt', field(ref(STATE_DRAFT_PRODUCT), F_PRODUCT_PRICE), literal(0)),
274
+ failureMode: { code: 'price-required', message: 'A product needs a price above zero.' },
275
+ },
276
+ ],
277
+ operations: [{ kind: 'insert', target: stateLocation(STATE_PRODUCTS), value: ref(STATE_DRAFT_PRODUCT) }],
278
+ });
279
+ graph.addNode({
280
+ id: ACTION_DELETE_PRODUCT,
281
+ kind: 'action',
282
+ name: 'Delete product',
283
+ destructive: true,
284
+ requiresConfirmation: true,
285
+ confirmationMessage: 'Delete this product?',
286
+ parameters: [{ id: PARAM_PRODUCT, valueType: primitiveType('string'), required: true }],
287
+ operations: [
288
+ {
289
+ kind: 'remove',
290
+ target: itemLocation(stateLocation(STATE_PRODUCTS), identitySelector(F_PRODUCT_ID, ref(PARAM_PRODUCT))),
291
+ },
292
+ ],
293
+ });
294
+ graph.addNode({
295
+ id: ACTION_RESTOCK,
296
+ kind: 'action',
297
+ name: 'Restock',
298
+ parameters: [
299
+ { id: PARAM_PRODUCT, valueType: primitiveType('string'), required: true },
300
+ { id: PARAM_AMOUNT, valueType: primitiveType('number'), required: true },
301
+ ],
302
+ guards: [
303
+ {
304
+ condition: binary('gt', ref(PARAM_AMOUNT), literal(0)),
305
+ failureMode: { code: 'invalid-amount', message: 'Restock by a positive amount.' },
306
+ },
307
+ ],
308
+ operations: [
309
+ {
310
+ kind: 'set',
311
+ target: fieldLocation(itemLocation(stateLocation(STATE_PRODUCTS), identitySelector(F_PRODUCT_ID, ref(PARAM_PRODUCT))), F_PRODUCT_STOCK),
312
+ value: binary('add', field(productById(ref(PARAM_PRODUCT)), F_PRODUCT_STOCK), ref(PARAM_AMOUNT)),
313
+ },
314
+ ],
315
+ });
316
+ graph.addNode({
317
+ id: ACTION_ADD_CUSTOMER,
318
+ kind: 'action',
319
+ name: 'Add customer',
320
+ guards: [
321
+ {
322
+ condition: call('non-empty', field(ref(STATE_DRAFT_CUSTOMER), F_CUSTOMER_NAME)),
323
+ failureMode: { code: 'name-required', message: 'A customer needs a name.' },
324
+ },
325
+ {
326
+ condition: call('non-empty', field(ref(STATE_DRAFT_CUSTOMER), F_CUSTOMER_EMAIL)),
327
+ failureMode: { code: 'email-required', message: 'A customer needs an email address.' },
328
+ },
329
+ ],
330
+ operations: [{ kind: 'insert', target: stateLocation(STATE_CUSTOMERS), value: ref(STATE_DRAFT_CUSTOMER) }],
331
+ });
332
+ graph.addNode({
333
+ id: ACTION_PLACE_ORDER,
334
+ kind: 'action',
335
+ name: 'Place order',
336
+ guards: [
337
+ {
338
+ condition: call('non-empty', field(ref(STATE_DRAFT_ORDER), F_ORDER_CUSTOMER)),
339
+ failureMode: { code: 'customer-required', message: 'Choose a customer.' },
340
+ },
341
+ {
342
+ condition: binary('gte', field(productById(field(ref(STATE_DRAFT_ORDER), F_ORDER_PRODUCT)), F_PRODUCT_STOCK), field(ref(STATE_DRAFT_ORDER), F_ORDER_QUANTITY)),
343
+ failureMode: { code: 'insufficient-stock', message: 'Not enough stock for that quantity.' },
344
+ },
345
+ ],
346
+ operations: [
347
+ { kind: 'insert', target: stateLocation(STATE_ORDERS), value: ref(STATE_DRAFT_ORDER) },
348
+ {
349
+ kind: 'set',
350
+ target: fieldLocation(itemLocation(stateLocation(STATE_PRODUCTS), identitySelector(F_PRODUCT_ID, field(ref(STATE_DRAFT_ORDER), F_ORDER_PRODUCT))), F_PRODUCT_STOCK),
351
+ value: binary('subtract', field(productById(field(ref(STATE_DRAFT_ORDER), F_ORDER_PRODUCT)), F_PRODUCT_STOCK), field(ref(STATE_DRAFT_ORDER), F_ORDER_QUANTITY)),
352
+ },
353
+ ],
354
+ });
355
+ graph.addNode({
356
+ id: ACTION_CONFIRM_ORDER,
357
+ kind: 'action',
358
+ name: 'Confirm order',
359
+ parameters: [{ id: PARAM_ORDER, valueType: primitiveType('string'), required: true }],
360
+ operations: [
361
+ {
362
+ kind: 'set',
363
+ target: fieldLocation(itemLocation(stateLocation(STATE_ORDERS), identitySelector(F_ORDER_ID, ref(PARAM_ORDER))), F_ORDER_STATUS),
364
+ value: literal('confirmed'),
365
+ },
366
+ ],
367
+ });
368
+ graph.addNode({
369
+ id: ACTION_CANCEL_ORDER,
370
+ kind: 'action',
371
+ name: 'Cancel order',
372
+ destructive: true,
373
+ parameters: [{ id: PARAM_ORDER, valueType: primitiveType('string'), required: true }],
374
+ guards: [
375
+ {
376
+ // The rule, not the dialog: an invocation that never went through the confirmation
377
+ // is refused here, whatever the UI did or did not render.
378
+ condition: binary('eq', ref(STATE_CANCELLING), ref(PARAM_ORDER)),
379
+ failureMode: { code: 'not-confirmed', message: 'Confirm the cancellation first.' },
380
+ },
381
+ ],
382
+ operations: [
383
+ {
384
+ kind: 'remove',
385
+ target: itemLocation(stateLocation(STATE_ORDERS), identitySelector(F_ORDER_ID, ref(PARAM_ORDER))),
386
+ },
387
+ // Closing is this action's business: it cancelled the order, so the dialog is done.
388
+ { kind: 'set', target: stateLocation(STATE_CANCELLING), value: literal('') },
389
+ ],
390
+ });
391
+ /**
392
+ * Bulk restock, guarded by the same named calculation the dashboard displays.
393
+ *
394
+ * The guard cannot disagree with the figure, because there is only one calculation.
395
+ */
396
+ graph.addNode({
397
+ id: ACTION_RESTOCK_ALL,
398
+ kind: 'action',
399
+ name: 'Restock everything low',
400
+ parameters: [{ id: PARAM_AMOUNT, valueType: primitiveType('number'), required: true }],
401
+ guards: [
402
+ {
403
+ condition: call('non-empty', lowStock()),
404
+ failureMode: { code: 'nothing-low', message: 'Nothing is at or below the threshold.' },
405
+ },
406
+ ],
407
+ operations: [
408
+ forEach(lowStock(), SCOPE_RESTOCK, [
409
+ {
410
+ kind: 'set',
411
+ target: fieldLocation(itemLocation(stateLocation(STATE_PRODUCTS), identitySelector(F_PRODUCT_ID, field(ref(SCOPE_RESTOCK), F_PRODUCT_ID))), F_PRODUCT_STOCK),
412
+ value: binary('add', field(ref(SCOPE_RESTOCK), F_PRODUCT_STOCK), ref(PARAM_AMOUNT)),
413
+ },
414
+ ]),
415
+ ],
416
+ });
417
+ /**
418
+ * Asking to cancel is not cancelling.
419
+ *
420
+ * Opening the dialog writes ephemeral state; the destructive action is guarded on that
421
+ * state, so invoking it without having asked is refused. Presentation never authorizes —
422
+ * a hidden or unrendered control is not a rule, and this is the rule.
423
+ */
424
+ graph.addNode({
425
+ id: ACTION_ASK_CANCEL,
426
+ kind: 'action',
427
+ name: 'Cancel order…',
428
+ parameters: [{ id: PARAM_ORDER, valueType: primitiveType('string'), required: true }],
429
+ operations: [{ kind: 'set', target: stateLocation(STATE_CANCELLING), value: ref(PARAM_ORDER) }],
430
+ });
431
+ graph.addNode({
432
+ id: ACTION_DISMISS_CANCEL,
433
+ kind: 'action',
434
+ name: 'Keep order',
435
+ operations: [{ kind: 'set', target: stateLocation(STATE_CANCELLING), value: literal('') }],
436
+ });
437
+ graph.addNode({
438
+ id: CONSTRAINT_STOCK,
439
+ kind: 'constraint',
440
+ name: 'Stock is never negative',
441
+ entityId: ENTITY_PRODUCT,
442
+ message: 'Stock can never fall below zero.',
443
+ expression: binary('gte', field(ref(ENTITY_PRODUCT), F_PRODUCT_STOCK), literal(0)),
444
+ });
445
+ graph.addNode({
446
+ id: CONSTRAINT_QUANTITY,
447
+ kind: 'constraint',
448
+ name: 'An order is for at least one unit',
449
+ entityId: ENTITY_ORDER,
450
+ message: 'An order must be for at least one unit.',
451
+ expression: binary('gte', field(ref(ENTITY_ORDER), F_ORDER_QUANTITY), literal(1)),
452
+ });
453
+ graph.addNode({
454
+ id: TRANSITION_CONFIRMED,
455
+ kind: 'transition-constraint',
456
+ name: 'A confirmed order never changes quantity',
457
+ entityId: ENTITY_ORDER,
458
+ previousScopeId: SCOPE_PREVIOUS,
459
+ proposedScopeId: SCOPE_PROPOSED,
460
+ message: 'A confirmed order cannot change quantity.',
461
+ expression: binary('or', binary('neq', field(ref(SCOPE_PREVIOUS), F_ORDER_STATUS), literal('confirmed')), binary('eq', field(ref(SCOPE_PROPOSED), F_ORDER_QUANTITY), field(ref(SCOPE_PREVIOUS), F_ORDER_QUANTITY))),
462
+ });
463
+ return graph;
464
+ }
465
+ /** Unused import guard: `optionalType`, `map` and `SCOPE_ORDER` are kept for domain growth. */
466
+ void optionalType;
467
+ void SCOPE_ORDER;
468
+ void map;
469
+ void SCOPE_LOW_STOCK;
@@ -0,0 +1,3 @@
1
+ export * from './domain.js';
2
+ export { createToolkitApplication } from './app.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export * from './domain.js';
2
+ export { createToolkitApplication } from './app.js';
@@ -0,0 +1,119 @@
1
+ import type { ApplicationGraph, NodeId, UINode } from '@cynodia/axiom-core';
2
+ import type { ExpansionModel, Ownership, PatternDeclaration, PatternDefinition, PatternFinding } from './pattern.js';
3
+ /**
4
+ * Expansion: a pattern declaration becomes canonical Axiom UI, and nothing else happens.
5
+ *
6
+ * No node kind is invented, no runtime is told anything, and the graph that comes out is a
7
+ * graph an author could have written by hand. The three models differ only in what, if
8
+ * anything, is remembered about where the nodes came from.
9
+ */
10
+ export interface ExpansionOptions {
11
+ /** Default: `provenance`. `macro` records nothing; `pattern-node` is the Model C probe. */
12
+ model?: ExpansionModel;
13
+ /**
14
+ * Who owns the generated nodes afterwards. Default `declaration`: the toolkit is an
15
+ * authoring layer, so the declaration is the source of truth and re-expansion is
16
+ * authoritative. Pass `graph` to expand once and own the result.
17
+ */
18
+ ownership?: Ownership;
19
+ }
20
+ /** What one pattern instance produced, for `inspectPattern` and for diagnostics. */
21
+ export interface PatternExpansion {
22
+ instance: string;
23
+ pattern: string;
24
+ declaration: PatternDeclaration;
25
+ ownership: Ownership;
26
+ patternVersion: string;
27
+ rootId: NodeId;
28
+ nodeIds: NodeId[];
29
+ /** Every generated node as expansion produced it, for drift comparison. */
30
+ generated: Record<string, UINode>;
31
+ /** Why the expansion chose what it chose, in the order it decided. */
32
+ explanations: string[];
33
+ findings: PatternFinding[];
34
+ parent?: string;
35
+ }
36
+ export declare class PatternExpansionError extends Error {
37
+ readonly findings: PatternFinding[];
38
+ constructor(findings: PatternFinding[]);
39
+ }
40
+ export interface Toolkit {
41
+ /** Every pattern this toolkit offers, by name. */
42
+ readonly patterns: ReadonlyMap<string, PatternDefinition<never>>;
43
+ expand(graph: ApplicationGraph, declaration: PatternDeclaration, options?: ExpansionOptions): NodeId;
44
+ /** Everything expanded into this graph, newest last. */
45
+ expansions(graph: ApplicationGraph): PatternExpansion[];
46
+ inspect(graph: ApplicationGraph, instance: string): PatternExpansion | undefined;
47
+ }
48
+ export declare function createToolkit(definitions: readonly PatternDefinition<never>[]): Toolkit;
49
+ /**
50
+ * Which pattern instance owns a node, read from the graph alone.
51
+ *
52
+ * This is the Model B claim under test: an agent holding only the expanded graph — no
53
+ * toolkit, no expansion record, no build step — can still recover the grouping.
54
+ */
55
+ export declare function nodesOfInstance(graph: ApplicationGraph, instance: string): NodeId[];
56
+ export declare function instancesOfPattern(graph: ApplicationGraph, pattern: string): string[];
57
+ /** One way a generated node no longer matches what expansion produced. */
58
+ export interface ExpansionDrift {
59
+ code: 'TOOLKIT_EXPANSION_DRIFT';
60
+ instance: string;
61
+ pattern: string;
62
+ nodeId: NodeId;
63
+ /** `removed`, `provenance-lost`, or the property that differs. */
64
+ property: string;
65
+ expected: unknown;
66
+ actual: unknown;
67
+ message: string;
68
+ }
69
+ /**
70
+ * Compares the graph against what expansion produced, property by property.
71
+ *
72
+ * Under `declaration` ownership this is the safety net: the declaration is the source of
73
+ * truth, so a hand-edited generated node will be silently overwritten on the next build
74
+ * unless something says so first. Under `graph` ownership drift is expected and this is
75
+ * merely a record of how far the graph has moved from its origin.
76
+ *
77
+ * It reports what changed rather than only that something did, because "your edit will be
78
+ * lost" is only actionable if it names the edit.
79
+ */
80
+ export declare function detectDrift(graph: ApplicationGraph, expansion: PatternExpansion): ExpansionDrift[];
81
+ /**
82
+ * Hands ownership of a pattern's generated nodes to the graph.
83
+ *
84
+ * The alternative to accidental drift. After this the declaration is history: the nodes stay
85
+ * exactly as they are, edits to them are legitimate, and re-expanding the declaration would
86
+ * be a mistake rather than a refresh. Provenance is kept — it still answers "where did this
87
+ * come from" — but re-marked so nothing treats the declaration as authoritative again.
88
+ *
89
+ * There is no un-detach. Recovering a declaration from an expanded graph is a different
90
+ * problem and this prototype does not attempt it.
91
+ */
92
+ export declare function materializePattern(graph: ApplicationGraph, expansion: PatternExpansion): PatternExpansion;
93
+ /**
94
+ * What would change if a declaration were expanded under a different toolkit.
95
+ *
96
+ * A toolkit upgrade must not silently reshape an application. This expands the same
97
+ * declaration into a throwaway graph under the target toolkit and reports the difference, so
98
+ * an upgrade is something an author approves rather than something an `npm install` performs.
99
+ */
100
+ export interface ExpansionDiff {
101
+ added: NodeId[];
102
+ removed: NodeId[];
103
+ changed: Array<{
104
+ nodeId: NodeId;
105
+ property: string;
106
+ from: unknown;
107
+ to: unknown;
108
+ }>;
109
+ }
110
+ export declare function diffPatternExpansion(expansion: PatternExpansion, target: Toolkit, graphForTarget: ApplicationGraph): ExpansionDiff;
111
+ /**
112
+ * Strips every trace of the toolkit from an expanded graph.
113
+ *
114
+ * §37 requires that doing this changes nothing but toolkit-aware introspection, and §66
115
+ * requires the result still validate, compile, execute and render. A function that performs
116
+ * the removal is how both are tested rather than asserted.
117
+ */
118
+ export declare function stripProvenance(graph: ApplicationGraph): ApplicationGraph;
119
+ //# sourceMappingURL=expand.d.ts.map