@happyvertical/smrt-comments 0.57.1 → 0.57.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.
Files changed (50) hide show
  1. package/README.md +72 -0
  2. package/dist/chunks/participation-LA_cfrM2.js +276 -0
  3. package/dist/chunks/participation-LA_cfrM2.js.map +1 -0
  4. package/dist/collections/CommentReportCollection.d.ts +6 -0
  5. package/dist/collections/CommentReportCollection.d.ts.map +1 -0
  6. package/dist/collections/CommentRevisionCollection.d.ts +6 -0
  7. package/dist/collections/CommentRevisionCollection.d.ts.map +1 -0
  8. package/dist/collections/ParticipationCollection.d.ts +14 -0
  9. package/dist/collections/ParticipationCollection.d.ts.map +1 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +364 -59
  13. package/dist/index.js.map +1 -1
  14. package/dist/manifest.json +1853 -224
  15. package/dist/models/Comment.d.ts +8 -1
  16. package/dist/models/Comment.d.ts.map +1 -1
  17. package/dist/models/CommentReport.d.ts +10 -0
  18. package/dist/models/CommentReport.d.ts.map +1 -0
  19. package/dist/models/CommentRevision.d.ts +9 -0
  20. package/dist/models/CommentRevision.d.ts.map +1 -0
  21. package/dist/models/ParticipationBan.d.ts +12 -0
  22. package/dist/models/ParticipationBan.d.ts.map +1 -0
  23. package/dist/models/ParticipationPolicy.d.ts +13 -0
  24. package/dist/models/ParticipationPolicy.d.ts.map +1 -0
  25. package/dist/models/ParticipationRateBucket.d.ts +8 -0
  26. package/dist/models/ParticipationRateBucket.d.ts.map +1 -0
  27. package/dist/participation-contract.d.ts +37 -0
  28. package/dist/participation-contract.d.ts.map +1 -0
  29. package/dist/participation.d.ts +4 -0
  30. package/dist/participation.d.ts.map +1 -0
  31. package/dist/participation.js +2 -0
  32. package/dist/recipes.d.ts +6 -1
  33. package/dist/recipes.d.ts.map +1 -1
  34. package/dist/services/CommentService.d.ts +10 -1
  35. package/dist/services/CommentService.d.ts.map +1 -1
  36. package/dist/services/ParticipationService.d.ts +25 -0
  37. package/dist/services/ParticipationService.d.ts.map +1 -0
  38. package/dist/smrt-knowledge.json +477 -12
  39. package/dist/svelte/ModerationQueue.svelte +60 -0
  40. package/dist/svelte/ModerationQueue.svelte.d.ts +17 -0
  41. package/dist/svelte/ModerationQueue.svelte.d.ts.map +1 -0
  42. package/dist/svelte/RecordComments.svelte +21 -6
  43. package/dist/svelte/RecordComments.svelte.d.ts +2 -0
  44. package/dist/svelte/RecordComments.svelte.d.ts.map +1 -1
  45. package/dist/svelte/index.d.ts +2 -0
  46. package/dist/svelte/index.d.ts.map +1 -1
  47. package/dist/svelte/index.js +1 -0
  48. package/dist/types.d.ts +7 -1
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +9 -5
@@ -3,20 +3,21 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-comments",
6
- "packageVersion": "0.57.1",
6
+ "packageVersion": "0.57.3",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "4df0795f711cfff4a2b013ddf337a490a45a6c31ff643fc3253185f13a35a65e",
11
- "packageJson": "40a6980fb9626e9133a2b3984d530024c37d4bcab691c8a76a8b064571f426fc",
10
+ "manifest": "0fc777811025297280fbfcb0e4d9ae5a77b652f60d5a95636ce17cd5639c8314",
11
+ "packageJson": "37080267a2b4a385978794442257d10013b7d9d6675480ea73d0fae511a3b733",
12
12
  "agents": "3e82791ea4a860351e1be18ee8cee07902b99643bd9c53d4859e1f18683979ec",
13
- "moduleDoc:agents/test-design.md": "df07180c4186d6358a841e23c806cd521f772a98582676e7f0ab73035d5cbb53",
14
- "moduleDoc:agents/idempotent-create.md": "97043e654f93fe59f1c2bbc2f5f3adbfad616e2741756c69fa5ca7ff6d84250f"
13
+ "moduleDoc:agents/test-design.md": "c30a6d4694f838b8f61b3762559cf9434588132d6df681a0da63087297c985d3",
14
+ "moduleDoc:agents/idempotent-create.md": "51e9512a7891ac7296e121167f9eed61842ee444e121d8896fb04a47315662c7"
15
15
  },
16
16
  "exports": [
17
17
  ".",
18
18
  "./manifest",
19
19
  "./manifest.json",
20
+ "./participation",
20
21
  "./svelte"
21
22
  ],
22
23
  "dependencies": {
@@ -45,7 +46,7 @@
45
46
  "cross-package"
46
47
  ],
47
48
  "risks": [
48
- "cross-package-refs:1",
49
+ "cross-package-refs:3",
49
50
  "polymorphic-associations:1",
50
51
  "sensitive-fields-excluded"
51
52
  ],
@@ -89,6 +90,91 @@
89
90
  "tags": [],
90
91
  "risks": []
91
92
  },
