@atproto/ozone 0.1.173 → 0.1.174

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 (411) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +40 -0
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/api/moderation/emitEvent.d.ts.map +1 -1
  6. package/dist/api/moderation/emitEvent.js +31 -0
  7. package/dist/api/moderation/emitEvent.js.map +1 -1
  8. package/dist/api/queue/assignModerator.d.ts +4 -0
  9. package/dist/api/queue/assignModerator.d.ts.map +1 -0
  10. package/dist/api/queue/assignModerator.js +28 -0
  11. package/dist/api/queue/assignModerator.js.map +1 -0
  12. package/dist/api/queue/createQueue.d.ts +4 -0
  13. package/dist/api/queue/createQueue.d.ts.map +1 -0
  14. package/dist/api/queue/createQueue.js +44 -0
  15. package/dist/api/queue/createQueue.js.map +1 -0
  16. package/dist/api/queue/deleteQueue.d.ts +4 -0
  17. package/dist/api/queue/deleteQueue.d.ts.map +1 -0
  18. package/dist/api/queue/deleteQueue.js +40 -0
  19. package/dist/api/queue/deleteQueue.js.map +1 -0
  20. package/dist/api/queue/getAssignments.d.ts +4 -0
  21. package/dist/api/queue/getAssignments.d.ts.map +1 -0
  22. package/dist/api/queue/getAssignments.js +19 -0
  23. package/dist/api/queue/getAssignments.js.map +1 -0
  24. package/dist/api/queue/listQueues.d.ts +4 -0
  25. package/dist/api/queue/listQueues.d.ts.map +1 -0
  26. package/dist/api/queue/listQueues.js +29 -0
  27. package/dist/api/queue/listQueues.js.map +1 -0
  28. package/dist/api/queue/routeReports.d.ts +4 -0
  29. package/dist/api/queue/routeReports.d.ts.map +1 -0
  30. package/dist/api/queue/routeReports.js +33 -0
  31. package/dist/api/queue/routeReports.js.map +1 -0
  32. package/dist/api/queue/unassignModerator.d.ts +4 -0
  33. package/dist/api/queue/unassignModerator.d.ts.map +1 -0
  34. package/dist/api/queue/unassignModerator.js +24 -0
  35. package/dist/api/queue/unassignModerator.js.map +1 -0
  36. package/dist/api/queue/updateQueue.d.ts +4 -0
  37. package/dist/api/queue/updateQueue.d.ts.map +1 -0
  38. package/dist/api/queue/updateQueue.js +39 -0
  39. package/dist/api/queue/updateQueue.js.map +1 -0
  40. package/dist/api/report/assignModerator.d.ts +4 -0
  41. package/dist/api/report/assignModerator.d.ts.map +1 -0
  42. package/dist/api/report/assignModerator.js +33 -0
  43. package/dist/api/report/assignModerator.js.map +1 -0
  44. package/dist/api/report/createActivity.d.ts +4 -0
  45. package/dist/api/report/createActivity.d.ts.map +1 -0
  46. package/dist/api/report/createActivity.js +44 -0
  47. package/dist/api/report/createActivity.js.map +1 -0
  48. package/dist/api/report/getAssignments.d.ts +4 -0
  49. package/dist/api/report/getAssignments.d.ts.map +1 -0
  50. package/dist/api/report/getAssignments.js +19 -0
  51. package/dist/api/report/getAssignments.js.map +1 -0
  52. package/dist/api/report/getHistoricalStats.d.ts +4 -0
  53. package/dist/api/report/getHistoricalStats.d.ts.map +1 -0
  54. package/dist/api/report/getHistoricalStats.js +32 -0
  55. package/dist/api/report/getHistoricalStats.js.map +1 -0
  56. package/dist/api/report/getLatestReport.d.ts +4 -0
  57. package/dist/api/report/getLatestReport.d.ts.map +1 -0
  58. package/dist/api/report/getLatestReport.js +31 -0
  59. package/dist/api/report/getLatestReport.js.map +1 -0
  60. package/dist/api/report/getLiveStats.d.ts +4 -0
  61. package/dist/api/report/getLiveStats.d.ts.map +1 -0
  62. package/dist/api/report/getLiveStats.js +25 -0
  63. package/dist/api/report/getLiveStats.js.map +1 -0
  64. package/dist/api/report/getReport.d.ts +4 -0
  65. package/dist/api/report/getReport.d.ts.map +1 -0
  66. package/dist/api/report/getReport.js +35 -0
  67. package/dist/api/report/getReport.js.map +1 -0
  68. package/dist/api/report/listActivities.d.ts +4 -0
  69. package/dist/api/report/listActivities.d.ts.map +1 -0
  70. package/dist/api/report/listActivities.js +25 -0
  71. package/dist/api/report/listActivities.js.map +1 -0
  72. package/dist/api/report/queryReports.d.ts +4 -0
  73. package/dist/api/report/queryReports.d.ts.map +1 -0
  74. package/dist/api/report/queryReports.js +29 -0
  75. package/dist/api/report/queryReports.js.map +1 -0
  76. package/dist/api/report/reassignQueue.d.ts +4 -0
  77. package/dist/api/report/reassignQueue.d.ts.map +1 -0
  78. package/dist/api/report/reassignQueue.js +45 -0
  79. package/dist/api/report/reassignQueue.js.map +1 -0
  80. package/dist/api/report/refreshStats.d.ts +4 -0
  81. package/dist/api/report/refreshStats.d.ts.map +1 -0
  82. package/dist/api/report/refreshStats.js +26 -0
  83. package/dist/api/report/refreshStats.js.map +1 -0
  84. package/dist/api/report/unassignModerator.d.ts +4 -0
  85. package/dist/api/report/unassignModerator.d.ts.map +1 -0
  86. package/dist/api/report/unassignModerator.js +21 -0
  87. package/dist/api/report/unassignModerator.js.map +1 -0
  88. package/dist/api/util.d.ts +2 -0
  89. package/dist/api/util.d.ts.map +1 -1
  90. package/dist/api/util.js +9 -1
  91. package/dist/api/util.js.map +1 -1
  92. package/dist/assignment/index.d.ts +89 -0
  93. package/dist/assignment/index.d.ts.map +1 -0
  94. package/dist/assignment/index.js +537 -0
  95. package/dist/assignment/index.js.map +1 -0
  96. package/dist/config/config.d.ts +14 -0
  97. package/dist/config/config.d.ts.map +1 -1
  98. package/dist/config/config.js +9 -0
  99. package/dist/config/config.js.map +1 -1
  100. package/dist/config/env.d.ts +3 -0
  101. package/dist/config/env.d.ts.map +1 -1
  102. package/dist/config/env.js +3 -0
  103. package/dist/config/env.js.map +1 -1
  104. package/dist/context.d.ts +9 -0
  105. package/dist/context.d.ts.map +1 -1
  106. package/dist/context.js +31 -10
  107. package/dist/context.js.map +1 -1
  108. package/dist/daemon/context.d.ts +6 -0
  109. package/dist/daemon/context.d.ts.map +1 -1
  110. package/dist/daemon/context.js +28 -4
  111. package/dist/daemon/context.js.map +1 -1
  112. package/dist/daemon/job-cursor.d.ts +5 -0
  113. package/dist/daemon/job-cursor.d.ts.map +1 -0
  114. package/dist/daemon/job-cursor.js +28 -0
  115. package/dist/daemon/job-cursor.js.map +1 -0
  116. package/dist/daemon/queue-router.d.ts +17 -0
  117. package/dist/daemon/queue-router.d.ts.map +1 -0
  118. package/dist/daemon/queue-router.js +114 -0
  119. package/dist/daemon/queue-router.js.map +1 -0
  120. package/dist/daemon/stats-computer.d.ts +51 -0
  121. package/dist/daemon/stats-computer.d.ts.map +1 -0
  122. package/dist/daemon/stats-computer.js +117 -0
  123. package/dist/daemon/stats-computer.js.map +1 -0
  124. package/dist/daemon/strike-expiry-processor.d.ts.map +1 -1
  125. package/dist/daemon/strike-expiry-processor.js +4 -19
  126. package/dist/daemon/strike-expiry-processor.js.map +1 -1
  127. package/dist/db/migrations/20260219T164523000Z-create-report-table.d.ts +4 -0
  128. package/dist/db/migrations/20260219T164523000Z-create-report-table.d.ts.map +1 -0
  129. package/dist/db/migrations/20260219T164523000Z-create-report-table.js +126 -0
  130. package/dist/db/migrations/20260219T164523000Z-create-report-table.js.map +1 -0
  131. package/dist/db/migrations/20260219T165302248Z-moderator-assignment.d.ts +4 -0
  132. package/dist/db/migrations/20260219T165302248Z-moderator-assignment.d.ts.map +1 -0
  133. package/dist/db/migrations/20260219T165302248Z-moderator-assignment.js +35 -0
  134. package/dist/db/migrations/20260219T165302248Z-moderator-assignment.js.map +1 -0
  135. package/dist/db/migrations/20260225T000000000Z-add-report-queue-table.d.ts +4 -0
  136. package/dist/db/migrations/20260225T000000000Z-add-report-queue-table.d.ts.map +1 -0
  137. package/dist/db/migrations/20260225T000000000Z-add-report-queue-table.js +36 -0
  138. package/dist/db/migrations/20260225T000000000Z-add-report-queue-table.js.map +1 -0
  139. package/dist/db/migrations/20260313T000000000Z-add-report-activity-table.d.ts +4 -0
  140. package/dist/db/migrations/20260313T000000000Z-add-report-activity-table.d.ts.map +1 -0
  141. package/dist/db/migrations/20260313T000000000Z-add-report-activity-table.js +39 -0
  142. package/dist/db/migrations/20260313T000000000Z-add-report-activity-table.js.map +1 -0
  143. package/dist/db/migrations/20260318T152058935Z-add-report-stat.d.ts +4 -0
  144. package/dist/db/migrations/20260318T152058935Z-add-report-stat.d.ts.map +1 -0
  145. package/dist/db/migrations/20260318T152058935Z-add-report-stat.js +34 -0
  146. package/dist/db/migrations/20260318T152058935Z-add-report-stat.js.map +1 -0
  147. package/dist/db/migrations/index.d.ts +5 -0
  148. package/dist/db/migrations/index.d.ts.map +1 -1
  149. package/dist/db/migrations/index.js +6 -1
  150. package/dist/db/migrations/index.js.map +1 -1
  151. package/dist/db/pagination.d.ts +31 -0
  152. package/dist/db/pagination.d.ts.map +1 -1
  153. package/dist/db/pagination.js +74 -1
  154. package/dist/db/pagination.js.map +1 -1
  155. package/dist/db/schema/index.d.ts +6 -1
  156. package/dist/db/schema/index.d.ts.map +1 -1
  157. package/dist/db/schema/index.js.map +1 -1
  158. package/dist/db/schema/moderator_assignment.d.ts +14 -0
  159. package/dist/db/schema/moderator_assignment.d.ts.map +1 -0
  160. package/dist/db/schema/moderator_assignment.js +5 -0
  161. package/dist/db/schema/moderator_assignment.js.map +1 -0
  162. package/dist/db/schema/report.d.ts +25 -0
  163. package/dist/db/schema/report.d.ts.map +1 -0
  164. package/dist/db/schema/report.js +5 -0
  165. package/dist/db/schema/report.js.map +1 -0
  166. package/dist/db/schema/report_activity.d.ts +18 -0
  167. package/dist/db/schema/report_activity.d.ts.map +1 -0
  168. package/dist/db/schema/report_activity.js +5 -0
  169. package/dist/db/schema/report_activity.js.map +1 -0
  170. package/dist/db/schema/report_queue.d.ts +19 -0
  171. package/dist/db/schema/report_queue.d.ts.map +1 -0
  172. package/dist/db/schema/report_queue.js +5 -0
  173. package/dist/db/schema/report_queue.js.map +1 -0
  174. package/dist/db/schema/report_stat.d.ts +20 -0
  175. package/dist/db/schema/report_stat.d.ts.map +1 -0
  176. package/dist/db/schema/report_stat.js +5 -0
  177. package/dist/db/schema/report_stat.js.map +1 -0
  178. package/dist/lexicon/index.d.ts +50 -0
  179. package/dist/lexicon/index.d.ts.map +1 -1
  180. package/dist/lexicon/index.js +120 -2
  181. package/dist/lexicon/index.js.map +1 -1
  182. package/dist/lexicon/lexicons.d.ts +10535 -7389
  183. package/dist/lexicon/lexicons.d.ts.map +1 -1
  184. package/dist/lexicon/lexicons.js +1789 -122
  185. package/dist/lexicon/lexicons.js.map +1 -1
  186. package/dist/lexicon/types/app/bsky/embed/external.d.ts +2 -0
  187. package/dist/lexicon/types/app/bsky/embed/external.d.ts.map +1 -1
  188. package/dist/lexicon/types/app/bsky/embed/external.js.map +1 -1
  189. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts +19 -0
  190. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts.map +1 -1
  191. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.js +9 -0
  192. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.js.map +1 -1
  193. package/dist/lexicon/types/tools/ozone/queue/assignModerator.d.ts +27 -0
  194. package/dist/lexicon/types/tools/ozone/queue/assignModerator.d.ts.map +1 -0
  195. package/dist/lexicon/types/tools/ozone/queue/assignModerator.js +7 -0
  196. package/dist/lexicon/types/tools/ozone/queue/assignModerator.js.map +1 -0
  197. package/dist/lexicon/types/tools/ozone/queue/createQueue.d.ts +35 -0
  198. package/dist/lexicon/types/tools/ozone/queue/createQueue.d.ts.map +1 -0
  199. package/dist/lexicon/types/tools/ozone/queue/createQueue.js +7 -0
  200. package/dist/lexicon/types/tools/ozone/queue/createQueue.js.map +1 -0
  201. package/dist/lexicon/types/tools/ozone/queue/defs.d.ts +62 -0
  202. package/dist/lexicon/types/tools/ozone/queue/defs.d.ts.map +1 -0
  203. package/dist/lexicon/types/tools/ozone/queue/defs.js +34 -0
  204. package/dist/lexicon/types/tools/ozone/queue/defs.js.map +1 -0
  205. package/dist/lexicon/types/tools/ozone/queue/deleteQueue.d.ts +29 -0
  206. package/dist/lexicon/types/tools/ozone/queue/deleteQueue.d.ts.map +1 -0
  207. package/dist/lexicon/types/tools/ozone/queue/deleteQueue.js +7 -0
  208. package/dist/lexicon/types/tools/ozone/queue/deleteQueue.js.map +1 -0
  209. package/dist/lexicon/types/tools/ozone/queue/getAssignments.d.ts +30 -0
  210. package/dist/lexicon/types/tools/ozone/queue/getAssignments.d.ts.map +1 -0
  211. package/dist/lexicon/types/tools/ozone/queue/getAssignments.js +7 -0
  212. package/dist/lexicon/types/tools/ozone/queue/getAssignments.js.map +1 -0
  213. package/dist/lexicon/types/tools/ozone/queue/listQueues.d.ts +32 -0
  214. package/dist/lexicon/types/tools/ozone/queue/listQueues.d.ts.map +1 -0
  215. package/dist/lexicon/types/tools/ozone/queue/listQueues.js +7 -0
  216. package/dist/lexicon/types/tools/ozone/queue/listQueues.js.map +1 -0
  217. package/dist/lexicon/types/tools/ozone/queue/routeReports.d.ts +31 -0
  218. package/dist/lexicon/types/tools/ozone/queue/routeReports.d.ts.map +1 -0
  219. package/dist/lexicon/types/tools/ozone/queue/routeReports.js +7 -0
  220. package/dist/lexicon/types/tools/ozone/queue/routeReports.js.map +1 -0
  221. package/dist/lexicon/types/tools/ozone/queue/unassignModerator.d.ts +18 -0
  222. package/dist/lexicon/types/tools/ozone/queue/unassignModerator.d.ts.map +1 -0
  223. package/dist/lexicon/types/tools/ozone/queue/unassignModerator.js +7 -0
  224. package/dist/lexicon/types/tools/ozone/queue/unassignModerator.js.map +1 -0
  225. package/dist/lexicon/types/tools/ozone/queue/updateQueue.d.ts +32 -0
  226. package/dist/lexicon/types/tools/ozone/queue/updateQueue.d.ts.map +1 -0
  227. package/dist/lexicon/types/tools/ozone/queue/updateQueue.js +7 -0
  228. package/dist/lexicon/types/tools/ozone/queue/updateQueue.js.map +1 -0
  229. package/dist/lexicon/types/tools/ozone/report/assignModerator.d.ts +31 -0
  230. package/dist/lexicon/types/tools/ozone/report/assignModerator.d.ts.map +1 -0
  231. package/dist/lexicon/types/tools/ozone/report/assignModerator.js +7 -0
  232. package/dist/lexicon/types/tools/ozone/report/assignModerator.js.map +1 -0
  233. package/dist/lexicon/types/tools/ozone/report/createActivity.d.ts +37 -0
  234. package/dist/lexicon/types/tools/ozone/report/createActivity.d.ts.map +1 -0
  235. package/dist/lexicon/types/tools/ozone/report/createActivity.js +7 -0
  236. package/dist/lexicon/types/tools/ozone/report/createActivity.js.map +1 -0
  237. package/dist/lexicon/types/tools/ozone/report/defs.d.ts +185 -0
  238. package/dist/lexicon/types/tools/ozone/report/defs.d.ts.map +1 -1
  239. package/dist/lexicon/types/tools/ozone/report/defs.js +108 -0
  240. package/dist/lexicon/types/tools/ozone/report/defs.js.map +1 -1
  241. package/dist/lexicon/types/tools/ozone/report/getAssignments.d.ts +30 -0
  242. package/dist/lexicon/types/tools/ozone/report/getAssignments.d.ts.map +1 -0
  243. package/dist/lexicon/types/tools/ozone/report/getAssignments.js +7 -0
  244. package/dist/lexicon/types/tools/ozone/report/getAssignments.js.map +1 -0
  245. package/dist/lexicon/types/tools/ozone/report/getHistoricalStats.d.ts +36 -0
  246. package/dist/lexicon/types/tools/ozone/report/getHistoricalStats.d.ts.map +1 -0
  247. package/dist/lexicon/types/tools/ozone/report/getHistoricalStats.js +7 -0
  248. package/dist/lexicon/types/tools/ozone/report/getHistoricalStats.js.map +1 -0
  249. package/dist/lexicon/types/tools/ozone/report/getLatestReport.d.ts +21 -0
  250. package/dist/lexicon/types/tools/ozone/report/getLatestReport.d.ts.map +1 -0
  251. package/dist/lexicon/types/tools/ozone/report/getLatestReport.js +7 -0
  252. package/dist/lexicon/types/tools/ozone/report/getLatestReport.js.map +1 -0
  253. package/dist/lexicon/types/tools/ozone/report/getLiveStats.d.ts +27 -0
  254. package/dist/lexicon/types/tools/ozone/report/getLiveStats.d.ts.map +1 -0
  255. package/dist/lexicon/types/tools/ozone/report/getLiveStats.js +7 -0
  256. package/dist/lexicon/types/tools/ozone/report/getLiveStats.js.map +1 -0
  257. package/dist/lexicon/types/tools/ozone/report/getReport.d.ts +22 -0
  258. package/dist/lexicon/types/tools/ozone/report/getReport.d.ts.map +1 -0
  259. package/dist/lexicon/types/tools/ozone/report/getReport.js +7 -0
  260. package/dist/lexicon/types/tools/ozone/report/getReport.js.map +1 -0
  261. package/dist/lexicon/types/tools/ozone/report/listActivities.d.ts +26 -0
  262. package/dist/lexicon/types/tools/ozone/report/listActivities.d.ts.map +1 -0
  263. package/dist/lexicon/types/tools/ozone/report/listActivities.js +7 -0
  264. package/dist/lexicon/types/tools/ozone/report/listActivities.js.map +1 -0
  265. package/dist/lexicon/types/tools/ozone/report/queryReports.d.ts +48 -0
  266. package/dist/lexicon/types/tools/ozone/report/queryReports.d.ts.map +1 -0
  267. package/dist/lexicon/types/tools/ozone/report/queryReports.js +7 -0
  268. package/dist/lexicon/types/tools/ozone/report/queryReports.js.map +1 -0
  269. package/dist/lexicon/types/tools/ozone/report/reassignQueue.d.ts +31 -0
  270. package/dist/lexicon/types/tools/ozone/report/reassignQueue.d.ts.map +1 -0
  271. package/dist/lexicon/types/tools/ozone/report/reassignQueue.js +7 -0
  272. package/dist/lexicon/types/tools/ozone/report/reassignQueue.js.map +1 -0
  273. package/dist/lexicon/types/tools/ozone/report/refreshStats.d.ts +28 -0
  274. package/dist/lexicon/types/tools/ozone/report/refreshStats.d.ts.map +1 -0
  275. package/dist/lexicon/types/tools/ozone/report/refreshStats.js +7 -0
  276. package/dist/lexicon/types/tools/ozone/report/refreshStats.js.map +1 -0
  277. package/dist/lexicon/types/tools/ozone/report/unassignModerator.d.ts +25 -0
  278. package/dist/lexicon/types/tools/ozone/report/unassignModerator.d.ts.map +1 -0
  279. package/dist/lexicon/types/tools/ozone/report/unassignModerator.js +7 -0
  280. package/dist/lexicon/types/tools/ozone/report/unassignModerator.js.map +1 -0
  281. package/dist/mod-service/index.d.ts +3 -1
  282. package/dist/mod-service/index.d.ts.map +1 -1
  283. package/dist/mod-service/index.js +39 -2
  284. package/dist/mod-service/index.js.map +1 -1
  285. package/dist/mod-service/report.d.ts +64 -0
  286. package/dist/mod-service/report.d.ts.map +1 -0
  287. package/dist/mod-service/report.js +282 -0
  288. package/dist/mod-service/report.js.map +1 -0
  289. package/dist/mod-service/status.d.ts +20 -0
  290. package/dist/mod-service/status.d.ts.map +1 -1
  291. package/dist/queue/service.d.ts +86 -0
  292. package/dist/queue/service.d.ts.map +1 -0
  293. package/dist/queue/service.js +430 -0
  294. package/dist/queue/service.js.map +1 -0
  295. package/dist/report/activity.d.ts +77 -0
  296. package/dist/report/activity.d.ts.map +1 -0
  297. package/dist/report/activity.js +141 -0
  298. package/dist/report/activity.js.map +1 -0
  299. package/dist/report/handle-report-update.d.ts +47 -0
  300. package/dist/report/handle-report-update.d.ts.map +1 -0
  301. package/dist/report/handle-report-update.js +178 -0
  302. package/dist/report/handle-report-update.js.map +1 -0
  303. package/dist/report/reassign.d.ts +10 -0
  304. package/dist/report/reassign.d.ts.map +1 -0
  305. package/dist/report/reassign.js +75 -0
  306. package/dist/report/reassign.js.map +1 -0
  307. package/dist/report/stats.d.ts +105 -0
  308. package/dist/report/stats.d.ts.map +1 -0
  309. package/dist/report/stats.js +619 -0
  310. package/dist/report/stats.js.map +1 -0
  311. package/dist/report/views.d.ts +111 -0
  312. package/dist/report/views.d.ts.map +1 -0
  313. package/dist/report/views.js +156 -0
  314. package/dist/report/views.js.map +1 -0
  315. package/dist/team/index.d.ts +1 -0
  316. package/dist/team/index.d.ts.map +1 -1
  317. package/dist/team/index.js +11 -0
  318. package/dist/team/index.js.map +1 -1
  319. package/package.json +3 -3
  320. package/src/api/index.ts +40 -0
  321. package/src/api/moderation/emitEvent.ts +38 -0
  322. package/src/api/queue/assignModerator.ts +31 -0
  323. package/src/api/queue/createQueue.ts +62 -0
  324. package/src/api/queue/deleteQueue.ts +56 -0
  325. package/src/api/queue/getAssignments.ts +19 -0
  326. package/src/api/queue/listQueues.ts +39 -0
  327. package/src/api/queue/routeReports.ts +44 -0
  328. package/src/api/queue/unassignModerator.ts +26 -0
  329. package/src/api/queue/updateQueue.ts +54 -0
  330. package/src/api/report/assignModerator.ts +36 -0
  331. package/src/api/report/createActivity.ts +57 -0
  332. package/src/api/report/getAssignments.ts +20 -0
  333. package/src/api/report/getHistoricalStats.ts +41 -0
  334. package/src/api/report/getLatestReport.ts +44 -0
  335. package/src/api/report/getLiveStats.ts +26 -0
  336. package/src/api/report/getReport.ts +55 -0
  337. package/src/api/report/listActivities.ts +34 -0
  338. package/src/api/report/queryReports.ts +44 -0
  339. package/src/api/report/reassignQueue.ts +68 -0
  340. package/src/api/report/refreshStats.ts +27 -0
  341. package/src/api/report/unassignModerator.ts +21 -0
  342. package/src/api/util.ts +12 -0
  343. package/src/assignment/index.ts +731 -0
  344. package/src/config/config.ts +27 -0
  345. package/src/config/env.ts +8 -0
  346. package/src/context.ts +31 -0
  347. package/src/daemon/context.ts +34 -0
  348. package/src/daemon/job-cursor.ts +33 -0
  349. package/src/daemon/queue-router.ts +101 -0
  350. package/src/daemon/stats-computer.ts +101 -0
  351. package/src/daemon/strike-expiry-processor.ts +4 -20
  352. package/src/db/migrations/20260219T164523000Z-create-report-table.ts +155 -0
  353. package/src/db/migrations/20260219T165302248Z-moderator-assignment.ts +42 -0
  354. package/src/db/migrations/20260225T000000000Z-add-report-queue-table.ts +41 -0
  355. package/src/db/migrations/20260313T000000000Z-add-report-activity-table.ts +48 -0
  356. package/src/db/migrations/20260318T152058935Z-add-report-stat.ts +35 -0
  357. package/src/db/migrations/index.ts +5 -0
  358. package/src/db/pagination.ts +85 -0
  359. package/src/db/schema/index.ts +10 -0
  360. package/src/db/schema/moderator_assignment.ts +16 -0
  361. package/src/db/schema/report.ts +27 -0
  362. package/src/db/schema/report_activity.ts +22 -0
  363. package/src/db/schema/report_queue.ts +21 -0
  364. package/src/db/schema/report_stat.ts +27 -0
  365. package/src/lexicon/index.ts +280 -0
  366. package/src/lexicon/lexicons.ts +1910 -160
  367. package/src/lexicon/types/app/bsky/embed/external.ts +2 -0
  368. package/src/lexicon/types/tools/ozone/moderation/emitEvent.ts +24 -0
  369. package/src/lexicon/types/tools/ozone/queue/assignModerator.ts +46 -0
  370. package/src/lexicon/types/tools/ozone/queue/createQueue.ts +54 -0
  371. package/src/lexicon/types/tools/ozone/queue/defs.ts +99 -0
  372. package/src/lexicon/types/tools/ozone/queue/deleteQueue.ts +48 -0
  373. package/src/lexicon/types/tools/ozone/queue/getAssignments.ts +48 -0
  374. package/src/lexicon/types/tools/ozone/queue/listQueues.ts +50 -0
  375. package/src/lexicon/types/tools/ozone/queue/routeReports.ts +50 -0
  376. package/src/lexicon/types/tools/ozone/queue/unassignModerator.ts +37 -0
  377. package/src/lexicon/types/tools/ozone/queue/updateQueue.ts +51 -0
  378. package/src/lexicon/types/tools/ozone/report/assignModerator.ts +50 -0
  379. package/src/lexicon/types/tools/ozone/report/createActivity.ts +60 -0
  380. package/src/lexicon/types/tools/ozone/report/defs.ts +327 -0
  381. package/src/lexicon/types/tools/ozone/report/getAssignments.ts +48 -0
  382. package/src/lexicon/types/tools/ozone/report/getHistoricalStats.ts +54 -0
  383. package/src/lexicon/types/tools/ozone/report/getLatestReport.ts +39 -0
  384. package/src/lexicon/types/tools/ozone/report/getLiveStats.ts +45 -0
  385. package/src/lexicon/types/tools/ozone/report/getReport.ts +38 -0
  386. package/src/lexicon/types/tools/ozone/report/listActivities.ts +44 -0
  387. package/src/lexicon/types/tools/ozone/report/queryReports.ts +72 -0
  388. package/src/lexicon/types/tools/ozone/report/reassignQueue.ts +55 -0
  389. package/src/lexicon/types/tools/ozone/report/refreshStats.ts +46 -0
  390. package/src/lexicon/types/tools/ozone/report/unassignModerator.ts +44 -0
  391. package/src/mod-service/index.ts +45 -3
  392. package/src/mod-service/report.ts +408 -0
  393. package/src/queue/service.ts +599 -0
  394. package/src/report/activity.ts +234 -0
  395. package/src/report/handle-report-update.ts +209 -0
  396. package/src/report/reassign.ts +109 -0
  397. package/src/report/stats.ts +850 -0
  398. package/src/report/views.ts +241 -0
  399. package/src/team/index.ts +11 -0
  400. package/tests/get-report.test.ts +136 -0
  401. package/tests/query-reports.test.ts +608 -0
  402. package/tests/queue-assignment.test.ts +428 -0
  403. package/tests/queue-router.test.ts +306 -0
  404. package/tests/queues.test.ts +690 -0
  405. package/tests/report-action.test.ts +308 -0
  406. package/tests/report-activity.test.ts +567 -0
  407. package/tests/report-assignment.test.ts +517 -0
  408. package/tests/report-reassign-queue.test.ts +340 -0
  409. package/tests/report-routing.test.ts +245 -0
  410. package/tests/report-stats.test.ts +545 -0
  411. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,55 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import { type ValidationResult, BlobRef } from '@atproto/lexicon'
