elanous 0.2.14 → 0.2.15

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 (293) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -0
  65. package/apps/pwa/out/ceo/index.txt +33 -0
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +14 -14
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +14 -14
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +14 -14
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +14 -14
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +14 -14
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +14 -14
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +14 -14
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +14 -14
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +14 -14
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +14 -14
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +14 -14
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +14 -14
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +14 -14
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +14 -14
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +14 -14
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +14 -14
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/heal/heal-loop.yaml +1 -1
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +4 -2
  147. package/package.json +1 -1
  148. package/scripts/brand/check.ts +7 -5
  149. package/scripts/ci-typecheck-changed.ts +50 -18
  150. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  151. package/scripts/expose-rubric-suggest.ts +18 -12
  152. package/scripts/lecture-note/node.ts +31 -11
  153. package/scripts/lessons/handbook-page.ts +30 -3
  154. package/scripts/marketing/generate-ax-brief.ts +140 -0
  155. package/scripts/marketing/generate-feature-map.ts +118 -0
  156. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  157. package/scripts/release-loop/gate-node.ts +80 -14
  158. package/scripts/release-loop/vault-note-node.ts +240 -8
  159. package/scripts/release-story/draft.ts +57 -9
  160. package/scripts/rhythm/daily.ts +263 -76
  161. package/scripts/rhythm/weekly.ts +30 -9
  162. package/scripts/smart-merge/resync-shadow.ts +147 -0
  163. package/src/agent-env/profile.ts +161 -0
  164. package/src/agent-substrate/pr-reviewer.ts +2 -0
  165. package/src/ask-user-question/pending-questions.ts +5 -0
  166. package/src/auth/github-app-token.ts +112 -13
  167. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  168. package/src/briefing/brief-items.ts +121 -0
  169. package/src/claims/claims-render.ts +14 -5
  170. package/src/cli/agent-env-cli.ts +21 -0
  171. package/src/cli/decisions-cli.ts +9 -0
  172. package/src/cli/doctor-cli.ts +23 -1
  173. package/src/cli/doctor-node-modules.ts +147 -0
  174. package/src/cli/intake-cli.ts +46 -17
  175. package/src/cli/pod-cli.ts +2 -0
  176. package/src/cli/release-cli.ts +61 -27
  177. package/src/cli/seat-requests-cli.ts +52 -0
  178. package/src/context-bus/context-now-public.ts +16 -6
  179. package/src/context-bus/context-now.ts +3 -1
  180. package/src/context-bus/coord-events.ts +6 -4
  181. package/src/decisions/decision-cards.ts +33 -7
  182. package/src/decisions/proact-meter.ts +286 -0
  183. package/src/decisions/proact1-lite.ts +86 -0
  184. package/src/decisions/web-push-decision-cards.ts +1 -1
  185. package/src/domains/alert-audit.ts +271 -0
  186. package/src/domains/core-tools.ts +4 -3
  187. package/src/domains/ops-facts-tool.ts +31 -1
  188. package/src/harness/harness-cli-command.ts +111 -15
  189. package/src/harness/harness-incidents.ts +2 -0
  190. package/src/harness/harness-pod-dispatch.ts +100 -11
  191. package/src/harness/harness-queue.ts +380 -50
  192. package/src/harness/helper-repair.ts +87 -0
  193. package/src/harness/helper-scan.ts +16 -4
  194. package/src/harness/launch-stamp.ts +236 -0
  195. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  196. package/src/harness/ledger-write-guard.ts +35 -0
  197. package/src/harness/pod-memory-policy.ts +15 -0
  198. package/src/harness/repository-name.ts +55 -0
  199. package/src/hq/fence-audit.ts +90 -0
  200. package/src/hq/hq.ts +62 -4
  201. package/src/hq/lease.ts +4 -4
  202. package/src/hq/move-plan.ts +126 -0
  203. package/src/hq/promote.ts +31 -0
  204. package/src/index.ts +77 -7
  205. package/src/intake-plane/check.ts +63 -0
  206. package/src/intake-plane/digest.ts +37 -15
  207. package/src/intake-plane/front-classifier.ts +6 -0
  208. package/src/intake-plane/items.ts +3 -2
  209. package/src/intake-plane/lens.ts +177 -0
  210. package/src/intake-plane/route.ts +145 -1
  211. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  212. package/src/launch-head/seat-goal-queue.ts +198 -0
  213. package/src/loops/budget.ts +36 -5
  214. package/src/loops/checker.ts +2 -2
  215. package/src/loops/orchestrator/author-depth.ts +65 -9
  216. package/src/loops/orchestrator/author-input-check.ts +8 -3
  217. package/src/loops/orchestrator/author-ledger.ts +3 -1
  218. package/src/loops/orchestrator/seat-share.ts +56 -0
  219. package/src/loops/orchestrator/tick.ts +11 -4
  220. package/src/loops/orchestrator/traffic.ts +117 -11
  221. package/src/loops/registry.ts +142 -61
  222. package/src/maturity/feature-maturity.ts +2 -1
  223. package/src/maturity/tui-slash-availability.ts +31 -0
  224. package/src/mss/logging/log-store.ts +35 -4
  225. package/src/nexus/api/http-server.ts +28 -14
  226. package/src/nexus/api/loop-edges.ts +173 -0
  227. package/src/nexus/api/schedules-read.ts +13 -0
  228. package/src/nexus/api/seat-requests.ts +79 -56
  229. package/src/prompt-enhance/enhance.ts +6 -1
  230. package/src/release-loop/auto-start.ts +6 -3
  231. package/src/release-loop/checklist.ts +13 -4
  232. package/src/release-loop/feature-store.ts +58 -31
  233. package/src/release-loop/merged-pr-checklist.ts +125 -0
  234. package/src/release-loop/placement.ts +86 -26
  235. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  236. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  237. package/src/seat-loop/seat-loop.ts +126 -10
  238. package/src/self-dev/dev-pipeline.ts +8 -4
  239. package/src/self-dev/draft-sweep.ts +90 -10
  240. package/src/self-dev/draft-triage-rules.ts +25 -8
  241. package/src/self-dev/orchestrate.ts +15 -1
  242. package/src/self-dev/review-provider-fallback.ts +6 -1
  243. package/src/self-dev/run-supervisor.ts +49 -2
  244. package/src/self-implement/frozen-merges.ts +3 -2
  245. package/src/self-implement/goal-author.ts +43 -5
  246. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  247. package/src/self-implement/heal-intake.ts +120 -2
  248. package/src/self-implement/host-regate.ts +9 -2
  249. package/src/self-implement/l8-merge-queue.ts +132 -0
  250. package/src/self-implement/orchestrator.ts +187 -40
  251. package/src/self-implement/pipeline-shape.ts +1 -1
  252. package/src/self-implement/revert-guard.ts +42 -7
  253. package/src/self-implement/run-ledger.ts +2 -0
  254. package/src/self-implement/run-status-mapping.ts +2 -0
  255. package/src/self-implement/seams.ts +1 -1
  256. package/src/self-implement/self-implement-cli.ts +1 -0
  257. package/src/self-implement/speed-claims.ts +338 -0
  258. package/src/steward/triage.ts +74 -3
  259. package/src/steward/working-backwards.ts +4 -1
  260. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  261. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  262. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  263. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  264. package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
  265. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  266. package/src/telegram.ts +25 -4
  267. package/src/user-config.ts +29 -5
  268. package/src/version/packed-revision.json +1 -1
  269. package/src/workflow-runtime/nodes/bash.ts +9 -1
  270. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
  291. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  292. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  293. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -0,0 +1,286 @@