93
+ {
94
+ "name": "CommentReportCollection",
95
+ "qualifiedName": "@happyvertical/smrt-comments:CommentReportCollection",
96
+ "collection": "commentreports",
97
+ "tableName": "comment_reports",
98
+ "packageName": "@happyvertical/smrt-comments",
99
+ "extends": "SmrtCollection",
100
+ "fields": [],
101
+ "relationships": [],
102
+ "methods": [],
103
+ "surfaces": [],
104
+ "relationshipFeatures": [
105
+ "uuidColumns"
106
+ ],
107
+ "tags": [],
108
+ "risks": []
109
+ },
110
+ {
111
+ "name": "CommentRevisionCollection",
112
+ "qualifiedName": "@happyvertical/smrt-comments:CommentRevisionCollection",
113
+ "collection": "commentrevisions",
114
+ "tableName": "comment_revisions",
115
+ "packageName": "@happyvertical/smrt-comments",
116
+ "extends": "SmrtCollection",
117
+ "fields": [],
118
+ "relationships": [],
119
+ "methods": [],
120
+ "surfaces": [],
121
+ "relationshipFeatures": [
122
+ "uuidColumns"
123
+ ],
124
+ "tags": [],
125
+ "risks": []
126
+ },
127
+ {
128
+ "name": "ParticipationPolicyCollection",
129
+ "qualifiedName": "@happyvertical/smrt-comments:ParticipationPolicyCollection",
130
+ "collection": "participationpolicies",
131
+ "tableName": "participation_policies",
132
+ "packageName": "@happyvertical/smrt-comments",
133
+ "extends": "SmrtCollection",
134
+ "fields": [],
135
+ "relationships": [],
136
+ "methods": [],
137
+ "surfaces": [],
138
+ "relationshipFeatures": [
139
+ "uuidColumns"
140
+ ],
141
+ "tags": [],
142
+ "risks": []
143
+ },
144
+ {
145
+ "name": "ParticipationBanCollection",
146
+ "qualifiedName": "@happyvertical/smrt-comments:ParticipationBanCollection",
147
+ "collection": "participationbans",
148
+ "tableName": "participation_bans",
149
+ "packageName": "@happyvertical/smrt-comments",
150
+ "extends": "SmrtCollection",
151
+ "fields": [],
152
+ "relationships": [],
153
+ "methods": [],
154
+ "surfaces": [],
155
+ "relationshipFeatures": [
156
+ "uuidColumns"
157
+ ],
158
+ "tags": [],
159
+ "risks": []
160
+ },
161
+ {
162
+ "name": "ParticipationRateBucketCollection",
163
+ "qualifiedName": "@happyvertical/smrt-comments:ParticipationRateBucketCollection",
164
+ "collection": "participationratebuckets",
165
+ "tableName": "participation_rate_buckets",
166
+ "packageName": "@happyvertical/smrt-comments",
167
+ "extends": "SmrtCollection",
168
+ "fields": [],
169
+ "relationships": [],
170
+ "methods": [],
171
+ "surfaces": [],
172
+ "relationshipFeatures": [
173
+ "uuidColumns"
174
+ ],
175
+ "tags": [],
176
+ "risks": []
177
+ },
92
178
  {
93
179
  "name": "Comment",
94
180
  "qualifiedName": "@happyvertical/smrt-comments:Comment",
@@ -167,6 +253,34 @@
167
253
  "required": false,
168
254
  "columnType": "TEXT",
169
255
  "readonly": true
256
+ },
257
+ {
258
+ "name": "parentCommentId",
259
+ "type": "foreignKey",
260
+ "required": false,
261
+ "related": "Comment",
262
+ "columnType": "UUID"
263
+ },
264
+ {
265
+ "name": "status",
266
+ "type": "text",
267
+ "required": true,
268
+ "columnType": "TEXT",
269
+ "default": "visible"
270
+ },
271
+ {
272
+ "name": "participationSnapshot",
273
+ "type": "text",
274
+ "required": false,
275
+ "columnType": "TEXT",
276
+ "readonly": true
277
+ },
278
+ {
279
+ "name": "editedAt",
280
+ "type": "datetime",
281
+ "required": false,
282
+ "columnType": "TIMESTAMP",
283
+ "readonly": true
170
284
  }
171
285
  ],
172
286
  "relationships": [
@@ -176,6 +290,13 @@
176
290
  "required": true,
177
291
  "related": "@happyvertical/smrt-users:User",
178
292
  "columnType": "UUID"
293
+ },
294
+ {
295
+ "name": "parentCommentId",
296
+ "type": "foreignKey",
297
+ "required": false,
298
+ "related": "Comment",
299
+ "columnType": "UUID"
179
300
  }
180
301
  ],
