interaqt 1.1.2 → 1.1.3

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.
@@ -15,12 +15,12 @@ const User = Entity.create({
15
15
  Property.create({ name: 'name', type: 'string' }),
16
16
  Property.create({ name: 'email', type: 'string' }),
17
17
  Property.create({ name: 'age', type: 'number' }),
18
- Property.create({ name: 'status', type: 'string', defaultValue: 'active' }),
18
+ Property.create({ name: 'status', type: 'string', defaultValue: () => 'active' }),
19
19
  Property.create({ name: 'createdAt', type: 'string', defaultValue: () => new Date().toISOString() }),
20
20
  Property.create({
21
21
  name: 'fullName',
22
22
  type: 'string',
23
- getValue: (record) => `${record.firstName} ${record.lastName}`
23
+ computed: (record) => `${record.firstName} ${record.lastName}`
24
24
  })
25
25
  ]
26
26
  })
@@ -52,7 +52,7 @@ The Klass pattern uses `generateUUID()` internally. Manual IDs risk collisions a
52
52
  ### Checklist
53
53
  - [ ] Entity name is PascalCase and singular (`User` not `users`)
54
54
  - [ ] No manual UUID assignment
55
- - [ ] Computed properties that depend only on the same record use `getValue`, NOT Transform
55
+ - [ ] Computed properties that depend only on the same record use `computed`, NOT Transform
56
56
  - [ ] Properties with reactive computations (Count, etc.) include `defaultValue`
57
57
 
58
58
  ---
@@ -148,7 +148,7 @@ Without `type`, the framework cannot determine cardinality. ALWAYS explicitly se
148
148
  | Check ANY related record matches condition | `Any` |
149
149
  | Derive new entities from events or other entities | `Transform` (on Entity `computation`) |
150
150
  | Update a property value based on state transitions | `StateMachine` (on Property `computation`) |
151
- | Simple computation from same-record fields | `getValue` (on Property) |
151
+ | Simple computation from same-record fields | `computed` (on Property) |
152
152
 
153
153
  ```typescript
154
154
  import { Entity, Property, Relation, Count, WeightedSummation, Transform, InteractionEventEntity } from 'interaqt'
@@ -184,12 +184,12 @@ Property.create({
184
184
  Property.create({
185
185
  name: 'formattedPrice',
186
186
  type: 'string',
187
- getValue: (record) => `$${record.price}`
187
+ computed: (record) => `$${record.price}`
188
188
  })
189
189
  ```
190
190
 
191
191
  ### WHY
192
- Transform creates new records in a computed entity collection. It CANNOT update a single property. Use `getValue` for same-entity property computations.
192
+ Transform creates new records in a computed entity collection. It CANNOT update a single property. Use `computed` for same-entity property computations.
193
193
 
194
194
  ### WRONG: Transform for counting
195
195
  ```typescript
@@ -220,8 +220,8 @@ Count uses incremental algorithms. Transform loads all records into memory, whic
220
220
  ```typescript
221
221
  // DON'T — Controller does NOT accept a computations parameter
222
222
  const controller = new Controller({
223
- system, entities, relations, activities, interactions,
224
- dict: [myComputation], // dict is for Dictionaries, not computations
223
+ system, entities, relations,
224
+ eventSources: [myComputation], // eventSources is for Interactions, not computations
225
225
  })
226
226
  ```
227
227
 
@@ -243,7 +243,7 @@ All computations are declared within the `computation` field of Entity, Relation
243
243
  - [ ] Transform is on `Entity.computation` or `Relation.computation`, NEVER on `Property.computation`
244
244
  - [ ] Count, WeightedSummation, Every, Any are on `Property.computation`
245
245
  - [ ] StateMachine is on `Property.computation`
246
- - [ ] `getValue` is used for same-record-only property derivations
246
+ - [ ] `computed` is used for same-record-only property derivations
247
247
  - [ ] Properties with computation ALWAYS have `defaultValue`
248
248
  - [ ] NEVER pass computations to Controller constructor
249
249
 
@@ -261,9 +261,9 @@ const CreatePost = Interaction.create({
261
261
  action: Action.create({ name: 'createPost' }),
262
262
  payload: Payload.create({
263
263
  items: [
264
- PayloadItem.create({ name: 'title', required: true }),
265
- PayloadItem.create({ name: 'content', required: true }),
266
- PayloadItem.create({ name: 'postId', base: Post, isRef: true })
264
+ PayloadItem.create({ name: 'title', type: 'string', required: true }),
265
+ PayloadItem.create({ name: 'content', type: 'string', required: true }),
266
+ PayloadItem.create({ name: 'postId', type: 'string', base: Post, isRef: true })
267
267
  ]
268
268
  })
269
269
  })
@@ -310,8 +310,7 @@ const controller = new Controller({
310
310
  system,
311
311
  entities: [User, Post],
312
312
  relations: [UserPosts],
313
- activities: [],
314
- interactions: [CreatePost],
313
+ eventSources: [CreatePost],
315
314
  dict: [],
316
315
  recordMutationSideEffects: []
317
316
  })
@@ -319,18 +318,18 @@ const controller = new Controller({
319
318
  await controller.setup(true)
320
319
  ```
321
320
 
322
- ### WRONG: Calling callInteraction before setup
321
+ ### WRONG: Calling dispatch before setup
323
322
  ```typescript
324
323
  // DON'T — setup MUST come first
325
- const controller = new Controller({ system, entities, relations, activities, interactions, dict: [] })
326
- await controller.callInteraction('CreatePost', { user: { id: '1' }, payload: { title: 'Hi' } })
324
+ const controller = new Controller({ system, entities, relations, eventSources: [CreatePost], dict: [] })
325
+ await controller.dispatch(CreatePost, { user: { id: '1' }, payload: { title: 'Hi' } })
327
326
  ```
328
327
 
329
328
  ### CORRECT:
330
329
  ```typescript
331
- const controller = new Controller({ system, entities, relations, activities, interactions, dict: [] })
330
+ const controller = new Controller({ system, entities, relations, eventSources: [CreatePost], dict: [] })
332
331
  await controller.setup(true)
333
- await controller.callInteraction('CreatePost', { user: { id: '1' }, payload: { title: 'Hi' } })
332
+ await controller.dispatch(CreatePost, { user: { id: '1' }, payload: { title: 'Hi' } })
334
333
  ```
335
334
 
336
335
  ### WHY