1
+ import { readdirSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { DecisionLedger, type DecisionEntry, type SeatDecisionRecord } from './decision-ledger.js';
4
+ import { createRequire } from 'node:module';
5
+ import { listCoordEvents, type CoordEvent } from '../context-bus/coord-events.js';
6
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
7
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
8
+
9
+ /** Directive row fields this meter reads — kept local because `src/directives/directive-index` is excluded from the public export. */
10
+ export interface DirectiveRow { ts: string; text: string; source_file: string; line_no: number }
11
+
12
+ /** Loads the private directive index at run time only, so public builds (no index) bundle cleanly and report `directivesError`. */
13
+ function openDirectiveIndex(stateDir?: string): { search(q: string, f: { since?: string; until?: string; limit?: number }): DirectiveRow[]; close(): void } {
14
+ const spec = ['..', 'directives', 'directive-index.js'].join('/');
15
+ const mod = createRequire(import.meta.url)(spec) as { DirectiveIndex: new (o: { stateDir?: string }) => ReturnType<typeof openDirectiveIndex> };
16
+ return new mod.DirectiveIndex(stateDir ? { stateDir } : {});
17
+ }
18
+
19
+ /** KST calendar day, matching the seat-loop ledger file name. */
20
+ export function proactKstDay(at: Date | string): string {
21
+ const date = typeof at === 'string' ? new Date(at) : at;
22
+ if (!Number.isFinite(date.getTime())) throw new Error('valid timestamp required');
23
+ return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(date);
24
+ }
25
+
26
+ export function proactWindow(now: Date, days = 7): { since: string; until: string; days: string[] } {
27
+ if (!Number.isFinite(now.getTime()) || !Number.isInteger(days) || days < 1) throw new Error('valid window required');
28
+ const today = proactKstDay(now);
29
+ const todayStart = Date.parse(`${today}T00:00:00+09:00`);
30
+ const list: string[] = [];
31
+ for (let i = days - 1; i >= 0; i--) list.push(proactKstDay(new Date(todayStart - i * 86_400_000)));
32
+ return { since: new Date(todayStart - (days - 1) * 86_400_000).toISOString(), until: now.toISOString(), days: list };
33
+ }
34
+
35
+ export type ProactKind = 'proposal' | 'alert' | 'action';
36
+ export type ProactSource = 'decision' | 'directive' | 'coord' | 'loop';
37
+ export interface ProactSample {
38
+ readonly day: string;
39
+ readonly bucket: 'offered' | 'adopted' | 'asked';
40
+ readonly source: ProactSource;
41
+ readonly id: string;
42
+ readonly at: string;
43
+ readonly title: string;
44
+ /** Where the original row lives, so a count can be opened again. */
45
+ readonly link: string;
46
+ }
47
+ export interface ProactDay {
48
+ readonly day: string;
49
+ /** System offered a proposal, alert, or action before the owner asked. */
50
+ readonly offered: number;
51
+ /** Of those, the ones the owner (or a delegated seat) adopted. */
52
+ readonly adopted: number;
53
+ /** Problems that stayed invisible until the owner asked first. */
54
+ readonly asked: number;
55
+ }
56
+ export interface ProactSourceStatus { readonly read: boolean; readonly reason?: string }
57
+ export interface ProactMeter {
58
+ readonly since: string;
59
+ readonly until: string;
60
+ readonly days: readonly ProactDay[];
61
+ readonly samples: readonly ProactSample[];
62
+ readonly sources: Readonly<Record<ProactSource, ProactSourceStatus>>;
63
+ }
64
+
65
+ export interface ProactMeterInput {
66
+ decisions?: readonly DecisionEntry[];
67
+ seatDecisions?: readonly SeatDecisionRecord[];
68
+ directives?: readonly DirectiveRow[];
69
+ coord?: readonly CoordEvent[];
70
+ loops?: readonly ProactLoopRow[];
71
+ decisionsError?: string;
72
+ directivesError?: string;
73
+ coordError?: string;
74
+ loopsError?: string;
75
+ }
76
+ export interface ProactLoopRow {
77
+ readonly seat: string;
78
+ readonly at: string;
79
+ readonly status: string;
80
+ readonly action?: string;
81
+ readonly id?: string;
82
+ readonly title?: string;
83
+ readonly file: string;
84
+ }
85
+
86
+ const ALERT = /경보|경고|장애|실패|막힘|alert|fail|blocked|stall/i;
87
+ const ACTION = /조치|실행|발사|launch|queued|launched/i;
88
+ const ASKED = /왜|문제|안 되|안돼|고장|버그|실패|막혀|뭐가|무슨 일|확인해/;
89
+
90
+ export function proactKind(text: string, action?: string): ProactKind {
91
+ if (action === 'decision' || action === 'harness' || action === 'seat-question' || ACTION.test(text)) return 'action';
92
+ if (ALERT.test(text)) return 'alert';
93
+ return 'proposal';
94
+ }
95
+
96
+ function sample(day: string, bucket: ProactSample['bucket'], source: ProactSource, id: string, at: string, title: string, link: string): ProactSample {
97
+ return { day, bucket, source, id, at, title: title.replace(/\s+/g, ' ').trim().slice(0, 160), link };
98
+ }
99
+
100
+ /** Count one row once. Adoption is a property of an offered row, never a second offered row. */
101
+ export function measureProact(input: ProactMeterInput, now: Date, days = 7): ProactMeter {
102
+ const window = proactWindow(now, days);
103
+ const inWindow = (at: string | undefined): at is string => !!at && at >= window.since && at <= window.until;
104
+ const offered: ProactSample[] = [];
105
+ const adopted: ProactSample[] = [];
106
+ const asked: ProactSample[] = [];
107
+
108
+ if (!input.decisionsError) {
109
+ for (const card of input.decisions ?? []) {
110
+ const at = card.raisedAt ?? card.importedAt;
111
+ if (!inWindow(at)) continue;
112
+ const day = proactKstDay(at);
113
+ const link = `decisions://${card.id}`;
114
+ const system = card.raisedBy.agent !== 'human' && card.raisedBy.agent !== 'owner';
115
+ if (system) {
116
+ offered.push(sample(day, 'offered', 'decision', card.id, at, card.title, link));
117
+ const decidedAt = card.decidedAt;
118
+ const adoptedDay = decidedAt ? proactKstDay(decidedAt) : '';
119
+ if (card.status === 'decided' && decidedAt && decidedAt <= window.until) {
120
+ adopted.push(sample(adoptedDay, 'adopted', 'decision', card.id, decidedAt, card.title, link));
121
+ }
122
+ } else if (ASKED.test(`${card.title} ${card.scqa.c}`)) {
123
+ asked.push(sample(day, 'asked', 'decision', card.id, at, card.title, link));
124
+ }
125
+ }
126
+ for (const seat of input.seatDecisions ?? []) {
127
+ if (!inWindow(seat.recordedAt)) continue;
128
+ const at = seat.decidedAt && seat.decidedAt <= window.until ? seat.decidedAt : seat.recordedAt;
129
+ const adoptedDay = proactKstDay(at);
130
+ const link = `decisions://${seat.id}`;
131
+ offered.push(sample(proactKstDay(seat.recordedAt), 'offered', 'decision', seat.id, seat.recordedAt, seat.title, link));
132
+ if (window.days.includes(adoptedDay)) adopted.push(sample(adoptedDay, 'adopted', 'decision', seat.id, at, seat.title, link));
133
+ }
134
+ }
135
+
136
+ if (!input.directivesError) {
137
+ for (const row of input.directives ?? []) {
138
+ if (!inWindow(row.ts) || !ASKED.test(row.text)) continue;
139
+ asked.push(sample(proactKstDay(row.ts), 'asked', 'directive', `${row.source_file}:${row.line_no}`, row.ts, row.text, `directives://${row.source_file}#L${row.line_no}`));
140
+ }
141
+ }
142
+
143
+ if (!input.coordError) {
144
+ for (const event of input.coord ?? []) {
145
+ if (!inWindow(event.at)) continue;
146
+ const day = proactKstDay(event.at);
147
+ const link = event.refs.url ?? `coord://${event.id}`;
148
+ const text = `${event.summary} ${event.kind}`;
149
+ const systemOffered = event.refs.seat && event.refs.seat !== 'CEO' && (event.kind === '제안' || event.kind === '보고' || event.kind === '사고' || ALERT.test(text) || ACTION.test(text));
150
+ if (systemOffered) {
151
+ offered.push(sample(day, 'offered', 'coord', event.id, event.at, event.summary, link));
152
+ } else if (event.refs.seat === 'CEO' && (event.kind === '요청' || ASKED.test(text))) {
153
+ asked.push(sample(day, 'asked', 'coord', event.id, event.at, event.summary, link));
154
+ }
155
+ }
156
+ }
157
+
158
+ if (!input.loopsError) {
159
+ const proactive = new Set(['queued', 'launched', 'asked', 'hitl', 'shadow', 'attempting']);
160
+ for (const row of input.loops ?? []) {
161
+ if (!inWindow(row.at)) continue;
162
+ const day = proactKstDay(row.at);
163
+ const id = row.id ?? `${row.seat}:${row.at}`;
164
+ const link = `loop://${row.file}#${id}`;
165
+ const title = row.title ?? `${row.seat} ${row.status}`;
166
+ if (proactive.has(row.status) || row.action === 'decision' || row.action === 'harness') {
167
+ offered.push(sample(day, 'offered', 'loop', id, row.at, `${proactKind(title, row.action)}:${title}`, link));
168
+ if (row.status === 'launched' || row.status === 'queued' || row.action === 'decision') {
169
+ adopted.push(sample(day, 'adopted', 'loop', id, row.at, title, link));
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ const tally = new Map<string, { day: string; offered: number; adopted: number; asked: number }>(window.days.map(day => [day, { day, offered: 0, adopted: 0, asked: 0 }]));
176
+ for (const row of offered) tally.get(row.day)!.offered += 1;
177
+ for (const row of adopted) {
178
+ const cell = tally.get(row.day);
179
+ if (cell) cell.adopted += 1;
180
+ }
181
+ for (const row of asked) tally.get(row.day)!.asked += 1;
182
+ const pick = (rows: ProactSample[]) => {
183
+ const seen = new Set<string>();
184
+ const out: ProactSample[] = [];
185
+ for (const source of ['decision', 'directive', 'coord', 'loop'] as const) {
186
+ for (const row of rows.filter(item => item.source === source).sort((a, b) => b.at.localeCompare(a.at) || a.id.localeCompare(b.id))) {
187
+ if (seen.has(row.link)) continue;
188
+ seen.add(row.link);
189
+ out.push(row);
190
+ if (out.length === 2) break;
191
+ }
192
+ }
193
+ return out;
194
+ };
195
+ const status = (error: string | undefined, read: boolean): ProactSourceStatus => error ? { read: false, reason: error } : { read };
196
+ return {
197
+ since: window.since, until: window.until,
198
+ days: window.days.map(day => tally.get(day)!),
199
+ samples: [...pick(offered), ...pick(adopted), ...pick(asked)],
200
+ sources: {
201
+ decision: status(input.decisionsError, !input.decisionsError),
202
+ directive: status(input.directivesError, !input.directivesError),
203
+ coord: status(input.coordError, !input.coordError),
204
+ loop: status(input.loopsError, !input.loopsError),
205
+ },
206
+ };
207
+ }
208
+
209
+ function readLoopRows(root: string, days: readonly string[]): ProactLoopRow[] {
210
+ const base = join(root, 'seat-loop');
211
+ const seats = readdirSync(base, { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => entry.name);
212
+ const rows: ProactLoopRow[] = [];
213
+ for (const seat of seats) {
214
+ for (const day of days) {
215
+ const file = join(base, seat, `${day}.jsonl`);
216
+ let text = '';
217
+ try { text = readFileSync(file, 'utf8'); } catch (error) {
218
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue;
219
+ throw error;
220
+ }
221
+ for (const line of text.split('\n')) {
222
+ if (!line.trim()) continue;
223
+ const parsed = JSON.parse(line) as { at?: string; ts?: string; status?: string; action?: string; item?: { id?: string; title?: string } };
224
+ const at = parsed.at ?? parsed.ts;
225
+ if (!at || !parsed.status) continue;
226
+ rows.push({ seat, at, status: parsed.status, action: parsed.action, id: parsed.item?.id, title: parsed.item?.title, file });
227
+ }
228
+ }
229
+ }
230
+ return rows;
231
+ }
232
+
233
+ export interface ProactMeterDeps {
234
+ readonly now?: Date;
235
+ readonly days?: number;
236
+ readonly stateDir?: string;
237
+ readonly instanceRoot?: string;
238
+ readonly readDecisions?: () => { decisions: DecisionEntry[]; seatDecisions: SeatDecisionRecord[] };
239
+ readonly readDirectives?: (since: string, until: string) => DirectiveRow[];
240
+ readonly readCoord?: (since: string) => CoordEvent[];
241
+ readonly readLoops?: (days: readonly string[]) => ProactLoopRow[];
242
+ }
243
+
244
+ /** Read the four ledgers. A source that cannot be opened is reported, never counted as zero evidence. */
245
+ export function readProactMeter(deps: ProactMeterDeps = {}): ProactMeter {
246
+ const now = deps.now ?? new Date();
247
+ const window = proactWindow(now, deps.days ?? 7);
248
+ const input: ProactMeterInput = {};
249
+ const stateDir = deps.stateDir;
250
+ try {
251
+ const read = deps.readDecisions ?? (() => {
252
+ const ledger = new DecisionLedger(stateDir ? { stateDir } : {});
253
+ return { decisions: ledger.list({ status: 'all', since: window.since }), seatDecisions: ledger.seatReport({ since: window.since }) };
254
+ });
255
+ const got = read();
256
+ input.decisions = got.decisions;
257
+ input.seatDecisions = got.seatDecisions;
258
+ } catch (error) { input.decisionsError = error instanceof Error ? error.message : String(error); }
259
+ try {
260
+ input.directives = (deps.readDirectives ?? ((since, until) => {
261
+ const index = openDirectiveIndex(stateDir);
262
+ try { return index.search('', { since, until, limit: 1000 }); }
263
+ finally { index.close(); }
264
+ }))(window.since, window.until);
265
+ } catch (error) { input.directivesError = error instanceof Error ? error.message : String(error); }
266
+ try {
267
+ input.coord = (deps.readCoord ?? ((since) => listCoordEvents({ since, channelOnly: true })))(window.since);
268
+ } catch (error) { input.coordError = error instanceof Error ? error.message : String(error); }
269
+ try {
270
+ const loopRoot = deps.instanceRoot ?? stateDir ?? effectiveInstanceRoot() ?? elanousStateRoot();
271
+ input.loops = (deps.readLoops ?? ((days) => readLoopRows(loopRoot, days)))(window.days);
272
+ } catch (error) { input.loopsError = error instanceof Error ? error.message : String(error); }
273
+ return measureProact(input, now, deps.days ?? 7);
274
+ }
275
+
276
+ export function formatProactMeter(meter: ProactMeter): string {
277
+ const lines = [`선제성 ${meter.since} ~ ${meter.until}`, '날짜 · 먼저 냄 · 채택 · 물어서야'];
278
+ for (const day of meter.days) lines.push(`${day.day} · ${day.offered} · ${day.adopted} · ${day.asked}`);
279
+ const unread = (Object.entries(meter.sources) as Array<[ProactSource, ProactSourceStatus]>).filter(([, status]) => !status.read);
280
+ if (unread.length) lines.push(`못 읽음: ${unread.map(([name, status]) => `${name}(${status.reason})`).join(' · ')}`);
281
+ if (meter.samples.length) {
282
+ lines.push('표본:');
283
+ for (const row of meter.samples) lines.push(` ${row.bucket} · ${row.source} · ${row.id} · ${row.link}`);
284
+ }
285
+ return lines.join('\n');
286
+ }
@@ -0,0 +1,86 @@
1
+ import type { DecisionEntry } from './decision-ledger.js';
2
+ import type { ChecklistItem } from '../release-loop/checklist.js';
3
+ import type { ReleaseSchedule } from '../release-loop/release-schedule.js';
4
+ import type { DraftSweepResult, SweepDraft } from '../self-dev/draft-sweep.js';
5
+
6
+ type ProactLoop = { readonly name: string; readonly state: 'alive' | 'late' | 'failing' | 'off' | 'unknown'; readonly scope?: string };
7
+ type ProactYellow = { readonly version: string; readonly item: Pick<ChecklistItem, 'id' | 'title' | 'status'> };
8
+ type ProactDrafts = { readonly drafts: readonly Pick<SweepDraft, 'number' | 'title'>[]; readonly sweep: Pick<DraftSweepResult, 'complete' | 'entries'> };
9
+ export interface ProactSignals {
10
+ /** A suspended run (resume) or an explicitly observed blocker, not merely an old open card. */
11
+ readonly decisions?: readonly (Pick<DecisionEntry, 'id' | 'title' | 'status' | 'raisedAt' | 'importedAt' | 'resume'> & { readonly blocked?: boolean })[];
12
+ readonly yellow?: readonly ProactYellow[];
13
+ readonly schedules?: readonly Pick<ReleaseSchedule, 'version' | 'landBy'>[];
14
+ readonly loops?: readonly ProactLoop[];
15
+ /** Only a complete, read-only runDraftSweep({ apply: false }) assessment proves draft staleness. */
16
+ readonly draftAssessment?: ProactDrafts;
17
+ }
18
+ type ProactSuggestion = {
19
+ readonly key: string;
20
+ readonly kind: 'decision' | 'yellow' | 'loop' | 'draft';
21
+ readonly title: string;
22
+ readonly reason: string;
23
+ readonly source: string;
24
+ readonly score: number;
25
+ };
26
+
27
+ const HOUR = 3_600_000;
28
+ const DECISION_AGE_HOURS = 24;
29
+ const LANDING_WINDOW_HOURS = 24;
30
+ const MAX_SUGGESTIONS = 3;
31
+
32
+ /** Rules-only, read-only selection. Missing/invalid observations are unknown, never evidence of urgency. */
33
+ export function selectProact1Lite(signals: ProactSignals, options: { now: Date; previouslySuggested?: ReadonlySet<string> }): ProactSuggestion[] {
34
+ const now = options.now.getTime();
35
+ if (!Number.isFinite(now)) throw new Error('valid now required');
36
+ const candidates: ProactSuggestion[] = [];
37
+ const elapsedHours = (iso: string | undefined): number => iso ? (now - Date.parse(iso)) / HOUR : NaN;
38
+ const valid = (text: string): boolean => text.trim().length > 0;
39
+
40
+ for (const card of signals.decisions ?? []) {
41
+ if (card.status !== 'open' || (card.blocked !== true && !card.resume) || !valid(card.id) || !valid(card.title)) continue;
42
+ // Imported historical cards have no known raise time; their import time is a lower bound on age.
43
+ const age = elapsedHours(card.raisedAt ?? card.importedAt);
44
+ if (!Number.isFinite(age) || age < DECISION_AGE_HOURS) continue;
45
+ candidates.push({ key: `decision:${card.id}`, kind: 'decision', title: card.title,
46
+ reason: `열린 결정 ${Math.floor(age)}시간 대기${card.raisedAt ? '' : ' (가져온 시각 기준)'}`,
47
+ source: card.id, score: 400 + Math.min(100, Math.floor(age / 24)) });
48
+ }
49
+
50
+ const deadlines = new Map((signals.schedules ?? []).map(schedule => [schedule.version, schedule.landBy]));
51
+ for (const { version, item } of signals.yellow ?? []) {
52
+ if (item.status !== 'yellow' || !valid(item.id) || !valid(item.title) || !valid(version)) continue;
53
+ const landBy = deadlines.get(version);
54
+ const remaining = landBy ? (Date.parse(landBy) - now) / HOUR : NaN;
55
+ if (!Number.isFinite(remaining) || remaining > LANDING_WINDOW_HOURS) continue;
56
+ candidates.push({ key: `yellow:${version}:${item.id}`, kind: 'yellow', title: item.title,
57
+ reason: remaining < 0 ? `착지 마감 ${Math.ceil(-remaining)}시간 경과` : `착지 마감 ${Math.ceil(remaining)}시간 전`,
58
+ source: `${version}/${item.id} · ${landBy}`, score: 500 + Math.min(100, Math.max(0, Math.ceil(-remaining))) });
59
+ }
60
+
61
+ for (const loop of signals.loops ?? []) {
62
+ if (loop.state !== 'late' || !valid(loop.name)) continue;
63
+ candidates.push({ key: `loop:${loop.scope ?? ''}:${loop.name}`, kind: 'loop', title: loop.name,
64
+ reason: '루프 실행 지연 관측', source: `${loop.scope ?? 'loop'}/${loop.name}`, score: 300 });
65
+ }
66
+
67
+ const assessment = signals.draftAssessment;
68
+ if (assessment?.sweep.complete) {
69
+ const drafts = new Map(assessment.drafts.map(draft => [draft.number, draft]));
70
+ for (const entry of assessment.sweep.entries) {
71
+ // A stale assessment is not a request to close: only surface the already-verified signal.
72
+ if (entry.action !== 'close' || entry.applied || !['stale-unobserved', 'claim-expired', 'claim-expired (self-implement.result final; worktree is not live)'].includes(entry.reason)) continue;
73
+ const draft = drafts.get(entry.number);
74
+ if (!draft || !valid(draft.title)) continue;
75
+ candidates.push({ key: `draft:${entry.number}`, kind: 'draft', title: draft.title,
76
+ reason: `draft PR 정체 (${entry.reason})`, source: `#${entry.number}`, score: 200 });
77
+ }
78
+ }
79
+
80
+ const selected = new Map<string, ProactSuggestion>();
81
+ for (const candidate of candidates) {
82
+ if (options.previouslySuggested?.has(candidate.key)) continue;
83
+ if (!selected.has(candidate.key) || selected.get(candidate.key)!.score < candidate.score) selected.set(candidate.key, candidate);
84
+ }
85
+ return [...selected.values()].sort((a, b) => b.score - a.score || a.key.localeCompare(b.key)).slice(0, MAX_SUGGESTIONS);
86
+ }
@@ -28,7 +28,7 @@ export function webPushDecisionTransport(
28
28
  const recommendation = 'skipped' in entry.recommendation ? '추천안 없음' : `추천안: ${entry.recommendation.option.toUpperCase()}`;
29
29
  const body = `${recommendation}${entry.dueAt ? ` · 기한: ${new Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(entry.dueAt))}` : ''}`;
30
30
  const result = await push({ title: title('대표 결정 · ', entry.title), body, url: url(id), tag: tag(id) });
31
- debug.log('decisions.webpush', 'card-sent', { id, delivered: result.delivered });
31
+ if (result.attempted) debug.log('decisions.webpush', 'card-sent', { id, delivered: result.delivered });
32
32
  return result.delivered ? { chat: 'webpush', message: id } : null;
33
33
  },
34
34
  edit: async (ref, _view) => {
@@ -0,0 +1,271 @@
1
+ // 대표에게 가는 알림·브리핑 전수 재고 (ALERT-AUDIT).
2
+ //
3
+ // «전달 ok» 는 발송 시도 기록이다. 도착은 배송 원장의 채널 ok 한 건뿐이다.
4
+ // 발송 설정이 꺼져 있으면 시도 기록만 남고 도착은 0건이 된다 — 그 칸을
5
+ // claimed-without-arrival 로 표시한다. 같은 내용이 둘 이상의 채널·봇에
6
+ // 도착하면 overlap, 레지스트리에 있는데 아무도 안 보내면 gap.
7
+
8
+ import type { ScheduleRow } from './schedule-registry.js';
9
+ import type { DeliveryRow } from '../nexus/outbound/delivery-ledger.js';
10
+
11
+ export type AlertSenderKind = 'loop' | 'cron' | 'script';
12
+ export type AlertMark = 'claimed-without-arrival' | 'overlap' | 'gap' | 'overlap-unknown' | 'none';
13
+
14
+ export interface AlertSenderRef {
15
+ kind: AlertSenderKind;
16
+ id: string;
17
+ }
18
+
19
+ export interface AlertRegistrySender {
20
+ kind: AlertSenderKind;
21
+ id: string;
22
+ /** 대표에게 보내는 알림·브리핑이면 true. false 는 대조 모집단에서 빠진다. */
23
+ representative: boolean;
24
+ what: string;
25
+ when: string;
26
+ channel: string;
27
+ bot: string;
28
+ /** 같은 내용으로 겹치는지 가르는 키. 비면 겹침은 미상이다. */
29
+ contentKey?: string;
30
+ }
31
+
32
+ export interface AlertClaim {
33
+ sender: AlertSenderRef;
34
+ what: string;
35
+ when: string;
36
+ channel: string;
37
+ bot: string;
38
+ contentKey?: string;
39
+ /** «전달 ok» 류의 발송 기록. 도착 증거가 아니다. */
40
+ recordedOk: boolean;
41
+ /** 발송 기록 시각. 도착이 없을 때 표의 «언제»는 이 시각이다. */
42
+ at?: string;
43
+ }
44
+
45
+ export interface AlertArrival {
46
+ sender: AlertSenderRef;
47
+ what: string;
48
+ channel: string;
49
+ bot: string;
50
+ contentKey?: string;
51
+ /** 채널이 실제로 받았다고 원장에 남은 시각. */
52
+ arrivedAt: string;
53
+ ok: boolean;
54
+ }
55
+
56
+ export interface AlertAuditInput {
57
+ registry: readonly AlertRegistrySender[];
58
+ /** 크론 레지스트리. representative 크론 발신자가 여기 없으면 누락이다. */
59
+ crons: readonly Pick<ScheduleRow, 'id' | 'name' | 'cron' | 'command' | 'enabled'>[];
60
+ /** 루프 레지스트리 id. representative 루프 발신자가 여기 없으면 누락이다. */
61
+ loops: readonly { id: string }[];
62
+ claims: readonly AlertClaim[];
63
+ arrivals: readonly AlertArrival[];
64
+ }
65
+
66
+ export interface AlertAuditRow {
67
+ who: string;
68
+ senderKind: AlertSenderKind;
69
+ senderId: string;
70
+ what: string;
71
+ when: string;
72
+ channel: string;
73
+ bot: string;
74
+ lastArrivedAt: string | null;
75
+ marks: AlertMark[];
76
+ }
77
+
78
+ export interface AlertAudit {
79
+ rows: AlertAuditRow[];
80
+ /** 레지스트리 representative 발신자 중 크론·루프 대조에서 빠진 수. 스크립트는 대조 대상이 아니다. */
81
+ missingFromRegistry: number;
82
+ missingSenders: AlertSenderRef[];
83
+ claimedWithoutArrival: AlertAuditRow[];
84
+ overlaps: AlertAuditRow[];
85
+ gaps: AlertAuditRow[];
86
+ /** contentKey 를 모르는 도착이 있어 겹침을 판정하지 못한 행. */
87
+ overlapUnknown: AlertAuditRow[];
88
+ verdict: 'pass' | 'fail';
89
+ }
90
+
91
+ export interface DeliveryArrivalView {
92
+ sender: AlertSenderRef;
93
+ what: string;
94
+ channel: string;
95
+ bot: string;
96
+ contentKey?: string;
97
+ }
98
+
99
+ const MARK_ORDER: AlertMark[] = ['claimed-without-arrival', 'overlap', 'gap', 'overlap-unknown'];
100
+
101
+ function senderKey(sender: AlertSenderRef): string {
102
+ return `${sender.kind}:${sender.id}`;
103
+ }
104
+
105
+ /** 세 표시(+미상)를 MARK_ORDER 순으로 남긴다. none 은 표시가 없을 때만. */
106
+ export function markOrder(marks: readonly AlertMark[]): AlertMark[] {
107
+ const ordered = MARK_ORDER.filter(mark => marks.includes(mark));
108
+ return ordered.length ? ordered : ['none'];
109
+ }
110
+
111
+ function knownContentKey(item: { contentKey?: string }): string | null {
112
+ const key = item.contentKey?.trim();
113
+ return key ? key : null;
114
+ }
115
+
116
+ function latestIso(values: readonly string[]): string | null {
117
+ const finite = values.filter(value => Number.isFinite(Date.parse(value)));
118
+ if (!finite.length) return null;
119
+ return finite.reduce((best, value) => Date.parse(value) > Date.parse(best) ? value : best);
120
+ }
121
+
122
+ function marksOf(row: { claimedWithoutArrival: boolean; overlap: boolean; gap: boolean; overlapUnknown: boolean }): AlertMark[] {
123
+ return markOrder([
124
+ ...(row.claimedWithoutArrival ? ['claimed-without-arrival' as const] : []),
125
+ ...(row.overlap ? ['overlap' as const] : []),
126
+ ...(row.gap ? ['gap' as const] : []),
127
+ ...(row.overlapUnknown ? ['overlap-unknown' as const] : []),
128
+ ]);
129
+ }
130
+
131
+ /** 배송 원장에서 ok 채널만 도착으로 읽는다. 실패 채널·빈 채널은 도착이 아니다. */
132
+ export function arrivalsFromDeliveries(
133
+ rows: readonly DeliveryRow[],
134
+ view: (row: DeliveryRow) => DeliveryArrivalView | null,
135
+ ): AlertArrival[] {
136
+ const arrivals: AlertArrival[] = [];
137
+ for (const row of rows) {
138
+ let channels: Array<{ type?: unknown; ok?: unknown; bot?: unknown }> = [];
139
+ try { channels = JSON.parse(row.channels) as typeof channels; } catch { channels = []; }
140
+ const mapped = view(row);
141
+ if (!mapped) continue;
142
+ const okChannels = channels.filter(channel => channel.ok === true);
143
+ if (!okChannels.length) continue;
144
+ const bots = [...new Set(okChannels.map(channel => typeof channel.bot === 'string' && channel.bot ? channel.bot : mapped.bot))];
145
+ for (const bot of bots) {
146
+ arrivals.push({
147
+ sender: mapped.sender,
148
+ what: mapped.what,
149
+ channel: mapped.channel,
150
+ bot,
151
+ ...(mapped.contentKey ? { contentKey: mapped.contentKey } : {}),
152
+ arrivedAt: row.ts,
153
+ ok: true,
154
+ });
155
+ }
156
+ }
157
+ return arrivals;
158
+ }
159
+
160
+ /** 발신자 전수를 한 장으로 접는다. 판정은 누락 0 이고, 세 표시는 행에 남는다. */
161
+ export function auditAlerts(input: AlertAuditInput): AlertAudit {
162
+ const cronIds = new Set(input.crons.map(row => row.id));
163
+ const loopIds = new Set(input.loops.map(row => row.id));
164
+ const representative = input.registry.filter(sender => sender.representative);
165
+ const missingSenders = representative.filter(sender => {
166
+ if (sender.kind === 'cron') return !cronIds.has(sender.id);
167
+ if (sender.kind === 'loop') return !loopIds.has(sender.id);
168
+ return false;
169
+ }).map(sender => ({ kind: sender.kind, id: sender.id }));
170
+
171
+ const arrivalsBySender = new Map<string, AlertArrival[]>();
172
+ for (const arrival of input.arrivals) {
173
+ if (!arrival.ok) continue;
174
+ const key = senderKey(arrival.sender);
175
+ const list = arrivalsBySender.get(key) ?? [];
176
+ list.push(arrival);
177
+ arrivalsBySender.set(key, list);
178
+ }
179
+ const claimsBySender = new Map<string, AlertClaim[]>();
180
+ for (const claim of input.claims) {
181
+ const key = senderKey(claim.sender);
182
+ const list = claimsBySender.get(key) ?? [];
183
+ list.push(claim);
184
+ claimsBySender.set(key, list);
185
+ }
186
+
187
+ const overlapKeys = new Set<string>();
188
+ const byContent = new Map<string, Set<string>>();
189
+ for (const arrival of input.arrivals) {
190
+ if (!arrival.ok) continue;
191
+ const key = knownContentKey(arrival);
192
+ if (!key) continue;
193
+ const routes = byContent.get(key) ?? new Set<string>();
194
+ routes.add(`${arrival.channel}|${arrival.bot}`);
195
+ byContent.set(key, routes);
196
+ if (routes.size >= 2) overlapKeys.add(key);
197
+ }
198
+
199
+ const rows: AlertAuditRow[] = representative.map(sender => {
200
+ const key = senderKey(sender);
201
+ const arrivals = arrivalsBySender.get(key) ?? [];
202
+ const claims = claimsBySender.get(key) ?? [];
203
+ const recordedOk = claims.some(claim => claim.recordedOk);
204
+ const claimedWithoutArrival = recordedOk && arrivals.length === 0;
205
+ const gap = !recordedOk && arrivals.length === 0;
206
+ const registryKey = knownContentKey(sender);
207
+ const arrivalKeys = arrivals.map(arrival => arrival.contentKey !== undefined ? knownContentKey(arrival) : registryKey);
208
+ const claimKeys = claims.map(claim => claim.contentKey !== undefined ? knownContentKey(claim) : registryKey);
209
+ const overlap = arrivalKeys.some(contentKey => contentKey !== null && overlapKeys.has(contentKey));
210
+ const overlapUnknown = !overlap && (
211
+ arrivals.some((_, index) => arrivalKeys[index] === null)
212
+ || (arrivals.length === 0 && claims.some((_, index) => claimKeys[index] === null && claims[index]?.recordedOk))
213
+ );
214
+ const last = latestIso(arrivals.map(arrival => arrival.arrivedAt));
215
+ const recorded = claims.find(claim => claim.recordedOk) ?? claims[0];
216
+ const channel = arrivals[0]?.channel ?? recorded?.channel ?? sender.channel;
217
+ const bot = arrivals[0]?.bot ?? recorded?.bot ?? sender.bot;
218
+ const what = arrivals[0]?.what ?? recorded?.what ?? sender.what;
219
+ const when = last ?? recorded?.at ?? recorded?.when ?? sender.when;
220
+ return {
221
+ who: `${sender.kind}:${sender.id}`,
222
+ senderKind: sender.kind,
223
+ senderId: sender.id,
224
+ what,
225
+ when,
226
+ channel,
227
+ bot,
228
+ lastArrivedAt: last,
229
+ marks: marksOf({ claimedWithoutArrival, overlap, gap, overlapUnknown }),
230
+ };
231
+ }).sort((a, b) => a.who.localeCompare(b.who) || a.what.localeCompare(b.what));
232
+
233
+ const claimedWithoutArrival = rows.filter(row => row.marks.includes('claimed-without-arrival'));
234
+ const overlaps = rows.filter(row => row.marks.includes('overlap'));
235
+ const gaps = rows.filter(row => row.marks.includes('gap'));
236
+ const overlapUnknown = rows.filter(row => row.marks.includes('overlap-unknown'));
237
+ return {
238
+ rows,
239
+ missingFromRegistry: missingSenders.length,
240
+ missingSenders,
241
+ claimedWithoutArrival,
242
+ overlaps,
243
+ gaps,
244
+ overlapUnknown,
245
+ verdict: missingSenders.length === 0 ? 'pass' : 'fail',
246
+ };
247
+ }
248
+
249
+ /** 한 장의 표. 세 표시가 비면 «없음» 과 그 근거를 표 아래에 적는다. */
250
+ export function formatAlertAudit(audit: AlertAudit): string {
251
+ const header = '| 누가 | 무엇을 | 언제 | 채널 | 봇 | 마지막 도착 | 표시 |';
252
+ const rule = '| --- | --- | --- | --- | --- | --- | --- |';
253
+ const body = audit.rows.map(row => {
254
+ const marks = markOrder(row.marks).filter(mark => mark !== 'none').join(', ') || 'none';
255
+ const arrived = row.lastArrivedAt ?? '없음';
256
+ return `| ${row.who} | ${row.what} | ${row.when} | ${row.channel} | ${row.bot} | ${arrived} | ${marks} |`;
257
+ });
258
+ const lines = [header, rule, ...body, '', `대조 누락: ${audit.missingFromRegistry}`, `판정: ${audit.verdict}`];
259
+ const note = (label: string, found: readonly AlertAuditRow[], emptyReason: string) => {
260
+ if (found.length) lines.push(`${label}: ${found.map(row => row.who).join(', ')}`);
261
+ else lines.push(`${label}: 없음 — ${emptyReason}`);
262
+ };
263
+ note('보낸다고 기록됐는데 도착 증거가 없는 것', audit.claimedWithoutArrival, 'recordedOk 인 발신자마다 도착이 있다');
264
+ if (audit.overlapUnknown.length) {
265
+ lines.push(`겹치는 것: 미상 — contentKey 를 모르는 도착이 있어 겹침을 판정하지 않음 (${audit.overlapUnknown.map(row => row.who).join(', ')})`);
266
+ } else {
267
+ note('겹치는 것', audit.overlaps, '같은 contentKey 가 둘 이상의 채널·봇에 도착한 적이 없다');
268
+ }
269
+ note('아무도 안 보내는 것', audit.gaps, 'representative 발신자는 모두 발송 기록이 있거나 도착이 있다');
270
+ return `${lines.join('\n')}\n`;
271
+ }