interaqt 1.1.0 → 1.1.2

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 (105) hide show
  1. package/README.md +329 -0
  2. package/agent/skill/interaqt-patterns.md +536 -0
  3. package/agent/skill/interaqt-recipes.md +501 -0
  4. package/agent/skill/interaqt-reference.md +409 -0
  5. package/dist/builtins/interaction/Interaction.d.ts +16 -10
  6. package/dist/builtins/interaction/Interaction.d.ts.map +1 -1
  7. package/dist/builtins/interaction/activity/ActivityManager.d.ts.map +1 -1
  8. package/dist/builtins/interaction/errors/ActivityErrors.d.ts +2 -2
  9. package/dist/builtins/interaction/errors/ActivityErrors.d.ts.map +1 -1
  10. package/dist/builtins/interaction/errors/InteractionErrors.d.ts +1 -1
  11. package/dist/builtins/interaction/errors/InteractionErrors.d.ts.map +1 -1
  12. package/dist/core/BoolExp.d.ts +2 -1
  13. package/dist/core/BoolExp.d.ts.map +1 -1
  14. package/dist/core/Computation.d.ts +3 -8
  15. package/dist/core/Computation.d.ts.map +1 -1
  16. package/dist/core/Custom.d.ts +2 -2
  17. package/dist/core/Custom.d.ts.map +1 -1
  18. package/dist/core/EventSource.d.ts +18 -16
  19. package/dist/core/EventSource.d.ts.map +1 -1
  20. package/dist/core/Property.d.ts +0 -3
  21. package/dist/core/Property.d.ts.map +1 -1
  22. package/dist/core/RealDictionary.d.ts +0 -3
  23. package/dist/core/RealDictionary.d.ts.map +1 -1
  24. package/dist/core/Relation.d.ts.map +1 -1
  25. package/dist/core/StateNode.d.ts +3 -3
  26. package/dist/core/StateNode.d.ts.map +1 -1
  27. package/dist/core/StateTransfer.d.ts +2 -6
  28. package/dist/core/StateTransfer.d.ts.map +1 -1
  29. package/dist/core/Transform.d.ts +2 -2
  30. package/dist/core/Transform.d.ts.map +1 -1
  31. package/dist/core/interfaces.d.ts +0 -9
  32. package/dist/core/interfaces.d.ts.map +1 -1
  33. package/dist/core/types.d.ts +0 -29
  34. package/dist/core/types.d.ts.map +1 -1
  35. package/dist/core/utils.d.ts +22 -6
  36. package/dist/core/utils.d.ts.map +1 -1
  37. package/dist/drivers/Mysql.d.ts +5 -5
  38. package/dist/drivers/Mysql.d.ts.map +1 -1
  39. package/dist/drivers/PGLite.d.ts +5 -5
  40. package/dist/drivers/PGLite.d.ts.map +1 -1
  41. package/dist/drivers/PostgreSQL.d.ts +5 -5
  42. package/dist/drivers/PostgreSQL.d.ts.map +1 -1
  43. package/dist/drivers/SQLite.d.ts +5 -5
  44. package/dist/drivers/SQLite.d.ts.map +1 -1
  45. package/dist/index.js +194 -228
  46. package/dist/index.js.map +1 -1
  47. package/dist/runtime/Controller.d.ts +14 -14
  48. package/dist/runtime/Controller.d.ts.map +1 -1
  49. package/dist/runtime/MonoSystem.d.ts +4 -4
  50. package/dist/runtime/MonoSystem.d.ts.map +1 -1
  51. package/dist/runtime/Scheduler.d.ts +3 -3
  52. package/dist/runtime/Scheduler.d.ts.map +1 -1
  53. package/dist/runtime/System.d.ts +50 -51
  54. package/dist/runtime/System.d.ts.map +1 -1
  55. package/dist/runtime/computations/Any.d.ts +4 -4
  56. package/dist/runtime/computations/Any.d.ts.map +1 -1
  57. package/dist/runtime/computations/Average.d.ts +2 -2
  58. package/dist/runtime/computations/Average.d.ts.map +1 -1
  59. package/dist/runtime/computations/Computation.d.ts +41 -47
  60. package/dist/runtime/computations/Computation.d.ts.map +1 -1
  61. package/dist/runtime/computations/Count.d.ts +4 -4
  62. package/dist/runtime/computations/Count.d.ts.map +1 -1
  63. package/dist/runtime/computations/Every.d.ts +4 -4
  64. package/dist/runtime/computations/Every.d.ts.map +1 -1
  65. package/dist/runtime/computations/RealTime.d.ts +9 -17
  66. package/dist/runtime/computations/RealTime.d.ts.map +1 -1
  67. package/dist/runtime/computations/StateMachine.d.ts +2 -2
  68. package/dist/runtime/computations/Summation.d.ts +2 -2
  69. package/dist/runtime/computations/Summation.d.ts.map +1 -1
  70. package/dist/runtime/computations/TransitionFinder.d.ts +9 -4
  71. package/dist/runtime/computations/TransitionFinder.d.ts.map +1 -1
  72. package/dist/runtime/computations/WeightedSummation.d.ts +2 -2
  73. package/dist/runtime/computations/WeightedSummation.d.ts.map +1 -1
  74. package/dist/runtime/errors/ComputationErrors.d.ts +3 -3
  75. package/dist/runtime/errors/ComputationErrors.d.ts.map +1 -1
  76. package/dist/runtime/errors/ConditionErrors.d.ts +6 -6
  77. package/dist/runtime/errors/ConditionErrors.d.ts.map +1 -1
  78. package/dist/runtime/errors/FrameworkError.d.ts +4 -4
  79. package/dist/runtime/errors/FrameworkError.d.ts.map +1 -1
  80. package/dist/runtime/errors/SideEffectError.d.ts +1 -1
  81. package/dist/runtime/errors/SideEffectError.d.ts.map +1 -1
  82. package/dist/runtime/errors/SystemErrors.d.ts +1 -1
  83. package/dist/runtime/errors/SystemErrors.d.ts.map +1 -1
  84. package/dist/runtime/errors/index.d.ts +5 -5
  85. package/dist/runtime/errors/index.d.ts.map +1 -1
  86. package/dist/runtime/types/computation.d.ts +11 -0
  87. package/dist/runtime/types/computation.d.ts.map +1 -0
  88. package/dist/runtime/util.d.ts +6 -6
  89. package/dist/runtime/util.d.ts.map +1 -1
  90. package/dist/storage/erstorage/MatchExp.d.ts +4 -4
  91. package/dist/storage/erstorage/MatchExp.d.ts.map +1 -1
  92. package/dist/storage/erstorage/QueryExecutor.d.ts +1 -1
  93. package/dist/storage/erstorage/QueryExecutor.d.ts.map +1 -1
  94. package/dist/storage/erstorage/RecordQuery.d.ts +5 -5
  95. package/dist/storage/erstorage/RecordQuery.d.ts.map +1 -1
  96. package/dist/storage/erstorage/SQLBuilder.d.ts +11 -11
  97. package/dist/storage/erstorage/SQLBuilder.d.ts.map +1 -1
  98. package/dist/storage/erstorage/Setup.d.ts +1 -1
  99. package/dist/storage/erstorage/util/RecursiveContext.d.ts +4 -10
  100. package/dist/storage/erstorage/util/RecursiveContext.d.ts.map +1 -1
  101. package/dist/storage/erstorage/util.d.ts +3 -3
  102. package/dist/storage/erstorage/util.d.ts.map +1 -1
  103. package/dist/storage/utils.d.ts +2 -2
  104. package/dist/storage/utils.d.ts.map +1 -1
  105. package/package.json +4 -1