5
+ import { CID } from 'multiformats/cid'
6
+ import { validate as _validate } from '../../../../lexicons'
7
+ import {
8
+ type $Typed,
9
+ is$typed as _is$typed,
10
+ type OmitKey,
11
+ } from '../../../../util'
12
+ import type * as ToolsOzoneReportDefs from './defs.js'
13
+
14
+ const is$typed = _is$typed,
15
+ validate = _validate
16
+ const id = 'tools.ozone.report.reassignQueue'
17
+
18
+ export type QueryParams = {}
19
+
20
+ export interface InputSchema {
21
+ /** ID of the report to reassign */
22
+ reportId: number
23
+ /** Target queue ID. Use -1 to unassign from any queue. */
24
+ queueId: number
25
+ /** Optional moderator-only note recorded on the resulting queueActivity as internalNote. */
26
+ comment?: string
27
+ }
28
+
29
+ export interface OutputSchema {
30
+ report: ToolsOzoneReportDefs.ReportView
31
+ }
32
+
33
+ export interface HandlerInput {
34
+ encoding: 'application/json'
35
+ body: InputSchema
36
+ }
37
+
38
+ export interface HandlerSuccess {
39
+ encoding: 'application/json'
40
+ body: OutputSchema
41
+ headers?: { [key: string]: string }
42
+ }
43
+
44
+ export interface HandlerError {
45
+ status: number
46
+ message?: string
47
+ error?:
48
+ | 'ReportNotFound'
49
+ | 'ReportClosed'
50
+ | 'AlreadyInTargetQueue'
51
+ | 'QueueNotFound'
52
+ | 'QueueDisabled'
53
+ }
54
+
55
+ export type HandlerOutput = HandlerError | HandlerSuccess
@@ -0,0 +1,46 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import { type ValidationResult, BlobRef } from '@atproto/lexicon'
5
+ import { CID } from 'multiformats/cid'
6
+ import { validate as _validate } from '../../../../lexicons'
7
+ import {
8
+ type $Typed,
9
+ is$typed as _is$typed,
10
+ type OmitKey,
11
+ } from '../../../../util'
12
+
13
+ const is$typed = _is$typed,
14
+ validate = _validate
15
+ const id = 'tools.ozone.report.refreshStats'
16
+
17
+ export type QueryParams = {}
18
+
19
+ export interface InputSchema {
20
+ /** Start date for recomputation, inclusive (YYYY-MM-DD). */
21
+ startDate: string
22
+ /** End date for recomputation, inclusive (YYYY-MM-DD). */
23
+ endDate: string
24
+ /** Optional list of queue IDs to recompute. Omit to recompute all groups. */
25
+ queueIds?: number[]
26
+ }
27
+
28
+ export interface OutputSchema {}
29
+
30
+ export interface HandlerInput {
31
+ encoding: 'application/json'
32
+ body: InputSchema
33
+ }
34
+
35
+ export interface HandlerSuccess {
36
+ encoding: 'application/json'
37
+ body: OutputSchema
38
+ headers?: { [key: string]: string }
39
+ }
40
+
41
+ export interface HandlerError {
42
+ status: number
43
+ message?: string
44
+ }
45
+
46
+ export type HandlerOutput = HandlerError | HandlerSuccess
@@ -0,0 +1,44 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import { type ValidationResult, BlobRef } from '@atproto/lexicon'
5
+ import { CID } from 'multiformats/cid'
6
+ import { validate as _validate } from '../../../../lexicons'
7
+ import {
8
+ type $Typed,
9
+ is$typed as _is$typed,
10
+ type OmitKey,
11
+ } from '../../../../util'
12
+ import type * as ToolsOzoneReportDefs from './defs.js'
13
+
14
+ const is$typed = _is$typed,
15
+ validate = _validate
16
+ const id = 'tools.ozone.report.unassignModerator'
17
+
18
+ export type QueryParams = {}
19
+
20
+ export interface InputSchema {
21
+ /** The ID of the report to unassign. */
22
+ reportId: number
23
+ }
24
+
25
+ export type OutputSchema = ToolsOzoneReportDefs.AssignmentView
26
+
27
+ export interface HandlerInput {
28
+ encoding: 'application/json'
29
+ body: InputSchema
30
+ }
31
+
32
+ export interface HandlerSuccess {
33
+ encoding: 'application/json'
34
+ body: OutputSchema
35
+ headers?: { [key: string]: string }
36
+ }
37
+
38
+ export interface HandlerError {
39
+ status: number
40
+ message?: string
41
+ error?: 'InvalidAssignment'
42
+ }
43
+
44
+ export type HandlerOutput = HandlerError | HandlerSuccess
@@ -62,6 +62,7 @@ import {
62
62
  import {
63
63
  ModEventType,
64
64
  ModerationEventRow,
65
+ ModerationEventRowWithHandle,
65
66
  ModerationSubjectStatusRow,
66
67
  ModerationSubjectStatusRowWithHandle,
67
68
  ReporterStats,
@@ -376,6 +377,29 @@ export class ModerationService {
376
377
  return { cursor: keyset.packFromResult(result), events: resultWithHandles }
377
378
  }
378
379
 
380
+ async getEventsByIds(ids: number[]): Promise<ModerationEventRowWithHandle[]> {
381
+ if (!ids.length) return []
382
+
383
+ const result = await this.db.db
384
+ .selectFrom('moderation_event')
385
+ .selectAll()
386
+ .where('id', 'in', ids)
387
+ .execute()
388
+
389
+ if (!result.length) return []
390
+
391
+ const infos = await this.views.getAccoutInfosByDid([
392
+ ...result.map((row) => row.subjectDid),
393
+ ...result.map((row) => row.createdBy),
394
+ ])
395
+
396
+ return result.map((r) => ({
397
+ ...r,
398
+ creatorHandle: infos.get(r.createdBy)?.handle,
399
+ subjectHandle: infos.get(r.subjectDid)?.handle,
400
+ }))
401
+ }
402
+
379
403
  async getReport(id: number): Promise<ModerationEventRow | undefined> {
380
404
  return await this.db.db
381
405
  .selectFrom('moderation_event')
@@ -591,6 +615,13 @@ export class ModerationService {
591
615
  if (isReportingMuted) {
592
616
  meta.isReporterMuted = true
593
617
  }
618
+ // Also capture whether the subject was muted at event-creation time, so
619
+ // the queue-router daemon can populate report.isMuted later without
620
+ // racing against subsequent mute/unmute changes.
621
+ const isSubjectMuted = await this.isSubjectMuted(subject.did)
622
+ if (isSubjectMuted) {
623
+ meta.isSubjectMuted = true
624
+ }
594
625
  }
595
626
 
596
627
  const subjectInfo = subject.info()
@@ -1035,7 +1066,7 @@ export class ModerationService {
1035
1066
  modTool,
1036
1067
  } = info
1037
1068
 
1038
- const result = await this.logEvent({
1069
+ return await this.logEvent({
1039
1070
  event: {
1040
1071
  $type: 'tools.ozone.moderation.defs#modEventReport',
1041
1072
  reportType: reasonType,
@@ -1046,8 +1077,6 @@ export class ModerationService {
1046
1077
  createdAt,
1047
1078
  modTool,
1048
1079
  })
1049
-
1050
- return result
1051
1080
  }
1052
1081
 
1053
1082
  async getSubjectStatuses({
@@ -1425,6 +1454,19 @@ export class ModerationService {
1425
1454
  return !!result
1426
1455
  }
1427
1456
 
1457
+ // Check if a subject (the account being reported) has an active mute
1458
+ async isSubjectMuted(did: string) {
1459
+ const result = await this.db.db
1460
+ .selectFrom('moderation_subject_status')
1461
+ .where('did', '=', did)
1462
+ .where('recordPath', '=', '')
1463
+ .where('muteUntil', '>', new Date().toISOString())
1464
+ .select(sql`true`.as('status'))
1465
+ .executeTakeFirst()
1466
+
1467
+ return !!result
1468
+ }
1469
+
1428
1470
  async formatAndCreateLabels(
1429
1471
  uri: string,
1430
1472
  cid: string | null,
@@ -0,0 +1,408 @@
1
+ import { sql } from 'kysely'
2
+ import { AtUri } from '@atproto/syntax'
3
+ import { Database } from '../db'
4
+ import { Report } from '../db/schema/report'
5
+ import { QueryParams } from '../lexicon/types/tools/ozone/report/queryReports'
6
+ import {
7
+ AlreadyInTargetState,
8
+ InvalidStateTransition,
9
+ handleReportUpdate,
10
+ } from '../report/handle-report-update'
11
+
12
+ export type ReportWithEvent = Omit<Report, 'id'> & {
13
+ id: number
14
+ subjectDid: string
15
+ subjectUri: string | null
16
+ subjectCid: string | null
17
+ reportedBy: string
18
+ comment: string | null
19
+ meta: Record<string, string | boolean | number> | null
20
+ }
21
+
22
+ export type QueryReportsResult = {
23
+ reports: ReportWithEvent[]
24
+ cursor: string | undefined
25
+ }
26
+ function reportQuery(db: Database) {
27
+ return db.db
28
+ .selectFrom('report as r')
29
+ .innerJoin('moderation_event as me', 'me.id', 'r.eventId')
30
+ .where('me.action', '=', 'tools.ozone.moderation.defs#modEventReport')
31
+ }
32
+
33
+ export async function queryReports(
34
+ db: Database,
35
+ params: QueryParams,
36
+ ): Promise<QueryReportsResult> {
37
+ let builder = reportQuery(db)
38
+
39
+ if (params.queueId !== undefined) {
40
+ builder = builder.where('r.queueId', '=', params.queueId)
41
+ }
42
+
43
+ builder = builder.where('r.status', '=', params.status)
44
+
45
+ if (params.subject) {
46
+ const isRecord = params.subject.startsWith('at://')
47
+ if (isRecord) {
48
+ const uri = new AtUri(params.subject)
49
+ builder = builder
50
+ .where('r.did', '=', uri.host)
51
+ .where('r.recordPath', '=', `${uri.collection}/${uri.rkey}`)
52
+ } else {
53
+ builder = builder
54
+ .where('r.did', '=', params.subject)
55
+ .where('r.recordPath', '=', '')
56
+ }
57
+ }
58
+
59
+ if (params.did) {
60
+ builder = builder.where('r.did', '=', params.did)
61
+ }
62
+
63
+ if (params.subjectType) {
64
+ const normalizedType = params.subjectType as 'account' | 'record'
65
+ if (normalizedType === 'account') {
66
+ builder = builder.where('r.recordPath', '=', '')
67
+ } else if (normalizedType === 'record') {
68
+ builder = builder.where('r.recordPath', '!=', '')
69
+ }
70
+ }
71
+
72
+ if (params.collections?.length) {
73
+ // Filter by collection prefix on recordPath (uses text_pattern_ops index)
74
+ const collectionConditions = params.collections.map(
75
+ (collection) => sql`r."recordPath" LIKE ${`${collection}/%`}`,
76
+ )
77
+ builder = builder.where(sql`(${sql.join(collectionConditions, sql` OR `)})`)
78
+ }
79
+
80
+ if (params.reportTypes?.length) {
81
+ builder = builder.where('r.reportType', 'in', params.reportTypes)
82
+ }
83
+
84
+ if (params.isMuted !== undefined) {
85
+ builder = builder.where('r.isMuted', '=', params.isMuted)
86
+ }
87
+
88
+ if (params.reportedAfter) {
89
+ builder = builder.where('r.createdAt', '>', params.reportedAfter)
90
+ }
91
+
92
+ if (params.reportedBefore) {
93
+ builder = builder.where('r.createdAt', '<', params.reportedBefore)
94
+ }
95
+
96
+ if (params.assignedTo) {
97
+ builder = builder.where('r.assignedTo', '=', params.assignedTo)
98
+ }
99
+
100
+ const sortField = params.sortField ?? 'createdAt'
101
+ const sortDirection = params.sortDirection ?? 'desc'
102
+
103
+ builder = builder
104
+ .orderBy(
105
+ sortField === 'updatedAt' ? 'r.updatedAt' : 'r.createdAt',
106
+ sortDirection,
107
+ )
108
+ .orderBy('r.id', 'desc')
109
+
110
+ const limit = params.limit ?? 50
111
+ if (params.cursor) {
112
+ const [sortValue, id] = params.cursor.split('::')
113
+ const sortCol = sortField === 'updatedAt' ? 'r.updatedAt' : 'r.createdAt'
114
+ if (sortDirection === 'desc') {
115
+ builder = builder.where(sql`(
116
+ ${sql.ref(sortCol)} < ${sortValue}
117
+ OR (${sql.ref(sortCol)} = ${sortValue} AND r.id < ${Number(id)})
118
+ )`)
119
+ } else {
120
+ builder = builder.where(sql`(
121
+ ${sql.ref(sortCol)} > ${sortValue}
122
+ OR (${sql.ref(sortCol)} = ${sortValue} AND r.id > ${Number(id)})
123
+ )`)
124
+ }
125
+ }
126
+
127
+ const finalQuery = builder
128
+ .selectAll('r')
129
+ .select([
130
+ 'me.subjectDid',
131
+ 'me.subjectUri',
132
+ 'me.subjectCid',
133
+ 'me.createdBy as reportedBy',
134
+ 'me.comment',
135
+ 'me.meta',
136
+ ])
137
+ .limit(limit + 1)
138
+
139
+ const reports = await finalQuery.execute()
140
+
141
+ let cursor: string | undefined
142
+ const hasMore = reports.length > limit
143
+ if (hasMore) {
144
+ const last = reports[limit - 1]
145
+ const sortValue =
146
+ sortField === 'updatedAt' ? last.updatedAt : last.createdAt
147
+ cursor = `${sortValue}::${last.id}`
148
+ }
149
+
150
+ const reportsToReturn = hasMore ? reports.slice(0, limit) : reports
151
+
152
+ return {
153
+ reports: reportsToReturn,
154
+ cursor,
155
+ }
156
+ }
157
+
158
+ export async function getReportById(
159
+ db: Database,
160
+ id: number,
161
+ ): Promise<ReportWithEvent | undefined> {
162
+ return reportQuery(db)
163
+ .where('r.id', '=', id)
164
+ .selectAll('r')
165
+ .select([
166
+ 'me.subjectDid',
167
+ 'me.subjectUri',
168
+ 'me.subjectCid',
169
+ 'me.createdBy as reportedBy',
170
+ 'me.comment',
171
+ 'me.meta',
172
+ ])
173
+ .executeTakeFirst()
174
+ }
175
+
176
+ export async function getLatestReport(
177
+ db: Database,
178
+ ): Promise<ReportWithEvent | undefined> {
179
+ return reportQuery(db)
180
+ .selectAll('r')
181
+ .select([
182
+ 'me.subjectDid',
183
+ 'me.subjectUri',
184
+ 'me.subjectCid',
185
+ 'me.createdBy as reportedBy',
186
+ 'me.comment',
187
+ 'me.meta',
188
+ ])
189
+ .orderBy('r.id', 'desc')
190
+ .limit(1)
191
+ .executeTakeFirst()
192
+ }
193
+
194
+ export type FindReportsForSubjectParams = {
195
+ subjectDid: string
196
+ subjectUri?: string | null
197
+ reportIds?: number[]
198
+ reportTypes?: string[]
199
+ targetAll?: boolean
200
+ }
201
+
202
+ export type ReportResult = {
203
+ id: number
204
+ eventId: number
205
+ queueId: number | null
206
+ queuedAt: string | null
207
+ actionEventIds: number[] | null
208
+ actionNote: string | null
209
+ isMuted: boolean
210
+ status: string
211
+ createdAt: string
212
+ updatedAt: string
213
+ }
214
+
215
+ export async function findReportsForSubject(
216
+ db: Database,
217
+ params: FindReportsForSubjectParams,
218
+ ): Promise<ReportResult[]> {
219
+ let builder = reportQuery(db).where('r.did', '=', params.subjectDid)
220
+
221
+ // Filter by subject URI (if provided, match exactly; if null/undefined, match repo-level)
222
+ if (params.subjectUri) {
223
+ const uri = new AtUri(params.subjectUri)
224
+ builder = builder.where(
225
+ 'r.recordPath',
226
+ '=',
227
+ `${uri.collection}/${uri.rkey}`,
228
+ )
229
+ } else {
230
+ builder = builder.where('r.recordPath', '=', '')
231
+ }
232
+
233
+ if (params.targetAll) {
234
+ // Target all open/escalated reports on the subject
235
+ builder = builder.where('r.status', 'not in', ['closed'])
236
+ } else if (params.reportIds?.length) {
237
+ // Target specific report IDs — still enforce state transition rules
238
+ builder = builder
239
+ .where('r.id', 'in', params.reportIds)
240
+ .where('r.status', 'not in', ['closed'])
241
+ } else if (params.reportTypes?.length) {
242
+ // Target reports matching specific report types
243
+ builder = builder
244
+ .where('r.reportType', 'in', params.reportTypes)
245
+ .where('r.status', 'not in', ['closed'])
246
+ } else {
247
+ // No targeting criteria provided
248
+ return []
249
+ }
250
+
251
+ const reports = await builder.selectAll('r').execute()
252
+
253
+ return reports
254
+ }
255
+
256
+ export type ProcessReportActionParams = {
257
+ db: Database
258
+ reportAction: {
259
+ ids?: number[]
260
+ types?: string[]
261
+ all?: boolean
262
+ note?: string
263
+ }
264
+ subjectDid: string
265
+ subjectUri: string | null
266
+ eventId: number
267
+ eventType: string
268
+ createdBy: string
269
+ }
270
+
271
+ /**
272
+ * Validates and processes a report action by:
273
+ * 1. Finding matching reports based on targeting criteria
274
+ * 2. Validating that specified report IDs exist and belong to the subject
275
+ * 3. Bulk-updating reports with the action event ID, note, and status
276
+ * 4. Bulk-inserting a report_activity row for each updated report
277
+ *
278
+ * @throws InvalidRequestError if validation fails
279
+ */
280
+ export async function processReportAction(
281
+ params: ProcessReportActionParams,
282
+ ): Promise<number> {
283
+ const {
284
+ db,
285
+ reportAction,
286
+ subjectDid,
287
+ subjectUri,
288
+ eventId,
289
+ eventType,
290
+ createdBy,
291
+ } = params
292
+
293
+ // Find reports matching the criteria
294
+ const matchingReports = await findReportsForSubject(db, {
295
+ subjectDid,
296
+ subjectUri,
297
+ reportIds: reportAction.ids,
298
+ reportTypes: reportAction.types,
299
+ targetAll: reportAction.all,
300
+ })
301
+
302
+ // Validate that reports were found for ids and types
303
+ if (matchingReports.length === 0) {
304
+ if (reportAction.ids?.length) {
305
+ throw new Error(
306
+ 'No matching reports found for the specified report IDs on this subject',
307
+ )
308
+ } else if (reportAction.types?.length) {
309
+ throw new Error(
310
+ 'No matching reports found for the specified report types on this subject',
311
+ )
312
+ }
313
+ // For 'all', it's okay if no reports exist
314
+ return 0
315
+ }
316
+
317
+ // Validate that all specified report IDs were found
318
+ if (reportAction.ids?.length) {
319
+ const foundIds = new Set(matchingReports.map((r) => r.id))
320
+ const requestedIds = new Set(reportAction.ids)
321
+ const missingIds = [...requestedIds].filter((id) => !foundIds.has(id))
322
+
323
+ if (missingIds.length > 0) {
324
+ throw new Error(
325
+ `Report IDs ${missingIds.join(', ')} do not exist, are already closed, or do not belong to this subject`,
326
+ )
327
+ }
328
+ }
329
+
330
+ // Determine per-report transitions via the pure state machine.
331
+ // Skip reports whose current status doesn't allow the transition.
332
+ const validUpdates: {
333
+ id: number
334
+ nextStatus: string
335
+ activityType: string
336
+ previousStatus: string
337
+ }[] = []
338
+
339
+ for (const report of matchingReports) {
340
+ try {
341
+ const result = handleReportUpdate(report.status, {
342
+ type: 'event',
343
+ eventType,
344
+ })
345
+ if (result.nextStatus && result.activity) {
346
+ validUpdates.push({
347
+ id: report.id,
348
+ nextStatus: result.nextStatus,
349
+ activityType: result.activity.activityType,
350
+ previousStatus: result.activity.previousStatus,
351
+ })
352
+ }
353
+ } catch (err) {
354
+ if (
355
+ err instanceof AlreadyInTargetState ||
356
+ err instanceof InvalidStateTransition
357
+ ) {
358
+ // Skip reports that can't transition — silent per design
359
+ continue
360
+ }
361
+ throw err
362
+ }
363
+ }
364
+
365
+ if (!validUpdates.length) {
366
+ return 0
367
+ }
368
+
369
+ const now = new Date().toISOString()
370
+ const updateIds = validUpdates.map((u) => u.id)
371
+
372
+ // Bulk UPDATE reports that passed validation
373
+ // All valid reports share the same target status since they come from the
374
+ // same event type, so a single UPDATE is sufficient.
375
+ const status = validUpdates[0].nextStatus
376
+ const closedAt = status === 'closed' ? now : null
377
+ await db.db
378
+ .updateTable('report')
379
+ .set({
380
+ actionEventIds: sql`COALESCE("actionEventIds", '[]'::jsonb) || ${JSON.stringify(eventId)}::jsonb`,
381
+ actionNote: reportAction.note ?? null,
382
+ status,
383
+ updatedAt: now,
384
+ closedAt,
385
+ })
386
+ .where('id', 'in', updateIds)
387
+ .execute()
388
+
389
+ // Bulk INSERT one activity per updated report
390
+ await db.db
391
+ .insertInto('report_activity')
392
+ .values(
393
+ validUpdates.map((u) => ({
394
+ reportId: u.id,
395
+ activityType: u.activityType,
396
+ previousStatus: u.previousStatus,
397
+ internalNote: null,
398
+ publicNote: reportAction.note ?? null,
399
+ meta: null,
400
+ isAutomated: false,
401
+ createdBy,
402
+ createdAt: now,
403
+ })),
404
+ )
405
+ .execute()
406
+
407
+ return validUpdates.length
408
+ }