interaqt 2.0.0-alpha.0 → 2.0.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.
Files changed (91) hide show
  1. package/README.md +1 -0
  2. package/agent/agentspace/knowledge/generator/api-reference.md +28 -5
  3. package/agent/agentspace/knowledge/generator/computation-implementation.md +61 -14
  4. package/agent/agentspace/knowledge/usage/00-mindset-shift.md +2 -0
  5. package/agent/agentspace/knowledge/usage/04-reactive-computations.md +158 -101
  6. package/agent/agentspace/knowledge/usage/05-interactions.md +67 -22
  7. package/agent/agentspace/knowledge/usage/08-activities.md +10 -0
  8. package/agent/agentspace/knowledge/usage/09-filtered-entities.md +1 -0
  9. package/agent/agentspace/knowledge/usage/13-testing.md +48 -97
  10. package/agent/agentspace/knowledge/usage/14-api-reference.md +22 -5
  11. package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +89 -28
  12. package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +11 -2
  13. package/dist/builtins/interaction/Action.d.ts.map +1 -1
  14. package/dist/builtins/interaction/Activity.d.ts.map +1 -1
  15. package/dist/builtins/interaction/Attributive.d.ts.map +1 -1
  16. package/dist/builtins/interaction/Condition.d.ts.map +1 -1
  17. package/dist/builtins/interaction/Conditions.d.ts.map +1 -1
  18. package/dist/builtins/interaction/Data.d.ts.map +1 -1
  19. package/dist/builtins/interaction/Event.d.ts.map +1 -1
  20. package/dist/builtins/interaction/Gateway.d.ts.map +1 -1
  21. package/dist/builtins/interaction/Interaction.d.ts.map +1 -1
  22. package/dist/builtins/interaction/Payload.d.ts.map +1 -1
  23. package/dist/builtins/interaction/PayloadItem.d.ts.map +1 -1
  24. package/dist/builtins/interaction/activity/ActivityCall.d.ts.map +1 -1
  25. package/dist/core/EventSource.d.ts +37 -0
  26. package/dist/core/EventSource.d.ts.map +1 -1
  27. package/dist/core/init.d.ts.map +1 -1
  28. package/dist/core/utils.d.ts.map +1 -1
  29. package/dist/drivers/Mysql.d.ts +4 -3
  30. package/dist/drivers/Mysql.d.ts.map +1 -1
  31. package/dist/drivers/PGLite.d.ts +2 -2
  32. package/dist/drivers/PGLite.d.ts.map +1 -1
  33. package/dist/drivers/PostgreSQL.d.ts +2 -2
  34. package/dist/drivers/PostgreSQL.d.ts.map +1 -1
  35. package/dist/drivers/SQLite.d.ts +3 -3
  36. package/dist/drivers/SQLite.d.ts.map +1 -1
  37. package/dist/drivers.d.ts +2 -0
  38. package/dist/drivers.js +963 -0
  39. package/dist/drivers.js.map +1 -0
  40. package/dist/index.js +4220 -4090
  41. package/dist/index.js.map +1 -1
  42. package/dist/runtime/ComputationSourceMap.d.ts +11 -0
  43. package/dist/runtime/ComputationSourceMap.d.ts.map +1 -1
  44. package/dist/runtime/Controller.d.ts.map +1 -1
  45. package/dist/runtime/Scheduler.d.ts +21 -3
  46. package/dist/runtime/Scheduler.d.ts.map +1 -1
  47. package/dist/runtime/System.d.ts +1 -0
  48. package/dist/runtime/System.d.ts.map +1 -1
  49. package/dist/runtime/computations/Any.d.ts +17 -52
  50. package/dist/runtime/computations/Any.d.ts.map +1 -1
  51. package/dist/runtime/computations/Average.d.ts +15 -49
  52. package/dist/runtime/computations/Average.d.ts.map +1 -1
  53. package/dist/runtime/computations/Computation.d.ts +8 -0
  54. package/dist/runtime/computations/Computation.d.ts.map +1 -1
  55. package/dist/runtime/computations/Count.d.ts +21 -64
  56. package/dist/runtime/computations/Count.d.ts.map +1 -1
  57. package/dist/runtime/computations/Custom.d.ts.map +1 -1
  58. package/dist/runtime/computations/Every.d.ts +18 -52
  59. package/dist/runtime/computations/Every.d.ts.map +1 -1
  60. package/dist/runtime/computations/StateMachine.d.ts +2 -2
  61. package/dist/runtime/computations/StateMachine.d.ts.map +1 -1
  62. package/dist/runtime/computations/Summation.d.ts +15 -49
  63. package/dist/runtime/computations/Summation.d.ts.map +1 -1
  64. package/dist/runtime/computations/Transform.d.ts.map +1 -1
  65. package/dist/runtime/computations/WeightedSummation.d.ts +18 -60
  66. package/dist/runtime/computations/WeightedSummation.d.ts.map +1 -1
  67. package/dist/runtime/computations/aggregationTemplate.d.ts +163 -0
  68. package/dist/runtime/computations/aggregationTemplate.d.ts.map +1 -0
  69. package/dist/runtime/migration.d.ts.map +1 -1
  70. package/dist/storage/erstorage/AttributeQuery.d.ts.map +1 -1
  71. package/dist/storage/erstorage/CreationExecutor.d.ts.map +1 -1
  72. package/dist/storage/erstorage/DeletionExecutor.d.ts.map +1 -1
  73. package/dist/storage/erstorage/EntityQueryHandle.d.ts +6 -0
  74. package/dist/storage/erstorage/EntityQueryHandle.d.ts.map +1 -1
  75. package/dist/storage/erstorage/EntityToTableMap.d.ts +1 -0
  76. package/dist/storage/erstorage/EntityToTableMap.d.ts.map +1 -1
  77. package/dist/storage/erstorage/LinkInfo.d.ts +1 -0
  78. package/dist/storage/erstorage/LinkInfo.d.ts.map +1 -1
  79. package/dist/storage/erstorage/MatchExp.d.ts +3 -2
  80. package/dist/storage/erstorage/MatchExp.d.ts.map +1 -1
  81. package/dist/storage/erstorage/MergedItemProcessor.d.ts +5 -0
  82. package/dist/storage/erstorage/MergedItemProcessor.d.ts.map +1 -1
  83. package/dist/storage/erstorage/NewRecordData.d.ts +1 -0
  84. package/dist/storage/erstorage/NewRecordData.d.ts.map +1 -1
  85. package/dist/storage/erstorage/QueryExecutor.d.ts.map +1 -1
  86. package/dist/storage/erstorage/RecordQueryAgent.d.ts +11 -0
  87. package/dist/storage/erstorage/RecordQueryAgent.d.ts.map +1 -1
  88. package/dist/storage/erstorage/Setup.d.ts +24 -0
  89. package/dist/storage/erstorage/Setup.d.ts.map +1 -1
  90. package/dist/storage/erstorage/UpdateExecutor.d.ts.map +1 -1
  91. package/package.json +5 -1