@@ -0,0 +1,501 @@
1
+ # interaqt Recipes
2
+
3
+ > Complete runnable scenarios. Read this when building a feature from scratch.
4
+
5
+ ---
6
+
7
+ # Recipe: Blog with Author Stats
8
+
9
+ ## Scenario
10
+ A blog system where users author posts. Each user has an auto-maintained `postCount` property. Demonstrates Entity, Relation (1:n), Count computation, Interaction-driven entity creation via Transform, and querying with nested attributeQuery.
11
+
12
+ ## Complete Implementation
13
+
14
+ ```typescript
15
+ import {
16
+ Entity, Property, Relation, Count,
17
+ Interaction, Action, Payload, PayloadItem,
18
+ Transform, InteractionEventEntity,
19
+ Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
20
+ } from 'interaqt'
21
+
22
+ // --- Entities ---
23
+
24
+ const User = Entity.create({
25
+ name: 'User',
26
+ properties: [
27
+ Property.create({ name: 'name', type: 'string' }),
28
+ Property.create({ name: 'email', type: 'string' }),
29
+ Property.create({
30
+ name: 'postCount',
31
+ type: 'number',
32
+ defaultValue: () => 0,
33
+ computation: Count.create({ record: UserPosts })
34
+ })
35
+ ]
36
+ })
37
+
38
+ const Post = Entity.create({
39
+ name: 'Post',
40
+ properties: [
41
+ Property.create({ name: 'title', type: 'string' }),
42
+ Property.create({ name: 'content', type: 'string' }),
43
+ Property.create({ name: 'createdAt', type: 'string' })
44
+ ],
45
+ computation: Transform.create({
46
+ record: InteractionEventEntity,
47
+ attributeQuery: ['interactionName', 'user', 'payload'],
48
+ callback: function(event) {
49
+ if (event.interactionName === 'CreatePost') {
50
+ return {
51
+ title: event.payload.title,
52
+ content: event.payload.content,
53
+ createdAt: new Date().toISOString(),
54
+ author: { id: event.user.id }
55
+ }
56
+ }
57
+ return null
58
+ }
59
+ })
60
+ })
61
+
62
+ // --- Relations ---
63
+
64
+ const UserPosts = Relation.create({
65
+ source: Post,
66
+ sourceProperty: 'author',
67
+ target: User,
68
+ targetProperty: 'posts',
69
+ type: 'n:1'
70
+ })
71
+
72
+ // --- Interactions ---
73
+
74
+ const CreatePost = Interaction.create({
75
+ name: 'CreatePost',
76
+ action: Action.create({ name: 'createPost' }),
77
+ payload: Payload.create({
78
+ items: [
79
+ PayloadItem.create({ name: 'title', required: true }),
80
+ PayloadItem.create({ name: 'content', required: true })
81
+ ]
82
+ })
83
+ })
84
+
85
+ // --- Controller Setup ---
86
+
87
+ const system = new MonoSystem(new PGLiteDB())
88
+ system.conceptClass = KlassByName
89
+
90
+ const controller = new Controller({
91
+ system,
92
+ entities: [User, Post],
93
+ relations: [UserPosts],
94
+ activities: [],
95
+ interactions: [CreatePost],
96
+ dict: [],
97
+ recordMutationSideEffects: []
98
+ })
99
+
100
+ await controller.setup(true)
101
+
102
+ // --- Usage ---
103
+
104
+ const adminUser = await system.storage.create('User', {
105
+ name: 'Alice', email: 'alice@example.com'
106
+ })
107
+
108
+ const result = await controller.callInteraction('CreatePost', {
109
+ user: adminUser,
110
+ payload: { title: 'First Post', content: 'Hello World' }
111
+ })
112
+
113
+ const user = await system.storage.findOne(
114
+ 'User',
115
+ MatchExp.atom({ key: 'id', value: ['=', adminUser.id] }),
116
+ undefined,
117
+ ['id', 'name', 'postCount', ['posts', { attributeQuery: ['id', 'title'] }]]
118
+ )
119
+ // user.postCount === 1
120
+ // user.posts[0].title === 'First Post'
121
+ ```
122
+
123
+ ## Design Decisions
124
+ - **Count on `postCount`**: Automatically maintained when UserPosts relations change. No manual update logic needed.
125
+ - **Transform on Post entity**: Posts are created reactively when `CreatePost` interaction fires. The Transform checks `interactionName` and returns entity data.
126
+ - **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
+
128
+ ---
129
+
130
+ # Recipe: Order Workflow with State Machine
131
+
132
+ ## Scenario
133
+ An order system with status transitions: pending → paid → shipped → delivered, plus cancellation. Demonstrates StateMachine, StateNode, StateTransfer, and multiple Interactions triggering state changes.
134
+
135
+ ## Complete Implementation
136
+
137
+ ```typescript
138
+ import {
139
+ Entity, Property,
140
+ Interaction, Action, Payload, PayloadItem,
141
+ Transform, InteractionEventEntity,
142
+ StateMachine, StateNode, StateTransfer,
143
+ Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
144
+ } from 'interaqt'
145
+
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
+ // --- State Nodes ---
190
+
191
+ const pendingState = StateNode.create({ name: 'pending' })
192
+ const paidState = StateNode.create({ name: 'paid' })
193
+ const shippedState = StateNode.create({ name: 'shipped' })
194
+ const deliveredState = StateNode.create({ name: 'delivered' })
195
+ const cancelledState = StateNode.create({ name: 'cancelled' })
196
+
197
+ // --- Entity ---
198
+
199
+ const Order = Entity.create({
200
+ name: 'Order',
201
+ properties: [
202
+ Property.create({ name: 'product', type: 'string' }),
203
+ Property.create({ name: 'quantity', type: 'number' }),
204
+ Property.create({ name: 'createdAt', type: 'string' }),
205
+ Property.create({
206
+ name: 'status',
207
+ type: 'string',
208
+ defaultValue: () => 'pending',
209
+ computation: StateMachine.create({
210
+ states: [pendingState, paidState, shippedState, deliveredState, cancelledState],
211
+ transfers: [
212
+ StateTransfer.create({
213
+ current: pendingState, next: paidState,
214
+ trigger: PayOrder,
215
+ computeTarget: (event) => ({ id: event.payload.orderId })
216
+ }),
217
+ StateTransfer.create({
218
+ current: paidState, next: shippedState,
219
+ trigger: ShipOrder,
220
+ computeTarget: (event) => ({ id: event.payload.orderId })
221
+ }),
222
+ StateTransfer.create({
223
+ current: pendingState, next: cancelledState,
224
+ trigger: CancelOrder,
225
+ computeTarget: (event) => ({ id: event.payload.orderId })
226
+ })
227
+ ],
228
+ initialState: pendingState
229
+ })
230
+ })
231
+ ],
232
+ computation: Transform.create({
233
+ record: InteractionEventEntity,
234
+ attributeQuery: ['interactionName', 'user', 'payload'],
235
+ callback: function(event) {
236
+ if (event.interactionName === 'SubmitOrder') {
237
+ return {
238
+ product: event.payload.product,
239
+ quantity: event.payload.quantity,
240
+ createdAt: new Date().toISOString()
241
+ }
242
+ }
243
+ return null
244
+ }
245
+ })
246
+ })
247
+
248
+ // --- Controller Setup & Usage ---
249
+
250
+ const system = new MonoSystem(new PGLiteDB())
251
+ system.conceptClass = KlassByName
252
+
253
+ const controller = new Controller({
254
+ system,
255
+ entities: [Order],
256
+ relations: [],
257
+ activities: [],
258
+ interactions: [SubmitOrder, PayOrder, ShipOrder, CancelOrder],
259
+ dict: [],
260
+ recordMutationSideEffects: []
261
+ })
262
+ await controller.setup(true)
263
+
264
+ const user = { id: 'user-1' }
265
+
266
+ // Submit order
267
+ const submitResult = await controller.callInteraction('SubmitOrder', {
268
+ user,
269
+ payload: { product: 'Widget', quantity: 3 }
270
+ })
271
+
272
+ const order = await system.storage.findOne('Order',
273
+ MatchExp.atom({ key: 'product', value: ['=', 'Widget'] }),
274
+ undefined, ['id', 'status', 'product', 'quantity']
275
+ )
276
+ // order.status === 'pending'
277
+
278
+ // Pay order
279
+ await controller.callInteraction('PayOrder', {
280
+ user,
281
+ payload: { orderId: order.id }
282
+ })
283
+ // order.status → 'paid'
284
+
285
+ // Ship order
286
+ await controller.callInteraction('ShipOrder', {
287
+ user,
288
+ payload: { orderId: order.id }
289
+ })
290
+ // order.status → 'shipped'
291
+ ```
292
+
293
+ ## Design Decisions
294
+ - **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.
296
+ - **Transform on Entity `computation`**: Creates order records reactively when `SubmitOrder` fires.
297
+ - **Cancellation only from `pending`**: Only one `cancelledState` transfer is defined (from `pending`). Attempting to cancel a paid order will have no effect.
298
+
299
+ ---
300
+
301
+ # Recipe: Student GPA with Weighted Summation
302
+
303
+ ## 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.
305
+
306
+ ## Complete Implementation
307
+
308
+ ```typescript
309
+ import {
310
+ Entity, Property, Relation, WeightedSummation, Count,
311
+ Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
312
+ } from 'interaqt'
313
+
314
+ // --- Entities ---
315
+
316
+ const Student = Entity.create({
317
+ name: 'Student',
318
+ properties: [
319
+ Property.create({ name: 'name', type: 'string' }),
320
+ Property.create({
321
+ name: 'gpa',
322
+ type: 'number',
323
+ defaultValue: () => 0,
324
+ computation: WeightedSummation.create({
325
+ record: StudentGrades,
326
+ attributeQuery: [['target', { attributeQuery: ['score', 'credit'] }]],
327
+ callback: (relation) => ({
328
+ weight: relation.target.credit,
329
+ value: relation.target.score
330
+ })
331
+ })
332
+ }),
333
+ Property.create({
334
+ name: 'totalCredits',
335
+ type: 'number',
336
+ 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
+ })
344
+ })
345
+ }),
346
+ Property.create({
347
+ name: 'courseCount',
348
+ type: 'number',
349
+ defaultValue: () => 0,
350
+ computation: Count.create({ record: StudentGrades })
351
+ })
352
+ ]
353
+ })
354
+
355
+ const Grade = Entity.create({
356
+ name: 'Grade',
357
+ properties: [
358
+ Property.create({ name: 'subject', type: 'string' }),
359
+ Property.create({ name: 'score', type: 'number' }),
360
+ Property.create({ name: 'credit', type: 'number' })
361
+ ]
362
+ })
363
+
364
+ // --- Relations ---
365
+
366
+ const StudentGrades = Relation.create({
367
+ source: Student,
368
+ sourceProperty: 'grades',
369
+ target: Grade,
370
+ targetProperty: 'student',
371
+ type: '1:n'
372
+ })
373
+
374
+ // --- Controller Setup & Usage ---
375
+
376
+ const system = new MonoSystem(new PGLiteDB())
377
+ system.conceptClass = KlassByName
378
+
379
+ const controller = new Controller({
380
+ system,
381
+ entities: [Student, Grade],
382
+ relations: [StudentGrades],
383
+ activities: [],
384
+ interactions: [],
385
+ dict: [],
386
+ recordMutationSideEffects: []
387
+ })
388
+ await controller.setup(true)
389
+
390
+ const student = await system.storage.create('Student', { name: 'Alice' })
391
+
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 })
394
+
395
+ const result = await system.storage.findOne('Student',
396
+ MatchExp.atom({ key: 'id', value: ['=', student.id] }),
397
+ undefined,
398
+ ['id', 'name', 'gpa', 'totalCredits', 'courseCount']
399
+ )
400
+ // result.gpa === (90*4 + 80*3) / (4+3) ≈ 85.7
401
+ // result.totalCredits === 7
402
+ // result.courseCount === 2
403
+ ```
404
+
405
+ ## 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.
409
+
410
+ ---
411
+
412
+ # Recipe: Interaction with Payload Validation
413
+
414
+ ## Scenario
415
+ A content moderation system where only published posts can be shared. Demonstrates Attributive-based payload validation on interactions.
416
+
417
+ ## Complete Implementation
418
+
419
+ ```typescript
420
+ import {
421
+ Entity, Property,
422
+ Interaction, Action, Payload, PayloadItem,
423
+ Attributive, BoolExp,
424
+ Controller, MonoSystem, PGLiteDB, KlassByName, MatchExp
425
+ } from 'interaqt'
426
+
427
+ // --- Entities ---
428
+
429
+ const Post = Entity.create({
430
+ name: 'Post',
431
+ properties: [
432
+ Property.create({ name: 'title', type: 'string' }),
433
+ Property.create({ name: 'status', type: 'string', defaultValue: 'draft' })
434
+ ]
435
+ })
436
+
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 ---
447
+
448
+ const SharePost = Interaction.create({
449
+ name: 'SharePost',
450
+ action: Action.create({ name: 'sharePost' }),
451
+ payload: Payload.create({
452
+ items: [
453
+ PayloadItem.create({
454
+ name: 'post',
455
+ base: Post,
456
+ isRef: true,
457
+ required: true,
458
+ attributives: PublishedPost
459
+ })
460
+ ]
461
+ })
462
+ })
463
+
464
+ // --- Controller Setup & Usage ---
465
+
466
+ const system = new MonoSystem(new PGLiteDB())
467
+ system.conceptClass = KlassByName
468
+
469
+ const controller = new Controller({
470
+ system,
471
+ entities: [Post],
472
+ relations: [],
473
+ activities: [],
474
+ interactions: [SharePost],
475
+ dict: [],
476
+ recordMutationSideEffects: []
477
+ })
478
+ await controller.setup(true)
479
+
480
+ const draftPost = await system.storage.create('Post', { title: 'Draft', status: 'draft' })
481
+ const publishedPost = await system.storage.create('Post', { title: 'Published', status: 'published' })
482
+
483
+ // Sharing a draft post fails validation
484
+ const failResult = await controller.callInteraction('SharePost', {
485
+ user: { id: 'user-1' },
486
+ payload: { post: { id: draftPost.id } }
487
+ })
488
+ // failResult.error is defined — draft post cannot be shared
489
+
490
+ // Sharing a published post succeeds
491
+ const successResult = await controller.callInteraction('SharePost', {
492
+ user: { id: 'user-1' },
493
+ payload: { post: { id: publishedPost.id } }
494
+ })
495
+ // successResult.error is undefined — success
496
+ ```
497
+
498
+ ## 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.