@@ -338,15 +337,15 @@ await controller.callInteraction('CreatePost', { user: { id: '1' }, payload: { t
338
337
 
339
338
  ### Checklist
340
339
  - [ ] `system.conceptClass = KlassByName` is set before creating Controller
341
- - [ ] `controller.setup(true)` is called BEFORE any `callInteraction`
340
+ - [ ] `controller.setup(true)` is called BEFORE any `dispatch`
342
341
  - [ ] `dict` contains only Dictionary instances, not computations
343
342
 
344
343
  ---
345
344
 
346
- ## When Calling Interactions
345
+ ## When Dispatching Interactions
347
346
 
348
347
  ```typescript
349
- const result = await controller.callInteraction('CreatePost', {
348
+ const result = await controller.dispatch(CreatePost, {
350
349
  user: { id: 'user-1', role: 'author' },
351
350
  payload: {
352
351
  title: 'My Post',
@@ -361,17 +360,17 @@ if (result.error) {
361
360
 
362
361
  ### WRONG: Using try-catch for error handling
363
362
  ```typescript
364
- // DON'T — interaqt does NOT throw exceptions
363
+ // DON'T — interaqt does NOT throw exceptions by default
365
364
  try {
366
- await controller.callInteraction('CreatePost', { user: { id: '1' }, payload: {} })
365
+ await controller.dispatch(CreatePost, { user: { id: '1' }, payload: {} })
367
366
  } catch (e) {
368
- // This code will NEVER execute
367
+ // This code will NEVER execute (unless forceThrowDispatchError is true)
369
368
  }
370
369
  ```
371
370
 
372
371
  ### CORRECT:
373
372
  ```typescript
374
- const result = await controller.callInteraction('CreatePost', {
373
+ const result = await controller.dispatch(CreatePost, {
375
374
  user: { id: '1' },
376
375
  payload: {}
377
376
  })
@@ -381,20 +380,26 @@ if (result.error) {
381
380
  ```
382
381
 
383
382
  ### WHY
384
- The framework catches all errors internally and returns them via `result.error`. Exceptions are never thrown to callers.
383
+ The framework catches all errors internally and returns them via `result.error`. Exceptions are never thrown to callers (unless `forceThrowDispatchError: true` is set on Controller).
384
+
385
+ ### WRONG: Passing a name string instead of instance
386
+ ```typescript
387
+ // DON'T — first argument must be the event source instance, not a string
388
+ controller.dispatch('CreatePost', payload)
389
+ ```
385
390
 
386
391
  ### WRONG: Using non-existent API methods
387
392
  ```typescript
388
393
  // DON'T — these methods do NOT exist
389
- controller.dispatch('CreatePost', payload)
394
+ controller.callInteraction('CreatePost', payload)
390
395
  controller.run()
391
396
  controller.execute()
392
397
  ```
393
398
 
394
399
  ### CORRECT:
395
400
  ```typescript
396
- // The ONLY method to trigger interactions
397
- await controller.callInteraction('CreatePost', {
401
+ // The ONLY method to trigger interactions — first arg is the instance reference
402
+ await controller.dispatch(CreatePost, {
398
403
  user: { id: 'user-1' },
399
404
  payload: { title: 'Hi' }
400
405
  })
@@ -403,7 +408,7 @@ await controller.callInteraction('CreatePost', {
403
408
  ### Checklist
404
409
  - [ ] ALWAYS pass a `user` object with at least `id`
405
410
  - [ ] ALWAYS check `result.error` — NEVER use try-catch
406
- - [ ] Use `controller.callInteraction(name, args)` — no other dispatch method exists
411
+ - [ ] Use `controller.dispatch(eventSourceInstance, args)` — first arg is the instance, NOT a name string
407
412
 
408
413
  ---
409
414
 
@@ -486,13 +491,13 @@ describe('Feature', () => {
486
491
  system = new MonoSystem(new PGLiteDB())
487
492
  system.conceptClass = KlassByName
488
493
  controller = new Controller({
489
- system, entities, relations, activities, interactions, dict: [], recordMutationSideEffects: []
494
+ system, entities, relations, eventSources, dict: [], recordMutationSideEffects: []
490
495
  })
491
496
  await controller.setup(true)
492
497
  })
493
498
 
494
499
  test('creates a post via interaction', async () => {
495
- const result = await controller.callInteraction('CreatePost', {
500
+ const result = await controller.dispatch(CreatePost, {
496
501
  user: { id: 'user-1' },
497
502
  payload: { title: 'Test', content: 'Hello' }
498
503
  })
@@ -518,8 +523,8 @@ const post = await system.storage.create('Post', { title: 'Test', content: 'Hell
518
523
 
519
524
  ### CORRECT:
520
525
  ```typescript
521
- // Use callInteraction to test business logic
522
- const result = await controller.callInteraction('CreatePost', {
526
+ // Use dispatch to test business logic
527
+ const result = await controller.dispatch(CreatePost, {
523
528
  user: { id: 'user-1' },
524
529
  payload: { title: 'Test', content: 'Hello' }
525
530
  })
@@ -531,6 +536,6 @@ const result = await controller.callInteraction('CreatePost', {
531
536
  ### Checklist
532
537
  - [ ] Use `PGLiteDB` for test databases
533
538
  - [ ] Call `controller.setup(true)` in `beforeEach`
534
- - [ ] Test business logic through `callInteraction`, not direct storage
539
+ - [ ] Test business logic through `controller.dispatch`, not direct storage
535
540
  - [ ] Check `result.error` — NEVER use try-catch
536
541
  - [ ] ALWAYS pass `attributeQuery` when asserting on query results
@@ -30,7 +30,7 @@ const User = Entity.create({
30
30
  name: 'postCount',
31
31
  type: 'number',
32
32
  defaultValue: () => 0,
33
- computation: Count.create({ record: UserPosts })
33
+ computation: Count.create({ property: 'posts' })
34
34
  })
35
35
  ]
36
36
  })
@@ -76,8 +76,8 @@ const CreatePost = Interaction.create({
76
76
  action: Action.create({ name: 'createPost' }),
77
77
  payload: Payload.create({
78
78
  items: [
79
- PayloadItem.create({ name: 'title', required: true }),
80
- PayloadItem.create({ name: 'content', required: true })
79
+ PayloadItem.create({ name: 'title', type: 'string', required: true }),
80
+ PayloadItem.create({ name: 'content', type: 'string', required: true })
81
81
  ]
82
82
  })
83
83
  })
@@ -91,8 +91,7 @@ const controller = new Controller({
91
91
  system,
92
92
  entities: [User, Post],
93
93
  relations: [UserPosts],
94
- activities: [],
95
- interactions: [CreatePost],
94
+ eventSources: [CreatePost],
96
95
  dict: [],
97
96
  recordMutationSideEffects: []
98
97
  })
@@ -105,7 +104,7 @@ const adminUser = await system.storage.create('User', {
105
104
  name: 'Alice', email: 'alice@example.com'
106
105
  })
107
106
 
108
- const result = await controller.callInteraction('CreatePost', {
107
+ const result = await controller.dispatch(CreatePost, {
109
108
  user: adminUser,
110
109
  payload: { title: 'First Post', content: 'Hello World' }
111
110
  })
@@ -121,7 +120,7 @@ const user = await system.storage.findOne(
121
120
  ```
122
121
 
123
122
  ## Design Decisions
124
- - **Count on `postCount`**: Automatically maintained when UserPosts relations change. No manual update logic needed.
123
+ - **Count on `postCount`**: Uses `property: 'posts'` to count related Post records via the `posts` navigation property. Automatically maintained when UserPosts relations change no manual update logic needed.
125
124
  - **Transform on Post entity**: Posts are created reactively when `CreatePost` interaction fires. The Transform checks `interactionName` and returns entity data.
126
125
  - **Relation direction**: `source: Post, target: User, type: 'n:1'` — many posts to one user. `sourceProperty: 'author'` lets you navigate from Post to User; `targetProperty: 'posts'` lets you navigate from User to Posts.
127
126
 
@@ -143,49 +142,6 @@ import {
143
142
  Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
144
143
  } from 'interaqt'
145
144
 
146
- // --- Interactions ---
147
-
148
- const SubmitOrder = Interaction.create({
149
- name: 'SubmitOrder',
150
- action: Action.create({ name: 'submitOrder' }),
151
- payload: Payload.create({
152
- items: [
153
- PayloadItem.create({ name: 'product', required: true }),
154
- PayloadItem.create({ name: 'quantity', required: true })
155
- ]
156
- })
157
- })
158
-
159
- const PayOrder = Interaction.create({
160
- name: 'PayOrder',
161
- action: Action.create({ name: 'payOrder' }),
162
- payload: Payload.create({
163
- items: [
164
- PayloadItem.create({ name: 'orderId', base: Order, isRef: true, required: true })
165
- ]
166
- })
167
- })
168
-
169
- const ShipOrder = Interaction.create({
170
- name: 'ShipOrder',
171
- action: Action.create({ name: 'shipOrder' }),
172
- payload: Payload.create({
173
- items: [
174
- PayloadItem.create({ name: 'orderId', base: Order, isRef: true, required: true })
175
- ]
176
- })
177
- })
178
-
179
- const CancelOrder = Interaction.create({
180
- name: 'CancelOrder',
181
- action: Action.create({ name: 'cancelOrder' }),
182
- payload: Payload.create({
183
- items: [
184
- PayloadItem.create({ name: 'orderId', base: Order, isRef: true, required: true })
185
- ]
186
- })
187
- })
188
-
189
145
  // --- State Nodes ---
190
146
 
191
147
  const pendingState = StateNode.create({ name: 'pending' })
@@ -211,18 +167,36 @@ const Order = Entity.create({
211
167
  transfers: [
212
168
  StateTransfer.create({
213
169
  current: pendingState, next: paidState,
214
- trigger: PayOrder,
215
- computeTarget: (event) => ({ id: event.payload.orderId })
170
+ trigger: {
171
+ recordName: InteractionEventEntity.name,
172
+ type: 'create',
173
+ record: { interactionName: 'PayOrder' }
174
+ },
175
+ computeTarget: function(mutationEvent) {
176
+ return { id: mutationEvent.record.payload.orderId }
177
+ }
216
178
  }),
217
179
  StateTransfer.create({
218
180
  current: paidState, next: shippedState,
219
- trigger: ShipOrder,
220
- computeTarget: (event) => ({ id: event.payload.orderId })
181
+ trigger: {
182
+ recordName: InteractionEventEntity.name,
183
+ type: 'create',
184
+ record: { interactionName: 'ShipOrder' }
185
+ },
186
+ computeTarget: function(mutationEvent) {
187
+ return { id: mutationEvent.record.payload.orderId }
188
+ }
221
189
  }),
222
190
  StateTransfer.create({
223
191
  current: pendingState, next: cancelledState,
224
- trigger: CancelOrder,
225
- computeTarget: (event) => ({ id: event.payload.orderId })
192
+ trigger: {
193
+ recordName: InteractionEventEntity.name,
194
+ type: 'create',
195
+ record: { interactionName: 'CancelOrder' }
196
+ },
197
+ computeTarget: function(mutationEvent) {
198
+ return { id: mutationEvent.record.payload.orderId }
199
+ }
226
200
  })
227
201
  ],
228
202
  initialState: pendingState
@@ -245,6 +219,49 @@ const Order = Entity.create({
245
219
  })
246
220
  })
247
221
 
222
+ // --- Interactions ---
223
+
224
+ const SubmitOrder = Interaction.create({
225
+ name: 'SubmitOrder',
226
+ action: Action.create({ name: 'submitOrder' }),
227
+ payload: Payload.create({
228
+ items: [
229
+ PayloadItem.create({ name: 'product', type: 'string', required: true }),
230
+ PayloadItem.create({ name: 'quantity', type: 'number', required: true })
231
+ ]
232
+ })
233
+ })
234
+
235
+ const PayOrder = Interaction.create({
236
+ name: 'PayOrder',
237
+ action: Action.create({ name: 'payOrder' }),
238
+ payload: Payload.create({
239
+ items: [
240
+ PayloadItem.create({ name: 'orderId', type: 'string', base: Order, isRef: true, required: true })
241
+ ]
242
+ })
243
+ })
244
+
245
+ const ShipOrder = Interaction.create({
246
+ name: 'ShipOrder',
247
+ action: Action.create({ name: 'shipOrder' }),
248
+ payload: Payload.create({
249
+ items: [
250
+ PayloadItem.create({ name: 'orderId', type: 'string', base: Order, isRef: true, required: true })
251
+ ]
252
+ })
253
+ })
254
+
255
+ const CancelOrder = Interaction.create({
256
+ name: 'CancelOrder',
257
+ action: Action.create({ name: 'cancelOrder' }),
258
+ payload: Payload.create({
259
+ items: [
260
+ PayloadItem.create({ name: 'orderId', type: 'string', base: Order, isRef: true, required: true })
261
+ ]
262
+ })
263
+ })
264
+
248
265
  // --- Controller Setup & Usage ---
249
266
 
250
267
  const system = new MonoSystem(new PGLiteDB())
@@ -254,8 +271,7 @@ const controller = new Controller({
254
271
  system,
255
272
  entities: [Order],
256
273
  relations: [],
257
- activities: [],
258
- interactions: [SubmitOrder, PayOrder, ShipOrder, CancelOrder],
274
+ eventSources: [SubmitOrder, PayOrder, ShipOrder, CancelOrder],
259
275
  dict: [],
260
276
  recordMutationSideEffects: []
261
277
  })
@@ -264,7 +280,7 @@ await controller.setup(true)
264
280
  const user = { id: 'user-1' }
265
281
 
266
282
  // Submit order
267
- const submitResult = await controller.callInteraction('SubmitOrder', {
283
+ const submitResult = await controller.dispatch(SubmitOrder, {
268
284
  user,
269
285
  payload: { product: 'Widget', quantity: 3 }
270
286
  })
@@ -276,14 +292,14 @@ const order = await system.storage.findOne('Order',
276
292
  // order.status === 'pending'
277
293
 
278
294
  // Pay order
279
- await controller.callInteraction('PayOrder', {
295
+ await controller.dispatch(PayOrder, {
280
296
  user,
281
297
  payload: { orderId: order.id }
282
298
  })
283
299
  // order.status → 'paid'
284
300
 
285
301
  // Ship order
286
- await controller.callInteraction('ShipOrder', {
302
+ await controller.dispatch(ShipOrder, {
287
303
  user,
288
304
  payload: { orderId: order.id }
289
305
  })
@@ -292,22 +308,27 @@ await controller.callInteraction('ShipOrder', {
292
308
 
293
309
  ## Design Decisions
294
310
  - **StateMachine on `status` property**: Status transitions are declarative. The framework enforces valid transitions — you cannot jump from `pending` to `shipped` directly.
295
- - **`computeTarget`**: Each StateTransfer uses `computeTarget` to identify WHICH order the transition applies to, using the orderId from the interaction payload.
311
+ - **`trigger` is a pattern object**: Each StateTransfer `trigger` is a `RecordMutationEventPattern` that matches against InteractionEvent creation events it is NOT an Interaction instance. The `record.interactionName` field matches the specific interaction by name string.
312
+ - **`computeTarget`**: Receives the `RecordMutationEvent` and returns which order the transition applies to. Access the InteractionEvent data via `mutationEvent.record` (e.g. `mutationEvent.record.payload.orderId`).
296
313
  - **Transform on Entity `computation`**: Creates order records reactively when `SubmitOrder` fires.
297
314
  - **Cancellation only from `pending`**: Only one `cancelledState` transfer is defined (from `pending`). Attempting to cancel a paid order will have no effect.
315
+ - **Declaration order**: Order entity is defined before the Interactions that reference it (via `base: Order`). The StateMachine triggers use interaction name strings (not variable references), avoiding circular dependencies.
298
316
 
299
317
  ---
300
318
 
301
319
  # Recipe: Student GPA with Weighted Summation
302
320
 
303
321
  ## Scenario
304
- A student grading system where each student has grades for multiple subjects, each with different credit weights. The student's GPA is automatically computed using WeightedSummation.
322
+ A student grading system where each student has grades for multiple subjects, each with different credit weights. The student's GPA is automatically computed using WeightedSummation. Grades are added via an Interaction to ensure computations trigger correctly.
305
323
 
306
324
  ## Complete Implementation
307
325
 
308
326
  ```typescript
309
327
  import {
310
- Entity, Property, Relation, WeightedSummation, Count,
328
+ Entity, Property, Relation,
329
+ WeightedSummation, Summation, Count,
330
+ Interaction, Action, Payload, PayloadItem,
331
+ Transform, InteractionEventEntity,
311
332
  Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
312
333
  } from 'interaqt'
313
334
 
@@ -322,7 +343,8 @@ const Student = Entity.create({
322
343
  type: 'number',
323
344
  defaultValue: () => 0,
324
345
  computation: WeightedSummation.create({
325
- record: StudentGrades,
346
+ property: 'grades',
347
+ direction: 'source',
326
348
  attributeQuery: [['target', { attributeQuery: ['score', 'credit'] }]],
327
349
  callback: (relation) => ({
328
350
  weight: relation.target.credit,
@@ -334,20 +356,17 @@ const Student = Entity.create({
334
356
  name: 'totalCredits',
335
357
  type: 'number',
336
358
  defaultValue: () => 0,
337
- computation: WeightedSummation.create({
338
- record: StudentGrades,
339
- attributeQuery: [['target', { attributeQuery: ['credit'] }]],
340
- callback: (relation) => ({
341
- weight: 1,
342
- value: relation.target.credit
343
- })
359
+ computation: Summation.create({
360
+ property: 'grades',
361
+ direction: 'source',
362
+ attributeQuery: [['target', { attributeQuery: ['credit'] }]]
344
363
  })
345
364
  }),
346
365
  Property.create({
347
366
  name: 'courseCount',
348
367
  type: 'number',
349
368
  defaultValue: () => 0,
350
- computation: Count.create({ record: StudentGrades })
369
+ computation: Count.create({ property: 'grades' })
351
370
  })
352
371
  ]
353
372
  })
@@ -358,7 +377,22 @@ const Grade = Entity.create({
358
377
  Property.create({ name: 'subject', type: 'string' }),
359
378
  Property.create({ name: 'score', type: 'number' }),
360
379
  Property.create({ name: 'credit', type: 'number' })
361
- ]
380
+ ],
381
+ computation: Transform.create({
382
+ record: InteractionEventEntity,
383
+ attributeQuery: ['interactionName', 'payload'],
384
+ callback: function(event) {
385
+ if (event.interactionName === 'AddGrade') {
386
+ return {
387
+ subject: event.payload.subject,
388
+ score: event.payload.score,
389
+ credit: event.payload.credit,
390
+ student: { id: event.payload.studentId }
391
+ }
392
+ }
393
+ return null
394
+ }
395
+ })
362
396
  })
363
397
 
364
398
  // --- Relations ---
@@ -371,6 +405,21 @@ const StudentGrades = Relation.create({
371
405
  type: '1:n'
372
406
  })
373
407
 
408
+ // --- Interactions ---
409
+
410
+ const AddGrade = Interaction.create({
411
+ name: 'AddGrade',
412
+ action: Action.create({ name: 'addGrade' }),
413
+ payload: Payload.create({
414
+ items: [
415
+ PayloadItem.create({ name: 'studentId', type: 'string', required: true }),
416
+ PayloadItem.create({ name: 'subject', type: 'string', required: true }),
417
+ PayloadItem.create({ name: 'score', type: 'number', required: true }),
418
+ PayloadItem.create({ name: 'credit', type: 'number', required: true })
419
+ ]
420
+ })
421
+ })
422
+
374
423
  // --- Controller Setup & Usage ---
375
424
 
376
425
  const system = new MonoSystem(new PGLiteDB())
@@ -380,8 +429,7 @@ const controller = new Controller({
380
429
  system,
381
430
  entities: [Student, Grade],
382
431
  relations: [StudentGrades],
383
- activities: [],
384
- interactions: [],
432
+ eventSources: [AddGrade],
385
433
  dict: [],
386
434
  recordMutationSideEffects: []
387
435
  })
@@ -389,8 +437,14 @@ await controller.setup(true)
389
437
 
390
438
  const student = await system.storage.create('Student', { name: 'Alice' })
391
439
 
392
- await system.storage.create('Grade', { subject: 'Math', score: 90, credit: 4, student: student.id })
393
- await system.storage.create('Grade', { subject: 'English', score: 80, credit: 3, student: student.id })
440
+ await controller.dispatch(AddGrade, {
441
+ user: { id: 'system' },
442
+ payload: { studentId: student.id, subject: 'Math', score: 90, credit: 4 }
443
+ })
444
+ await controller.dispatch(AddGrade, {
445
+ user: { id: 'system' },
446
+ payload: { studentId: student.id, subject: 'English', score: 80, credit: 3 }
447
+ })
394
448
 
395
449
  const result = await system.storage.findOne('Student',
396
450
  MatchExp.atom({ key: 'id', value: ['=', student.id] }),
@@ -403,16 +457,17 @@ const result = await system.storage.findOne('Student',
403
457
  ```
404
458
 
405
459
  ## Design Decisions
406
- - **WeightedSummation for GPA**: The `weight` is the credit value, and the `value` is the score. The framework computes `sum(weight*value) / sum(weight)` automatically.
407
- - **Separate Count for courseCount**: Even though totalCredits could imply count, Count is more efficient and semantically clear for counting.
408
- - **`attributeQuery` in computation**: Specifies which fields of related records to fetch, avoiding loading unnecessary data.
460
+ - **WeightedSummation for GPA**: Uses `property: 'grades'` (property-level mode) to aggregate per-student. The `weight` is the credit value, and the `value` is the score. The framework computes `sum(weight*value) / sum(weight)` automatically.
461
+ - **Summation for totalCredits**: Uses `Summation` (not `WeightedSummation`) because `totalCredits` is a simple sum. `WeightedSummation` with `weight=1` would compute an average, not a sum.
462
+ - **Count for courseCount**: More efficient and semantically clear for counting than Summation or WeightedSummation.
463
+ - **Grades added via Interaction + Transform**: Using `controller.dispatch` ensures reactive computations (WeightedSummation, Summation, Count) are triggered. Direct `storage.create` bypasses reactive computations and should only be used for prerequisite data (like creating the Student record).
409
464
 
410
465
  ---
411
466
 
412
- # Recipe: Interaction with Payload Validation
467
+ # Recipe: Interaction with Condition Validation
413
468
 
414
469
  ## Scenario
415
- A content moderation system where only published posts can be shared. Demonstrates Attributive-based payload validation on interactions.
470
+ A content moderation system where only published posts can be shared. Demonstrates Condition-based validation on Interactions: the framework checks the condition before allowing the interaction to proceed.
416
471
 
417
472
  ## Complete Implementation
418
473
 
@@ -420,7 +475,7 @@ A content moderation system where only published posts can be shared. Demonstrat
420
475
  import {
421
476
  Entity, Property,
422
477
  Interaction, Action, Payload, PayloadItem,
423
- Attributive, BoolExp,
478
+ Condition,
424
479
  Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
425
480
  } from 'interaqt'
426
481
 
@@ -430,20 +485,11 @@ const Post = Entity.create({
430
485
  name: 'Post',
431
486
  properties: [
432
487
  Property.create({ name: 'title', type: 'string' }),
433
- Property.create({ name: 'status', type: 'string', defaultValue: 'draft' })
488
+ Property.create({ name: 'status', type: 'string', defaultValue: () => 'draft' })
434
489
  ]
435
490
  })
436
491
 
437
- // --- Attributive (validation rule) ---
438
-
439
- const PublishedPost = Attributive.create({
440
- name: 'PublishedPost',
441
- content: function(post, eventArgs) {
442
- return post.status === 'published'
443
- }
444
- })
445
-
446
- // --- Interaction with validation ---
492
+ // --- Interaction with condition ---
447
493
 
448
494
  const SharePost = Interaction.create({
449
495
  name: 'SharePost',
@@ -452,12 +498,23 @@ const SharePost = Interaction.create({
452
498
  items: [
453
499
  PayloadItem.create({
454
500
  name: 'post',
501
+ type: 'string',
455
502
  base: Post,
456
503
  isRef: true,
457
- required: true,
458
- attributives: PublishedPost
504
+ required: true
459
505
  })
460
506
  ]
507
+ }),
508
+ conditions: Condition.create({
509
+ name: 'postMustBePublished',
510
+ content: async function(event) {
511
+ const post = await this.system.storage.findOne('Post',
512
+ MatchExp.atom({ key: 'id', value: ['=', event.payload.post] }),
513
+ undefined,
514
+ ['id', 'status']
515
+ )
516
+ return post?.status === 'published'
517
+ }
461
518
  })
462
519
  })
463
520
 
@@ -470,8 +527,7 @@ const controller = new Controller({
470
527
  system,
471
528
  entities: [Post],
472
529
  relations: [],
473
- activities: [],
474
- interactions: [SharePost],
530
+ eventSources: [SharePost],
475
531
  dict: [],
476
532
  recordMutationSideEffects: []
477
533
  })
@@ -480,22 +536,22 @@ await controller.setup(true)
480
536
  const draftPost = await system.storage.create('Post', { title: 'Draft', status: 'draft' })
481
537
  const publishedPost = await system.storage.create('Post', { title: 'Published', status: 'published' })
482
538
 
483
- // Sharing a draft post fails validation
484
- const failResult = await controller.callInteraction('SharePost', {
539
+ // Sharing a draft post fails the condition check
540
+ const failResult = await controller.dispatch(SharePost, {
485
541
  user: { id: 'user-1' },
486
- payload: { post: { id: draftPost.id } }
542
+ payload: { post: draftPost.id }
487
543
  })
488
- // failResult.error is defined — draft post cannot be shared
544
+ // failResult.error is defined — condition rejected: post is not published
489
545
 
490
546
  // Sharing a published post succeeds
491
- const successResult = await controller.callInteraction('SharePost', {
547
+ const successResult = await controller.dispatch(SharePost, {
492
548
  user: { id: 'user-1' },
493
- payload: { post: { id: publishedPost.id } }
549
+ payload: { post: publishedPost.id }
494
550
  })
495
551
  // successResult.error is undefined — success
496
552
  ```
497
553
 
498
554
  ## Design Decisions
499
- - **Attributive on PayloadItem**: The `PublishedPost` attributive is attached directly to the PayloadItem, so the framework validates the referenced entity's data before the interaction proceeds.
500
- - **`isRef: true`**: The payload contains only an ID reference. The framework loads the full record and runs the attributive check against it.
501
- - **Error in result, not exception**: Validation failures are returned in `result.error`, consistent with all interaqt error handling.
555
+ - **Condition on Interaction**: The `Condition.create` is attached to the Interaction's `conditions` field. The `content` function receives the event args and returns `true` to allow or `false` to reject. The `this` context is bound to the Controller, providing access to `this.system.storage` for database queries.
556
+ - **`isRef: true`**: The payload contains only an ID reference. With `isRef: true`, the payload value is the entity ID directly (e.g., `payload: { post: draftPost.id }`).
557
+ - **Error in result, not exception**: Condition failures return `{ error: { type: 'condition check failed' } }`, consistent with all interaqt error handling. Never use try-catch.
@@ -8,18 +8,21 @@
8
8
 
9
9
  ```typescript
10
10
  Entity.create(args: {
11
- name: string // PascalCase, singular, unique
12
- properties: PropertyInstance[] // Array of Property.create() results
11
+ name: string // PascalCase, singular, unique, must match /^[a-zA-Z0-9_]+$/
12
+ properties?: PropertyInstance[] // Array of Property.create() results (defaults to [])
13
13
  computation?: ComputationInstance // Transform for derived entities
14
- baseEntity?: EntityInstance // For filtered entities
15
- filterCondition?: MatchExp // For filtered entities
14
+ baseEntity?: EntityInstance | RelationInstance // For filtered entities
15
+ matchExpression?: MatchExp // Filter condition for filtered entities
16
+ inputEntities?: EntityInstance[] // For merged entities
17
+ commonProperties?: PropertyInstance[] // Shared attributes for merged entities
16
18
  }): EntityInstance
17
19
  ```
18
20
 
19
21
  Constraints:
20
22
  - NEVER pass `uuid` — the framework generates it
21
- - `name` must match `/^[a-zA-Z0-9_]+$/`
22
23
  - `computation` accepts only Transform (for creating derived entity collections)
24
+ - **Filtered entity**: set `baseEntity` + `matchExpression`
25
+ - **Merged entity**: set `inputEntities` + `commonProperties` (cannot define own `properties`)
23
26
 
24
27
  ---
25
28
 
@@ -27,21 +30,21 @@ Constraints:
27
30
 
28
31
  ```typescript
29
32
  Property.create(args: {
30
- name: string // Property name
31
- type?: 'string' | 'number' | 'boolean' | 'object'
33
+ name: string // Must match /^[a-zA-Z0-9_]+$/
34
+ type: string // Required: 'string' | 'number' | 'boolean' | 'object'
32
35
  collection?: boolean // true for array types
33
- defaultValue?: any | (() => any) // Static value or factory function
34
- getValue?: (record: any) => any // Computed from same-record fields (not persisted)
35
- computed?: (record: any) => any // Alias for getValue
36
- computation?: ComputationInstance // Reactive: Count, WeightedSummation, Every, Any, StateMachine
36
+ defaultValue?: Function // Factory function returning default value
37
+ computed?: (record: any) => any // Computed from same-record fields (not persisted)
38
+ computation?: ComputationInstance // Reactive: Count, Summation, WeightedSummation, Every, Any, StateMachine, Custom
37
39
  }): PropertyInstance
38
40
  ```
39
41
 
40
42
  Constraints:
41
- - `getValue`/`computed` are for same-record derivations only — NOT persisted
43
+ - `type` is REQUIRED always specify it
44
+ - `computed` is for same-record derivations only — NOT persisted
42
45
  - `computation` results ARE persisted and auto-updated
43
- - When using `computation`, ALWAYS provide `defaultValue`
44
- - NEVER use Transform on Property `computation` — Transform belongs on Entity `computation`
46
+ - When using `computation`, provide `defaultValue`
47
+ - NEVER use Transform on Property `computation` — Transform belongs on Entity/Relation `computation`
45
48
 
46
49
  ---
47
50
 
@@ -49,20 +52,33 @@ Constraints:
49
52
 
50
53
  ```typescript
51
54
  Relation.create(args: {
52
- source: EntityInstance // Source entity
55
+ // Base relation (all required for normal relations):
56
+ name?: string // Optional — auto-generated if omitted
57
+ source: EntityInstance | RelationInstance
53
58
  sourceProperty: string // Navigation property on source
54
- target: EntityInstance // Target entity
59
+ target: EntityInstance | RelationInstance
55
60
  targetProperty: string // Navigation property on target
56
61
  type: '1:1' | '1:n' | 'n:1' | 'n:n'
57
62
  properties?: PropertyInstance[] // Relation's own properties
58
63
  computation?: ComputationInstance // Transform for computed relations
64
+ isTargetReliance?: boolean // Defaults to false
65
+
66
+ // Filtered relation:
67
+ baseRelation?: RelationInstance // Base relation to filter from
68
+ matchExpression?: MatchExp // Filter condition
69
+
70
+ // Merged relation:
71
+ inputRelations?: RelationInstance[] // Relations to merge (must share same source/target)
72
+ commonProperties?: PropertyInstance[] // Shared attributes for merged relations
59
73
  }): RelationInstance
60
74
  ```
61
75
 
62
76
  Constraints:
63
- - NEVER specify `name` — auto-generated from source+target entity names
64
- - ALWAYS specify `type` explicitly
77
+ - `name` is optional — auto-generated as `${source.name}_${sourceProperty}_${targetProperty}_${target.name}`
78
+ - ALWAYS specify `type` explicitly for base relations
65
79
  - Symmetric relations: set `source === target` AND `sourceProperty === targetProperty`
80
+ - **Filtered relation**: requires `baseRelation` + `matchExpression` + `sourceProperty` + `targetProperty`
81
+ - **Merged relation**: requires `inputRelations` + `sourceProperty` + `targetProperty` (cannot specify `source`/`target`/`properties`)
66
82
 
67
83
  ---
68
84
 
@@ -73,10 +89,16 @@ Interaction.create(args: {
73
89
  name: string // Interaction identifier
74
90
  action: ActionInstance // Action.create() result (identifier only)
75
91
  payload?: PayloadInstance // Payload.create() result
76
- conditions?: ConditionInstance // Execution conditions
92
+ conditions?: ConditionsInstance | ConditionInstance // Execution conditions
93
+ data?: EntityInstance | RelationInstance // Entity/Relation to query (for data retrieval)
94
+ dataPolicy?: DataPolicyInstance // Fixed data access constraints
77
95
  }): InteractionInstance
78
96
  ```
79
97
 
98
+ Constraints:
99
+ - For data retrieval, use `GetAction` as action and specify `data`
100
+ - `conditions` accepts either a single `Condition` or a `Conditions` (combined with BoolExp)
101
+
80
102
  ---
81
103
 
82
104
  ## Action.create
@@ -89,6 +111,7 @@ Action.create(args: {
89
111
 
90
112
  Constraints:
91
113
  - Action is ONLY an identifier — no `handler`, `execute`, or `callback`
114
+ - Use `GetAction` (pre-built) for data retrieval interactions
92
115
 
93
116
  ---
94
117
 
@@ -101,37 +124,34 @@ Payload.create(args: {
101
124
 
102
125
  PayloadItem.create(args: {
103
126
  name: string // Parameter name
127
+ type: string // Required: data type
104
128
  base?: EntityInstance // Entity reference for validation
105
129
  isRef?: boolean // true = reference by ID to existing entity
106
130
  required?: boolean // true = mandatory parameter
107
131
  isCollection?: boolean // true = array of items
108
- attributives?: AttributiveInstance // Validation rules (only works when base is set)
132
+ itemRef?: AttributiveInstance | EntityInstance // Reference to entities defined in other interactions (for Activity)
109
133
  }): PayloadItemInstance
110
134
  ```
111
135
 
112
- Constraints:
113
- - Without `base`: framework only checks required/collection, no concept validation
114
- - With `base` + `isRef: true`: framework verifies entity exists by ID
115
- - With `base` + `attributives`: framework validates data against attributive rules
116
- - `attributives` are checked for EVERY item when `isCollection: true`
117
-
118
136
  ---
119
137
 
120
138
  ## Count.create
121
139
 
122
140
  ```typescript
123
141
  Count.create(args: {
124
- record: EntityInstance | RelationInstance // What to count
142
+ record?: EntityInstance | RelationInstance // What to count (for entity/global level)
143
+ property?: string // Relation property name (for property level)
125
144
  direction?: 'source' | 'target' // For relation counting
126
145
  callback?: (record: any) => boolean // Filter function
127
146
  attributeQuery?: AttributeQueryData // Fields to load for callback
128
- dataDeps?: DataDepsConfig // External data dependencies
147
+ dataDeps?: DataDependencies // External data dependencies
129
148
  }): CountInstance
130
149
  ```
131
150
 
132
151
  Constraints:
133
- - Place on Property `computation`, not Entity `computation`
134
- - ALWAYS provide `defaultValue` on the Property
152
+ - Use `record` for global/entity-level counting, `property` for property-level counting
153
+ - Place on Property `computation` or Dictionary `computation`
154
+ - Provide `defaultValue` on the Property when using as property computation
135
155
 
136
156
  ---
137
157
 
@@ -139,10 +159,12 @@ Constraints:
139
159
 
140
160
  ```typescript
141
161
  WeightedSummation.create(args: {
142
- record: RelationInstance // Relation to aggregate
143
- callback: (relation: any) => { weight: number, value: number }
162
+ record?: EntityInstance | RelationInstance // Entity/relation to aggregate (for global level)
163
+ property?: string // Relation property name (for property level)
164
+ direction?: 'source' | 'target' // For relation-based computation
165
+ callback: (record: any) => { weight: number, value: number }
144
166
  attributeQuery?: AttributeQueryData
145
- dataDeps?: DataDepsConfig
167
+ dataDeps?: DataDependencies
146
168
  }): WeightedSummationInstance
147
169
  ```
148
170
 
@@ -150,19 +172,41 @@ Result: `sum(weight * value) / sum(weight)`
150
172
 
151
173
  ---
152
174
 
175
+ ## Summation.create
176
+
177
+ ```typescript
178
+ Summation.create(args: {
179
+ record?: EntityInstance | RelationInstance // Entity/relation to sum (for global level)
180
+ property?: string // Relation property name (for property level)
181
+ direction?: 'source' | 'target' // For relation-based summation
182
+ attributeQuery: AttributeQueryData // Required: specifies field path to sum
183
+ }): SummationInstance
184
+ ```
185
+
186
+ Sums the field pointed to by the leftmost path in `attributeQuery`. Undefined/null/NaN/Infinity values are treated as 0.
187
+
188
+ ---
189
+
153
190
  ## Every.create / Any.create
154
191
 
155
192
  ```typescript
156
193
  Every.create(args: {
157
- record: RelationInstance
158
- callback: (relation: any) => boolean
194
+ record?: EntityInstance | RelationInstance // For global level
195
+ property?: string // Relation property name (for property level)
196
+ direction?: 'source' | 'target'
197
+ callback: (record: any) => boolean
159
198
  attributeQuery?: AttributeQueryData
199
+ dataDeps?: DataDependencies
200
+ notEmpty?: boolean // Return value when collection is empty
160
201
  }): EveryInstance
161
202
 
162
203
  Any.create(args: {
163
- record: RelationInstance
164
- callback: (relation: any) => boolean
204
+ record?: EntityInstance | RelationInstance
205
+ property?: string
206
+ direction?: 'source' | 'target'
207
+ callback: (record: any) => boolean
165
208
  attributeQuery?: AttributeQueryData
209
+ dataDeps?: DataDependencies
166
210
  }): AnyInstance
167
211
  ```
168
212
 
@@ -175,17 +219,33 @@ Any.create(args: {
175
219
 
176
220
  ```typescript
177
221
  Transform.create(args: {
178
- record: EntityInstance | RelationInstance // Source data
179
- callback: (record: any, dataDeps?: any) => any | null
222
+ // Mode 1: Entity/Relation Transform
223
+ record?: EntityInstance | RelationInstance // Source data
180
224
  attributeQuery?: AttributeQueryData
181
- dataDeps?: DataDepsConfig
225
+
226
+ // Mode 2: Event-Driven Transform
227
+ eventDeps?: {
228
+ [key: string]: {
229
+ recordName: string
230
+ type: 'create' | 'update' | 'delete'
231
+ record?: Record<string, unknown>
232
+ oldRecord?: Record<string, unknown>
233
+ }
234
+ }
235
+
236
+ // Common
237
+ callback: Function // (this: Controller, record/mutationEvent) => any | any[] | null
238
+ dataDeps?: { [key: string]: DataDep }
182
239
  }): TransformInstance
183
240
  ```
184
241
 
185
242
  Constraints:
186
243
  - Place on Entity `computation` or Relation `computation`, NEVER on Property
187
- - Return `null` from callback to skip (conditional transformation)
244
+ - Return `null`/`undefined` from callback to skip (conditional transformation)
245
+ - Return array to create multiple records from one source
188
246
  - NEVER reference the entity being defined as `record` (circular reference)
247
+ - Use `eventDeps` mode for interaction-based transformations (recommended)
248
+ - Use `record` mode for deriving entities from other entities
189
249
 
190
250
  ---
191
251
 
@@ -208,12 +268,16 @@ Place on Property `computation`. The property value equals the current state nod
208
268
  ```typescript
209
269
  StateNode.create(args: {
210
270
  name: string
211
- computeValue?: (lastValue: any) => any // Dynamic value when entering this state
271
+ computeValue?: (this: Controller, lastValue: any, event?: any) => any
212
272
  }): StateNodeInstance
213
273
  ```
214
274
 
215
275
  - Without `computeValue`: property value is the state name string
216
276
  - With `computeValue`: property value is the function's return value
277
+ - `lastValue`: previous property value before transition (undefined for initial state)
278
+ - `event`: the event record that triggered the transition (undefined during initialization)
279
+ - For interaction triggers: access `event.payload`, `event.user`, `event.interactionName`
280
+ - `this` is bound to the Controller instance — async functions can use `this.system.storage`
217
281
 
218
282
  ---
219
283
 
@@ -223,12 +287,28 @@ StateNode.create(args: {
223
287
  StateTransfer.create(args: {
224
288
  current: StateNodeInstance // From state
225
289
  next: StateNodeInstance // To state
226
- trigger: InteractionInstance // Interaction that causes transition
227
- computeTarget: (event: any) => { id: any } // Identifies which record to transition
290
+ trigger: RecordMutationEventPattern // Pattern to match against mutation events
291
+ computeTarget?: Function // Determines which records to transition
228
292
  }): StateTransferInstance
229
293
  ```
230
294
 
231
- `computeTarget` extracts the target entity ID from the interaction event payload.
295
+ **`trigger`** a partial pattern object, NOT an Interaction instance:
296
+ ```typescript
297
+ trigger: {
298
+ recordName: string // e.g. InteractionEventEntity.name
299
+ type: 'create' | 'update' | 'delete'
300
+ record?: Record<string, any> // deep partial match, e.g. { interactionName: myInteraction.name }
301
+ oldRecord?: Record<string, any>
302
+ keys?: string[]
303
+ }
304
+ ```
305
+
306
+ **`computeTarget`** — receives the mutation event, returns which record(s) to transition:
307
+ - Entity: `{ id: string }` or `{ id: string }[]`
308
+ - Relation: `{ source: { id: string }, target: { id: string } }`
309
+ - Return `undefined` to skip
310
+ - `this` is bound to Controller — async functions can use `this.system.storage`
311
+ - Required for property-level StateMachines; omit for global StateMachines
232
312
 
233
313
  ---
234
314
 
@@ -236,26 +316,34 @@ StateTransfer.create(args: {
236
316
 
237
317
  ```typescript
238
318
  new Controller(args: {
239
- system: MonoSystem
240
- entities: EntityInstance[]
241
- relations: RelationInstance[]
242
- activities: ActivityInstance[]
243
- interactions: InteractionInstance[]
244
- dict: DictionaryInstance[] // Global dictionaries, NOT computations
245
- recordMutationSideEffects?: any[]
319
+ system: System
320
+ entities?: EntityInstance[]
321
+ relations?: RelationInstance[]
322
+ eventSources?: EventSourceInstance[] // Interactions, custom EventSources, etc.
323
+ dict?: DictionaryInstance[] // Global dictionaries
324
+ recordMutationSideEffects?: RecordMutationSideEffect[]
325
+ computations?: (new (...args: any[]) => Computation)[] // Additional computation handle classes
326
+ ignoreGuard?: boolean // Skip guard checks when true
327
+ forceThrowDispatchError?: boolean // Throw errors instead of returning them
246
328
  }): Controller
247
329
 
248
- controller.setup(install: boolean): Promise<void>
249
- controller.callInteraction(name: string, args: {
250
- user: { id: string, [key: string]: any }
251
- payload?: { [key: string]: any }
252
- }): Promise<{ error?: { message: string, [key: string]: any }, [key: string]: any }>
330
+ controller.setup(install?: boolean): Promise<void>
331
+
332
+ controller.dispatch<TArgs, TResult>(
333
+ eventSource: EventSourceInstance<TArgs, TResult>,
334
+ args: TArgs
335
+ ): Promise<DispatchResponse>
336
+
337
+ // DispatchResponse = { error?, data?, effects?, sideEffects?, context? }
253
338
  ```
254
339
 
255
340
  Constraints:
256
- - ALWAYS call `setup(true)` before any `callInteraction`
341
+ - ALWAYS call `setup(true)` before any `dispatch`
257
342
  - `dict` is for Dictionary instances ONLY — never pass computations here
258
- - `callInteraction` NEVER throws errors are in `result.error`
343
+ - `dispatch` first parameter is the event source object reference, NOT a name string
344
+ - `dispatch` NEVER throws by default — errors are in `result.error`
345
+ - Set `forceThrowDispatchError: true` to make dispatch throw instead
346
+ - Controller automatically registers event source entities (e.g. `InteractionEventEntity`)
259
347
 
260
348
  ---
261
349
 
@@ -284,20 +372,29 @@ system.storage.find(
284
372
 
285
373
  system.storage.findOne(
286
374
  entityName: string,
287
- matchExp: MatchExp,
375
+ matchExp?: MatchExp,
288
376
  modifier?: any,
289
377
  attributeQuery?: AttributeQuery
290
378
  ): Promise<any>
291
379
 
292
380
  system.storage.create(entityName: string, data: object): Promise<any>
293
- system.storage.update(entityName: string, matchExp: MatchExp, data: object): Promise<void>
381
+ system.storage.update(entityName: string, matchExp: MatchExp, data: object): Promise<any>
294
382
  system.storage.delete(entityName: string, matchExp: MatchExp): Promise<void>
383
+
384
+ // Dictionary-specific API
385
+ system.storage.dict.get(key: string): Promise<any>
386
+ system.storage.dict.set(key: string, value: any): Promise<void>
387
+
388
+ // General KV storage
389
+ system.storage.get(itemName: string, id: string, initialValue?: any): Promise<any>
390
+ system.storage.set(itemName: string, id: string, value: any): Promise<any>
295
391
  ```
296
392
 
297
393
  Constraints:
298
394
  - ALWAYS pass `attributeQuery` to `find`/`findOne` — without it, only `id` is returned
299
395
  - Use `['*']` for all fields
300
396
  - `create`/`update`/`delete` bypass all validation — use ONLY for test setup
397
+ - When querying relations, use dot notation for source/target: `{ key: 'source.id', value: ['=', id] }`
301
398
 
302
399
  ---
303
400
 
@@ -306,12 +403,15 @@ Constraints:
306
403
  ```typescript
307
404
  MatchExp.atom(args: { key: string, value: [operator, value] }): MatchExp
308
405
 
309
- // Operators: '=', '!=', '>', '>=', '<', '<=', 'like', 'in', 'between', 'not', 'exist'
406
+ // Operators: '=', '!=', '>', '>=', '<', '<=', 'like', 'in', 'between', 'not'
310
407
 
311
408
  // Chaining
312
409
  matchExp.and(args: { key: string, value: [operator, value] }): MatchExp
313
410
  matchExp.or(args: { key: string, value: [operator, value] }): MatchExp
314
411
 
412
+ // From object (all AND)
413
+ MatchExp.fromObject({ status: 'active', age: 25 }): MatchExp
414
+
315
415
  // Nested field access
316
416
  MatchExp.atom({ key: 'author.name', value: ['=', 'Alice'] })
317
417
  ```
@@ -346,24 +446,43 @@ MatchExp.atom({ key: 'author.name', value: ['=', 'Alice'] })
346
446
 
347
447
  ```typescript
348
448
  Attributive.create(args: {
349
- name: string
449
+ name?: string
350
450
  content: (record: any, eventArgs: any) => boolean
351
451
  }): AttributiveInstance
352
452
  ```
353
453
 
354
- Used on PayloadItem `attributives` to validate referenced entities.
454
+ Used on Interaction `userAttributives` to validate user context.
355
455
 
356
456
  ---
357
457
 
358
458
  ## BoolExp
359
459
 
360
460
  ```typescript
361
- BoolExp.atom(attributive: AttributiveInstance): BoolExp
362
- boolExp.and(other: BoolExp): BoolExp
363
- boolExp.or(other: BoolExp): BoolExp
461
+ BoolExp.atom(data: T): BoolExp
462
+ boolExp.and(other: BoolExp | T): BoolExp
463
+ boolExp.or(other: BoolExp | T): BoolExp
364
464
  ```
365
465
 
366
- Combines multiple Attributives for complex validation logic.
466
+ Combines multiple Attributives, Conditions, or other expressions for complex logic.
467
+
468
+ ---
469
+
470
+ ## Condition.create / Conditions.create
471
+
472
+ ```typescript
473
+ Condition.create(args: {
474
+ name?: string
475
+ content: (this: Controller, event: InteractionEventArgs) => Promise<boolean>
476
+ }): ConditionInstance
477
+
478
+ Conditions.create(args: {
479
+ content: BoolExp<ConditionInstance> // Combined with AND/OR logic
480
+ }): ConditionsInstance
481
+ ```
482
+
483
+ - `content` returns `true` to allow, `false` to reject
484
+ - `this` is bound to Controller — can access `this.system.storage`
485
+ - Failed conditions return `{ error: { type: 'condition check failed' } }`
367
486
 
368
487
  ---
369
488
 
@@ -374,12 +493,53 @@ Dictionary.create(args: {
374
493
  name: string
375
494
  type: 'string' | 'number' | 'boolean' | 'object'
376
495
  collection?: boolean
377
- defaultValue?: any | (() => any)
496
+ defaultValue?: Function
378
497
  computation?: ComputationInstance
379
498
  }): DictionaryInstance
380
499
  ```
381
500
 
382
- Global state values. Pass to Controller's `dict` parameter. Access via `system.storage.get('state', name)`.
501
+ Global state values. Pass to Controller's `dict` parameter. Access via `system.storage.dict.get(name)` / `system.storage.dict.set(name, value)`.
502
+
503
+ ---
504
+
505
+ ## EventSource.create
506
+
507
+ ```typescript
508
+ EventSource.create(args: {
509
+ name: string // Event source identifier
510
+ entity: EntityInstance // Entity to persist event records
511
+ guard?: (this: Controller, args: TArgs) => Promise<void>
512
+ mapEventData?: (args: TArgs) => Record<string, any>
513
+ resolve?: (this: Controller, args: TArgs) => Promise<TResult>
514
+ afterDispatch?: (this: Controller, args: TArgs, result: { data?: TResult }) => Promise<Record<string, unknown> | void>
515
+ }): EventSourceInstance
516
+ ```
517
+
518
+ Custom event source for scheduled tasks, webhooks, or any non-interaction trigger. Dispatch via `controller.dispatch(eventSource, args)`.
519
+
520
+ ---
521
+
522
+ ## HardDeletionProperty.create
523
+
524
+ ```typescript
525
+ HardDeletionProperty.create(): PropertyInstance
526
+ ```
527
+
528
+ Creates a property named `_isDeleted_`. When its value transitions to `true` (via StateMachine), the Controller physically deletes the record. Use with `DELETED_STATE` / `NON_DELETED_STATE`.
529
+
530
+ ---
531
+
532
+ ## RecordMutationSideEffect.create
533
+
534
+ ```typescript
535
+ RecordMutationSideEffect.create(args: {
536
+ name: string
537
+ record: { name: string } // Entity/relation name to monitor
538
+ content: (this: Controller, event: RecordMutationEvent) => Promise<any>
539
+ }): RecordMutationSideEffect
540
+ ```
541
+
542
+ Triggers custom logic on record mutations within dispatch context. Results available in `dispatchResult.sideEffects`.
383
543
 
384
544
  ---
385
545
 
@@ -387,23 +547,52 @@ Global state values. Pass to Controller's `dict` parameter. Access via `system.s
387
547
 
388
548
  ```typescript
389
549
  import {
550
+ // Data model
390
551
  Entity, Property, Relation,
391
- Interaction, Action, Payload, PayloadItem,
552
+
553
+ // Event sources
554
+ EventSource, Interaction, Action, GetAction, Payload, PayloadItem,
392
555
  Activity,
393
- Count, Every, Any, Sum, Summation, WeightedSummation, Average,
556
+
557
+ // Computations
558
+ Count, Every, Any, Summation, WeightedSummation, Average,
394
559
  Transform, StateMachine, StateNode, StateTransfer,
395
- RealTime, Expression, Inequality, Equation, MathResolver,
396
- Attributive, Attributives, Condition, Conditions,
560
+ RealTime, Custom,
561
+
562
+ // Math (for RealTime)
563
+ Expression, Inequality, Equation, MathResolver,
564
+
565
+ // Validation & conditions
566
+ Attributive, Attributives, DataAttributive, DataAttributives,
567
+ Condition, Conditions,
568
+
569
+ // Data policy
570
+ DataPolicy,
571
+
572
+ // Expressions
397
573
  BoolExp, MatchExp,
574
+
575
+ // System
398
576
  Controller, MonoSystem, Dictionary,
577
+ RecordMutationSideEffect,
578
+ HardDeletionProperty, HARD_DELETION_PROPERTY_NAME,
579
+ NON_DELETED_STATE, DELETED_STATE,
580
+
581
+ // Built-in entities
399
582
  InteractionEventEntity,
583
+
584
+ // Drivers
400
585
  PGLiteDB, SQLiteDB, PostgreSQLDB, MysqlDB,
586
+
587
+ // Utilities
401
588
  KlassByName
402
589
  } from 'interaqt'
403
590
  ```
404
591
 
405
592
  Non-existent exports (commonly mistaken):
406
593
  - `InteractionEvent` → use `InteractionEventEntity`
407
- - `FilteredEntity` → use `Entity.create` with `baseEntity` + `filterCondition`
594
+ - `FilteredEntity` → use `Entity.create` with `baseEntity` + `matchExpression`
408
595
  - `RelationBasedEvery` → use `Every`
596
+ - `Sum` → use `Summation`
597
+ - `callInteraction` → use `controller.dispatch(eventSource, args)`
409
598
  - `User`, `Post`, etc. → no pre-built entities exist
package/package.json CHANGED
@@ -42,7 +42,7 @@
42
42
  "sync:agent": "tsx scripts/sync-agent-files.ts",
43
43
  "release": "release-it"
44
44
  },
45
- "version": "1.1.2",
45
+ "version": "1.1.3",
46
46
  "main": "dist/index.js",
47
47
  "files": [
48
48
  "dist",