package/README.md CHANGED
@@ -99,6 +99,7 @@ import {
99
99
  Payload, PayloadItem, Controller, MonoSystem,
100
100
  Count, Transform, InteractionEventEntity
101
101
  } from 'interaqt'
102
+ import { PGLiteDB } from 'interaqt/drivers'
102
103
 
103
104
  // --- Define your data model ---
104
105
 
@@ -225,7 +225,7 @@ const deletionStateMachine = StateMachine.create({
225
225
  current: NON_DELETED_STATE,
226
226
  next: DELETED_STATE,
227
227
  computeTarget: function(event) {
228
- return { id: event.payload.userId }
228
+ return { id: event.record.payload.userId }
229
229
  }
230
230
  })
231
231
  ]
@@ -313,7 +313,7 @@ deletionProperty.computation = StateMachine.create({
313
313
  current: NON_DELETED_STATE,
314
314
  next: DELETED_STATE,
315
315
  computeTarget: function(event) {
316
- return { id: event.payload.articleId }
316
+ return { id: event.record.payload.articleId }
317
317
  }
318
318
  })
319
319
  ]
@@ -799,6 +799,7 @@ const userTotalScore = Property.create({
799
799
  type: 'number',
800
800
  computation: WeightedSummation.create({
801
801
  property: 'scores', // Use property name from relation
802
+ attributeQuery: ['multiplier', 'points'], // Required when callback reads fields
802
803
  callback: function(scoreRecord) {
803
804
  return {
804
805
  weight: scoreRecord.multiplier || 1,
@@ -811,6 +812,7 @@ const userTotalScore = Property.create({
811
812
  // Global weighted summation
812
813
  const globalWeightedScore = WeightedSummation.create({
813
814
  record: ScoreRecord,
815
+ attributeQuery: ['difficulty', 'score'], // Required when callback reads fields
814
816
  callback: function(record) {
815
817
  return {
816
818
  weight: record.difficulty,
@@ -1698,6 +1700,16 @@ Custom.create(config: CustomConfig): CustomInstance
1698
1700
 
1699
1701
  If `incrementalCompute` or `incrementalPatchCompute` is provided without `incrementalDataDeps` or `planIncremental`, runtime setup fails with `ComputationProtocolError`. Incremental callbacks receive only the planned partial `dataDeps`, not all declared dependencies.
1700
1702
 
1703
+ **Mutation event snapshots**
1704
+
1705
+ Snapshot shapes differ by event type:
1706
+
1707
+ - `create`: `record` = the written fields (defaults + payload), `oldRecord` = `undefined`
1708
+ - `update`: `record` = **only the changed fields** (plus `id`), `oldRecord` = full pre-update snapshot
1709
+ - `delete`: `record` = full deleted snapshot, `oldRecord` = `undefined`
1710
+
1711
+ Never read a field straight off `mutationEvent.record` on update unless you know it was part of the update — it silently evaluates to `undefined` otherwise and corrupts the incremental result. Reconstruct the full new state with `{ ...mutationEvent.oldRecord, ...mutationEvent.record }`, or re-fetch the record via `this.controller.system.storage.findOne(...)` when computed columns or related data are needed (this is what the built-in aggregations do). The same applies to the third `record` parameter: it is only guaranteed to identify the dirty record, not to carry all of its fields.
1712
+
1701
1713
  **Retry safety**
1702
1714
 
1703
1715
  Custom `compute`, `incrementalCompute`, `incrementalPatchCompute`, and `asyncReturn` callbacks may be replayed after transaction retry. Keep them deterministic and avoid irreversible external IO. Put post-commit external work in `recordMutationSideEffects`.
@@ -1772,9 +1784,20 @@ const counterDict = Dictionary.create({
1772
1784
  },
1773
1785
  incrementalCompute: async function(lastValue, mutationEvent, record, dataDeps) {
1774
1786
  console.log('Previous value:', lastValue);
1775
- const delta = mutationEvent.type === 'delete'
1776
- ? -(mutationEvent.oldRecord?.value || 0)
1777
- : (mutationEvent.record?.value || 0) - (mutationEvent.oldRecord?.value || 0);
1787
+ // CAUTION mutation event snapshot shapes differ by type:
1788
+ // - create: record = written fields (defaults + payload), oldRecord = undefined
1789
+ // - update: record = ONLY the changed fields (+ id), oldRecord = full pre-update snapshot
1790
+ // - delete: record = full deleted snapshot, oldRecord = undefined
1791
+ // Reading a field straight off mutationEvent.record on update silently yields
1792
+ // undefined when that field was not part of the update. Reconstruct the full
1793
+ // new state by overlaying the changed fields on the old snapshot.
1794
+ const newRecord = mutationEvent.type === 'delete'
1795
+ ? undefined
1796
+ : { ...(mutationEvent.oldRecord || {}), ...(mutationEvent.record || {}) };
1797
+ const previousRecord = mutationEvent.type === 'delete'
1798
+ ? mutationEvent.record
1799
+ : mutationEvent.oldRecord;
1800
+ const delta = (newRecord?.value || 0) - (previousRecord?.value || 0);
1778
1801
  const total = (lastValue?.total || 0) + delta;
1779
1802
 
1780
1803
  return {
@@ -124,7 +124,11 @@ When using `InteractionEventEntity` as the Transform input source, understand th
124
124
  states: [updatedState],
125
125
  transfers: [
126
126
  StateTransfer.create({
127
- trigger: UpdateStyleInteraction,
127
+ trigger: {
128
+ recordName: InteractionEventEntity.name,
129
+ type: 'create',
130
+ record: { interactionName: UpdateStyleInteraction.name }
131
+ },
128
132
  current: updatedState,
129
133
  next: updatedState,
130
134
  computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
@@ -142,7 +146,11 @@ When using `InteractionEventEntity` as the Transform input source, understand th
142
146
  initialState: activeState, // StateMachine controls initial value
143
147
  transfers: [
144
148
  StateTransfer.create({
145
- trigger: DeleteStyleInteraction,
149
+ trigger: {
150
+ recordName: InteractionEventEntity.name,
151
+ type: 'create',
152
+ record: { interactionName: DeleteStyleInteraction.name }
153
+ },
146
154
  current: activeState,
147
155
  next: deletedState,
148
156
  computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
@@ -336,19 +344,30 @@ Used for status changes and field updates.
336
344
 
337
345
  **🔴 IMPORTANT: StateTransfer Trigger Parameter**
338
346
 
339
- The `trigger` parameter in `StateTransfer.create()` must ALWAYS be an Interaction instance reference, NOT a string!
347
+ The `trigger` parameter in `StateTransfer.create()` is a **RecordMutationEventPattern** an object pattern matched against record mutation events (`{ recordName, type, keys?, record?, oldRecord? }`). It is NOT a string and NOT an Interaction instance. For interaction-triggered transitions, match the creation of the interaction's event record:
340
348
 
341
349
  ```typescript
342
- // ❌ WRONG: Using string as trigger
350
+ // ❌ WRONG: Using a string as trigger
343
351
  StateTransfer.create({
344
- trigger: 'PublishStyle', // ERROR! Don't use string!
352
+ trigger: 'PublishStyle', // ERROR! Never matches any event
345
353
  current: draftState,
346
354
  next: activeState
347
355
  })
348
356
 
349
- // CORRECT: Using Interaction instance reference
357
+ // WRONG: Using an Interaction instance as trigger
350
358
  StateTransfer.create({
351
- trigger: PublishStyle, // Correct! Reference to Interaction instance
359
+ trigger: PublishStyle, // ERROR! Never matches any event
360
+ current: draftState,
361
+ next: activeState
362
+ })
363
+
364
+ // ✅ CORRECT: Match the interaction's event record creation
365
+ StateTransfer.create({
366
+ trigger: {
367
+ recordName: InteractionEventEntity.name,
368
+ type: 'create',
369
+ record: { interactionName: PublishStyle.name }
370
+ },
352
371
  current: draftState,
353
372
  next: activeState
354
373
  })
@@ -397,13 +416,21 @@ statusProperty.computation = StateMachine.create({
397
416
  StateTransfer.create({
398
417
  current: draftState,
399
418
  next: activeState,
400
- trigger: PublishStyle,
419
+ trigger: {
420
+ recordName: InteractionEventEntity.name,
421
+ type: 'create',
422
+ record: { interactionName: PublishStyle.name }
423
+ },
401
424
  computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
402
425
  }),
403
426
  StateTransfer.create({
404
427
  current: activeState,
405
428
  next: offlineState,
406
- trigger: DeleteStyle,
429
+ trigger: {
430
+ recordName: InteractionEventEntity.name,
431
+ type: 'create',
432
+ record: { interactionName: DeleteStyle.name }
433
+ },
407
434
  computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
408
435
  })
409
436
  ]
@@ -443,7 +470,11 @@ updatedAtProperty.computation = StateMachine.create({
443
470
  StateTransfer.create({
444
471
  current: updatedState,
445
472
  next: updatedState, // Self-loop to same state
446
- trigger: UpdateStyle,
473
+ trigger: {
474
+ recordName: InteractionEventEntity.name,
475
+ type: 'create',
476
+ record: { interactionName: UpdateStyle.name }
477
+ },
447
478
  computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
448
479
  })
449
480
  ]
@@ -506,7 +537,11 @@ modificationInfoProperty.computation = StateMachine.create({
506
537
  StateTransfer.create({
507
538
  current: modifiedState,
508
539
  next: modifiedState,
509
- trigger: UpdateArticle,
540
+ trigger: {
541
+ recordName: InteractionEventEntity.name,
542
+ type: 'create',
543
+ record: { interactionName: UpdateArticle.name }
544
+ },
510
545
  computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
511
546
  })
512
547
  ]
@@ -869,7 +904,11 @@ updatedAtProperty.computation = StateMachine.create({
869
904
  StateTransfer.create({
870
905
  current: updatedState,
871
906
  next: updatedState,
872
- trigger: UpdateStyle,
907
+ trigger: {
908
+ recordName: InteractionEventEntity.name,
909
+ type: 'create',
910
+ record: { interactionName: UpdateStyle.name }
911
+ },
873
912
  computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
874
913
  })
875
914
  ]
@@ -905,7 +944,11 @@ statusProperty.computation = StateMachine.create({
905
944
  StateTransfer.create({
906
945
  current: activeState,
907
946
  next: offlineState,
908
- trigger: DeleteStyle,
947
+ trigger: {
948
+ recordName: InteractionEventEntity.name,
949
+ type: 'create',
950
+ record: { interactionName: DeleteStyle.name }
951
+ },
909
952
  computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
910
953
  })
911
954
  ]
@@ -977,7 +1020,11 @@ updatedAtProperty.computation = StateMachine.create({
977
1020
  StateTransfer.create({
978
1021
  current: updatedState,
979
1022
  next: updatedState,
980
- trigger: UpdateInteraction,
1023
+ trigger: {
1024
+ recordName: InteractionEventEntity.name,
1025
+ type: 'create',
1026
+ record: { interactionName: UpdateInteraction.name }
1027
+ },
981
1028
  computeTarget: (mutationEvent) => ({ id: mutationEvent.record.payload.id })
982
1029
  })
983
1030
  ]
@@ -215,6 +215,7 @@ const Product = Entity.create({
215
215
  name: 'currentStock',
216
216
  computation: WeightedSummation.create({
217
217
  record: OrderItemRelation,
218
+ attributeQuery: ['quantity'], // Required when callback reads fields
218
219
  callback: (orderItem) => ({
219
220
  weight: -1, // Reduce inventory
220
221
  value: orderItem.quantity
@@ -225,6 +226,7 @@ const Product = Entity.create({
225
226
  name: 'totalSales',
226
227
  computation: WeightedSummation.create({
227
228
  record: OrderItemRelation,
229
+ attributeQuery: ['quantity'], // Required when callback reads fields
228
230
  callback: (orderItem) => ({
229
231
  weight: 1,
230
232
  value: orderItem.quantity
@@ -89,6 +89,11 @@ Custom computations are different because interaqt cannot inspect user callback
89
89
 
90
90
  Custom incremental computations must declare an incremental plan. Use `incrementalDataDeps: [...]` for the common case, or `planIncremental(event, record, context)` when the computation needs to choose between incremental, full recompute, and skip per event. Incremental callbacks receive only the planned partial `dataDeps`; the runtime no longer resolves all declared dependencies before incremental execution.
91
91
 
92
+ Two contract details for Custom `dataDeps`:
93
+
94
+ - **`records` dataDeps must declare `attributeQuery`.** Setup fails fast otherwise, because an undeclared field set would resolve to id-only records and field updates would never re-trigger the computation. Declare the fields your `compute` reads (e.g. `attributeQuery: ['price']`), or pass an explicit `attributeQuery: []` when the computation only depends on record membership (create/delete).
95
+ - **`incrementalDataDeps` names the dependency values to resolve and pass into `incrementalCompute`; it is not an event filter.** Events from every declared `dataDep` reach `incrementalCompute` — distinguish sources by `event.recordName` inside the callback, or use `planIncremental` to return different plans per dependency. (Create/update events of unrelated global dicts are already filtered by key at the source-map level and never trigger the computation.)
96
+
92
97
  Because retry replays the transaction attempt, `guard`, `mapEventData`, `resolve`, computation callbacks, `afterDispatch`, and `asyncReturn` must be deterministic and retry-safe. Put irreversible external IO in `postCommit` for interaction-specific effects or `recordMutationSideEffects` for mutation-driven effects; both run after the final commit.
93
98
 
94
99
  ### Scoped Atomic Sequences
@@ -490,6 +495,7 @@ const Student = Entity.create({
490
495
  defaultValue: () => 0,
491
496
  computation: WeightedSummation.create({
492
497
  record: StudentGrades,
498
+ attributeQuery: [['target', { attributeQuery: ['credit', 'score'] }]], // Required when callback reads fields
493
499
  callback: (relation) => ({
494
500
  weight: relation.target.credit,
495
501
  value: relation.target.score
@@ -503,6 +509,7 @@ const Student = Entity.create({
503
509
  defaultValue: () => 0,
504
510
  computation: WeightedSummation.create({
505
511
  record: StudentGrades,
512
+ attributeQuery: [['target', { attributeQuery: ['credit'] }]], // Required when callback reads fields
506
513
  callback: (relation) => ({
507
514
  weight: 1,
508
515
  value: relation.target.credit
@@ -529,6 +536,7 @@ const Student = Entity.create({
529
536
  defaultValue: () => 0,
530
537
  computation: WeightedSummation.create({
531
538
  record: StudentGrades,
539
+ attributeQuery: [['target', { attributeQuery: ['score', 'credit'] }]], // Required when callback reads fields
532
540
  callback: (relation) => {
533
541
  // Only count subjects with score >= 60
534
542
  if (relation.target.score >= 60) {
@@ -568,6 +576,7 @@ const Project = Entity.create({
568
576
  defaultValue: () => false,
569
577
  computation: Every.create({
570
578
  record: ProjectTasks,
579
+ attributeQuery: [['target', { attributeQuery: ['status'] }]], // Required when callback reads fields
571
580
  callback: (relation) => relation.target.status === 'completed'
572
581
  })
573
582
  })
@@ -605,6 +614,7 @@ const Project = Entity.create({
605
614
  defaultValue: () => false,
606
615
  computation: Any.create({
607
616
  record: ProjectMember,
617
+ attributeQuery: ['role'], // Required when callback reads fields
608
618
  callback: (relation) => relation.role === 'admin'
609
619
  })
610
620
  })
@@ -639,6 +649,7 @@ const Order = Entity.create({
639
649
  defaultValue: () => false,
640
650
  computation: Every.create({
641
651
  record: OrderItems,
652
+ attributeQuery: [['target', { attributeQuery: ['quantity', 'stockQuantity'] }]], // Required when callback reads fields
642
653
  callback: (relation) => {
643
654
  const item = relation.target;
644
655
  return item.quantity > 0 && item.stockQuantity >= item.quantity;
@@ -652,6 +663,7 @@ const Order = Entity.create({
652
663
  defaultValue: () => false,
653
664
  computation: Any.create({
654
665
  record: OrderItems,
666
+ attributeQuery: [['target', { attributeQuery: ['quantity', 'price'] }]], // Required when callback reads fields
655
667
  callback: (relation) => {
656
668
  const item = relation.target;
657
669
  return (item.quantity * item.price) > 1000;
@@ -1118,71 +1130,75 @@ StateMachine is used for state transition-based computations, particularly suita
1118
1130
 
1119
1131
  ### Basic State Machine
1120
1132
 
1121
- ```javascript
1122
- import { StateMachine } from 'interaqt';
1123
-
1124
- const Order = Entity.create({
1125
- name: 'Order',
1126
- properties: [
1127
- Property.create({ name: 'orderNumber', type: 'string' }),
1128
- Property.create({
1129
- name: 'status',
1130
- type: 'string',
1131
- computation: new StateMachine({
1132
- states: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'],
1133
- initialState: 'pending',
1134
- transitions: [
1135
- { from: 'pending', to: 'paid', condition: 'payment_received' },
1136
- { from: 'paid', to: 'shipped', condition: 'order_shipped' },
1137
- { from: 'shipped', to: 'delivered', condition: 'delivery_confirmed' },
1138
- { from: 'pending', to: 'cancelled', condition: 'order_cancelled' },
1139
- { from: 'paid', to: 'cancelled', condition: 'order_cancelled' }
1140
- ]
1141
- })
1142
- })
1143
- ]
1144
- });
1145
- ```
1133
+ A StateMachine is declared from three parts: `StateNode` instances (the states), `StateTransfer` instances (the transitions), and an `initialState`. States are always object references, never strings.
1146
1134
 
1147
- ### Event-Based State Transitions
1135
+ Each `StateTransfer` declares:
1136
+ - `current` / `next`: the state nodes the transfer connects
1137
+ - `trigger`: a `RecordMutationEventPattern` (`{ recordName, type, keys?, record?, oldRecord? }`) matched against record mutation events — for interaction-triggered transitions, match the creation of the interaction's event record
1138
+ - `computeTarget` (required for property-level state machines): maps the matched event to the record(s) whose property should transition
1148
1139
 
1149
1140
  ```javascript
1150
- // Define state transition events
1151
- const PaymentReceived = Interaction.create({
1152
- name: 'PaymentReceived',
1153
- action: Action.create({
1154
- name: 'recordPayment',
1155
- payload: Payload.create({
1156
- items: [
1157
- PayloadItem.create({ name: 'orderId', base: Order, isRef: true }),
1158
- PayloadItem.create({ name: 'amount' }),
1159
- PayloadItem.create({ name: 'paymentMethod' })
1160
- ]
1161
- })
1141
+ import { StateMachine, StateNode, StateTransfer, InteractionEventEntity } from 'interaqt';
1142
+
1143
+ // 1. Declare state nodes
1144
+ const pendingState = StateNode.create({ name: 'pending' });
1145
+ const paidState = StateNode.create({ name: 'paid' });
1146
+ const shippedState = StateNode.create({ name: 'shipped' });
1147
+ const deliveredState = StateNode.create({ name: 'delivered' });
1148
+ const cancelledState = StateNode.create({ name: 'cancelled' });
1149
+
1150
+ // 2. Declare the interactions that drive the transitions
1151
+ const PayOrder = Interaction.create({
1152
+ name: 'PayOrder',
1153
+ action: Action.create({ name: 'payOrder' }),
1154
+ payload: Payload.create({
1155
+ items: [PayloadItem.create({ name: 'orderId', type: 'string', required: true })]
1162
1156
  })
1163
1157
  });
1158
+ // ... ShipOrder / ConfirmDelivery / CancelOrder declared the same way
1164
1159
 
1165
- // State machine listens to these events and automatically transitions states
1160
+ // 3. Wire them together on the property
1166
1161
  const Order = Entity.create({
1167
1162
  name: 'Order',
1168
1163
  properties: [
1164
+ Property.create({ name: 'orderNumber', type: 'string' }),
1169
1165
  Property.create({
1170
1166
  name: 'status',
1171
1167
  type: 'string',
1172
- computation: new StateMachine({
1173
- states: ['pending', 'paid', 'shipped', 'delivered'],
1174
- initialState: 'pending',
1175
- transitions: [
1176
- {
1177
- from: 'pending',
1178
- to: 'paid',
1179
- on: 'PaymentReceived' // Listen to interaction events
1180
- },
1181
- {
1182
- from: 'paid',
1183
- to: 'shipped',
1184
- on: 'OrderShipped'
1185
- }
1168
+ computation: StateMachine.create({
1169
+ states: [pendingState, paidState, shippedState, deliveredState, cancelledState],
1170
+ initialState: pendingState,
1171
+ transfers: [
1172
+ StateTransfer.create({
1173
+ current: pendingState,
1174
+ next: paidState,
1175
+ trigger: {
1176
+ recordName: InteractionEventEntity.name,
1177
+ type: 'create',
1178
+ record: { interactionName: PayOrder.name }
1179
+ },
1180
+ computeTarget: (event) => ({ id: event.record.payload.orderId })
1181
+ }),
1182
+ StateTransfer.create({
1183
+ current: paidState,
1184
+ next: shippedState,
1185
+ trigger: {
1186
+ recordName: InteractionEventEntity.name,
1187
+ type: 'create',
1188
+ record: { interactionName: ShipOrder.name }
1189
+ },
1190
+ computeTarget: (event) => ({ id: event.record.payload.orderId })
1191
+ }),
1192
+ StateTransfer.create({
1193
+ current: pendingState,
1194
+ next: cancelledState,
1195
+ trigger: {
1196
+ recordName: InteractionEventEntity.name,
1197
+ type: 'create',
1198
+ record: { interactionName: CancelOrder.name }
1199
+ },
1200
+ computeTarget: (event) => ({ id: event.record.payload.orderId })
1201
+ })
1186
1202
  ]
1187
1203
  })
1188
1204
  })
@@ -1190,48 +1206,62 @@ const Order = Entity.create({
1190
1206
  });
1191
1207
  ```
1192
1208
 
1193
- ### Conditional State Transitions
1209
+ ### Data-Based State Transitions
1210
+
1211
+ `trigger` matches **any** record mutation event, not just interaction events. This means state transitions can also be driven by data changes — for example, transition when a specific field of the host record is updated (`keys` uses subset semantics: the transfer fires when the update touched all the listed fields):
1194
1212
 
1195
1213
  ```javascript
1196
- const LeaveRequest = Entity.create({
1197
- name: 'LeaveRequest',
1198
- properties: [
1199
- Property.create({ name: 'employeeId', type: 'string' }),
1200
- Property.create({ name: 'startDate', type: 'string' }),
1201
- Property.create({ name: 'endDate', type: 'string' }),
1202
- Property.create({ name: 'reason', type: 'string' }),
1203
- Property.create({
1204
- name: 'status',
1205
- type: 'string',
1206
- computation: new StateMachine({
1207
- states: ['draft', 'submitted', 'approved', 'rejected', 'cancelled'],
1208
- initialState: 'draft',
1209
- transitions: [
1210
- {
1211
- from: 'draft',
1212
- to: 'submitted',
1213
- on: 'SubmitRequest',
1214
- condition: (record) => record.reason && record.startDate && record.endDate
1215
- },
1216
- {
1217
- from: 'submitted',
1218
- to: 'approved',
1219
- on: 'ApproveRequest',
1220
- condition: (record, context) => context.user.role === 'manager'
1221
- },
1222
- {
1223
- from: 'submitted',
1224
- to: 'rejected',
1225
- on: 'RejectRequest',
1226
- condition: (record, context) => context.user.role === 'manager'
1227
- }
1228
- ]
1214
+ const draftState = StateNode.create({ name: 'draft' });
1215
+ const publishedState = StateNode.create({ name: 'published' });
1216
+
1217
+ Property.create({
1218
+ name: 'status',
1219
+ type: 'string',
1220
+ computation: StateMachine.create({
1221
+ states: [draftState, publishedState],
1222
+ initialState: draftState,
1223
+ transfers: [
1224
+ StateTransfer.create({
1225
+ current: draftState,
1226
+ next: publishedState,
1227
+ // Fires when a Document update touches the `reviewed` field
1228
+ trigger: { recordName: 'Document', type: 'update', keys: ['reviewed'] },
1229
+ computeTarget: (event) => ({ id: event.oldRecord?.id ?? event.record?.id })
1229
1230
  })
1230
- })
1231
- ]
1231
+ ]
1232
+ })
1232
1233
  });
1233
1234
  ```
1234
1235
 
1236
+ Note: if several transfers share the same `current` state and their triggers match the same event but lead to different next states, the framework throws an "ambiguous transition" error — make the triggers mutually exclusive (e.g. via `record` / `keys` patterns).
1237
+
1238
+ ### Guarding Transitions
1239
+
1240
+ There is no `condition` field on `StateTransfer`. Preconditions belong to the interaction layer (use `conditions` / `userAttributives` on the Interaction so unauthorized or invalid calls never produce an event), and fine-grained per-record filtering is expressed in `computeTarget` — return `null`/`undefined` (or an empty array) to skip the transition for records that should not transition:
1241
+
1242
+ ```javascript
1243
+ StateTransfer.create({
1244
+ current: submittedState,
1245
+ next: approvedState,
1246
+ trigger: {
1247
+ recordName: InteractionEventEntity.name,
1248
+ type: 'create',
1249
+ record: { interactionName: ApproveRequest.name }
1250
+ },
1251
+ computeTarget: async function (event) {
1252
+ // `this` is the Controller: you can query before deciding the target
1253
+ const request = await this.system.storage.findOne(
1254
+ 'LeaveRequest',
1255
+ MatchExp.atom({ key: 'id', value: ['=', event.record.payload.requestId] }),
1256
+ undefined,
1257
+ ['id', 'reason', 'startDate', 'endDate']
1258
+ );
1259
+ if (!request?.reason || !request?.startDate || !request?.endDate) return null;
1260
+ return { id: request.id };
1261
+ }
1262
+ })
1263
+ ```
1264
+
1235
1265
  ### Dynamic Value Computation with StateNode
1236
1266
 
1237
1267
  StateMachine supports dynamic value computation through the `computeValue` function in StateNode. This allows you to compute and update property values during state transitions.
@@ -1263,8 +1293,12 @@ const EventEntity = Entity.create({
1263
1293
  // Self-transition: stays in the same state but triggers computeValue
1264
1294
  current: triggeredState,
1265
1295
  next: triggeredState,
1266
- trigger: TriggerEventInteraction,
1267
- computeTarget: (event) => ({ id: event.payload.eventId })
1296
+ trigger: {
1297
+ recordName: InteractionEventEntity.name,
1298
+ type: 'create',
1299
+ record: { interactionName: TriggerEventInteraction.name }
1300
+ },
1301
+ computeTarget: (event) => ({ id: event.record.payload.eventId })
1268
1302
  })
1269
1303
  ],
1270
1304
  initialState: triggeredState
@@ -1308,14 +1342,22 @@ const CounterEntity = Entity.create({
1308
1342
  StateTransfer.create({
1309
1343
  current: idleState,
1310
1344
  next: incrementingState,
1311
- trigger: IncrementInteraction,
1312
- computeTarget: (event) => ({ id: event.payload.counterId })
1345
+ trigger: {
1346
+ recordName: InteractionEventEntity.name,
1347
+ type: 'create',
1348
+ record: { interactionName: IncrementInteraction.name }
1349
+ },
1350
+ computeTarget: (event) => ({ id: event.record.payload.counterId })
1313
1351
  }),
1314
1352
  StateTransfer.create({
1315
1353
  current: incrementingState,
1316
1354
  next: idleState,
1317
- trigger: ResetInteraction,
1318
- computeTarget: (event) => ({ id: event.payload.counterId })
1355
+ trigger: {
1356
+ recordName: InteractionEventEntity.name,
1357
+ type: 'create',
1358
+ record: { interactionName: ResetInteraction.name }
1359
+ },
1360
+ computeTarget: (event) => ({ id: event.record.payload.counterId })
1319
1361
  })
1320
1362
  ],
1321
1363
  initialState: idleState
@@ -1369,20 +1411,32 @@ const TaskEntity = Entity.create({
1369
1411
  StateTransfer.create({
1370
1412
  current: newState,
1371
1413
  next: inProgressState,
1372
- trigger: StartTaskInteraction,
1373
- computeTarget: (event) => ({ id: event.payload.taskId })
1414
+ trigger: {
1415
+ recordName: InteractionEventEntity.name,
1416
+ type: 'create',
1417
+ record: { interactionName: StartTaskInteraction.name }
1418
+ },
1419
+ computeTarget: (event) => ({ id: event.record.payload.taskId })
1374
1420
  }),
1375
1421
  StateTransfer.create({
1376
1422
  current: inProgressState,
1377
1423
  next: completedState,
1378
- trigger: CompleteTaskInteraction,
1379
- computeTarget: (event) => ({ id: event.payload.taskId })
1424
+ trigger: {
1425
+ recordName: InteractionEventEntity.name,
1426
+ type: 'create',
1427
+ record: { interactionName: CompleteTaskInteraction.name }
1428
+ },
1429
+ computeTarget: (event) => ({ id: event.record.payload.taskId })
1380
1430
  }),
1381
1431
  StateTransfer.create({
1382
1432
  current: inProgressState,
1383
1433
  next: cancelledState,
1384
- trigger: CancelTaskInteraction,
1385
- computeTarget: (event) => ({ id: event.payload.taskId })
1434
+ trigger: {
1435
+ recordName: InteractionEventEntity.name,
1436
+ type: 'create',
1437
+ record: { interactionName: CancelTaskInteraction.name }
1438
+ },
1439
+ computeTarget: (event) => ({ id: event.record.payload.taskId })
1386
1440
  })
1387
1441
  ],
1388
1442
  initialState: newState
@@ -1858,6 +1912,7 @@ const Post = Entity.create({
1858
1912
  defaultValue: () => 0,
1859
1913
  computation: WeightedSummation.create({
1860
1914
  record: PostInteractions,
1915
+ attributeQuery: [['target', { attributeQuery: ['type'] }]], // Required when callback reads fields
1861
1916
  callback: (relation) => {
1862
1917
  const interaction = relation.target;
1863
1918
  switch (interaction.type) {
@@ -1889,6 +1944,7 @@ const Post = Entity.create({
1889
1944
  defaultValue: () => false,
1890
1945
  computation: Every.create({
1891
1946
  record: PostComments,
1947
+ attributeQuery: [['target', { attributeQuery: ['status'] }]], // Required when callback reads fields
1892
1948
  callback: (relation) => relation.target.status === 'approved'
1893
1949
  })
1894
1950
  })
@@ -1933,6 +1989,7 @@ Property.create({
1933
1989
  defaultValue: () => 0,
1934
1990
  computation: Count.create({
1935
1991
  record: User,
1992
+ attributeQuery: ['status'], // Required when callback reads fields
1936
1993
  callback: (user) => user.status === 'active'
1937
1994
  })
1938
1995
  });