181
302
  "methods": [
@@ -211,6 +332,345 @@
211
332
  "relationshipFeatures": [
212
333
  "SmrtPolymorphicAssociation",
213
334
  "crossPackageRef",
335
+ "foreignKey",
336
+ "uuidColumns"
337
+ ],
338
+ "tags": [],
339
+ "risks": []
340
+ },
341
+ {
342
+ "name": "CommentReport",
343
+ "qualifiedName": "@happyvertical/smrt-comments:CommentReport",
344
+ "collection": "commentreports",
345
+ "description": "A tenant-local report; moderation resolution never deletes the report.",
346
+ "tableName": "comment_reports",
347
+ "packageName": "@happyvertical/smrt-comments",
348
+ "extends": "SmrtObject",
349
+ "fields": [
350
+ {
351
+ "name": "tenantId",
352
+ "type": "text",
353
+ "required": true,
354
+ "columnType": "UUID"
355
+ },
356
+ {
357
+ "name": "commentId",
358
+ "type": "foreignKey",
359
+ "required": true,
360
+ "related": "Comment",
361
+ "columnType": "UUID"
362
+ },
363
+ {
364
+ "name": "reporterUserId",
365
+ "type": "crossPackageRef",
366
+ "required": true,
367
+ "related": "@happyvertical/smrt-users:User",
368
+ "columnType": "UUID"
369
+ },
370
+ {
371
+ "name": "reason",
372
+ "type": "text",
373
+ "required": true,
374
+ "columnType": "TEXT"
375
+ },
376
+ {
377
+ "name": "status",
378
+ "type": "text",
379
+ "required": true,
380
+ "columnType": "TEXT",
381
+ "default": "open"
382
+ }
383
+ ],
384
+ "relationships": [
385
+ {
386
+ "name": "commentId",
387
+ "type": "foreignKey",
388
+ "required": true,
389
+ "related": "Comment",
390
+ "columnType": "UUID"
391
+ },
392
+ {
393
+ "name": "reporterUserId",
394
+ "type": "crossPackageRef",
395
+ "required": true,
396
+ "related": "@happyvertical/smrt-users:User",
397
+ "columnType": "UUID"
398
+ }
399
+ ],
400
+ "methods": [],
401
+ "tenant": {
402
+ "scoped": true,
403
+ "mode": "required",
404
+ "field": "tenantId"
405
+ },
406
+ "conflictColumns": [
407
+ "tenant_id",
408
+ "slug",
409
+ "context"
410
+ ],
411
+ "surfaces": [],
412
+ "relationshipFeatures": [
413
+ "crossPackageRef",
414
+ "foreignKey",
415
+ "uuidColumns"
416
+ ],
417
+ "tags": [],
418
+ "risks": []
419
+ },
420
+ {
421
+ "name": "CommentRevision",
422
+ "qualifiedName": "@happyvertical/smrt-comments:CommentRevision",
423
+ "collection": "commentrevisions",
424
+ "description": "Immutable prior body retained whenever an author edits a comment.",
425
+ "tableName": "comment_revisions",
426
+ "packageName": "@happyvertical/smrt-comments",
427
+ "extends": "SmrtObject",
428
+ "fields": [
429
+ {
430
+ "name": "tenantId",
431
+ "type": "text",
432
+ "required": true,
433
+ "columnType": "UUID"
434
+ },
435
+ {
436
+ "name": "commentId",
437
+ "type": "foreignKey",
438
+ "required": true,
439
+ "related": "Comment",
440
+ "columnType": "UUID"
441
+ },
442
+ {
443
+ "name": "editorUserId",
444
+ "type": "crossPackageRef",
445
+ "required": true,
446
+ "related": "@happyvertical/smrt-users:User",
447
+ "columnType": "UUID"
448
+ },
449
+ {
450
+ "name": "body",
451
+ "type": "text",
452
+ "required": true,
453
+ "columnType": "TEXT"
454
+ }
455
+ ],
456
+ "relationships": [
457
+ {
458
+ "name": "commentId",
459
+ "type": "foreignKey",
460
+ "required": true,
461
+ "related": "Comment",
462
+ "columnType": "UUID"
463
+ },
464
+ {
465
+ "name": "editorUserId",
466
+ "type": "crossPackageRef",
467
+ "required": true,
468
+ "related": "@happyvertical/smrt-users:User",
469
+ "columnType": "UUID"
470
+ }
471
+ ],
472
+ "methods": [],
473
+ "tenant": {
474
+ "scoped": true,
475
+ "mode": "required",
476
+ "field": "tenantId"
477
+ },
478
+ "conflictColumns": [
479
+ "tenant_id",
480
+ "slug",
481
+ "context"
482
+ ],
483
+ "surfaces": [],
484
+ "relationshipFeatures": [
485
+ "crossPackageRef",
486
+ "foreignKey",
487
+ "uuidColumns"
488
+ ],
489
+ "tags": [],
490
+ "risks": []
491
+ },
492
+ {
493
+ "name": "ParticipationBan",
494
+ "qualifiedName": "@happyvertical/smrt-comments:ParticipationBan",
495
+ "collection": "participationbans",
496
+ "description": "Tenant-local, reusable participation ban; a blank target applies tenant-wide.",
497
+ "tableName": "participation_bans",
498
+ "packageName": "@happyvertical/smrt-comments",
499
+ "extends": "SmrtObject",
500
+ "fields": [
501
+ {
502
+ "name": "tenantId",
503
+ "type": "text",
504
+ "required": true,
505
+ "columnType": "UUID"
506
+ },
507
+ {
508
+ "name": "actorUserId",
509
+ "type": "text",
510
+ "required": true,
511
+ "columnType": "TEXT"
512
+ },
513
+ {
514
+ "name": "recordType",
515
+ "type": "text",
516
+ "required": false,
517
+ "columnType": "TEXT"
518
+ },
519
+ {
520
+ "name": "recordId",
521
+ "type": "text",
522
+ "required": false,
523
+ "columnType": "TEXT"
524
+ },
525
+ {
526
+ "name": "reason",
527
+ "type": "text",
528
+ "required": true,
529
+ "columnType": "TEXT"
530
+ },
531
+ {
532
+ "name": "expiresAt",
533
+ "type": "datetime",
534
+ "required": false,
535
+ "columnType": "TIMESTAMP"
536
+ }
537
+ ],
538
+ "relationships": [],
539
+ "methods": [],
540
+ "tenant": {
541
+ "scoped": true,
542
+ "mode": "required",
543
+ "field": "tenantId"
544
+ },
545
+ "conflictColumns": [
546
+ "tenant_id",
547
+ "slug",
548
+ "context"
549
+ ],
550
+ "surfaces": [],
551
+ "relationshipFeatures": [
552
+ "uuidColumns"
553
+ ],
554
+ "tags": [],
555
+ "risks": []
556
+ },
557
+ {
558
+ "name": "ParticipationPolicy",
559
+ "qualifiedName": "@happyvertical/smrt-comments:ParticipationPolicy",
560
+ "collection": "participationpolicies",
561
+ "description": "A generic tenant default or exact-record override for participation actions.",
562
+ "tableName": "participation_policies",
563
+ "packageName": "@happyvertical/smrt-comments",
564
+ "extends": "SmrtObject",
565
+ "fields": [
566
+ {
567
+ "name": "tenantId",
568
+ "type": "text",
569
+ "required": true,
570
+ "columnType": "UUID"
571
+ },
572
+ {
573
+ "name": "recordType",
574
+ "type": "text",
575
+ "required": false,
576
+ "columnType": "TEXT"
577
+ },
578
+ {
579
+ "name": "recordId",
580
+ "type": "text",
581
+ "required": false,
582
+ "columnType": "TEXT"
583
+ },
584
+ {
585
+ "name": "action",
586
+ "type": "text",
587
+ "required": true,
588
+ "columnType": "TEXT"
589
+ },
590
+ {
591
+ "name": "moderationMode",
592
+ "type": "text",
593
+ "required": true,
594
+ "columnType": "TEXT",
595
+ "default": "open"
596
+ },
597
+ {
598
+ "name": "maxActions",
599
+ "type": "integer",
600
+ "required": true,
601
+ "columnType": "INTEGER",
602
+ "default": 0
603
+ },
604
+ {
605
+ "name": "windowSeconds",
606
+ "type": "integer",
607
+ "required": true,
608
+ "columnType": "INTEGER",
609
+ "default": 0
610
+ }
611
+ ],
612
+ "relationships": [],
613
+ "methods": [],
614
+ "tenant": {
615
+ "scoped": true,
616
+ "mode": "required",
617
+ "field": "tenantId"
618
+ },
619
+ "conflictColumns": [
620
+ "tenant_id",
621
+ "slug",
622
+ "context"
623
+ ],
624
+ "surfaces": [],
625
+ "relationshipFeatures": [
626
+ "uuidColumns"
627
+ ],
628
+ "tags": [],
629
+ "risks": []
630
+ },
631
+ {
632
+ "name": "ParticipationRateBucket",
633
+ "qualifiedName": "@happyvertical/smrt-comments:ParticipationRateBucket",
634
+ "collection": "participationratebuckets",
635
+ "description": "Service-owned fixed-window counter. Never delete a currently active bucket.",
636
+ "tableName": "participation_rate_buckets",
637
+ "packageName": "@happyvertical/smrt-comments",
638
+ "extends": "SmrtObject",
639
+ "fields": [
640
+ {
641
+ "name": "tenantId",
642
+ "type": "text",
643
+ "required": true,
644
+ "columnType": "UUID"
645
+ },
646
+ {
647
+ "name": "bucketKey",
648
+ "type": "text",
649
+ "required": true,
650
+ "columnType": "TEXT"
651
+ },
652
+ {
653
+ "name": "actionCount",
654
+ "type": "integer",
655
+ "required": true,
656
+ "columnType": "INTEGER",
657
+ "default": 0
658
+ }
659
+ ],
660
+ "relationships": [],
661
+ "methods": [],
662
+ "tenant": {
663
+ "scoped": true,
664
+ "mode": "required",
665
+ "field": "tenantId"
666
+ },
667
+ "conflictColumns": [
668
+ "tenant_id",
669
+ "slug",
670
+ "context"
671
+ ],
672
+ "surfaces": [],
673
+ "relationshipFeatures": [
214
674
  "uuidColumns"
215
675
  ],
216
676
  "tags": [],
@@ -220,24 +680,24 @@
220
680
  "surfaces": [],
221
681
  "prompts": [],
222
682
  "relationshipsV2": {
223
- "foreignKeyFields": 0,
224
- "crossPackageRefFields": 1,
683
+ "foreignKeyFields": 3,
684
+ "crossPackageRefFields": 3,
225
685
  "junctionCollections": 0,
226
686
  "hierarchicalObjects": 0,
227
687
  "polymorphicAssociations": 1,
228
- "uuidColumns": 6
688
+ "uuidColumns": 36
229
689
  },
230
690
  "agentDoc": "# @happyvertical/smrt-comments\n\nRecord discussions attach to arbitrary records through `Comment`'s inherited\n`metaType` and `metaId`; preserve these as a qualified polymorphic pair and\nnever replace them with a text foreign key. `CommentService` owns persistence\nand invokes the optional mention adapter only after the comment is saved.\n\nThe package must not depend on `smrt-messages`: hosts compose\n`CommentMentionNotificationAdapter` with `UserNotificationService` and retain\ntheir own notification delivery policy. The Svelte surface receives authorized\ncomment projections and callbacks; it does not fetch or authorize records.\n`CommentService` requires a trusted actor and host record authorization callback.\n`RecordComments.contextKey` includes tenant, actor and record identity.\nSee [test design](agents/test-design.md) and [idempotent creation](agents/idempotent-create.md) for behavior matrices and retry invariants.\n\nRun `pnpm --filter @happyvertical/smrt-comments test`, `test:postgres`, `typecheck`, `check`, and\n`npx --yes @biomejs/biome@2.5.2 check packages/comments` after changes.\n",
231
691
  "moduleDocs": [
232
692
  {
233
693
  "path": "agents/test-design.md",
234
694
  "module": "test-design",
235
- "content": "# Comments behavior evidence (#3714)\n\n| Behavior / trigger | Positive / failure | Actor / executor | Runtime / edge | Level / command |\n| --- | --- | --- | --- | --- |\n| Create/read a comment | Persist and reload; deny missing record permission, forged author or tenant | Trusted session actor; same supplied DB handle under tenant context | Node, SQLite/DuckDB/PostgreSQL; polymorphic record authorization callback | Integration, `test` / `test:postgres` |\n| Shared record identity in different tenants | Tenant A sees A; never B | Two tenant contexts, same actor and DB | All three SQL dialects; explicit tenant filter | Integration, `test` / `test:postgres` |\n| Mention creation | Deduplicate and exclude author; malformed UUID rejected | Authorized author, recipient access callback | All three SQL dialects; missing adapter is supported | Integration, `test` / `test:postgres` |\n| Mention delivery | Real UserNotificationService row; denied recipient gets none; replay deduplicates | Supplied notification service/DB | All three SQL dialects; upstream failure leaves saved comment | Integration, `test` / `test:postgres` |\n| UI submission | Trim/post; empty body and rejected callback keep actionable feedback | Host authorized projections | Browser Svelte; async callback | Component, `test` |\n| Actor/record switch during submission | Reset draft; stale success/error cannot mutate new draft | Changed composite contextKey | Browser Svelte; async completion | Component, `test` |\n| Recipe/schema | Recipe includes Comment; generated schema used by real persistence | Registry/test migrator | All three SQL dialects | Integration, `test` / `test:postgres`; `build` |\n\nNew feature, so a base-regression comparison is N/A. Comment insertion is one\nsave on the provided executor, with validation/authorization before mutation.\nNotification delivery intentionally occurs after save and is not atomic with it;\nthe failure test asserts the saved-row contract. Unkeyed creates have no retry contract. Explicit keyed creates now support\nretry recovery; see [idempotent creation](idempotent-create.md).\nThe fixture-only playground does not replace the authenticated record surface.\n\nRecipe metadata follow-up (#3604): the comments catalog group, embedded-only\nnavigation and help are checked in the generated manifest and knowledge artifact\nafter building with refreshed scanner/core artifacts. Model exposure stays off.\nThis change does not affect persistence or delivery; prior three-dialect evidence\nremains applicable. Full package tests, typecheck, Svelte and Biome are rerun.\n\n#3708 follow-up: the browser-only playground renders fixture projections and\naccepts local posts without storage or notification access. Component tests\nverify display, posting, instance isolation and reset on remount. Build assertions\nverify runtime, playground and demo exports in manifest and knowledge artifacts.\nPersistence and authorization are unchanged; existing three-dialect evidence\napplies. Route/settings/shell/provider contracts are N/A because they cannot\nprovide the parent record authorization context.\n\n## Accepted review6084422109 round 1 regressions\n\n- Invalid UUID and 51 distinct mentions: authorized actor, each SQL dialect,\n failed create must leave zero rows for the record. Baseline failed on SQLite,\n DuckDB and PostgreSQL; public model setter now validates before the one insert.\n- Actual SQLite CommentService notification rejection composed with the Svelte\n submit callback: saved row remains, draft remains, alert instructs refresh and\n checking the discussion before reposting. Baseline produced “Try again”.\n- Commands: full comments `test`, `test:postgres`, `typecheck`, `check`, `build`,\n and Biome; evidence under `/private/tmp/smrt-track-b-evidence/3714-r1-*`.\n\n## Accepted review6084422109 round 6 persistence-key injection\n\nActor may comment on record A only; another author owns a comment on record B\nin the same tenant. Injecting B's slug/context, with and without its primary id,\nmust create a separate A row and preserve B's identity, author and body. Injected\ndatabase and constructor options must not reach persistence or alter defaults.\nReal SQLite/DuckDB/PostgreSQL baselines fail these assertions; the service now\nallowlists CreateCommentInput and generates identity with insert-only persistence.\nHydration options remain available only on the trusted model path. Full package,\nthree-dialect, type/Svelte, build and Biome evidence: `3714-round6-*` logs.\n"
695
+ "content": "# Comments behavior evidence (#3714)\n\n| Behavior / trigger | Positive / failure | Actor / executor | Runtime / edge | Level / command |\n| --- | --- | --- | --- | --- |\n| Create/read a comment | Persist and reload; deny missing record permission, forged author or tenant | Trusted session actor; same supplied DB handle under tenant context | Node, SQLite/DuckDB/PostgreSQL; polymorphic record authorization callback | Integration, `test` / `test:postgres` |\n| Shared record identity in different tenants | Tenant A sees A; never B | Two tenant contexts, same actor and DB | All three SQL dialects; explicit tenant filter | Integration, `test` / `test:postgres` |\n| Mention creation | Deduplicate and exclude author; malformed UUID rejected | Authorized author, recipient access callback | All three SQL dialects; missing adapter is supported | Integration, `test` / `test:postgres` |\n| Mention delivery | Real UserNotificationService row; denied recipient gets none; replay deduplicates | Supplied notification service/DB | All three SQL dialects; upstream failure leaves saved comment | Integration, `test` / `test:postgres` |\n| UI submission | Trim/post; empty body and rejected callback keep actionable feedback | Host authorized projections | Browser Svelte; async callback | Component, `test` |\n| Actor/record switch during submission | Reset draft; stale success/error cannot mutate new draft | Changed composite contextKey | Browser Svelte; async completion | Component, `test` |\n| Recipe/schema | Recipe includes Comment; generated schema used by real persistence | Registry/test migrator | All three SQL dialects | Integration, `test` / `test:postgres`; `build` |\n| Participation gate | Authorized post persists its immutable snapshot and pre-moderation status | Trusted actor; host-injected authorizer | All three SQL dialects; deny/banned/rate-limited decision writes no row | Integration, `test` / `test:postgres` |\n| Reply, edit and moderation | Same-record reply, immutable prior-body revision, moderator status transition/report queue | Author or authorized moderator; supplied DB | All three SQL dialects; cross-record reply and another author's edit denied | Integration, `test` / `test:postgres` |\n\nNew feature, so a base-regression comparison is N/A. Comment insertion is one\nsave on the provided executor, with validation/authorization before mutation.\nNotification delivery intentionally occurs after save and is not atomic with it;\nthe failure test asserts the saved-row contract. Unkeyed creates have no retry contract. Explicit keyed creates now support\nretry recovery; see [idempotent creation](idempotent-create.md).\nThe fixture-only playground does not replace the authenticated record surface.\n\nRecipe metadata follow-up (#3604): the comments catalog group, embedded-only\nnavigation and help are checked in the generated manifest and knowledge artifact\nafter building with refreshed scanner/core artifacts. Model exposure stays off.\nThis change does not affect persistence or delivery; prior three-dialect evidence\nremains applicable. Full package tests, typecheck, Svelte and Biome are rerun.\n\n#3708 follow-up: the browser-only playground renders fixture projections and\naccepts local posts without storage or notification access. Component tests\nverify display, posting, instance isolation and reset on remount. Build assertions\nverify runtime, playground and demo exports in manifest and knowledge artifacts.\nPersistence and authorization are unchanged; existing three-dialect evidence\napplies. Route/settings/shell/provider contracts are N/A because they cannot\nprovide the parent record authorization context.\n\n## Accepted review6084422109 round 1 regressions\n\n- Invalid UUID and 51 distinct mentions: authorized actor, each SQL dialect,\n failed create must leave zero rows for the record. Baseline failed on SQLite,\n DuckDB and PostgreSQL; public model setter now validates before the one insert.\n- Actual SQLite CommentService notification rejection composed with the Svelte\n submit callback: saved row remains, draft remains, alert instructs refresh and\n checking the discussion before reposting. Baseline produced “Try again”.\n- Commands: full comments `test`, `test:postgres`, `typecheck`, `check`, `build`,\n and Biome; evidence under `/private/tmp/smrt-track-b-evidence/3714-r1-*`.\n\n## Accepted review6084422109 round 6 persistence-key injection\n\nActor may comment on record A only; another author owns a comment on record B\nin the same tenant. Injecting B's slug/context, with and without its primary id,\nmust create a separate A row and preserve B's identity, author and body. Injected\ndatabase and constructor options must not reach persistence or alter defaults.\nReal SQLite/DuckDB/PostgreSQL baselines fail these assertions; the service now\nallowlists CreateCommentInput and generates identity with insert-only persistence.\nHydration options remain available only on the trusted model path. Full package,\nthree-dialect, type/Svelte, build and Biome evidence: `3714-round6-*` logs.\n\n## #3836 generic participation enforcement\n\n| Behavior/invariant | Reachable trigger; positive / negative | Actor/context; executor/transaction | Runtime/dialect; external edge | Test level / validation |\n| --- | --- | --- | --- | --- |\n| Policy selection | Tenant default and exact target override; closed/unknown policy denies | Trusted actor × own/other tenant × mismatched ambient tenant; supplied database | Node SQLite/DuckDB/PostgreSQL; missing policy defaults open | Integration `test`, `test:postgres` |\n| Membership and snapshots | Member allowed with frozen evidence; absent/false/malformed entitlement and upstream rejection deny | Host trusted identity/entitlement, no subscription coupling | All three dialects; optional snapshot and unknown decision values | Integration `test`, `test:postgres` |\n| Ban enforcement | Tenant/target ban denies; expired/other tenant/other target permits | Tenant-bound query independent of ambient context | All three dialects; malformed expiry fails closed | Integration `test`, `test:postgres` |\n| Persisted quota | Concurrent requests never exceed fixed-window quota; actors/tenants isolated, next window permits | Reservation and mutation on same transaction; failure rolls both back | All three dialects; independent service instances sharing DB | Integration `test`, `test:postgres` |\n| Retry identity | Keyed comment replay consumes no additional quota, changed payload denied | Comment runOnce transaction and nested participation transaction use same executor | All three dialects; retained comment retry binding | Integration `test`, `test:postgres` |\n| UI moderation | Pending/reported rows and callback transitions | Authorized host projections; no browser authorization | Browser Svelte; callback errors and parent-specific reply callback | Component `test`, Svelte `check` |\n\nThe generic fixture is a worker co-op project task. New feature: base failure\ncomparison is N/A. Rate windows are fixed UTC epoch windows, per actor/action\nand selected tenant-default or exact-target scope. Policy changes take effect\non subsequent evaluations; concurrent administrative changes are not retroactive.\n\nAuthor edits serialize on a database row write lock and retain the prior body in\nthe same transaction. The failed-save regression proves both history and body\nroll back; foreign-author edits, foreign-tenant moderation and cross-record replies\nare rejected in all three dialects. Models in the recipe include every service\nstorage dependency. Built participation subpath is imported by a standalone Node\nconsumer to verify the runtime export, alongside package typechecking.\n\nAuthor edits also reauthorize participation and spend the comment-action quota;\nbanned and rate-limited edits fail before changing the retained body/history.\n\nVisible edits under pre-moderation return to pending; editing hidden comments\npreserves hidden status. The three-dialect edit test proves both transitions.\n\n## Review6204625041 round 1 accepted regressions\n\n- F1: seed a pre-upgrade parentless request digest, replay with retained receipt,\n remove receipt, replay again; same comment identity in all three dialects.\n- F2: create with key, edit twice, replay original before/after receipt retention;\n return current body and reject edited body as a changed create payload.\n- F3: concurrent duplicate reports retain one actor/comment row and one quota unit;\n receipt cleanup preserves deduplication, excess quota and banned retries deny,\n hidden/pending/deleted targets deny. SQLite/DuckDB/restricted PostgreSQL.\n- Full comments test/test:postgres plus types, Svelte, build and Biome. Public\n contract adds report action so quota policy can be configured independently.\n\n## Review6204625041 round 2 R1 regression\n\nWith tenancy interception enabled, create and edit a keyed comment under each\nUUID casing, replay from the opposite casing before/after receipt cleanup.\nThe baseline rejects both original-spelling writes under the canonical guard\ncontext and revision reads under the replay context. Mutation callbacks and\nhistory reads use the stored comment tenant spelling; quota/policy scope remains\ncanonical. The full SQLite/DuckDB/PostgreSQL suite validates the correction.\n\n## PR4039 accepted external feedback / round4\n\n- Canonical parent UUID: keyed reply with uppercase parent succeeds and replays\n lowercase/uppercase before and after receipt cleanup on all SQL dialects.\n- Notification visibility: pending create and hidden/deleted replay never call\n the adapter; explicit visible replay can deliver. Approval alone does not notify.\n- Moderation lifecycle: real `_smrt_changes` update exists after moderation save;\n transaction locks and reloads before save to preserve concurrent body edits.\n- Reply-only component has no root composer before selection, after success or\n after cancel. Baseline regression run fails five SQLite/DuckDB/component cases.\n"
236
696
  },
237
697
  {
238
698
  "path": "agents/idempotent-create.md",
239
699
  "module": "idempotent-create",
240
- "content": "# Idempotent comment creation (#3941)\n\n`CommentService.create({...input, idempotencyKey})` returns a `Comment`, including\nits server-generated UUID. Keys are optional, trimmed nonempty strings of at most\n200 characters, unique within a tenant. Omitted keys retain the original create\nbehavior. The service allowlists input: identity, slug, context, executor and\nrequestDigest and retryTenantId cannot be supplied by the caller.\n\nValid UUID tenant/author identities use lowercase for equality, arbitration and\ndigest comparisons. Authorization and stored tenant/author references preserve the\ntrusted actor spelling, including existing SQLite identities. A separate nullable\n`retryTenantId` binds keyed rows to the canonical tenant; its unique index with\n`idempotencyKey` matches arbitration and retention lookup. Opaque textual tenant\nIDs remain case-sensitive, and parent record keys remain exact.\nThe trusted actor and record authorization run before every attempt, including\nreplays. A key binds the actual author, qualified parent, exact body and sorted,\ndeduplicated validated mentions. Reusing a key with different bound content throws\n`CommentIdempotencyError` with code `COMMENT_REQUEST_CONFLICT`. Replays return the\noriginal unchanged comment. A retained receipt referring to a missing or modified\ncomment throws `COMMENT_REQUEST_UNAVAILABLE`, never recreates it.\n\nThe service reuses core `runOnce` for atomic arbitration. Its fixed internal actor\n`smrt-comments.create:v1` is only an arbitration namespace: different authenticated\nauthors racing the same tenant/key must contend together. The actual author is\nbound in the separate immutable request digest. Claim insertion uses core's\nconflict-safe SQL; comment insertion uses the callback's executor and remains\ninsert-only with a random server UUID. Nullable key/digest/retry-tenant fields on Comment retain the binding after normal\ngeneric claim retention. Existing rows need no key or canonical binding.\n\nInternal lookups use parameterized queries against the resolved public\n`CommentCollection.tableName`, honoring `smrt.tableNames` deployment bindings.\nThe table identifier is quoted separately from bound values (mapping configuration\naccepts a single lowercase SQL identifier, not schema-qualified names). Lookups are\nrestricted to canonical retry tenant plus key (or exact authorized parent for\nlisting). The stored tenant must be UUID-equivalent to the authorized tenant before\nhydration in that original tenant scope; no system-context bypass is used. Lists\nmerge legacy exact-scope rows with equivalent-tenant keyed rows, deduplicate by\nserver UUID and sort by public creation timestamp with UUID as a tie-breaker. This\npreserves old unkeyed listing semantics while showing keyed replies across trusted\nactor UUID spellings. Stored tenant and author references are never rewritten.\n\nThe caller's DB must support transactions for keyed creates. Nested transaction\nsupport follows the database adapter: unsupported nested transactions fail before\nwork, rather than silently widening the supplied executor. Hosts needing atomic\ncomment/notification rollback compose the service and notification adapter with\nthe same outer transaction handle. Notification delivery happens after comment\narbitration, including on identical retries. The default messages adapter's stable\ncomment sourceRef deduplicates delivered mentions and retries missing delivery.\nArbitrary adapters must provide their own replay safety; no exactly-once external\ndelivery is promised. Without an outer transaction a delivery failure leaves the\ncomment saved, and retrying the same key is supported.\n\nThe service exposes no edit/delete lifecycle. Trusted model/collection bypasses\nmust preserve key/digest binding. Deleting both the comment and retained core\nclaim intentionally destroys retry history and is outside this contract. Do not\npurge retry history while clients may still retry. Runtime does not create schema;\napply normal SMRT migrations before using keyed creates.\n\n## Test-design contract\n\nAll rows use the authenticated author and explicit tenant context, a real supplied\nDB executor, and SQLite/DuckDB/PostgreSQL unless noted. Commands are package `test`\nand `test:postgres`; public TypeScript contract is also checked by `typecheck`.\n\n| Invariant / reachable trigger | Positive | Negative / failure | Executor / edge / level |\n| --- | --- | --- | --- |\n| Optional keyed create and retry | Same key and normalized mentions return one UUID | Empty, oversized or nonstring keys rejected before mutation | Root DB, public input, integration |\n| Immutable tenant/request binding | Different tenants may reuse key | Changed author, parent, body or mentions conflicts | Authenticated actor, public service, integration |\n| Authorization precedes replay | Authorized replay succeeds | Revoked record access or forged tenant/actor denied | Same DB, host authorization callback, integration |\n| Concurrent arbitration | Simultaneous identical creates produce one row | Competing authors/payloads yield one winner, one typed conflict | Root DB; PostgreSQL outer transactions also remain usable, integration |\n| Transaction affinity | Comment and claim use runOnce callback executor | Caller rollback removes comment, claim and composed notifications | PostgreSQL/SQLite supported nesting; DuckDB rejects unsupported nesting before writes, integration |\n| Delivery recovery | Default notifications replay once per recipient | Partial adapter failure retains comment; same-key retry finishes missing notification | Real messages adapter and supplied DB; integration |\n| Durable binding | All 16 actor/input UUID casing combinations, before/after claim retention, reuse one comment | Deleted/modified retained comment or forged canonical tenant binding is unavailable | Cold concurrent cross-case actors, unique index independent of claim, all dialects |\n| Legacy references/listing | Uppercase Tenant/User references and legacy NULL-binding rows remain usable | Another tenant/parent and differently cased opaque IDs stay isolated | Mixed legacy/keyed order and dedup; old-to-new migration repeat |\n| Persistence-option injection | Generated UUID remains server-owned | Injected id/slug/context/db/digest cannot replace another row | Existing three-dialect regression suite plus keyed case |\n\nNew additive API: baseline rejects `idempotencyKey` at the TypeScript boundary and\nignores it at runtime, creating duplicate rows. Record an executable baseline\nretry failure before final evidence. Browser behavior is unchanged; existing Svelte\nchecks/tests remain required, and a new browser flow is N/A.\n"
700
+ "content": "# Idempotent comment creation (#3941)\n\n`CommentService.create({...input, idempotencyKey})` returns a `Comment`, including\nits server-generated UUID. Keys are optional, trimmed nonempty strings of at most\n200 characters, unique within a tenant. Omitted keys retain the original create\nbehavior. The service allowlists input: identity, slug, context, executor and\nrequestDigest and retryTenantId cannot be supplied by the caller.\n\nValid UUID tenant/author identities use lowercase for equality, arbitration and\ndigest comparisons. Authorization and stored tenant/author references preserve the\ntrusted actor spelling, including existing SQLite identities. A separate nullable\n`retryTenantId` binds keyed rows to the canonical tenant; its unique index with\n`idempotencyKey` matches arbitration and retention lookup. Opaque textual tenant\nIDs remain case-sensitive, and parent record keys remain exact.\nThe trusted actor and record authorization run before every attempt, including\nreplays. A key binds the actual author, qualified parent, exact body and sorted,\ndeduplicated validated mentions. Reusing a key with different bound content throws\n`CommentIdempotencyError` with code `COMMENT_REQUEST_CONFLICT`. Replays return the\nsame comment identity with its current body after authorized edits. The immutable\ncreation digest is verified against retained revision history when edited; history\nmust remain while the keyed comment is retained. Parentless requests omit the parent\nfield from the v1 digest to preserve pre-thread upgrade bindings. A retained receipt\nreferring to a missing comment or a mismatched creation binding throws\n`COMMENT_REQUEST_UNAVAILABLE`, never recreates it.\n\nThe service reuses core `runOnce` for atomic arbitration. Its fixed internal actor\n`smrt-comments.create:v1` is only an arbitration namespace: different authenticated\nauthors racing the same tenant/key must contend together. The actual author is\nbound in the separate immutable request digest. Claim insertion uses core's\nconflict-safe SQL; comment insertion uses the callback's executor and remains\ninsert-only with a random server UUID. Nullable key/digest/retry-tenant fields on Comment retain the binding after normal\ngeneric claim retention. Existing rows need no key or canonical binding.\n\nInternal lookups use parameterized queries against the resolved public\n`CommentCollection.tableName`, honoring `smrt.tableNames` deployment bindings.\nThe table identifier is quoted separately from bound values (mapping configuration\naccepts a single lowercase SQL identifier, not schema-qualified names). Lookups are\nrestricted to canonical retry tenant plus key (or exact authorized parent for\nlisting). The stored tenant must be UUID-equivalent to the authorized tenant before\nhydration in that original tenant scope; no system-context bypass is used. Lists\nmerge legacy exact-scope rows with equivalent-tenant keyed rows, deduplicate by\nserver UUID and sort by public creation timestamp with UUID as a tie-breaker. This\npreserves old unkeyed listing semantics while showing keyed replies across trusted\nactor UUID spellings. Stored tenant and author references are never rewritten.\n\nThe caller's DB must support transactions for keyed creates. Nested transaction\nsupport follows the database adapter: unsupported nested transactions fail before\nwork, rather than silently widening the supplied executor. Hosts needing atomic\ncomment/notification rollback compose the service and notification adapter with\nthe same outer transaction handle. Notification delivery happens after comment\narbitration, including on identical retries. The default messages adapter's stable\ncomment sourceRef deduplicates delivered mentions and retries missing delivery.\nArbitrary adapters must provide their own replay safety; no exactly-once external\ndelivery is promised. Without an outer transaction a delivery failure leaves the\ncomment saved, and retrying the same key is supported.\n\nThe service exposes no edit/delete lifecycle. Trusted model/collection bypasses\nmust preserve key/digest binding. Deleting both the comment and retained core\nclaim intentionally destroys retry history and is outside this contract. Do not\npurge retry history while clients may still retry. Runtime does not create schema;\napply normal SMRT migrations before using keyed creates.\n\n## Test-design contract\n\nAll rows use the authenticated author and explicit tenant context, a real supplied\nDB executor, and SQLite/DuckDB/PostgreSQL unless noted. Commands are package `test`\nand `test:postgres`; public TypeScript contract is also checked by `typecheck`.\n\n| Invariant / reachable trigger | Positive | Negative / failure | Executor / edge / level |\n| --- | --- | --- | --- |\n| Optional keyed create and retry | Same key and normalized mentions return one UUID | Empty, oversized or nonstring keys rejected before mutation | Root DB, public input, integration |\n| Immutable tenant/request binding | Different tenants may reuse key | Changed author, parent, body or mentions conflicts | Authenticated actor, public service, integration |\n| Authorization precedes replay | Authorized replay succeeds | Revoked record access or forged tenant/actor denied | Same DB, host authorization callback, integration |\n| Concurrent arbitration | Simultaneous identical creates produce one row | Competing authors/payloads yield one winner, one typed conflict | Root DB; PostgreSQL outer transactions also remain usable, integration |\n| Transaction affinity | Comment and claim use runOnce callback executor | Caller rollback removes comment, claim and composed notifications | PostgreSQL/SQLite supported nesting; DuckDB rejects unsupported nesting before writes, integration |\n| Delivery recovery | Default notifications replay once per recipient | Partial adapter failure retains comment; same-key retry finishes missing notification | Real messages adapter and supplied DB; integration |\n| Durable binding | All 16 actor/input UUID casing combinations, before/after claim retention, reuse one comment | Deleted/modified retained comment or forged canonical tenant binding is unavailable | Cold concurrent cross-case actors, unique index independent of claim, all dialects |\n| Legacy references/listing | Uppercase Tenant/User references and legacy NULL-binding rows remain usable | Another tenant/parent and differently cased opaque IDs stay isolated | Mixed legacy/keyed order and dedup; old-to-new migration repeat |\n| Persistence-option injection | Generated UUID remains server-owned | Injected id/slug/context/db/digest cannot replace another row | Existing three-dialect regression suite plus keyed case |\n\nNew additive API: baseline rejects `idempotencyKey` at the TypeScript boundary and\nignores it at runtime, creating duplicate rows. Record an executable baseline\nretry failure before final evidence. Browser behavior is unchanged; existing Svelte\nchecks/tests remain required, and a new browser flow is N/A.\n"
241
701
  }
242
702
  ],
243
703
  "recipes": [
@@ -252,7 +712,12 @@
252
712
  "discussion"
253
713
  ],
254
714
  "models": [
255
- "@happyvertical/smrt-comments:Comment"
715
+ "@happyvertical/smrt-comments:Comment",
716
+ "@happyvertical/smrt-comments:CommentRevision",
717
+ "@happyvertical/smrt-comments:CommentReport",
718
+ "@happyvertical/smrt-comments:ParticipationPolicy",
719
+ "@happyvertical/smrt-comments:ParticipationBan",
720
+ "@happyvertical/smrt-comments:ParticipationRateBucket"
256
721
  ],
257
722
  "nav": [],
258
723
  "requires": [],