interaqt 1.1.2 → 1.2.0

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 (61) hide show
  1. package/agent/agentspace/knowledge/generator/api-reference.md +19 -21
  2. package/agent/agentspace/knowledge/generator/computation-implementation.md +6 -0
  3. package/agent/agentspace/knowledge/generator/integration-implementation-handler.md +2 -0
  4. package/agent/agentspace/knowledge/usage/04-reactive-computations.md +8 -0
  5. package/agent/agentspace/knowledge/usage/05-interactions.md +13 -0
  6. package/agent/agentspace/knowledge/usage/10-async-computations.md +13 -0
  7. package/agent/agentspace/knowledge/usage/13-testing.md +12 -2
  8. package/agent/agentspace/knowledge/usage/14-api-reference.md +10 -0
  9. package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +6 -1
  10. package/agent/agentspace/knowledge/usage/20-postgresql-concurrency-migration.md +105 -0
  11. package/agent/agentspace/knowledge/usage/README.md +1 -0
  12. package/agent/skill/interaqt-patterns.md +41 -36
  13. package/agent/skill/interaqt-recipes.md +164 -108
  14. package/agent/skill/interaqt-reference.md +264 -75
  15. package/dist/core/Custom.d.ts +18 -0
  16. package/dist/core/Custom.d.ts.map +1 -1
  17. package/dist/core/EventSource.d.ts +17 -0
  18. package/dist/core/EventSource.d.ts.map +1 -1
  19. package/dist/drivers/PGLite.d.ts +2 -0
  20. package/dist/drivers/PGLite.d.ts.map +1 -1
  21. package/dist/drivers/PostgreSQL.d.ts +27 -5
  22. package/dist/drivers/PostgreSQL.d.ts.map +1 -1
  23. package/dist/drivers/SQLite.d.ts +2 -0
  24. package/dist/drivers/SQLite.d.ts.map +1 -1
  25. package/dist/index.js +3651 -3042
  26. package/dist/index.js.map +1 -1
  27. package/dist/runtime/ComputationSourceMap.d.ts.map +1 -1
  28. package/dist/runtime/Controller.d.ts +1 -0
  29. package/dist/runtime/Controller.d.ts.map +1 -1
  30. package/dist/runtime/MonoSystem.d.ts +2 -0
  31. package/dist/runtime/MonoSystem.d.ts.map +1 -1
  32. package/dist/runtime/Scheduler.d.ts +14 -1
  33. package/dist/runtime/Scheduler.d.ts.map +1 -1
  34. package/dist/runtime/System.d.ts +40 -6
  35. package/dist/runtime/System.d.ts.map +1 -1
  36. package/dist/runtime/computations/Any.d.ts.map +1 -1
  37. package/dist/runtime/computations/Average.d.ts +2 -2
  38. package/dist/runtime/computations/Average.d.ts.map +1 -1
  39. package/dist/runtime/computations/Computation.d.ts +17 -0
  40. package/dist/runtime/computations/Computation.d.ts.map +1 -1
  41. package/dist/runtime/computations/Count.d.ts +5 -1
  42. package/dist/runtime/computations/Count.d.ts.map +1 -1
  43. package/dist/runtime/computations/Every.d.ts +1 -2
  44. package/dist/runtime/computations/Every.d.ts.map +1 -1
  45. package/dist/runtime/computations/StateMachine.d.ts.map +1 -1
  46. package/dist/runtime/computations/Summation.d.ts +3 -1
  47. package/dist/runtime/computations/Summation.d.ts.map +1 -1
  48. package/dist/runtime/computations/Transform.d.ts.map +1 -1
  49. package/dist/runtime/computations/WeightedSummation.d.ts +3 -1
  50. package/dist/runtime/computations/WeightedSummation.d.ts.map +1 -1
  51. package/dist/runtime/index.d.ts +1 -0
  52. package/dist/runtime/index.d.ts.map +1 -1
  53. package/dist/runtime/transaction.d.ts +15 -0
  54. package/dist/runtime/transaction.d.ts.map +1 -0
  55. package/dist/storage/erstorage/EntityQueryHandle.d.ts +1 -0
  56. package/dist/storage/erstorage/EntityQueryHandle.d.ts.map +1 -1
  57. package/dist/storage/erstorage/QueryExecutor.d.ts +1 -1
  58. package/dist/storage/erstorage/QueryExecutor.d.ts.map +1 -1
  59. package/dist/storage/erstorage/RecordQueryAgent.d.ts +1 -0
  60. package/dist/storage/erstorage/RecordQueryAgent.d.ts.map +1 -1
  61. package/package.json +2 -1
@@ -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.