@quickchatai/quickreview 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1479) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +401 -2
  3. package/THIRD-PARTY-NOTICES.md +134 -0
  4. package/agents/pass-abstraction.md +64 -0
  5. package/agents/pass-ask-human.md +134 -0
  6. package/agents/pass-backcompat.md +67 -0
  7. package/agents/pass-baseline.md +61 -0
  8. package/agents/pass-behaviour-delta.md +64 -0
  9. package/agents/pass-bounds.md +36 -0
  10. package/agents/pass-claims.md +31 -0
  11. package/agents/pass-concurrency.md +168 -0
  12. package/agents/pass-consistency.md +57 -0
  13. package/agents/pass-contracts.md +30 -0
  14. package/agents/pass-data-safety.md +54 -0
  15. package/agents/pass-deferred.md +35 -0
  16. package/agents/pass-delivery.md +37 -0
  17. package/agents/pass-duplication.md +60 -0
  18. package/agents/pass-intent.md +63 -0
  19. package/agents/pass-lint-conformance.md +46 -0
  20. package/agents/pass-maintainability.md +67 -0
  21. package/agents/pass-placement.md +68 -0
  22. package/agents/pass-prod-risk.md +24 -0
  23. package/agents/pass-rollout.md +32 -0
  24. package/agents/pass-roundtrip.md +46 -0
  25. package/agents/pass-security.md +31 -0
  26. package/agents/pass-simplicity.md +68 -0
  27. package/agents/pass-statelessness.md +27 -0
  28. package/agents/pass-style.md +37 -0
  29. package/agents/pass-sweep.md +57 -0
  30. package/agents/pass-tests.md +58 -0
  31. package/agents/pass-ui.md +23 -0
  32. package/agents/pass-validate.md +50 -0
  33. package/agents/pass-widened.md +46 -0
  34. package/agents/scan/pass-scan.md +86 -0
  35. package/bin/qcr.js +11 -0
  36. package/config.example.toml +1099 -0
  37. package/deploy/ansible/README.md +356 -0
  38. package/deploy/ansible/group_vars/all.yml +212 -0
  39. package/deploy/ansible/inventory.example.ini +25 -0
  40. package/deploy/ansible/playbook.yml +360 -0
  41. package/deploy/collect-secrets.sh +289 -0
  42. package/deploy/collect-slack-secrets.mjs +1190 -0
  43. package/deploy/github-app-manifest.json +31 -0
  44. package/deploy/manifest.schema.json +75 -0
  45. package/deploy/qcr-update.sh +803 -0
  46. package/deploy/reviewer/AGENTS.override.md +12 -0
  47. package/dist/extensions/cli/bash-guard-ambient.js +42 -0
  48. package/dist/extensions/cli/bash-guard-ambient.js.map +1 -0
  49. package/dist/extensions/cli/bash-guard.js +5 -0
  50. package/dist/extensions/cli/bash-guard.js.map +1 -0
  51. package/dist/extensions/cli/companion-guard.js +5 -0
  52. package/dist/extensions/cli/companion-guard.js.map +1 -0
  53. package/dist/extensions/cli/debug-round.js +8 -0
  54. package/dist/extensions/cli/debug-round.js.map +1 -0
  55. package/dist/extensions/cli/inject-bridge.js +5 -0
  56. package/dist/extensions/cli/inject-bridge.js.map +1 -0
  57. package/dist/extensions/cli/ledger-pin.js +5 -0
  58. package/dist/extensions/cli/ledger-pin.js.map +1 -0
  59. package/dist/extensions/cli/lens.js +13 -0
  60. package/dist/extensions/cli/lens.js.map +1 -0
  61. package/dist/extensions/cli/login-route.js +5 -0
  62. package/dist/extensions/cli/login-route.js.map +1 -0
  63. package/dist/extensions/cli/muted-theme.js +5 -0
  64. package/dist/extensions/cli/muted-theme.js.map +1 -0
  65. package/dist/extensions/cli/onboarding.js +5 -0
  66. package/dist/extensions/cli/onboarding.js.map +1 -0
  67. package/dist/extensions/cli/setup.js +5 -0
  68. package/dist/extensions/cli/setup.js.map +1 -0
  69. package/dist/extensions/cli/tui-view.js +5 -0
  70. package/dist/extensions/cli/tui-view.js.map +1 -0
  71. package/dist/extensions/companion-guard.js +361 -0
  72. package/dist/extensions/companion-guard.js.map +1 -0
  73. package/dist/extensions/debug-round.js +204 -0
  74. package/dist/extensions/debug-round.js.map +1 -0
  75. package/dist/extensions/inject-bridge.js +603 -0
  76. package/dist/extensions/inject-bridge.js.map +1 -0
  77. package/dist/extensions/login-route.js +180 -0
  78. package/dist/extensions/login-route.js.map +1 -0
  79. package/dist/extensions/muted-theme.js +12 -0
  80. package/dist/extensions/muted-theme.js.map +1 -0
  81. package/dist/extensions/onboarding.js +198 -0
  82. package/dist/extensions/onboarding.js.map +1 -0
  83. package/dist/extensions/setup-replay.js +269 -0
  84. package/dist/extensions/setup-replay.js.map +1 -0
  85. package/dist/extensions/setup-scan.js +998 -0
  86. package/dist/extensions/setup-scan.js.map +1 -0
  87. package/dist/extensions/setup-triage.js +175 -0
  88. package/dist/extensions/setup-triage.js.map +1 -0
  89. package/dist/extensions/setup.js +1160 -0
  90. package/dist/extensions/setup.js.map +1 -0
  91. package/dist/extensions/tui-view.js +389 -0
  92. package/dist/extensions/tui-view.js.map +1 -0
  93. package/dist/extensions/vendor/openviking-pi/client.js +376 -0
  94. package/dist/extensions/vendor/openviking-pi/client.js.map +1 -0
  95. package/dist/extensions/vendor/openviking-pi/config.js +189 -0
  96. package/dist/extensions/vendor/openviking-pi/config.js.map +1 -0
  97. package/dist/extensions/vendor/openviking-pi/index.js +421 -0
  98. package/dist/extensions/vendor/openviking-pi/index.js.map +1 -0
  99. package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +86 -0
  100. package/dist/extensions/vendor/openviking-pi/lib/capture-adapter.mjs.map +1 -0
  101. package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs +408 -0
  102. package/dist/extensions/vendor/openviking-pi/lib/takeover-core.mjs.map +1 -0
  103. package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +41 -0
  104. package/dist/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs.map +1 -0
  105. package/dist/extensions/vendor/openviking-pi/qcr-hint.js +290 -0
  106. package/dist/extensions/vendor/openviking-pi/qcr-hint.js.map +1 -0
  107. package/dist/extensions/vendor/openviking-pi/qcr-honest.js +148 -0
  108. package/dist/extensions/vendor/openviking-pi/qcr-honest.js.map +1 -0
  109. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +68 -0
  110. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -0
  111. package/dist/extensions/vendor/openviking-pi/qcr-leaf.js +77 -0
  112. package/dist/extensions/vendor/openviking-pi/qcr-leaf.js.map +1 -0
  113. package/dist/extensions/vendor/openviking-pi/qcr-lesson.js +216 -0
  114. package/dist/extensions/vendor/openviking-pi/qcr-lesson.js.map +1 -0
  115. package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js +154 -0
  116. package/dist/extensions/vendor/openviking-pi/qcr-namespaces.js.map +1 -0
  117. package/dist/extensions/vendor/openviking-pi/qcr-recall.js +254 -0
  118. package/dist/extensions/vendor/openviking-pi/qcr-recall.js.map +1 -0
  119. package/dist/extensions/vendor/openviking-pi/qcr-shim.js +94 -0
  120. package/dist/extensions/vendor/openviking-pi/qcr-shim.js.map +1 -0
  121. package/dist/extensions/vendor/openviking-pi/qcr-write.js +333 -0
  122. package/dist/extensions/vendor/openviking-pi/qcr-write.js.map +1 -0
  123. package/dist/extensions/vendor/openviking-pi/recall.js +108 -0
  124. package/dist/extensions/vendor/openviking-pi/recall.js.map +1 -0
  125. package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs +529 -0
  126. package/dist/extensions/vendor/openviking-pi/shared/capture-utils.mjs.map +1 -0
  127. package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs +205 -0
  128. package/dist/extensions/vendor/openviking-pi/shared/credentials.mjs.map +1 -0
  129. package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs +442 -0
  130. package/dist/extensions/vendor/openviking-pi/shared/pending-queue.mjs.map +1 -0
  131. package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs +286 -0
  132. package/dist/extensions/vendor/openviking-pi/shared/profile-inject.mjs.map +1 -0
  133. package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +189 -0
  134. package/dist/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs.map +1 -0
  135. package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs +580 -0
  136. package/dist/extensions/vendor/openviking-pi/shared/recall-core.mjs.map +1 -0
  137. package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs +11 -0
  138. package/dist/extensions/vendor/openviking-pi/shared/retryable.mjs.map +1 -0
  139. package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs +63 -0
  140. package/dist/extensions/vendor/openviking-pi/shared/session-model.mjs.map +1 -0
  141. package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs +58 -0
  142. package/dist/extensions/vendor/openviking-pi/shared/uri-guard.mjs.map +1 -0
  143. package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
  144. package/dist/extensions/vendor/openviking-pi/shared/workspace-peer.mjs.map +1 -0
  145. package/dist/extensions/vendor/openviking-pi/sync.js +127 -0
  146. package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -0
  147. package/dist/extensions/vendor/openviking-pi/takeover.js +25 -0
  148. package/dist/extensions/vendor/openviking-pi/takeover.js.map +1 -0
  149. package/dist/extensions/vendor/openviking-pi/tools.js +417 -0
  150. package/dist/extensions/vendor/openviking-pi/tools.js.map +1 -0
  151. package/dist/src/anomalies.js +483 -0
  152. package/dist/src/anomalies.js.map +1 -0
  153. package/dist/src/answerworker.js +180 -0
  154. package/dist/src/answerworker.js.map +1 -0
  155. package/dist/src/appstamp.js +68 -0
  156. package/dist/src/appstamp.js.map +1 -0
  157. package/dist/src/archive/admission.js +3 -0
  158. package/dist/src/archive/admission.js.map +1 -0
  159. package/dist/src/archive/archivedb.js +474 -0
  160. package/dist/src/archive/archivedb.js.map +1 -0
  161. package/dist/src/archive/archiver.js +1770 -0
  162. package/dist/src/archive/archiver.js.map +1 -0
  163. package/dist/src/archive/archivewrite.js +198 -0
  164. package/dist/src/archive/archivewrite.js.map +1 -0
  165. package/dist/src/archive/bundle.js +266 -0
  166. package/dist/src/archive/bundle.js.map +1 -0
  167. package/dist/src/archive/dbmove.js +638 -0
  168. package/dist/src/archive/dbmove.js.map +1 -0
  169. package/dist/src/archive/duplicatecounts.js +80 -0
  170. package/dist/src/archive/duplicatecounts.js.map +1 -0
  171. package/dist/src/archive/eligibility.js +113 -0
  172. package/dist/src/archive/eligibility.js.map +1 -0
  173. package/dist/src/archive/extract.js +288 -0
  174. package/dist/src/archive/extract.js.map +1 -0
  175. package/dist/src/archive/fsutil.js +330 -0
  176. package/dist/src/archive/fsutil.js.map +1 -0
  177. package/dist/src/archive/gzfile.js +277 -0
  178. package/dist/src/archive/gzfile.js.map +1 -0
  179. package/dist/src/archive/kinds.js +22 -0
  180. package/dist/src/archive/kinds.js.map +1 -0
  181. package/dist/src/archive/livecheckpoint.js +178 -0
  182. package/dist/src/archive/livecheckpoint.js.map +1 -0
  183. package/dist/src/archive/liveretention.js +145 -0
  184. package/dist/src/archive/liveretention.js.map +1 -0
  185. package/dist/src/archive/manifest.js +284 -0
  186. package/dist/src/archive/manifest.js.map +1 -0
  187. package/dist/src/archive/overseer.js +283 -0
  188. package/dist/src/archive/overseer.js.map +1 -0
  189. package/dist/src/archive/piglobal.js +129 -0
  190. package/dist/src/archive/piglobal.js.map +1 -0
  191. package/dist/src/archive/plan.js +398 -0
  192. package/dist/src/archive/plan.js.map +1 -0
  193. package/dist/src/archive/prarchive.js +831 -0
  194. package/dist/src/archive/prarchive.js.map +1 -0
  195. package/dist/src/archive/prfiles.js +303 -0
  196. package/dist/src/archive/prfiles.js.map +1 -0
  197. package/dist/src/archive/report.js +169 -0
  198. package/dist/src/archive/report.js.map +1 -0
  199. package/dist/src/archive/restore.js +276 -0
  200. package/dist/src/archive/restore.js.map +1 -0
  201. package/dist/src/archive/rows.js +106 -0
  202. package/dist/src/archive/rows.js.map +1 -0
  203. package/dist/src/archive/seal.js +480 -0
  204. package/dist/src/archive/seal.js.map +1 -0
  205. package/dist/src/archive/status.js +528 -0
  206. package/dist/src/archive/status.js.map +1 -0
  207. package/dist/src/archive/storepass.js +409 -0
  208. package/dist/src/archive/storepass.js.map +1 -0
  209. package/dist/src/backfill.js +217 -0
  210. package/dist/src/backfill.js.map +1 -0
  211. package/dist/src/bench/cost.js +87 -0
  212. package/dist/src/bench/exec.js +76 -0
  213. package/dist/src/bench/values.js +14 -0
  214. package/dist/src/botcontrol.js +127 -0
  215. package/dist/src/botcontrol.js.map +1 -0
  216. package/dist/src/botworker.js +805 -0
  217. package/dist/src/botworker.js.map +1 -0
  218. package/dist/src/capture.js +234 -0
  219. package/dist/src/capture.js.map +1 -0
  220. package/dist/src/cases.js +1491 -0
  221. package/dist/src/cases.js.map +1 -0
  222. package/dist/src/channelfs.js +27 -0
  223. package/dist/src/channelfs.js.map +1 -0
  224. package/dist/src/checklist.js +312 -0
  225. package/dist/src/checklist.js.map +1 -0
  226. package/dist/src/ciretry.js +92 -0
  227. package/dist/src/ciretry.js.map +1 -0
  228. package/dist/src/cli.js +1947 -0
  229. package/dist/src/cli.js.map +1 -0
  230. package/dist/src/cliops.js +334 -0
  231. package/dist/src/cliops.js.map +1 -0
  232. package/dist/src/clonedir.js +15 -0
  233. package/dist/src/clonedir.js.map +1 -0
  234. package/dist/src/commandeffects.js +43 -0
  235. package/dist/src/commandeffects.js.map +1 -0
  236. package/dist/src/commands.js +1335 -0
  237. package/dist/src/commands.js.map +1 -0
  238. package/dist/src/companioninbox.js +203 -0
  239. package/dist/src/companioninbox.js.map +1 -0
  240. package/dist/src/compare.js +673 -0
  241. package/dist/src/compare.js.map +1 -0
  242. package/dist/src/config.js +1041 -0
  243. package/dist/src/config.js.map +1 -0
  244. package/dist/src/configcheck.js +111 -0
  245. package/dist/src/configcheck.js.map +1 -0
  246. package/dist/src/coreext.js +43 -0
  247. package/dist/src/coreext.js.map +1 -0
  248. package/dist/src/daemon.js +945 -0
  249. package/dist/src/daemon.js.map +1 -0
  250. package/dist/src/debuground.js +731 -0
  251. package/dist/src/debuground.js.map +1 -0
  252. package/dist/src/debugroundpi.js +223 -0
  253. package/dist/src/debugroundpi.js.map +1 -0
  254. package/dist/src/diagnostics.js +14 -0
  255. package/dist/src/diagnostics.js.map +1 -0
  256. package/dist/src/digest.js +139 -0
  257. package/dist/src/digest.js.map +1 -0
  258. package/dist/src/dispatch.js +1727 -0
  259. package/dist/src/dispatch.js.map +1 -0
  260. package/dist/src/engine/answerguard.js +18 -0
  261. package/dist/src/engine/answerguard.js.map +1 -0
  262. package/dist/src/engine/botsdk.js +262 -0
  263. package/dist/src/engine/botsdk.js.map +1 -0
  264. package/dist/src/engine/caserunner-sdk.js +332 -0
  265. package/dist/src/engine/caserunner-sdk.js.map +1 -0
  266. package/dist/src/engine/debugpi.js +206 -0
  267. package/dist/src/engine/debugpi.js.map +1 -0
  268. package/dist/src/eod.js +473 -0
  269. package/dist/src/eod.js.map +1 -0
  270. package/dist/src/eodpinwatch.js +391 -0
  271. package/dist/src/eodpinwatch.js.map +1 -0
  272. package/dist/src/escalation.js +1009 -0
  273. package/dist/src/escalation.js.map +1 -0
  274. package/dist/src/eventloop.js +779 -0
  275. package/dist/src/eventloop.js.map +1 -0
  276. package/dist/src/execboundary.js +437 -0
  277. package/dist/src/execboundary.js.map +1 -0
  278. package/dist/src/feedback.js +915 -0
  279. package/dist/src/feedback.js.map +1 -0
  280. package/dist/src/findingscheck.js +200 -0
  281. package/dist/src/findingscheck.js.map +1 -0
  282. package/dist/src/fsdenied.js +26 -0
  283. package/dist/src/fsdenied.js.map +1 -0
  284. package/dist/src/ghread.js +181 -0
  285. package/dist/src/ghread.js.map +1 -0
  286. package/dist/src/github.js +1098 -0
  287. package/dist/src/github.js.map +1 -0
  288. package/dist/src/githubtoken.js +346 -0
  289. package/dist/src/githubtoken.js.map +1 -0
  290. package/dist/src/homes.js +103 -0
  291. package/dist/src/homes.js.map +1 -0
  292. package/dist/src/hostroute.js +24 -0
  293. package/dist/src/hostroute.js.map +1 -0
  294. package/dist/src/improvements.js +625 -0
  295. package/dist/src/improvements.js.map +1 -0
  296. package/dist/src/install.js +946 -0
  297. package/dist/src/install.js.map +1 -0
  298. package/dist/src/installkind.js +40 -0
  299. package/dist/src/installkind.js.map +1 -0
  300. package/dist/src/ipc/capability.js +286 -0
  301. package/dist/src/ipc/capability.js.map +1 -0
  302. package/dist/src/ipc/client.js +226 -0
  303. package/dist/src/ipc/client.js.map +1 -0
  304. package/dist/src/ipc/control.js +70 -0
  305. package/dist/src/ipc/control.js.map +1 -0
  306. package/dist/src/ipc/ghreadshim.js +77 -0
  307. package/dist/src/ipc/ghreadshim.js.map +1 -0
  308. package/dist/src/ipc/launcher.js +65 -0
  309. package/dist/src/ipc/launcher.js.map +1 -0
  310. package/dist/src/ipc/permits.js +293 -0
  311. package/dist/src/ipc/permits.js.map +1 -0
  312. package/dist/src/ipc/postshim.js +137 -0
  313. package/dist/src/ipc/postshim.js.map +1 -0
  314. package/dist/src/ipc/protocol.js +239 -0
  315. package/dist/src/ipc/protocol.js.map +1 -0
  316. package/dist/src/ipc/server.js +312 -0
  317. package/dist/src/ipc/server.js.map +1 -0
  318. package/dist/src/ipc/workerspec.js +148 -0
  319. package/dist/src/ipc/workerspec.js.map +1 -0
  320. package/dist/src/learner/learner.js +1272 -0
  321. package/dist/src/learner/learner.js.map +1 -0
  322. package/dist/src/learner/miner.js +948 -0
  323. package/dist/src/learner/miner.js.map +1 -0
  324. package/dist/src/learner/packet.js +216 -0
  325. package/dist/src/learner/packet.js.map +1 -0
  326. package/dist/src/learner/receipt.js +135 -0
  327. package/dist/src/learner/receipt.js.map +1 -0
  328. package/dist/src/learner/status.js +38 -0
  329. package/dist/src/learner/status.js.map +1 -0
  330. package/dist/src/ledgercheck.js +120 -0
  331. package/dist/src/ledgercheck.js.map +1 -0
  332. package/dist/src/lessonledger.js +451 -0
  333. package/dist/src/lessonledger.js.map +1 -0
  334. package/dist/src/lessonselect.js +639 -0
  335. package/dist/src/lessonselect.js.map +1 -0
  336. package/dist/src/lessonshadow.js +540 -0
  337. package/dist/src/lessonshadow.js.map +1 -0
  338. package/dist/src/levers.js +302 -0
  339. package/dist/src/levers.js.map +1 -0
  340. package/dist/src/live/projection.js +450 -0
  341. package/dist/src/live/projection.js.map +1 -0
  342. package/dist/src/live/publish.js +83 -0
  343. package/dist/src/live/publish.js.map +1 -0
  344. package/dist/src/live/read.js +60 -0
  345. package/dist/src/live/read.js.map +1 -0
  346. package/dist/src/livecli.js +288 -0
  347. package/dist/src/livecli.js.map +1 -0
  348. package/dist/src/loginhint.js +47 -0
  349. package/dist/src/loginhint.js.map +1 -0
  350. package/dist/src/loginrefresh/atomic.js +66 -0
  351. package/dist/src/loginrefresh/atomic.js.map +1 -0
  352. package/dist/src/loginrefresh/lock.js +93 -0
  353. package/dist/src/loginrefresh/lock.js.map +1 -0
  354. package/dist/src/loginrefresh/main.js +58 -0
  355. package/dist/src/loginrefresh/main.js.map +1 -0
  356. package/dist/src/loginrefresh/pirefresh.js +52 -0
  357. package/dist/src/loginrefresh/pirefresh.js.map +1 -0
  358. package/dist/src/loginrefresh/refresher.js +286 -0
  359. package/dist/src/loginrefresh/refresher.js.map +1 -0
  360. package/dist/src/loginrefresh/supervisor.js +253 -0
  361. package/dist/src/loginrefresh/supervisor.js.map +1 -0
  362. package/dist/src/loginrefresh/types.js +4 -0
  363. package/dist/src/loginrefresh/types.js.map +1 -0
  364. package/dist/src/memory/context.js +251 -0
  365. package/dist/src/memory/context.js.map +1 -0
  366. package/dist/src/memory/datafence.js +5 -0
  367. package/dist/src/memory/datafence.js.map +1 -0
  368. package/dist/src/memory/delivery.js +131 -0
  369. package/dist/src/memory/delivery.js.map +1 -0
  370. package/dist/src/memory/eventtags.js +81 -0
  371. package/dist/src/memory/eventtags.js.map +1 -0
  372. package/dist/src/memory/health.js +713 -0
  373. package/dist/src/memory/health.js.map +1 -0
  374. package/dist/src/memory/import.js +504 -0
  375. package/dist/src/memory/import.js.map +1 -0
  376. package/dist/src/memory/layout.js +252 -0
  377. package/dist/src/memory/layout.js.map +1 -0
  378. package/dist/src/memory/legacy-file.js +100 -0
  379. package/dist/src/memory/legacy-file.js.map +1 -0
  380. package/dist/src/memory/lessons.js +370 -0
  381. package/dist/src/memory/lessons.js.map +1 -0
  382. package/dist/src/memory/ovsession.js +449 -0
  383. package/dist/src/memory/ovsession.js.map +1 -0
  384. package/dist/src/memory/relayout.js +259 -0
  385. package/dist/src/memory/relayout.js.map +1 -0
  386. package/dist/src/memory/roundactivity.js +180 -0
  387. package/dist/src/memory/roundactivity.js.map +1 -0
  388. package/dist/src/memory/selection.js +690 -0
  389. package/dist/src/memory/selection.js.map +1 -0
  390. package/dist/src/memory/shadowstatus.js +69 -0
  391. package/dist/src/memory/shadowstatus.js.map +1 -0
  392. package/dist/src/memory/store.js +83 -0
  393. package/dist/src/memory/store.js.map +1 -0
  394. package/dist/src/memory/textidentity.js +8 -0
  395. package/dist/src/memory/textidentity.js.map +1 -0
  396. package/dist/src/metrics.js +773 -0
  397. package/dist/src/metrics.js.map +1 -0
  398. package/dist/src/missingconfig.js +8 -0
  399. package/dist/src/missingconfig.js.map +1 -0
  400. package/dist/src/npmverbs.js +228 -0
  401. package/dist/src/npmverbs.js.map +1 -0
  402. package/dist/src/onboard/choices.js +219 -0
  403. package/dist/src/onboard/choices.js.map +1 -0
  404. package/dist/src/onboard/engine.js +329 -0
  405. package/dist/src/onboard/engine.js.map +1 -0
  406. package/dist/src/onboard/launch.js +75 -0
  407. package/dist/src/onboard/launch.js.map +1 -0
  408. package/dist/src/onboard/pipin.js +22 -0
  409. package/dist/src/onboard/pipin.js.map +1 -0
  410. package/dist/src/onboard/stepforward.js +41 -0
  411. package/dist/src/onboard/stepforward.js.map +1 -0
  412. package/dist/src/onboard/steps/appslug.js +214 -0
  413. package/dist/src/onboard/steps/appslug.js.map +1 -0
  414. package/dist/src/onboard/steps/bringup.js +408 -0
  415. package/dist/src/onboard/steps/bringup.js.map +1 -0
  416. package/dist/src/onboard/steps/compose.js +372 -0
  417. package/dist/src/onboard/steps/compose.js.map +1 -0
  418. package/dist/src/onboard/steps/configdelivery.js +50 -0
  419. package/dist/src/onboard/steps/configdelivery.js.map +1 -0
  420. package/dist/src/onboard/steps/configfill.js +1218 -0
  421. package/dist/src/onboard/steps/configfill.js.map +1 -0
  422. package/dist/src/onboard/steps/deploysh.js +80 -0
  423. package/dist/src/onboard/steps/deploysh.js.map +1 -0
  424. package/dist/src/onboard/steps/embeddings.js +472 -0
  425. package/dist/src/onboard/steps/embeddings.js.map +1 -0
  426. package/dist/src/onboard/steps/escalation.js +571 -0
  427. package/dist/src/onboard/steps/escalation.js.map +1 -0
  428. package/dist/src/onboard/steps/githubaccount.js +595 -0
  429. package/dist/src/onboard/steps/githubaccount.js.map +1 -0
  430. package/dist/src/onboard/steps/githubapp.js +1150 -0
  431. package/dist/src/onboard/steps/githubapp.js.map +1 -0
  432. package/dist/src/onboard/steps/golive.js +508 -0
  433. package/dist/src/onboard/steps/golive.js.map +1 -0
  434. package/dist/src/onboard/steps/index.js +78 -0
  435. package/dist/src/onboard/steps/index.js.map +1 -0
  436. package/dist/src/onboard/steps/ingress.js +817 -0
  437. package/dist/src/onboard/steps/ingress.js.map +1 -0
  438. package/dist/src/onboard/steps/inventory.js +420 -0
  439. package/dist/src/onboard/steps/inventory.js.map +1 -0
  440. package/dist/src/onboard/steps/llmauth.js +966 -0
  441. package/dist/src/onboard/steps/llmauth.js.map +1 -0
  442. package/dist/src/onboard/steps/memory.js +1112 -0
  443. package/dist/src/onboard/steps/memory.js.map +1 -0
  444. package/dist/src/onboard/steps/memorykeys.js +43 -0
  445. package/dist/src/onboard/steps/memorykeys.js.map +1 -0
  446. package/dist/src/onboard/steps/preflight.js +172 -0
  447. package/dist/src/onboard/steps/preflight.js.map +1 -0
  448. package/dist/src/onboard/steps/proof.js +812 -0
  449. package/dist/src/onboard/steps/proof.js.map +1 -0
  450. package/dist/src/onboard/steps/ssh.js +300 -0
  451. package/dist/src/onboard/steps/ssh.js.map +1 -0
  452. package/dist/src/onboard/steps/topology.js +291 -0
  453. package/dist/src/onboard/steps/topology.js.map +1 -0
  454. package/dist/src/onboard/steps/transport.js +519 -0
  455. package/dist/src/onboard/steps/transport.js.map +1 -0
  456. package/dist/src/onboard/steps/tuiroute.js +102 -0
  457. package/dist/src/onboard/steps/tuiroute.js.map +1 -0
  458. package/dist/src/onboard/steps/verifyup.js +378 -0
  459. package/dist/src/onboard/steps/verifyup.js.map +1 -0
  460. package/dist/src/onboard/steps/vmdeploy.js +368 -0
  461. package/dist/src/onboard/steps/vmdeploy.js.map +1 -0
  462. package/dist/src/onboard/toml.js +53 -0
  463. package/dist/src/onboard/toml.js.map +1 -0
  464. package/dist/src/onboard/toolnames.js +30 -0
  465. package/dist/src/onboard/toolnames.js.map +1 -0
  466. package/dist/src/opencli.js +508 -0
  467. package/dist/src/opencli.js.map +1 -0
  468. package/dist/src/openpr.js +457 -0
  469. package/dist/src/openpr.js.map +1 -0
  470. package/dist/src/openviking/client.js +821 -0
  471. package/dist/src/openviking/client.js.map +1 -0
  472. package/dist/src/openviking/context.js +218 -0
  473. package/dist/src/openviking/context.js.map +1 -0
  474. package/dist/src/openviking/embedder.js +457 -0
  475. package/dist/src/openviking/embedder.js.map +1 -0
  476. package/dist/src/openviking/gapsink.js +38 -0
  477. package/dist/src/openviking/gapsink.js.map +1 -0
  478. package/dist/src/openviking/health.js +69 -0
  479. package/dist/src/openviking/health.js.map +1 -0
  480. package/dist/src/openviking/index.js +24 -0
  481. package/dist/src/openviking/index.js.map +1 -0
  482. package/dist/src/openviking/lessonfind.js +54 -0
  483. package/dist/src/openviking/lessonfind.js.map +1 -0
  484. package/dist/src/openviking/probe.js +89 -0
  485. package/dist/src/openviking/probe.js.map +1 -0
  486. package/dist/src/openviking/proc.js +95 -0
  487. package/dist/src/openviking/proc.js.map +1 -0
  488. package/dist/src/openviking/provisionreport.js +144 -0
  489. package/dist/src/openviking/provisionreport.js.map +1 -0
  490. package/dist/src/openviking/reembed.js +317 -0
  491. package/dist/src/openviking/reembed.js.map +1 -0
  492. package/dist/src/openviking/relayout.js +153 -0
  493. package/dist/src/openviking/relayout.js.map +1 -0
  494. package/dist/src/openviking/sidecar-main.js +39 -0
  495. package/dist/src/openviking/sidecar-main.js.map +1 -0
  496. package/dist/src/openviking/sidecar.js +79 -0
  497. package/dist/src/openviking/sidecar.js.map +1 -0
  498. package/dist/src/openviking/store.js +1758 -0
  499. package/dist/src/openviking/store.js.map +1 -0
  500. package/dist/src/openviking/supervisor.js +422 -0
  501. package/dist/src/openviking/supervisor.js.map +1 -0
  502. package/dist/src/openviking/types.js +5 -0
  503. package/dist/src/openviking/types.js.map +1 -0
  504. package/dist/src/operator-recovery.js +172 -0
  505. package/dist/src/operator-recovery.js.map +1 -0
  506. package/dist/src/operatormessage.js +44 -0
  507. package/dist/src/operatormessage.js.map +1 -0
  508. package/dist/src/operatorwrite.js +23 -0
  509. package/dist/src/operatorwrite.js.map +1 -0
  510. package/dist/src/outcomes.js +1364 -0
  511. package/dist/src/outcomes.js.map +1 -0
  512. package/dist/src/piauthcopy.js +5 -0
  513. package/dist/src/piauthcopy.js.map +1 -0
  514. package/dist/src/picli.js +45 -0
  515. package/dist/src/picli.js.map +1 -0
  516. package/dist/src/picwd.js +25 -0
  517. package/dist/src/picwd.js.map +1 -0
  518. package/dist/src/pilogin.js +35 -0
  519. package/dist/src/pilogin.js.map +1 -0
  520. package/dist/src/pingpong.js +1028 -0
  521. package/dist/src/pingpong.js.map +1 -0
  522. package/dist/src/posting-journal.js +442 -0
  523. package/dist/src/posting-journal.js.map +1 -0
  524. package/dist/src/posting-outbox.js +666 -0
  525. package/dist/src/posting-outbox.js.map +1 -0
  526. package/dist/src/posting-receipt.js +216 -0
  527. package/dist/src/posting-receipt.js.map +1 -0
  528. package/dist/src/posting.js +348 -0
  529. package/dist/src/posting.js.map +1 -0
  530. package/dist/src/postops.js +637 -0
  531. package/dist/src/postops.js.map +1 -0
  532. package/dist/src/pr-activity.js +11 -0
  533. package/dist/src/pr-activity.js.map +1 -0
  534. package/dist/src/pr-author.js +5 -0
  535. package/dist/src/pr-author.js.map +1 -0
  536. package/dist/src/pr-conversation.js +355 -0
  537. package/dist/src/pr-conversation.js.map +1 -0
  538. package/dist/src/pr-session-lifecycle.js +542 -0
  539. package/dist/src/pr-session-lifecycle.js.map +1 -0
  540. package/dist/src/preflight.js +341 -0
  541. package/dist/src/preflight.js.map +1 -0
  542. package/dist/src/priorresolve.js +275 -0
  543. package/dist/src/priorresolve.js.map +1 -0
  544. package/dist/src/qcrbin.js +180 -0
  545. package/dist/src/qcrbin.js.map +1 -0
  546. package/dist/src/recheck.js +1399 -0
  547. package/dist/src/recheck.js.map +1 -0
  548. package/dist/src/reconcile.js +840 -0
  549. package/dist/src/reconcile.js.map +1 -0
  550. package/dist/src/recordedfindings.js +15 -0
  551. package/dist/src/recordedfindings.js.map +1 -0
  552. package/dist/src/regression.js +546 -0
  553. package/dist/src/regression.js.map +1 -0
  554. package/dist/src/replay.js +598 -0
  555. package/dist/src/replay.js.map +1 -0
  556. package/dist/src/requiredpasses.js +113 -0
  557. package/dist/src/requiredpasses.js.map +1 -0
  558. package/dist/src/resumecheck.js +168 -0
  559. package/dist/src/resumecheck.js.map +1 -0
  560. package/dist/src/reviewcli.js +671 -0
  561. package/dist/src/reviewcli.js.map +1 -0
  562. package/dist/src/reviewcontext.js +22 -0
  563. package/dist/src/reviewcontext.js.map +1 -0
  564. package/dist/src/reviewdetach.js +574 -0
  565. package/dist/src/reviewdetach.js.map +1 -0
  566. package/dist/src/reviewhistory.js +310 -0
  567. package/dist/src/reviewhistory.js.map +1 -0
  568. package/dist/src/reviewhome.js +26 -0
  569. package/dist/src/reviewhome.js.map +1 -0
  570. package/dist/src/reviewmemory.js +110 -0
  571. package/dist/src/reviewmemory.js.map +1 -0
  572. package/dist/src/reviewnow.js +45 -0
  573. package/dist/src/reviewnow.js.map +1 -0
  574. package/dist/src/reviewrendercli.js +104 -0
  575. package/dist/src/reviewrendercli.js.map +1 -0
  576. package/dist/src/round-work.js +20 -0
  577. package/dist/src/round-work.js.map +1 -0
  578. package/dist/src/runhost.js +72 -0
  579. package/dist/src/runhost.js.map +1 -0
  580. package/dist/src/scanauth.js +25 -0
  581. package/dist/src/scanauth.js.map +1 -0
  582. package/dist/src/scanbox.js +846 -0
  583. package/dist/src/scanbox.js.map +1 -0
  584. package/dist/src/scancli.js +1074 -0
  585. package/dist/src/scancli.js.map +1 -0
  586. package/dist/src/scandriverlog.js +30 -0
  587. package/dist/src/scandriverlog.js.map +1 -0
  588. package/dist/src/session-bridge.js +75 -0
  589. package/dist/src/session-bridge.js.map +1 -0
  590. package/dist/src/session-command-payload.js +29 -0
  591. package/dist/src/session-command-payload.js.map +1 -0
  592. package/dist/src/session-command-supervisor.js +939 -0
  593. package/dist/src/session-command-supervisor.js.map +1 -0
  594. package/dist/src/sessionjsonl.js +85 -0
  595. package/dist/src/sessionjsonl.js.map +1 -0
  596. package/dist/src/sessions.js +541 -0
  597. package/dist/src/sessions.js.map +1 -0
  598. package/dist/src/setup/adapters.js +330 -0
  599. package/dist/src/setup/adapters.js.map +1 -0
  600. package/dist/src/setup/baselines.js +64 -0
  601. package/dist/src/setup/baselines.js.map +1 -0
  602. package/dist/src/setup/caserunner.js +554 -0
  603. package/dist/src/setup/caserunner.js.map +1 -0
  604. package/dist/src/setup/configcommit.js +358 -0
  605. package/dist/src/setup/configcommit.js.map +1 -0
  606. package/dist/src/setup/engine.js +672 -0
  607. package/dist/src/setup/engine.js.map +1 -0
  608. package/dist/src/setup/facts.js +593 -0
  609. package/dist/src/setup/facts.js.map +1 -0
  610. package/dist/src/setup/findings.js +948 -0
  611. package/dist/src/setup/findings.js.map +1 -0
  612. package/dist/src/setup/history.js +338 -0
  613. package/dist/src/setup/history.js.map +1 -0
  614. package/dist/src/setup/interview.js +185 -0
  615. package/dist/src/setup/interview.js.map +1 -0
  616. package/dist/src/setup/ledger.js +207 -0
  617. package/dist/src/setup/ledger.js.map +1 -0
  618. package/dist/src/setup/pins.js +43 -0
  619. package/dist/src/setup/pins.js.map +1 -0
  620. package/dist/src/setup/portrait.js +432 -0
  621. package/dist/src/setup/portrait.js.map +1 -0
  622. package/dist/src/setup/presentation.js +9 -0
  623. package/dist/src/setup/presentation.js.map +1 -0
  624. package/dist/src/setup/profile.js +417 -0
  625. package/dist/src/setup/profile.js.map +1 -0
  626. package/dist/src/setup/replay.js +240 -0
  627. package/dist/src/setup/replay.js.map +1 -0
  628. package/dist/src/setup/replaybuild.js +311 -0
  629. package/dist/src/setup/replaybuild.js.map +1 -0
  630. package/dist/src/setup/replayrun.js +806 -0
  631. package/dist/src/setup/replayrun.js.map +1 -0
  632. package/dist/src/setup/replaysource.js +169 -0
  633. package/dist/src/setup/replaysource.js.map +1 -0
  634. package/dist/src/setup/retune.js +333 -0
  635. package/dist/src/setup/retune.js.map +1 -0
  636. package/dist/src/setup/retunelog.js +51 -0
  637. package/dist/src/setup/retunelog.js.map +1 -0
  638. package/dist/src/setup/scan.js +725 -0
  639. package/dist/src/setup/scan.js.map +1 -0
  640. package/dist/src/setup/scanbuild.js +133 -0
  641. package/dist/src/setup/scanbuild.js.map +1 -0
  642. package/dist/src/setup/scanfacts.js +96 -0
  643. package/dist/src/setup/scanfacts.js.map +1 -0
  644. package/dist/src/setup/scanlease.js +251 -0
  645. package/dist/src/setup/scanlease.js.map +1 -0
  646. package/dist/src/setup/scanreport.js +289 -0
  647. package/dist/src/setup/scanreport.js.map +1 -0
  648. package/dist/src/setup/scanrun.js +1733 -0
  649. package/dist/src/setup/scanrun.js.map +1 -0
  650. package/dist/src/setup/scantree.js +257 -0
  651. package/dist/src/setup/scantree.js.map +1 -0
  652. package/dist/src/setup/stage2.js +617 -0
  653. package/dist/src/setup/stage2.js.map +1 -0
  654. package/dist/src/setup/stage2request.js +259 -0
  655. package/dist/src/setup/stage2request.js.map +1 -0
  656. package/dist/src/setup/survey.js +307 -0
  657. package/dist/src/setup/survey.js.map +1 -0
  658. package/dist/src/setup/surveyrun.js +549 -0
  659. package/dist/src/setup/surveyrun.js.map +1 -0
  660. package/dist/src/setup/textsniff.js +89 -0
  661. package/dist/src/setup/textsniff.js.map +1 -0
  662. package/dist/src/setup/trackersink.js +395 -0
  663. package/dist/src/setup/trackersink.js.map +1 -0
  664. package/dist/src/setup/triage.js +636 -0
  665. package/dist/src/setup/triage.js.map +1 -0
  666. package/dist/src/setup/tuning.js +585 -0
  667. package/dist/src/setup/tuning.js.map +1 -0
  668. package/dist/src/setup/unitcut.js +437 -0
  669. package/dist/src/setup/unitcut.js.map +1 -0
  670. package/dist/src/singleflight.js +72 -0
  671. package/dist/src/singleflight.js.map +1 -0
  672. package/dist/src/slack/announce.js +725 -0
  673. package/dist/src/slack/announce.js.map +1 -0
  674. package/dist/src/slack/api.js +227 -0
  675. package/dist/src/slack/api.js.map +1 -0
  676. package/dist/src/slack/messages.js +158 -0
  677. package/dist/src/slack/messages.js.map +1 -0
  678. package/dist/src/slack/origin.js +550 -0
  679. package/dist/src/slack/origin.js.map +1 -0
  680. package/dist/src/slack/policy.js +225 -0
  681. package/dist/src/slack/policy.js.map +1 -0
  682. package/dist/src/slack/reply.js +153 -0
  683. package/dist/src/slack/reply.js.map +1 -0
  684. package/dist/src/slack/router.js +189 -0
  685. package/dist/src/slack/router.js.map +1 -0
  686. package/dist/src/slack/socket.js +323 -0
  687. package/dist/src/slack/socket.js.map +1 -0
  688. package/dist/src/slack/types.js +11 -0
  689. package/dist/src/slack/types.js.map +1 -0
  690. package/dist/src/sqlitehint.js +30 -0
  691. package/dist/src/sqlitehint.js.map +1 -0
  692. package/dist/src/ssh-observation.js +25 -0
  693. package/dist/src/ssh-observation.js.map +1 -0
  694. package/dist/src/state.js +5869 -0
  695. package/dist/src/state.js.map +1 -0
  696. package/dist/src/statemachine.js +223 -0
  697. package/dist/src/statemachine.js.map +1 -0
  698. package/dist/src/stubs.js +52 -0
  699. package/dist/src/stubs.js.map +1 -0
  700. package/dist/src/takeover.js +506 -0
  701. package/dist/src/takeover.js.map +1 -0
  702. package/dist/src/takeoverpi.js +262 -0
  703. package/dist/src/takeoverpi.js.map +1 -0
  704. package/dist/src/terminal-failure.js +26 -0
  705. package/dist/src/terminal-failure.js.map +1 -0
  706. package/dist/src/thread-resolution-refusal.js +4 -0
  707. package/dist/src/thread-resolution-refusal.js.map +1 -0
  708. package/dist/src/tmux.js +126 -0
  709. package/dist/src/tmux.js.map +1 -0
  710. package/dist/src/trackerid.js +43 -0
  711. package/dist/src/trackerid.js.map +1 -0
  712. package/dist/src/transcriptindex.js +512 -0
  713. package/dist/src/transcriptindex.js.map +1 -0
  714. package/dist/src/tui/actions.js +404 -0
  715. package/dist/src/tui/actions.js.map +1 -0
  716. package/dist/src/tui/alerts.js +523 -0
  717. package/dist/src/tui/alerts.js.map +1 -0
  718. package/dist/src/tui/app.js +1884 -0
  719. package/dist/src/tui/app.js.map +1 -0
  720. package/dist/src/tui/archiveops.js +164 -0
  721. package/dist/src/tui/archiveops.js.map +1 -0
  722. package/dist/src/tui/autofile.js +191 -0
  723. package/dist/src/tui/autofile.js.map +1 -0
  724. package/dist/src/tui/board.js +1219 -0
  725. package/dist/src/tui/board.js.map +1 -0
  726. package/dist/src/tui/candidates.js +699 -0
  727. package/dist/src/tui/candidates.js.map +1 -0
  728. package/dist/src/tui/cellwidth.js +151 -0
  729. package/dist/src/tui/cellwidth.js.map +1 -0
  730. package/dist/src/tui/changelog-store.js +404 -0
  731. package/dist/src/tui/changelog-store.js.map +1 -0
  732. package/dist/src/tui/chord.js +26 -0
  733. package/dist/src/tui/chord.js.map +1 -0
  734. package/dist/src/tui/clipboard.js +81 -0
  735. package/dist/src/tui/clipboard.js.map +1 -0
  736. package/dist/src/tui/cmdbar.js +256 -0
  737. package/dist/src/tui/cmdbar.js.map +1 -0
  738. package/dist/src/tui/companion.js +1339 -0
  739. package/dist/src/tui/companion.js.map +1 -0
  740. package/dist/src/tui/companionplan.js +200 -0
  741. package/dist/src/tui/companionplan.js.map +1 -0
  742. package/dist/src/tui/companiontab.js +61 -0
  743. package/dist/src/tui/companiontab.js.map +1 -0
  744. package/dist/src/tui/confighome.js +105 -0
  745. package/dist/src/tui/confighome.js.map +1 -0
  746. package/dist/src/tui/connect.js +50 -0
  747. package/dist/src/tui/connect.js.map +1 -0
  748. package/dist/src/tui/deploy-ceremony.js +75 -0
  749. package/dist/src/tui/deploy-ceremony.js.map +1 -0
  750. package/dist/src/tui/deploy-drain.js +93 -0
  751. package/dist/src/tui/deploy-drain.js.map +1 -0
  752. package/dist/src/tui/deploy-process.js +234 -0
  753. package/dist/src/tui/deploy-process.js.map +1 -0
  754. package/dist/src/tui/deploytab.js +1263 -0
  755. package/dist/src/tui/deploytab.js.map +1 -0
  756. package/dist/src/tui/design.js +111 -0
  757. package/dist/src/tui/design.js.map +1 -0
  758. package/dist/src/tui/digesttab.js +148 -0
  759. package/dist/src/tui/digesttab.js.map +1 -0
  760. package/dist/src/tui/emptystate.js +32 -0
  761. package/dist/src/tui/emptystate.js.map +1 -0
  762. package/dist/src/tui/evidence.js +790 -0
  763. package/dist/src/tui/evidence.js.map +1 -0
  764. package/dist/src/tui/feed.js +415 -0
  765. package/dist/src/tui/feed.js.map +1 -0
  766. package/dist/src/tui/feedbacktab.js +907 -0
  767. package/dist/src/tui/feedbacktab.js.map +1 -0
  768. package/dist/src/tui/filters.js +75 -0
  769. package/dist/src/tui/filters.js.map +1 -0
  770. package/dist/src/tui/ghread.js +328 -0
  771. package/dist/src/tui/ghread.js.map +1 -0
  772. package/dist/src/tui/historypull.js +61 -0
  773. package/dist/src/tui/historypull.js.map +1 -0
  774. package/dist/src/tui/inboxpull.js +197 -0
  775. package/dist/src/tui/inboxpull.js.map +1 -0
  776. package/dist/src/tui/index.js +1511 -0
  777. package/dist/src/tui/index.js.map +1 -0
  778. package/dist/src/tui/issuesink.js +206 -0
  779. package/dist/src/tui/issuesink.js.map +1 -0
  780. package/dist/src/tui/jsonfile.js +32 -0
  781. package/dist/src/tui/jsonfile.js.map +1 -0
  782. package/dist/src/tui/keys.js +97 -0
  783. package/dist/src/tui/keys.js.map +1 -0
  784. package/dist/src/tui/layout.js +70 -0
  785. package/dist/src/tui/layout.js.map +1 -0
  786. package/dist/src/tui/learning.js +469 -0
  787. package/dist/src/tui/learning.js.map +1 -0
  788. package/dist/src/tui/lessonswitch.js +103 -0
  789. package/dist/src/tui/lessonswitch.js.map +1 -0
  790. package/dist/src/tui/livecontrol.js +167 -0
  791. package/dist/src/tui/livecontrol.js.map +1 -0
  792. package/dist/src/tui/memforward.js +347 -0
  793. package/dist/src/tui/memforward.js.map +1 -0
  794. package/dist/src/tui/memincident.js +287 -0
  795. package/dist/src/tui/memincident.js.map +1 -0
  796. package/dist/src/tui/memory.js +485 -0
  797. package/dist/src/tui/memory.js.map +1 -0
  798. package/dist/src/tui/memtab.js +513 -0
  799. package/dist/src/tui/memtab.js.map +1 -0
  800. package/dist/src/tui/metricstab.js +979 -0
  801. package/dist/src/tui/metricstab.js.map +1 -0
  802. package/dist/src/tui/model.js +129 -0
  803. package/dist/src/tui/model.js.map +1 -0
  804. package/dist/src/tui/mouse.js +83 -0
  805. package/dist/src/tui/mouse.js.map +1 -0
  806. package/dist/src/tui/msg.js +196 -0
  807. package/dist/src/tui/msg.js.map +1 -0
  808. package/dist/src/tui/nativecompanion.js +321 -0
  809. package/dist/src/tui/nativecompanion.js.map +1 -0
  810. package/dist/src/tui/openurl.js +17 -0
  811. package/dist/src/tui/openurl.js.map +1 -0
  812. package/dist/src/tui/paint.js +501 -0
  813. package/dist/src/tui/paint.js.map +1 -0
  814. package/dist/src/tui/panes.js +74 -0
  815. package/dist/src/tui/panes.js.map +1 -0
  816. package/dist/src/tui/pipane.js +166 -0
  817. package/dist/src/tui/pipane.js.map +1 -0
  818. package/dist/src/tui/poll-hooks.js +11 -0
  819. package/dist/src/tui/poll-hooks.js.map +1 -0
  820. package/dist/src/tui/poll.js +113 -0
  821. package/dist/src/tui/poll.js.map +1 -0
  822. package/dist/src/tui/proposals.js +715 -0
  823. package/dist/src/tui/proposals.js.map +1 -0
  824. package/dist/src/tui/prpause.js +139 -0
  825. package/dist/src/tui/prpause.js.map +1 -0
  826. package/dist/src/tui/reload.js +351 -0
  827. package/dist/src/tui/reload.js.map +1 -0
  828. package/dist/src/tui/remote.js +318 -0
  829. package/dist/src/tui/remote.js.map +1 -0
  830. package/dist/src/tui/rollover.js +767 -0
  831. package/dist/src/tui/rollover.js.map +1 -0
  832. package/dist/src/tui/route.js +143 -0
  833. package/dist/src/tui/route.js.map +1 -0
  834. package/dist/src/tui/scantab.js +1053 -0
  835. package/dist/src/tui/scantab.js.map +1 -0
  836. package/dist/src/tui/setupentry.js +81 -0
  837. package/dist/src/tui/setupentry.js.map +1 -0
  838. package/dist/src/tui/setupstate.js +29 -0
  839. package/dist/src/tui/setupstate.js.map +1 -0
  840. package/dist/src/tui/sidebar.js +602 -0
  841. package/dist/src/tui/sidebar.js.map +1 -0
  842. package/dist/src/tui/sshtriage.js +490 -0
  843. package/dist/src/tui/sshtriage.js.map +1 -0
  844. package/dist/src/tui/startup.js +172 -0
  845. package/dist/src/tui/startup.js.map +1 -0
  846. package/dist/src/tui/tabs.js +41 -0
  847. package/dist/src/tui/tabs.js.map +1 -0
  848. package/dist/src/tui/termmirror.js +217 -0
  849. package/dist/src/tui/termmirror.js.map +1 -0
  850. package/dist/src/tui/theme.js +96 -0
  851. package/dist/src/tui/theme.js.map +1 -0
  852. package/dist/src/tui/ticketstab.js +224 -0
  853. package/dist/src/tui/ticketstab.js.map +1 -0
  854. package/dist/src/tui/tmuxsocket.js +101 -0
  855. package/dist/src/tui/tmuxsocket.js.map +1 -0
  856. package/dist/src/tui/trackertickets.js +77 -0
  857. package/dist/src/tui/trackertickets.js.map +1 -0
  858. package/dist/src/tui/unwedge.js +235 -0
  859. package/dist/src/tui/unwedge.js.map +1 -0
  860. package/dist/src/tui/viewer.js +479 -0
  861. package/dist/src/tui/viewer.js.map +1 -0
  862. package/dist/src/tui/viewerinput.js +125 -0
  863. package/dist/src/tui/viewerinput.js.map +1 -0
  864. package/dist/src/updatenotice.js +143 -0
  865. package/dist/src/updatenotice.js.map +1 -0
  866. package/dist/src/usage.js +1007 -0
  867. package/dist/src/usage.js.map +1 -0
  868. package/dist/src/verdicts.js +3860 -0
  869. package/dist/src/verdicts.js.map +1 -0
  870. package/dist/src/version.js +58 -0
  871. package/dist/src/version.js.map +1 -0
  872. package/dist/src/wakes.js +411 -0
  873. package/dist/src/wakes.js.map +1 -0
  874. package/dist/src/webhook.js +736 -0
  875. package/dist/src/webhook.js.map +1 -0
  876. package/dist/src/webhookheal.js +238 -0
  877. package/dist/src/webhookheal.js.map +1 -0
  878. package/dist/src/wire.js +1823 -0
  879. package/dist/src/wire.js.map +1 -0
  880. package/dist/src/worker-session-host.js +531 -0
  881. package/dist/src/worker-session-host.js.map +1 -0
  882. package/dist/src/workerbreaker.js +112 -0
  883. package/dist/src/workerbreaker.js.map +1 -0
  884. package/dist/src/workercontrol.js +49 -0
  885. package/dist/src/workercontrol.js.map +1 -0
  886. package/dist/src/workerlease.js +351 -0
  887. package/dist/src/workerlease.js.map +1 -0
  888. package/dist/src/workerliveness.js +84 -0
  889. package/dist/src/workerliveness.js.map +1 -0
  890. package/dist/src/workerlog.js +65 -0
  891. package/dist/src/workerlog.js.map +1 -0
  892. package/dist/src/workerruntime.js +10 -0
  893. package/dist/src/workerruntime.js.map +1 -0
  894. package/dist/src/workersupervisor.js +1206 -0
  895. package/dist/src/workersupervisor.js.map +1 -0
  896. package/dist/src/workerusage.js +23 -0
  897. package/dist/src/workerusage.js.map +1 -0
  898. package/dist/src/workerworld.js +338 -0
  899. package/dist/src/workerworld.js.map +1 -0
  900. package/dist/src/worktrees.js +984 -0
  901. package/dist/src/worktrees.js.map +1 -0
  902. package/docker/Dockerfile +284 -0
  903. package/docker/Dockerfile.dockerignore +35 -0
  904. package/docker/README.md +221 -0
  905. package/docker/companion/Dockerfile +139 -0
  906. package/docker/companion/agents/evidence.md +34 -0
  907. package/docker/companion/agents/explore.md +26 -0
  908. package/docker/companion/agents/pass-survey.md +49 -0
  909. package/docker/companion/agents/study.md +26 -0
  910. package/docker/companion/entrypoint.sh +362 -0
  911. package/docker/companion/memory-health.mjs +269 -0
  912. package/docker/entrypoint.sh +799 -0
  913. package/docker/memory/Dockerfile +191 -0
  914. package/docker/memory/Dockerfile.dockerignore +20 -0
  915. package/docker/memory/README.md +60 -0
  916. package/docker/memory/entrypoint.sh +163 -0
  917. package/docker/setid/qcr-setid.c +109 -0
  918. package/docker/tunnel/Dockerfile.cloudflared +51 -0
  919. package/docker/tunnel/Dockerfile.ngrok +40 -0
  920. package/docker/tunnel/sidecar.mjs +411 -0
  921. package/docker/vm/tmux.conf +26 -0
  922. package/docker-compose.yml +348 -0
  923. package/extensions/cli/bash-guard-ambient.ts +71 -0
  924. package/extensions/cli/bash-guard.ts +12 -0
  925. package/extensions/cli/companion-guard.ts +8 -0
  926. package/extensions/cli/debug-round.ts +9 -0
  927. package/extensions/cli/inject-bridge.ts +8 -0
  928. package/extensions/cli/ledger-pin.ts +13 -0
  929. package/extensions/cli/lens.ts +22 -0
  930. package/extensions/cli/login-route.ts +10 -0
  931. package/extensions/cli/muted-theme.ts +9 -0
  932. package/extensions/cli/onboarding.ts +8 -0
  933. package/extensions/cli/setup.ts +8 -0
  934. package/extensions/cli/tui-view.ts +8 -0
  935. package/extensions/companion-guard.ts +411 -0
  936. package/extensions/debug-round.ts +281 -0
  937. package/extensions/inject-bridge.ts +630 -0
  938. package/extensions/inline-bash.ts +102 -0
  939. package/extensions/login-route.ts +220 -0
  940. package/extensions/muted-theme.json +68 -0
  941. package/extensions/muted-theme.ts +23 -0
  942. package/extensions/onboarding.ts +262 -0
  943. package/extensions/setup-replay.ts +336 -0
  944. package/extensions/setup-scan.ts +1236 -0
  945. package/extensions/setup-triage.ts +190 -0
  946. package/extensions/setup.ts +1428 -0
  947. package/extensions/tui-view.ts +489 -0
  948. package/extensions/vendor/openviking-pi/LICENSE +201 -0
  949. package/extensions/vendor/openviking-pi/NOTICE +68 -0
  950. package/extensions/vendor/openviking-pi/README.md +550 -0
  951. package/extensions/vendor/openviking-pi/client.ts +588 -0
  952. package/extensions/vendor/openviking-pi/config.json +28 -0
  953. package/extensions/vendor/openviking-pi/config.ts +271 -0
  954. package/extensions/vendor/openviking-pi/index.ts +523 -0
  955. package/extensions/vendor/openviking-pi/lib/capture-adapter.d.mts +10 -0
  956. package/extensions/vendor/openviking-pi/lib/capture-adapter.mjs +88 -0
  957. package/extensions/vendor/openviking-pi/lib/takeover-core.d.mts +80 -0
  958. package/extensions/vendor/openviking-pi/lib/takeover-core.mjs +406 -0
  959. package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.d.mts +1 -0
  960. package/extensions/vendor/openviking-pi/lib/uri-guard-adapter.mjs +42 -0
  961. package/extensions/vendor/openviking-pi/patches/0000-vendoring-and-build-conformance.patch +726 -0
  962. package/extensions/vendor/openviking-pi/patches/0001-remove-session-before-compact-handler.patch +101 -0
  963. package/extensions/vendor/openviking-pi/patches/0002-no-ambient-work-in-subagent-leaves.patch +202 -0
  964. package/extensions/vendor/openviking-pi/patches/0003-never-auto-create-openviking-sessions.patch +120 -0
  965. package/extensions/vendor/openviking-pi/patches/0004-viking-write-durable-batch-write.patch +521 -0
  966. package/extensions/vendor/openviking-pi/patches/0005-second-memory-namespace.patch +786 -0
  967. package/extensions/vendor/openviking-pi/patches/0006-writes-that-stick-and-honest-errors.patch +996 -0
  968. package/extensions/vendor/openviking-pi/patches/0007-bounded-recall-no-commit.patch +794 -0
  969. package/extensions/vendor/openviking-pi/patches/0008-pr-derived-recall.patch +379 -0
  970. package/extensions/vendor/openviking-pi/patches/0009-pr-session-tools-in-round-areas.patch +375 -0
  971. package/extensions/vendor/openviking-pi/patches/0010-typed-lessons.patch +448 -0
  972. package/extensions/vendor/openviking-pi/patches/0011-review-session-memory-boundary.patch +382 -0
  973. package/extensions/vendor/openviking-pi/patches/0012-typecheck-against-the-real-pi.patch +129 -0
  974. package/extensions/vendor/openviking-pi/patches/0013-createopenviking-factory.patch +1168 -0
  975. package/extensions/vendor/openviking-pi/qcr-hint.ts +313 -0
  976. package/extensions/vendor/openviking-pi/qcr-honest.ts +183 -0
  977. package/extensions/vendor/openviking-pi/qcr-hooks.ts +87 -0
  978. package/extensions/vendor/openviking-pi/qcr-leaf.ts +78 -0
  979. package/extensions/vendor/openviking-pi/qcr-lesson.ts +231 -0
  980. package/extensions/vendor/openviking-pi/qcr-namespaces.ts +198 -0
  981. package/extensions/vendor/openviking-pi/qcr-recall.ts +288 -0
  982. package/extensions/vendor/openviking-pi/qcr-shim.ts +142 -0
  983. package/extensions/vendor/openviking-pi/qcr-write.ts +427 -0
  984. package/extensions/vendor/openviking-pi/recall.ts +149 -0
  985. package/extensions/vendor/openviking-pi/shared/capture-utils.d.mts +9 -0
  986. package/extensions/vendor/openviking-pi/shared/capture-utils.mjs +515 -0
  987. package/extensions/vendor/openviking-pi/shared/credentials.d.mts +14 -0
  988. package/extensions/vendor/openviking-pi/shared/credentials.mjs +228 -0
  989. package/extensions/vendor/openviking-pi/shared/pending-queue.d.mts +19 -0
  990. package/extensions/vendor/openviking-pi/shared/pending-queue.mjs +458 -0
  991. package/extensions/vendor/openviking-pi/shared/profile-inject.d.mts +17 -0
  992. package/extensions/vendor/openviking-pi/shared/profile-inject.mjs +294 -0
  993. package/extensions/vendor/openviking-pi/shared/recall-compress-core.mjs +195 -0
  994. package/extensions/vendor/openviking-pi/shared/recall-core.d.mts +13 -0
  995. package/extensions/vendor/openviking-pi/shared/recall-core.mjs +609 -0
  996. package/extensions/vendor/openviking-pi/shared/retryable.mjs +9 -0
  997. package/extensions/vendor/openviking-pi/shared/session-model.d.mts +3 -0
  998. package/extensions/vendor/openviking-pi/shared/session-model.mjs +57 -0
  999. package/extensions/vendor/openviking-pi/shared/uri-guard.mjs +56 -0
  1000. package/extensions/vendor/openviking-pi/shared/workspace-peer.d.mts +5 -0
  1001. package/extensions/vendor/openviking-pi/shared/workspace-peer.mjs +16 -0
  1002. package/extensions/vendor/openviking-pi/sync.ts +163 -0
  1003. package/extensions/vendor/openviking-pi/takeover.ts +36 -0
  1004. package/extensions/vendor/openviking-pi/tools.ts +479 -0
  1005. package/extensions/vendor/openviking-pi/upstream.json +48 -0
  1006. package/package.json +138 -4
  1007. package/profiles/default/README.md +21 -0
  1008. package/profiles/default/addenda/.gitkeep +0 -0
  1009. package/profiles/default/profile.toml +17 -0
  1010. package/profiles/example-django/README.md +21 -0
  1011. package/profiles/example-django/addenda/10-migration-discipline.md +13 -0
  1012. package/profiles/example-django/addenda/20-new-column-nullability.md +12 -0
  1013. package/profiles/example-django/addenda/30-test-doctrine.md +12 -0
  1014. package/profiles/example-django/addenda/40-orm-usage.md +19 -0
  1015. package/profiles/example-django/addenda/50-declared-shape.md +10 -0
  1016. package/profiles/example-django/addenda/60-config-placement.md +14 -0
  1017. package/profiles/example-django/heuristics.toml +24 -0
  1018. package/profiles/example-django/profile.toml +9 -0
  1019. package/profiles/fast/README.md +24 -0
  1020. package/profiles/fast/profile.toml +59 -0
  1021. package/profiles/minimal/README.md +17 -0
  1022. package/profiles/minimal/addenda/.gitkeep +0 -0
  1023. package/profiles/minimal/profile.toml +8 -0
  1024. package/profiles/snappy/README.md +24 -0
  1025. package/profiles/snappy/addenda/10-race-as-question.md +6 -0
  1026. package/profiles/snappy/profile.toml +46 -0
  1027. package/prompts/companion-day-open.md +46 -0
  1028. package/prompts/companion-first-run.md +104 -0
  1029. package/prompts/companion.md +821 -0
  1030. package/prompts/conversation-system.md +117 -0
  1031. package/prompts/eod-sweep.md +120 -0
  1032. package/prompts/onboarding.md +113 -0
  1033. package/prompts/pass-common.md +52 -0
  1034. package/prompts/pass-quality-common.md +22 -0
  1035. package/prompts/round-first.md +874 -0
  1036. package/prompts/round-follow-up.md +991 -0
  1037. package/prompts/round-re-review.md +889 -0
  1038. package/prompts/round-recheck.md +196 -0
  1039. package/prompts/round-staleness.md +856 -0
  1040. package/schemas/review-result.v1.json +391 -0
  1041. package/schemas/scan-result.v1.json +421 -0
  1042. package/schemas/version.v1.json +57 -0
  1043. package/scripts/approve-pr.sh +16 -0
  1044. package/scripts/build-stamp.mjs +67 -0
  1045. package/scripts/commit-verdict.sh +155 -0
  1046. package/scripts/companion-e2e.sh +250 -0
  1047. package/scripts/companion-memory-forward.mjs +1966 -0
  1048. package/scripts/config-fingerprint.sh +28 -0
  1049. package/scripts/deploy-manifest.mjs +141 -0
  1050. package/scripts/deploy.sh +1435 -0
  1051. package/scripts/directory-lock.mjs +193 -0
  1052. package/scripts/heartbeat-fresh.mjs +54 -0
  1053. package/scripts/install-locator.sh +328 -0
  1054. package/scripts/install-wrapper.sh +76 -0
  1055. package/scripts/memory-search-check.mjs +98 -0
  1056. package/scripts/mint-token.sh +233 -0
  1057. package/scripts/openviking-embedder.py +392 -0
  1058. package/scripts/openviking-ov-conf.mjs +161 -0
  1059. package/scripts/openviking-provision.mjs +1122 -0
  1060. package/scripts/openviking-reembed.mjs +1154 -0
  1061. package/scripts/post-review.sh +16 -0
  1062. package/scripts/pr-checks.sh +256 -0
  1063. package/scripts/pr-context.py +174 -0
  1064. package/scripts/pr-context.sh +63 -0
  1065. package/scripts/pr-self.sh +144 -0
  1066. package/scripts/pr-threads.sh +145 -0
  1067. package/scripts/qcr +15 -0
  1068. package/scripts/qcr-host.sh +769 -0
  1069. package/scripts/render-review.sh +24 -0
  1070. package/scripts/reply-thread.sh +16 -0
  1071. package/scripts/required-passes.py +112 -0
  1072. package/scripts/smoke-companion.sh +211 -0
  1073. package/scripts/tui-deploy-runner.mjs +61 -0
  1074. package/scripts/verify-dist.mjs +201 -0
  1075. package/scripts/vm-ghcr-login.sh +188 -0
  1076. package/seeds/agents/pass-baseline.md +61 -0
  1077. package/seeds/agents/pass-contracts.md +30 -0
  1078. package/seeds/agents/pass-data-safety.md +54 -0
  1079. package/seeds/agents/pass-delivery.md +37 -0
  1080. package/seeds/agents/pass-prod-risk.md +24 -0
  1081. package/seeds/agents/pass-security.md +31 -0
  1082. package/seeds/agents/pass-statelessness.md +27 -0
  1083. package/seeds/agents/pass-ui.md +23 -0
  1084. package/seeds/agents/pass-validate.md +50 -0
  1085. package/seeds/prompts/conversation-system.md +117 -0
  1086. package/seeds/prompts/eod-sweep.md +120 -0
  1087. package/seeds/prompts/pass-common.md +52 -0
  1088. package/skills/companion/adjudicate/SKILL.md +247 -0
  1089. package/skills/companion/evidence/SKILL.md +150 -0
  1090. package/skills/companion/land/SKILL.md +200 -0
  1091. package/skills/companion/replay/SKILL.md +138 -0
  1092. package/skills/companion/setup/SKILL.md +167 -0
  1093. package/skills/companion/survey/SKILL.md +160 -0
  1094. package/skills/selfimprove/README.md +34 -0
  1095. package/skills/selfimprove/SKILL.md +127 -0
  1096. package/skills/selfimprove/agents/openai.yaml +2 -0
  1097. package/skills/selfimprove/commands/selfimprove.md +6 -0
  1098. package/skills/selfimprove/references/fix-loop.md +49 -0
  1099. package/skills/selfimprove/references/ledger.md +20 -0
  1100. package/skills/selfimprove/references/setup.md +40 -0
  1101. package/src/anomalies.ts +566 -0
  1102. package/src/answerworker.ts +251 -0
  1103. package/src/appstamp.ts +65 -0
  1104. package/src/archive/admission.ts +3 -0
  1105. package/src/archive/archivedb.ts +548 -0
  1106. package/src/archive/archiver.ts +1963 -0
  1107. package/src/archive/archivewrite.ts +279 -0
  1108. package/src/archive/bundle.ts +333 -0
  1109. package/src/archive/dbmove.ts +911 -0
  1110. package/src/archive/duplicatecounts.ts +98 -0
  1111. package/src/archive/eligibility.ts +181 -0
  1112. package/src/archive/extract.ts +342 -0
  1113. package/src/archive/fsutil.ts +415 -0
  1114. package/src/archive/gzfile.ts +370 -0
  1115. package/src/archive/kinds.ts +21 -0
  1116. package/src/archive/livecheckpoint.ts +217 -0
  1117. package/src/archive/liveretention.ts +180 -0
  1118. package/src/archive/manifest.ts +519 -0
  1119. package/src/archive/overseer.ts +336 -0
  1120. package/src/archive/piglobal.ts +174 -0
  1121. package/src/archive/plan.ts +612 -0
  1122. package/src/archive/prarchive.ts +984 -0
  1123. package/src/archive/prfiles.ts +406 -0
  1124. package/src/archive/report.ts +252 -0
  1125. package/src/archive/restore.ts +330 -0
  1126. package/src/archive/rows.ts +187 -0
  1127. package/src/archive/seal.ts +630 -0
  1128. package/src/archive/status.ts +719 -0
  1129. package/src/archive/storepass.ts +581 -0
  1130. package/src/backfill.ts +362 -0
  1131. package/src/bench/cost.ts +113 -0
  1132. package/src/bench/exec.ts +90 -0
  1133. package/src/bench/values.ts +13 -0
  1134. package/src/botcontrol.ts +176 -0
  1135. package/src/botworker.ts +953 -0
  1136. package/src/capture.ts +321 -0
  1137. package/src/cases.ts +1789 -0
  1138. package/src/channelfs.ts +26 -0
  1139. package/src/checklist.ts +506 -0
  1140. package/src/ciretry.ts +142 -0
  1141. package/src/cli.ts +2306 -0
  1142. package/src/cliops.ts +385 -0
  1143. package/src/clonedir.ts +15 -0
  1144. package/src/commandeffects.ts +57 -0
  1145. package/src/commands.ts +1704 -0
  1146. package/src/companioninbox.ts +239 -0
  1147. package/src/compare.ts +957 -0
  1148. package/src/config.ts +2070 -0
  1149. package/src/configcheck.ts +133 -0
  1150. package/src/coreext.ts +47 -0
  1151. package/src/daemon.ts +1327 -0
  1152. package/src/debuground.ts +927 -0
  1153. package/src/debugroundpi.ts +272 -0
  1154. package/src/diagnostics.ts +14 -0
  1155. package/src/digest.ts +183 -0
  1156. package/src/dispatch.ts +2251 -0
  1157. package/src/engine/answerguard.ts +27 -0
  1158. package/src/engine/botsdk.ts +336 -0
  1159. package/src/engine/caserunner-sdk.ts +423 -0
  1160. package/src/engine/debugpi.ts +247 -0
  1161. package/src/eod.ts +669 -0
  1162. package/src/eodpinwatch.ts +556 -0
  1163. package/src/escalation.ts +1253 -0
  1164. package/src/eventloop.ts +1045 -0
  1165. package/src/execboundary.ts +645 -0
  1166. package/src/feedback.ts +1162 -0
  1167. package/src/findingscheck.ts +298 -0
  1168. package/src/fsdenied.ts +24 -0
  1169. package/src/ghread.ts +242 -0
  1170. package/src/github.ts +1539 -0
  1171. package/src/githubtoken.ts +435 -0
  1172. package/src/homes.ts +175 -0
  1173. package/src/hostroute.ts +53 -0
  1174. package/src/improvements.ts +787 -0
  1175. package/src/install.ts +1103 -0
  1176. package/src/installkind.ts +50 -0
  1177. package/src/ipc/capability.ts +333 -0
  1178. package/src/ipc/client.ts +269 -0
  1179. package/src/ipc/control.ts +90 -0
  1180. package/src/ipc/ghreadshim.ts +94 -0
  1181. package/src/ipc/launcher.ts +65 -0
  1182. package/src/ipc/permits.ts +362 -0
  1183. package/src/ipc/postshim.ts +164 -0
  1184. package/src/ipc/protocol.ts +342 -0
  1185. package/src/ipc/server.ts +371 -0
  1186. package/src/ipc/workerspec.ts +218 -0
  1187. package/src/learner/learner.ts +1655 -0
  1188. package/src/learner/miner.ts +1290 -0
  1189. package/src/learner/packet.ts +340 -0
  1190. package/src/learner/receipt.ts +168 -0
  1191. package/src/learner/status.ts +107 -0
  1192. package/src/ledgercheck.ts +157 -0
  1193. package/src/lessonledger.ts +703 -0
  1194. package/src/lessonselect.ts +888 -0
  1195. package/src/lessonshadow.ts +810 -0
  1196. package/src/levers.ts +393 -0
  1197. package/src/live/projection.ts +491 -0
  1198. package/src/live/publish.ts +92 -0
  1199. package/src/live/read.ts +86 -0
  1200. package/src/livecli.ts +336 -0
  1201. package/src/loginhint.ts +61 -0
  1202. package/src/loginrefresh/atomic.ts +76 -0
  1203. package/src/loginrefresh/lock.ts +104 -0
  1204. package/src/loginrefresh/main.ts +59 -0
  1205. package/src/loginrefresh/pirefresh.ts +60 -0
  1206. package/src/loginrefresh/refresher.ts +326 -0
  1207. package/src/loginrefresh/supervisor.ts +273 -0
  1208. package/src/loginrefresh/types.ts +35 -0
  1209. package/src/memory/context.ts +385 -0
  1210. package/src/memory/datafence.ts +9 -0
  1211. package/src/memory/delivery.ts +171 -0
  1212. package/src/memory/eventtags.ts +80 -0
  1213. package/src/memory/health.ts +953 -0
  1214. package/src/memory/import.ts +664 -0
  1215. package/src/memory/layout.ts +261 -0
  1216. package/src/memory/legacy-file.ts +104 -0
  1217. package/src/memory/lessons.ts +447 -0
  1218. package/src/memory/ovsession.ts +601 -0
  1219. package/src/memory/relayout.ts +335 -0
  1220. package/src/memory/roundactivity.ts +209 -0
  1221. package/src/memory/selection.ts +989 -0
  1222. package/src/memory/shadowstatus.ts +125 -0
  1223. package/src/memory/store.ts +275 -0
  1224. package/src/memory/textidentity.ts +8 -0
  1225. package/src/metrics.ts +1056 -0
  1226. package/src/missingconfig.ts +7 -0
  1227. package/src/npmverbs.ts +266 -0
  1228. package/src/onboard/choices.ts +255 -0
  1229. package/src/onboard/engine.ts +537 -0
  1230. package/src/onboard/launch.ts +103 -0
  1231. package/src/onboard/pipin.ts +31 -0
  1232. package/src/onboard/stepforward.ts +61 -0
  1233. package/src/onboard/steps/appslug.ts +268 -0
  1234. package/src/onboard/steps/bringup.ts +482 -0
  1235. package/src/onboard/steps/compose.ts +490 -0
  1236. package/src/onboard/steps/configdelivery.ts +58 -0
  1237. package/src/onboard/steps/configfill.ts +1440 -0
  1238. package/src/onboard/steps/deploysh.ts +105 -0
  1239. package/src/onboard/steps/embeddings.ts +550 -0
  1240. package/src/onboard/steps/escalation.ts +640 -0
  1241. package/src/onboard/steps/githubaccount.ts +716 -0
  1242. package/src/onboard/steps/githubapp.ts +1498 -0
  1243. package/src/onboard/steps/golive.ts +579 -0
  1244. package/src/onboard/steps/index.ts +82 -0
  1245. package/src/onboard/steps/ingress.ts +974 -0
  1246. package/src/onboard/steps/inventory.ts +526 -0
  1247. package/src/onboard/steps/llmauth.ts +1180 -0
  1248. package/src/onboard/steps/memory.ts +1347 -0
  1249. package/src/onboard/steps/memorykeys.ts +85 -0
  1250. package/src/onboard/steps/preflight.ts +191 -0
  1251. package/src/onboard/steps/proof.ts +1042 -0
  1252. package/src/onboard/steps/ssh.ts +377 -0
  1253. package/src/onboard/steps/topology.ts +338 -0
  1254. package/src/onboard/steps/transport.ts +680 -0
  1255. package/src/onboard/steps/tuiroute.ts +119 -0
  1256. package/src/onboard/steps/verifyup.ts +467 -0
  1257. package/src/onboard/steps/vmdeploy.ts +442 -0
  1258. package/src/onboard/toml.ts +50 -0
  1259. package/src/onboard/toolnames.ts +34 -0
  1260. package/src/opencli.ts +591 -0
  1261. package/src/openpr.ts +568 -0
  1262. package/src/openviking/client.ts +1131 -0
  1263. package/src/openviking/context.ts +263 -0
  1264. package/src/openviking/embedder.ts +516 -0
  1265. package/src/openviking/gapsink.ts +63 -0
  1266. package/src/openviking/health.ts +83 -0
  1267. package/src/openviking/index.ts +119 -0
  1268. package/src/openviking/lessonfind.ts +92 -0
  1269. package/src/openviking/probe.ts +129 -0
  1270. package/src/openviking/proc.ts +110 -0
  1271. package/src/openviking/provisionreport.ts +160 -0
  1272. package/src/openviking/reembed.ts +412 -0
  1273. package/src/openviking/relayout.ts +177 -0
  1274. package/src/openviking/sidecar-main.ts +38 -0
  1275. package/src/openviking/sidecar.ts +106 -0
  1276. package/src/openviking/store.ts +2025 -0
  1277. package/src/openviking/supervisor.ts +474 -0
  1278. package/src/openviking/types.ts +174 -0
  1279. package/src/operator-recovery.ts +237 -0
  1280. package/src/operatormessage.ts +52 -0
  1281. package/src/operatorwrite.ts +25 -0
  1282. package/src/outcomes.ts +1677 -0
  1283. package/src/piauthcopy.ts +12 -0
  1284. package/src/picli.ts +49 -0
  1285. package/src/picwd.ts +25 -0
  1286. package/src/pilogin.ts +37 -0
  1287. package/src/pingpong.ts +1339 -0
  1288. package/src/posting-journal.ts +554 -0
  1289. package/src/posting-outbox.ts +810 -0
  1290. package/src/posting-receipt.ts +341 -0
  1291. package/src/posting.ts +603 -0
  1292. package/src/postops.ts +832 -0
  1293. package/src/pr-activity.ts +20 -0
  1294. package/src/pr-author.ts +6 -0
  1295. package/src/pr-conversation.ts +432 -0
  1296. package/src/pr-session-lifecycle.ts +631 -0
  1297. package/src/preflight.ts +391 -0
  1298. package/src/priorresolve.ts +343 -0
  1299. package/src/qcrbin.ts +186 -0
  1300. package/src/recheck.ts +1776 -0
  1301. package/src/reconcile.ts +1056 -0
  1302. package/src/recordedfindings.ts +27 -0
  1303. package/src/regression.ts +833 -0
  1304. package/src/replay.ts +816 -0
  1305. package/src/requiredpasses.ts +148 -0
  1306. package/src/resumecheck.ts +246 -0
  1307. package/src/reviewcli.ts +787 -0
  1308. package/src/reviewcontext.ts +35 -0
  1309. package/src/reviewdetach.ts +734 -0
  1310. package/src/reviewhistory.ts +441 -0
  1311. package/src/reviewhome.ts +36 -0
  1312. package/src/reviewmemory.ts +133 -0
  1313. package/src/reviewnow.ts +76 -0
  1314. package/src/reviewrendercli.ts +139 -0
  1315. package/src/round-work.ts +42 -0
  1316. package/src/runhost.ts +108 -0
  1317. package/src/scanauth.ts +36 -0
  1318. package/src/scanbox.ts +1081 -0
  1319. package/src/scancli.ts +1296 -0
  1320. package/src/scandriverlog.ts +32 -0
  1321. package/src/session-bridge.ts +139 -0
  1322. package/src/session-command-payload.ts +35 -0
  1323. package/src/session-command-supervisor.ts +1142 -0
  1324. package/src/sessionjsonl.ts +88 -0
  1325. package/src/sessions.ts +659 -0
  1326. package/src/setup/adapters.ts +335 -0
  1327. package/src/setup/baselines.ts +110 -0
  1328. package/src/setup/caserunner.ts +611 -0
  1329. package/src/setup/configcommit.ts +443 -0
  1330. package/src/setup/engine.ts +1001 -0
  1331. package/src/setup/facts.ts +627 -0
  1332. package/src/setup/findings.ts +1264 -0
  1333. package/src/setup/history.ts +443 -0
  1334. package/src/setup/interview.ts +243 -0
  1335. package/src/setup/ledger.ts +324 -0
  1336. package/src/setup/pins.ts +66 -0
  1337. package/src/setup/portrait.ts +635 -0
  1338. package/src/setup/presentation.ts +18 -0
  1339. package/src/setup/profile.ts +508 -0
  1340. package/src/setup/replay.ts +370 -0
  1341. package/src/setup/replaybuild.ts +392 -0
  1342. package/src/setup/replayrun.ts +1096 -0
  1343. package/src/setup/replaysource.ts +198 -0
  1344. package/src/setup/retune.ts +415 -0
  1345. package/src/setup/retunelog.ts +95 -0
  1346. package/src/setup/scan.ts +1033 -0
  1347. package/src/setup/scanbuild.ts +168 -0
  1348. package/src/setup/scanfacts.ts +107 -0
  1349. package/src/setup/scanlease.ts +297 -0
  1350. package/src/setup/scanreport.ts +373 -0
  1351. package/src/setup/scanrun.ts +2268 -0
  1352. package/src/setup/scantree.ts +259 -0
  1353. package/src/setup/stage2.ts +926 -0
  1354. package/src/setup/stage2request.ts +347 -0
  1355. package/src/setup/survey.ts +458 -0
  1356. package/src/setup/surveyrun.ts +758 -0
  1357. package/src/setup/textsniff.ts +95 -0
  1358. package/src/setup/trackersink.ts +433 -0
  1359. package/src/setup/triage.ts +768 -0
  1360. package/src/setup/tuning.ts +766 -0
  1361. package/src/setup/unitcut.ts +523 -0
  1362. package/src/singleflight.ts +86 -0
  1363. package/src/slack/announce.ts +911 -0
  1364. package/src/slack/api.ts +303 -0
  1365. package/src/slack/messages.ts +288 -0
  1366. package/src/slack/origin.ts +599 -0
  1367. package/src/slack/policy.ts +354 -0
  1368. package/src/slack/reply.ts +201 -0
  1369. package/src/slack/router.ts +285 -0
  1370. package/src/slack/socket.ts +384 -0
  1371. package/src/slack/types.ts +108 -0
  1372. package/src/sqlitehint.ts +32 -0
  1373. package/src/ssh-observation.ts +30 -0
  1374. package/src/state.ts +8261 -0
  1375. package/src/statemachine.ts +372 -0
  1376. package/src/stubs.ts +68 -0
  1377. package/src/takeover.ts +634 -0
  1378. package/src/takeoverpi.ts +306 -0
  1379. package/src/terminal-failure.ts +41 -0
  1380. package/src/thread-resolution-refusal.ts +2 -0
  1381. package/src/tmux.ts +153 -0
  1382. package/src/trackerid.ts +41 -0
  1383. package/src/transcriptindex.ts +665 -0
  1384. package/src/tui/actions.ts +484 -0
  1385. package/src/tui/alerts.ts +666 -0
  1386. package/src/tui/app.ts +2026 -0
  1387. package/src/tui/archiveops.ts +192 -0
  1388. package/src/tui/autofile.ts +233 -0
  1389. package/src/tui/board.ts +1529 -0
  1390. package/src/tui/candidates.ts +889 -0
  1391. package/src/tui/cellwidth.ts +148 -0
  1392. package/src/tui/changelog-store.ts +503 -0
  1393. package/src/tui/chord.ts +25 -0
  1394. package/src/tui/clipboard.ts +92 -0
  1395. package/src/tui/cmdbar.ts +299 -0
  1396. package/src/tui/companion.ts +1495 -0
  1397. package/src/tui/companionplan.ts +247 -0
  1398. package/src/tui/companiontab.ts +83 -0
  1399. package/src/tui/confighome.ts +155 -0
  1400. package/src/tui/connect.ts +74 -0
  1401. package/src/tui/deploy-ceremony.ts +104 -0
  1402. package/src/tui/deploy-drain.ts +129 -0
  1403. package/src/tui/deploy-process.ts +265 -0
  1404. package/src/tui/deploytab.ts +1523 -0
  1405. package/src/tui/design.ts +125 -0
  1406. package/src/tui/digesttab.ts +214 -0
  1407. package/src/tui/emptystate.ts +39 -0
  1408. package/src/tui/evidence.ts +999 -0
  1409. package/src/tui/feed.ts +604 -0
  1410. package/src/tui/feedbacktab.ts +1077 -0
  1411. package/src/tui/filters.ts +94 -0
  1412. package/src/tui/ghread.ts +388 -0
  1413. package/src/tui/historypull.ts +81 -0
  1414. package/src/tui/inboxpull.ts +236 -0
  1415. package/src/tui/index.ts +1656 -0
  1416. package/src/tui/issuesink.ts +238 -0
  1417. package/src/tui/jsonfile.ts +42 -0
  1418. package/src/tui/keys.ts +109 -0
  1419. package/src/tui/layout.ts +108 -0
  1420. package/src/tui/learning.ts +627 -0
  1421. package/src/tui/lessonswitch.ts +118 -0
  1422. package/src/tui/livecontrol.ts +228 -0
  1423. package/src/tui/memforward.ts +492 -0
  1424. package/src/tui/memincident.ts +363 -0
  1425. package/src/tui/memory.ts +620 -0
  1426. package/src/tui/memtab.ts +606 -0
  1427. package/src/tui/metricstab.ts +1177 -0
  1428. package/src/tui/model.ts +165 -0
  1429. package/src/tui/mouse.ts +98 -0
  1430. package/src/tui/msg.ts +268 -0
  1431. package/src/tui/nativecompanion.ts +417 -0
  1432. package/src/tui/openurl.ts +21 -0
  1433. package/src/tui/paint.ts +511 -0
  1434. package/src/tui/panes.ts +98 -0
  1435. package/src/tui/pipane.ts +219 -0
  1436. package/src/tui/poll-hooks.ts +12 -0
  1437. package/src/tui/poll.ts +147 -0
  1438. package/src/tui/proposals.ts +838 -0
  1439. package/src/tui/prpause.ts +159 -0
  1440. package/src/tui/reload.ts +436 -0
  1441. package/src/tui/remote.ts +421 -0
  1442. package/src/tui/rollover.ts +969 -0
  1443. package/src/tui/route.ts +173 -0
  1444. package/src/tui/scantab.ts +1290 -0
  1445. package/src/tui/setupentry.ts +93 -0
  1446. package/src/tui/setupstate.ts +52 -0
  1447. package/src/tui/sidebar.ts +749 -0
  1448. package/src/tui/sshtriage.ts +631 -0
  1449. package/src/tui/startup.ts +231 -0
  1450. package/src/tui/tabs.ts +45 -0
  1451. package/src/tui/termmirror.ts +224 -0
  1452. package/src/tui/theme.ts +96 -0
  1453. package/src/tui/ticketstab.ts +310 -0
  1454. package/src/tui/tmuxsocket.ts +129 -0
  1455. package/src/tui/trackertickets.ts +84 -0
  1456. package/src/tui/unwedge.ts +274 -0
  1457. package/src/tui/viewer.ts +575 -0
  1458. package/src/tui/viewerinput.ts +173 -0
  1459. package/src/updatenotice.ts +178 -0
  1460. package/src/usage.ts +1154 -0
  1461. package/src/verdicts.ts +4707 -0
  1462. package/src/version.ts +80 -0
  1463. package/src/wakes.ts +476 -0
  1464. package/src/webhook.ts +951 -0
  1465. package/src/webhookheal.ts +276 -0
  1466. package/src/wire.ts +2182 -0
  1467. package/src/worker-session-host.ts +602 -0
  1468. package/src/workerbreaker.ts +156 -0
  1469. package/src/workercontrol.ts +104 -0
  1470. package/src/workerlease.ts +441 -0
  1471. package/src/workerliveness.ts +103 -0
  1472. package/src/workerlog.ts +79 -0
  1473. package/src/workerruntime.ts +30 -0
  1474. package/src/workersupervisor.ts +1538 -0
  1475. package/src/workerusage.ts +42 -0
  1476. package/src/workerworld.ts +433 -0
  1477. package/src/worktrees.ts +1138 -0
  1478. package/tests/fixtures/regression-cases.json +795 -0
  1479. package/tests/fixtures/seed-manifest.json +19 -0
@@ -0,0 +1,1677 @@
1
+ import { prActivity, type PrActivity } from "./pr-activity.js";
2
+ // Outcome capture: what became of every finding the reviewer posted.
3
+ //
4
+ // Why. `round_findings` records why each candidate was raised or killed; this
5
+ // module records what happened after a finding reached the PR — did the
6
+ // author fix it, argue it away, or leave it standing? — and links each posted
7
+ // finding to the GitHub comment that carried it, so a human reply joins back
8
+ // to the lens that raised the finding. The learner (finding cases, lessons)
9
+ // needs exactly that join. This module builds it, daemon-side and deterministic — no model in the
10
+ // loop, no GitHub write, nothing a round or a bench prompt can see.
11
+ //
12
+ // Three tables, owned here (on the state database, like the ping-pong
13
+ // ledger's):
14
+ //
15
+ // - `finding_comments` — one row per ROOT inline comment the bot posted, with
16
+ // the facts needed to link it (path, original line, review id, the marker,
17
+ // the title line) and the link itself: `(round_no, finding_key)` and how it
18
+ // was made (`via`). Linking, most trusted first:
19
+ // 1. `marker` — the hidden `<!-- qcr:f=<round>:<key> -->` the live render
20
+ // route stamps into every comment (packages/pi-plugin/src/findingkey.ts). Exact.
21
+ // 2. `thread_id` — the record's own `thread_id` names the comment.
22
+ // 3. `path_line` — the one posted finding at that path and line in the
23
+ // comment's round (the round its review id names, else the registry's
24
+ // attribution); ties broken by the title line (`path_line_title`), then
25
+ // the tag (`path_line_tag`). A comment none of these settles stays
26
+ // UNMATCHED (null) — never a guess.
27
+ // Facts arrive from the reconcile comment sweep's existing read (the bot's
28
+ // own inline comments), from the human replies
29
+ // already in `pr_comments` (a reply names its thread's root, path and line —
30
+ // GitHub's CURRENT line, which drifts as the branch moves, so it is the
31
+ // weakest fact), and from the record's `thread_id`. A fallback link is kept
32
+ // while its facts stand; when the bot's own comment supplies different ones
33
+ // (the line it was posted on, its review id), the link is re-opened and
34
+ // resolved again. No new GitHub read, and no gate: this only ever reads.
35
+ // - `finding_outcomes` — per posted finding, a FATE from the closed set
36
+ // below, with the rule that decided it (`basis`), the evidence and when.
37
+ // - `review_outcomes` — the WHOLE-REVIEW signals (a review a human set aside,
38
+ // a merge over a standing block), recorded per review and never smeared over
39
+ // the review's findings: nothing says which finding made a human set a
40
+ // review aside.
41
+ //
42
+ // The fates, and what decides them. Every piece of evidence is timestamped;
43
+ // the LATEST decisive one wins, so a finding upheld after a reply and fixed by
44
+ // a later push reads `fixed`.
45
+ // fixed — the session's thread decision said `fixed`; a human
46
+ // reply in the thread cites a commit made after the
47
+ // review (checked in git: a sha-shaped token that is
48
+ // no such commit — a UUID segment, a colour, the
49
+ // commit that introduced the behaviour — is no fix);
50
+ // the thread was resolved after a push that changed
51
+ // the anchored lines; or the finding is gone at the
52
+ // next round and the code at its anchor changed.
53
+ // refuted_conceded — the session's decision said `refuted`; a later round
54
+ // recorded it `conceded` with the code at its anchor
55
+ // unchanged and no sha cited; or the session resolved
56
+ // the thread with no push since the review and no
57
+ // unobserved head. The last two are INFERRED from the session's own act, so a
58
+ // human reply that accepts the point (`concedesPoint`:
59
+ // "fair catch", "fixed", "updated the description")
60
+ // turns them into `unknown`, basis `conflict:…`.
61
+ // upheld_after_reply — a human replied and the bot held: the decision said
62
+ // `held` or kept the thread open, or the next round
63
+ // posted the finding again. A thread kept open right
64
+ // after a reply citing a commit is no hold: a fix git
65
+ // verified stands, one it cannot check is `unknown`.
66
+ // open_at_close — none of the above, and the PR is closed.
67
+ // unknown — none of the above, and the PR is open.
68
+ // A `conceded` record is not read as a refutation on its own: most of them
69
+ // are fixes ("Fixed in <sha>"). It counts as `fixed` when a
70
+ // sha was cited or the anchored code changed, as `refuted_conceded` only when
71
+ // the code provably did not, and as no evidence when that cannot be told — a
72
+ // cited sha git cannot check included.
73
+ //
74
+ // A resolve is the session's: the event exists only when its thread decision
75
+ // said `resolve`. GitHub answering "already resolved" (a human clicked first)
76
+ // changes who clicked, not what the session judged, so it reads as the
77
+ // session's resolve; only the human-only refusals (`[check this]`,
78
+ // `[deferred]`) are no resolve at all. A decision that names its own
79
+ // `outcome` overrides the resolve it made.
80
+ //
81
+ // A later round's entry is this finding only when that is provable: it names
82
+ // this finding's thread or `settled:` it, or it names this finding's round in
83
+ // `raised_in_round` and either carries this finding's key or — same path,
84
+ // shared lens, within `CONTINUATION_LINES` — is nearer to this finding than
85
+ // to any other entry of that round. Only such a STRONG match carries
86
+ // evidence. A WEAK one (the next round, same path and lens, nearby, any
87
+ // round named or none) may be a different finding, so it only keeps this one
88
+ // from reading "gone", erring to `unknown`.
89
+ //
90
+ // A finding the next round posted again is CARRIED: the earlier row inherits
91
+ // the later one's evidence, so one finding raised in three rounds reads one
92
+ // fate three times rather than three different ones.
93
+ //
94
+ // Bounded. The reconcile pass runs `sweep()`: open PRs and PRs closed in the
95
+ // last day, at most `OUTCOME_SWEEP_MAX_PRS`, and a PR whose inputs have not
96
+ // moved since its last capture is skipped. Git reads (`AnchorGit`) are local,
97
+ // read-only and memoised; a commit the clone does not hold is UNKNOWN, never a
98
+ // change and never no-change. Every failure is the sweep's own — reconcile's
99
+ // health (which lifts a deploy drain) never waits on it.
100
+ import { carriesOwnMarker } from "@quickchatai/pi-quickreview/src/identity.js";
101
+ import {
102
+ carriedToRef,
103
+ isPostedRow,
104
+ predecessorOf,
105
+ sameSite,
106
+ } from "@quickchatai/pi-quickreview/src/finding-chain.js";
107
+ import { parseFindingMarker } from "@quickchatai/pi-quickreview/src/findingkey.js";
108
+ import { storedLensIds } from "@quickchatai/pi-quickreview/src/lensids.js";
109
+ import type { ReviewCommentFull } from "./github.js";
110
+ import { bareTag } from "@quickchatai/pi-quickreview/src/severity.js";
111
+ import { yieldToLoop } from "@quickchatai/pi-quickreview/src/yieldloop.js";
112
+ import type {
113
+ PrCommentRow,
114
+ PrStateRow,
115
+ RoundFindingRow,
116
+ RoundRow,
117
+ StateStore,
118
+ PriorFindingCheckRow,
119
+ } from "./state.js";
120
+
121
+ export const FINDING_FATES = [
122
+ "fixed",
123
+ "refuted_conceded",
124
+ "upheld_after_reply",
125
+ "open_at_close",
126
+ "unknown",
127
+ ] as const;
128
+ export type FindingFate = (typeof FINDING_FATES)[number];
129
+
130
+ type DecisiveFate = "fixed" | "refuted_conceded" | "upheld_after_reply";
131
+ /** What one piece of evidence can say: a decisive fate, or `unknown` — a
132
+ * rule that would have decided, held back by evidence that contradicts it
133
+ * (its basis names the conflict). The latest piece still wins. */
134
+ type EvidenceFate = DecisiveFate | "unknown";
135
+
136
+ /** How a posted comment was linked to its finding, most trusted first. */
137
+ export type LinkVia = "marker" | "thread_id" | "path_line" | "path_line_title" | "path_line_tag";
138
+
139
+ /** A whole-review signal, recorded per review. */
140
+ export type ReviewOutcomeKind = "set_aside" | "merged_over_block";
141
+
142
+ /** The git reads the fate rules need. `null` = unknown, never a guess.
143
+ * `WorktreeManager` implements both over the base clone. */
144
+ export interface AnchorGit {
145
+ /** Did the code at a finding's anchor change between two commits? */
146
+ anchorTouched(
147
+ path: string,
148
+ line: number | null,
149
+ from: string,
150
+ to: string,
151
+ ): Promise<boolean | null>;
152
+ /** Is `sha` a commit `to` contains and `from` does not — made after the
153
+ * head a finding was raised at? */
154
+ introducedBetween(sha: string, from: string, to: string): Promise<boolean | null>;
155
+ }
156
+
157
+ /** Bump when a rule changes, so every PR is re-captured once. */
158
+ export const OUTCOME_RULES_VERSION = 5;
159
+ /** PRs captured per reconcile pass. */
160
+ export const OUTCOME_SWEEP_MAX_PRS = 25;
161
+ /** How long after a close a PR is still captured — the close is evidence. */
162
+ export const OUTCOME_CLOSED_WINDOW_MS = 24 * 60 * 60 * 1000;
163
+ /**
164
+ * Does the bounded pass (`OutcomeCapture.sweep`) still visit this PR? Open
165
+ * PRs, and PRs closed within `OUTCOME_CLOSED_WINDOW_MS`; a PR with no
166
+ * `pr_state` row never. The ONE spelling of the window: the case writer
167
+ * (src/cases.ts) catches up the PRs outside it itself.
168
+ */
169
+ export function inCaptureWindow(
170
+ p: Pick<PrStateRow, "state" | "updated_at"> | null | undefined,
171
+ nowMs: number,
172
+ ): boolean {
173
+ if (!p) return false;
174
+ return p.state !== "closed" || nowMs - (ms(p.updated_at) ?? 0) <= OUTCOME_CLOSED_WINDOW_MS;
175
+ }
176
+ /** A PR whose inputs have not moved is still re-captured this often: a git
177
+ * answer that was unknown (a commit not yet fetched) may since be known. */
178
+ export const OUTCOME_RECAPTURE_MS = 6 * 60 * 60 * 1000;
179
+ /** Wall time one pass may spend before it stops STARTING PRs: the pass runs
180
+ * inside reconcile, ahead of the dispatch pump, so the first capture after a
181
+ * deploy (every open PR, cold git) must not hold the pump for long. The PRs
182
+ * left over come first next pass. */
183
+ export const OUTCOME_SWEEP_BUDGET_MS = 10_000;
184
+
185
+ const SCHEMA = `
186
+ CREATE TABLE IF NOT EXISTS finding_comments (
187
+ comment_id INTEGER PRIMARY KEY,
188
+ pr INTEGER NOT NULL,
189
+ review_id INTEGER,
190
+ path TEXT,
191
+ line INTEGER,
192
+ head TEXT,
193
+ marker_round INTEGER,
194
+ marker_key TEXT,
195
+ created_at TEXT,
196
+ -- 'github' (the bot's own comment, read) | 'reply' (a human reply named it)
197
+ -- | 'record' (a findings record's thread_id named it)
198
+ source TEXT NOT NULL,
199
+ round_no INTEGER,
200
+ finding_key TEXT,
201
+ via TEXT,
202
+ linked_at TEXT,
203
+ seen_at TEXT NOT NULL
204
+ );
205
+ CREATE INDEX IF NOT EXISTS finding_comments_finding
206
+ ON finding_comments (pr, round_no, finding_key);
207
+ CREATE TABLE IF NOT EXISTS finding_outcomes (
208
+ pr INTEGER NOT NULL,
209
+ round_no INTEGER NOT NULL,
210
+ finding_key TEXT NOT NULL,
211
+ comment_id INTEGER,
212
+ fate TEXT NOT NULL,
213
+ basis TEXT,
214
+ evidence TEXT,
215
+ decided_at TEXT,
216
+ human_replies INTEGER NOT NULL DEFAULT 0,
217
+ carried_to TEXT,
218
+ fate_changed_at TEXT NOT NULL,
219
+ updated_at TEXT NOT NULL,
220
+ PRIMARY KEY (pr, round_no, finding_key)
221
+ );
222
+ CREATE TABLE IF NOT EXISTS review_outcomes (
223
+ signal_id INTEGER PRIMARY KEY,
224
+ pr INTEGER NOT NULL,
225
+ kind TEXT NOT NULL,
226
+ round_no INTEGER,
227
+ gh_review_id INTEGER,
228
+ at TEXT NOT NULL,
229
+ recorded_at TEXT NOT NULL
230
+ );
231
+ CREATE TABLE IF NOT EXISTS outcome_capture_activity (
232
+ pr INTEGER PRIMARY KEY,
233
+ captured_at TEXT NOT NULL,
234
+ comments_max INTEGER NOT NULL,
235
+ signals_max INTEGER NOT NULL
236
+ );
237
+ CREATE TABLE IF NOT EXISTS outcome_capture_marks (
238
+ pr INTEGER PRIMARY KEY,
239
+ signature TEXT NOT NULL,
240
+ captured_at TEXT NOT NULL
241
+ );
242
+ `;
243
+
244
+ export interface FindingCommentRow {
245
+ comment_id: number;
246
+ pr: number;
247
+ review_id: number | null;
248
+ path: string | null;
249
+ line: number | null;
250
+ head: string | null;
251
+ marker_round: number | null;
252
+ marker_key: string | null;
253
+ created_at: string | null;
254
+ source: string;
255
+ round_no: number | null;
256
+ finding_key: string | null;
257
+ via: LinkVia | null;
258
+ linked_at: string | null;
259
+ /** When the row was first written or its facts last changed. */
260
+ seen_at: string;
261
+ }
262
+
263
+ /**
264
+ * A retried posting can stamp one finding's marker on two comments: the
265
+ * earliest keeps the link (`via = marker`), every later one stays unlinked
266
+ * with the marker. Per linked comment, its twins' ids, ascending — the other
267
+ * threads that are the same finding's. One rule for the outcome capture and
268
+ * the finding cases.
269
+ */
270
+ export function markerTwins(rows: readonly FindingCommentRow[]): Map<number, number[]> {
271
+ const linked = new Map<string, FindingCommentRow>();
272
+ for (const c of rows)
273
+ if (c.round_no !== null && c.finding_key !== null)
274
+ linked.set(fkey(c.round_no, c.finding_key), c);
275
+ const out = new Map<number, number[]>();
276
+ for (const c of [...rows].sort((a, b) => a.comment_id - b.comment_id)) {
277
+ if (c.finding_key !== null || c.marker_round === null || c.marker_key === null) continue;
278
+ const root = linked.get(fkey(c.marker_round, c.marker_key));
279
+ if (root?.via !== "marker") continue;
280
+ out.set(root.comment_id, [...(out.get(root.comment_id) ?? []), c.comment_id]);
281
+ }
282
+ return out;
283
+ }
284
+
285
+ /** A `finding_outcomes.carried_to` address: `<round>:<key>`. */
286
+ export { carriedToRef } from "@quickchatai/pi-quickreview/src/finding-chain.js";
287
+
288
+ /** Which rows reached the PR, and which earlier entry a later one continues:
289
+ * the review core reads them too (packages/pi-plugin/src/priorfindings.ts). */
290
+ export {
291
+ CONTINUATION_LINES,
292
+ isPostedRow,
293
+ predecessorOf,
294
+ } from "@quickchatai/pi-quickreview/src/finding-chain.js";
295
+
296
+ /** The finding a `carried_to` address names — split at the FIRST ':', since
297
+ * the key itself may hold ':' — or null when the round part is not an
298
+ * integer. */
299
+ export function parseCarriedTo(ref: string): { round_no: number; finding_key: string } | null {
300
+ const cut = ref.indexOf(":");
301
+ const round = Number(ref.slice(0, cut));
302
+ if (cut <= 0 || !Number.isInteger(round)) return null;
303
+ return { round_no: round, finding_key: ref.slice(cut + 1) };
304
+ }
305
+
306
+ export interface FindingOutcomeRow {
307
+ pr: number;
308
+ round_no: number;
309
+ finding_key: string;
310
+ comment_id: number | null;
311
+ fate: FindingFate;
312
+ basis: string | null;
313
+ evidence: string | null;
314
+ decided_at: string | null;
315
+ human_replies: number;
316
+ carried_to: string | null;
317
+ fate_changed_at: string;
318
+ updated_at: string;
319
+ }
320
+
321
+ export interface ReviewOutcomeRow {
322
+ signal_id: number;
323
+ pr: number;
324
+ kind: ReviewOutcomeKind;
325
+ round_no: number | null;
326
+ gh_review_id: number | null;
327
+ at: string;
328
+ recorded_at: string;
329
+ }
330
+
331
+ export interface CaptureResult {
332
+ posted: number;
333
+ linked: number;
334
+ unmatched: number;
335
+ /** Findings whose fate is new or changed by this capture. */
336
+ changed: number;
337
+ fates: Record<FindingFate, number>;
338
+ }
339
+
340
+ export interface OutcomeSweepReport {
341
+ considered: number;
342
+ captured: number;
343
+ skipped: number;
344
+ fates_changed: number;
345
+ errors: string[];
346
+ }
347
+
348
+ // ---- small pure helpers ---------------------------------------------------
349
+
350
+ /** How many sha-shaped tokens of one reply are checked in git. */
351
+ const MAX_CITED_SHAS = 5;
352
+ /** How many later heads a cited sha is checked against. */
353
+ const MAX_LATER_HEADS = 8;
354
+
355
+ /** The sha-shaped tokens a human reply carries (7–40 hex characters with at
356
+ * least one digit and one letter, standing alone — "Fixed in 3f2a9c1", a
357
+ * commit URL; never `#`-prefixed like a colour, never a dash-joined run like
358
+ * a UUID's segments), distinct, in order, at most `MAX_CITED_SHAS`. A token
359
+ * is only a CANDIDATE: whether it is a fix is git's question
360
+ * (`AnchorGit.introducedBetween`). */
361
+ export function citedShas(text: string | null | undefined): string[] {
362
+ if (typeof text !== "string") return [];
363
+ const out: string[] = [];
364
+ for (const m of text.matchAll(/(?<![0-9a-z#-])[0-9a-f]{7,40}(?![0-9a-z-])/gi)) {
365
+ const t = m[0].toLowerCase();
366
+ if (/[0-9]/.test(t) && /[a-f]/.test(t) && !out.includes(t)) out.push(t);
367
+ if (out.length >= MAX_CITED_SHAS) break;
368
+ }
369
+ return out;
370
+ }
371
+
372
+ /** The first candidate `citedShas` finds, or null. */
373
+ export function citedSha(text: string | null | undefined): string | null {
374
+ return citedShas(text)[0] ?? null;
375
+ }
376
+
377
+ /** The tags and title a comment opens with: `[blocker] title`,
378
+ * `**[check this]** title`, `[ask your human] [question] title`. */
379
+ export function commentHead(body: string | null | undefined): { tags: string[]; title: string } {
380
+ const first =
381
+ (typeof body === "string" ? body : "").split("\n").find((l) => l.trim() !== "") ?? "";
382
+ let rest = first.trim();
383
+ const tags: string[] = [];
384
+ for (;;) {
385
+ rest = rest.replace(/^[*_`~>\s]+/, "");
386
+ const m = /^\[([^\]]{1,40})\][*_`~\s]*/.exec(rest);
387
+ if (!m) break;
388
+ tags.push(m[1]!.trim().toLowerCase());
389
+ rest = rest.slice(m[0].length);
390
+ }
391
+ return { tags, title: rest.replace(/[*_~\s]+$/, "").trim() };
392
+ }
393
+
394
+ function normTitle(t: string | null | undefined): string {
395
+ return (t ?? "")
396
+ .toLowerCase()
397
+ .replace(/[`*_~]/g, "")
398
+ .replace(/\s+/g, " ")
399
+ .replace(/[.!?:;\s]+$/, "")
400
+ .trim();
401
+ }
402
+
403
+ function ms(iso: string | null | undefined): number | null {
404
+ if (!iso) return null;
405
+ const t = Date.parse(iso);
406
+ return Number.isFinite(t) ? t : null;
407
+ }
408
+
409
+ const fkey = (round: number, key: string): string => `${round}\u0000${key}`;
410
+ const FATE_RANK: Record<EvidenceFate, number> = {
411
+ fixed: 3,
412
+ refuted_conceded: 2,
413
+ upheld_after_reply: 1,
414
+ unknown: 0,
415
+ };
416
+
417
+ interface Evidence {
418
+ fate: EvidenceFate;
419
+ basis: string;
420
+ at: number;
421
+ detail: string;
422
+ }
423
+
424
+ /**
425
+ * Does a human reply ACCEPT the finding — agree it was right, or say it was
426
+ * fixed or the description corrected? A deliberately small lexicon, read
427
+ * only to hold back a refutation the record would otherwise infer from the
428
+ * session's own act (it resolved the thread, or a later round conceded) with
429
+ * the anchored code unchanged. Many such "refutations" are agreements: "fair
430
+ * catch", "You are right, … I have corrected it", "Updated: the description
431
+ * now says …", a fix made away from the anchor. It over-matches on purpose
432
+ * — a hit only turns a refutation into `unknown`, never into a fix.
433
+ */
434
+ const CONCESSION_RE =
435
+ /\b(?:you(?:'|\u2019)?re (?:right|correct)|you are (?:right|correct)|(?:good|fair|great|nice) (?:catch|point|spot)|fixed|addressed|corrected|updated|amended|reworded|clarified|agreed|accepted|acknowledged|my (?:omission|mistake|bad)|will fix|made (?:it )?part of this pr)\b|^\s*done\b/im;
436
+
437
+ export function concedesPoint(text: string | null | undefined): boolean {
438
+ return typeof text === "string" && CONCESSION_RE.test(text);
439
+ }
440
+
441
+ /** The latest evidence; at one instant, fixed > refuted > upheld > unknown. */
442
+ export function decideFate<E extends { fate: EvidenceFate; at: number }>(
443
+ evidence: readonly E[],
444
+ ): E | null {
445
+ let best: E | null = null;
446
+ for (const e of evidence)
447
+ if (!best || e.at > best.at || (e.at === best.at && FATE_RANK[e.fate] > FATE_RANK[best.fate]))
448
+ best = e;
449
+ return best;
450
+ }
451
+
452
+ // ---- the per-PR facts one capture reads -------------------------------------
453
+
454
+ interface PrFacts {
455
+ rows: RoundFindingRow[];
456
+ priorReports: PriorFindingCheckRow[];
457
+ posted: RoundFindingRow[];
458
+ byRound: Map<number, RoundFindingRow[]>;
459
+ /** Rounds with a parsed findings record: when, and at which head. */
460
+ recorded: Map<number, { at: number; head: string | null }>;
461
+ rounds: RoundRow[];
462
+ links: Map<string, FindingCommentRow>;
463
+ /** Per linked comment, its marker twins (`markerTwins`): each is still
464
+ * that finding's thread, so its replies and decisions are its evidence. */
465
+ twins: Map<number, number[]>;
466
+ replies: Map<number, PrCommentRow[]>;
467
+ decisions: Array<{
468
+ at: number;
469
+ thread_id: number;
470
+ comment_id: number;
471
+ resolution: string;
472
+ outcome: string | null;
473
+ }>;
474
+ /** The session's resolves. `skipped` is GitHub's answer: null (the bot
475
+ * resolved it), `already-resolved` (a human had), or a human-only refusal. */
476
+ resolves: Array<{ at: number; thread_id: number; comment_id: number; skipped: string | null }>;
477
+ pushes: Array<{ at: number; head_sha: string }>;
478
+ /** The PR's head now, as the state database knows it. */
479
+ currentHead: string | null;
480
+ closedAt: number | null;
481
+ }
482
+
483
+ /** What a human reply — or the human replies of a thread — say about a fix
484
+ * commit: a cited sha git confirmed (`verified`), one git could not check
485
+ * (`unverified` — no evidence, and no "no sha was cited" either), or none
486
+ * (`none`: no token, or every token provably not a later commit). */
487
+ type CitedFix =
488
+ | { state: "verified"; sha: string; reply: PrCommentRow }
489
+ | { state: "unverified" }
490
+ | { state: "none" };
491
+
492
+ /** The thread's reading (`overall`: the LATEST verified citation, so a fix
493
+ * cited after an intervening hold still supersedes it) and each reply's own
494
+ * (`byReply`, by comment id: a decision on one reply is judged by THAT reply's
495
+ * citation, never another's). */
496
+ interface CitedFixes {
497
+ overall: CitedFix;
498
+ byReply: Map<number, CitedFix["state"]>;
499
+ }
500
+
501
+ /** When a human reply was made: GitHub's time, else when it was captured. */
502
+ function replyMs(r: PrCommentRow): number {
503
+ return ms(r.created_at) ?? ms(r.captured_at) ?? 0;
504
+ }
505
+
506
+ /** A resolve that counts: the bot's own, or one GitHub answered "already
507
+ * resolved" (a human clicked first; the session still decided `resolve`). The
508
+ * human-only refusals are no resolve at all. */
509
+ function resolveCounts(skipped: string | null): boolean {
510
+ return skipped === null || skipped === "already-resolved";
511
+ }
512
+
513
+ function intOf(v: unknown): number | null {
514
+ if (typeof v === "number" && Number.isInteger(v)) return v;
515
+ if (typeof v === "string" && /^\d+$/.test(v)) return Number(v);
516
+ return null;
517
+ }
518
+
519
+ export interface OutcomeCaptureDeps {
520
+ store: StateStore;
521
+ /** `[github] bot_login` — whose root comments are findings. */
522
+ botLogin: string;
523
+ /** `[github] auth = "user"`: the login is the person's, so a root comment is
524
+ * a finding only when it carries the reviewer's marker, and the person's
525
+ * own replies are a human's. */
526
+ personMode?: boolean;
527
+ /** Absent ⇒ every "did the code change" question is unknown. */
528
+ git?: AnchorGit;
529
+ now?: () => Date;
530
+ }
531
+
532
+ export class OutcomeCapture {
533
+ private readonly store: StateStore;
534
+ private readonly now: () => Date;
535
+
536
+ constructor(private readonly deps: OutcomeCaptureDeps) {
537
+ this.store = deps.store;
538
+ this.now = deps.now ?? (() => new Date());
539
+ this.store.db.exec(SCHEMA);
540
+ }
541
+
542
+ private ts(): string {
543
+ return this.now().toISOString();
544
+ }
545
+
546
+ // ---- facts in ---------------------------------------------------------------
547
+
548
+ /**
549
+ * Keep the link facts of the bot's own ROOT inline comments from a read the
550
+ * caller already made (the reconcile comment sweep's `listReviewCommentsFull`).
551
+ * Replies, other authors' comments and the body beyond its first line are
552
+ * not kept. Returns how many rows were new or gained facts.
553
+ *
554
+ * Facts that change re-open the row's link: a fallback link made from a
555
+ * human reply's drifted line, or before the review id was known, was a
556
+ * resolution of the OLD facts, so it goes back to unmatched and the next
557
+ * `linkComments` resolves it from the new ones. Only a marker link whose
558
+ * marker is unchanged stands.
559
+ */
560
+ observeBotComments(pr: number, comments: readonly ReviewCommentFull[]): number {
561
+ const keep =
562
+ "(finding_comments.via = 'marker'" +
563
+ " AND finding_comments.marker_round IS excluded.marker_round" +
564
+ " AND finding_comments.marker_key IS excluded.marker_key)";
565
+ const stmt = this.store.db.prepare(
566
+ `INSERT INTO finding_comments
567
+ (comment_id, pr, review_id, path, line, head, marker_round, marker_key, created_at,
568
+ source, seen_at)
569
+ VALUES (@comment_id, @pr, @review_id, @path, @line, @head, @marker_round, @marker_key,
570
+ @created_at, 'github', @seen_at)
571
+ ON CONFLICT(comment_id) DO UPDATE SET
572
+ review_id = excluded.review_id, path = excluded.path, line = excluded.line,
573
+ head = excluded.head, marker_round = excluded.marker_round,
574
+ marker_key = excluded.marker_key, created_at = excluded.created_at, source = 'github',
575
+ seen_at = excluded.seen_at,
576
+ round_no = CASE WHEN ${keep} THEN finding_comments.round_no ELSE NULL END,
577
+ finding_key = CASE WHEN ${keep} THEN finding_comments.finding_key ELSE NULL END,
578
+ via = CASE WHEN ${keep} THEN finding_comments.via ELSE NULL END,
579
+ linked_at = CASE WHEN ${keep} THEN finding_comments.linked_at ELSE NULL END
580
+ WHERE finding_comments.source != 'github'
581
+ OR finding_comments.review_id IS NOT excluded.review_id
582
+ OR finding_comments.path IS NOT excluded.path
583
+ OR finding_comments.line IS NOT excluded.line
584
+ OR finding_comments.head IS NOT excluded.head
585
+ OR finding_comments.marker_round IS NOT excluded.marker_round
586
+ OR finding_comments.marker_key IS NOT excluded.marker_key`,
587
+ );
588
+ let changed = 0;
589
+ const seen = this.ts();
590
+ this.store.db.transaction(() => {
591
+ for (const c of comments) {
592
+ if (c.user !== this.deps.botLogin || c.in_reply_to_id !== null) continue;
593
+ if (this.deps.personMode && !carriesOwnMarker(c.body)) continue;
594
+ const marker = parseFindingMarker(c.body);
595
+ const head = commentHead(c.body);
596
+ const info = stmt.run({
597
+ comment_id: c.id,
598
+ pr,
599
+ review_id: c.pull_request_review_id ?? null,
600
+ path: c.path || null,
601
+ // The line it was POSTED on — `line` drifts as the branch moves.
602
+ line: c.original_line ?? c.line ?? null,
603
+ head: [head.tags.map((t) => `[${t}]`).join(" "), head.title]
604
+ .filter(Boolean)
605
+ .join(" ")
606
+ .slice(0, 300),
607
+ marker_round: marker?.round_no ?? null,
608
+ marker_key: marker?.key ?? null,
609
+ created_at: c.created_at ?? null,
610
+ seen_at: seen,
611
+ });
612
+ changed += info.changes;
613
+ }
614
+ })();
615
+ return changed;
616
+ }
617
+
618
+ /** Link facts the state database already holds: the root a human reply
619
+ * names (when the registry says the bot posted it — its path, and its line
620
+ * as GitHub reported it then, which may have drifted from the posted one)
621
+ * and a record's own `thread_id`. Never overwrites a row: the bot's own
622
+ * comment, when read, replaces these facts (and re-opens their link). */
623
+ private ingestLocalFacts(pr: number, posted: readonly RoundFindingRow[]): void {
624
+ const insert = this.store.db.prepare(
625
+ `INSERT OR IGNORE INTO finding_comments (comment_id, pr, path, line, created_at, source, seen_at)
626
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
627
+ );
628
+ // The registry knows when the bot posted the comment. Without it a
629
+ // reply- or record-sourced row has no posting time, and a fate rule
630
+ // falls back to the round's record time — a later round's, on a link
631
+ // the registry contradicts.
632
+ const backfill = this.store.db.prepare(
633
+ "UPDATE finding_comments SET created_at = ? WHERE comment_id = ? AND created_at IS NULL",
634
+ );
635
+ const seen = this.ts();
636
+ const add = (commentId: number, path: string | null, line: number | null, source: string) => {
637
+ const postedAt = this.store.botCommentPostedAt(commentId, "review_comment");
638
+ insert.run(commentId, pr, path, line, postedAt, source, seen);
639
+ if (postedAt !== null) backfill.run(postedAt, commentId);
640
+ };
641
+ this.store.db.transaction(() => {
642
+ for (const c of this.store.prComments(pr)) {
643
+ if (c.source !== "review_comment" || c.in_reply_to_id === null) continue;
644
+ if (!this.store.isBotComment(c.in_reply_to_id, "review_comment")) continue;
645
+ add(c.in_reply_to_id, c.path, c.line, "reply");
646
+ }
647
+ for (const f of posted) {
648
+ if (f.thread_id === null || !this.store.isBotComment(f.thread_id, "review_comment"))
649
+ continue;
650
+ add(f.thread_id, f.path, f.line, "record");
651
+ }
652
+ })();
653
+ }
654
+
655
+ // ---- linking ------------------------------------------------------------------
656
+
657
+ /** The rounds of a PR that have a parsed findings record. */
658
+ private recordedRounds(pr: number, rows: readonly RoundFindingRow[]) {
659
+ const out = new Map<number, { at: number; head: string | null }>();
660
+ for (const r of rows) {
661
+ const at = ms(r.recorded_at) ?? 0;
662
+ const prev = out.get(r.round_no);
663
+ out.set(r.round_no, {
664
+ at: prev ? Math.min(prev.at, at) : at,
665
+ head: prev?.head || r.head_sha || null,
666
+ });
667
+ }
668
+ // A record with no entries leaves no rows — its event is the evidence
669
+ // the round was recorded at all.
670
+ for (const e of this.store.prEvents(pr, ["findings_recorded"])) {
671
+ const round = intOf(e.payload.round_no);
672
+ if (round === null || out.has(round)) continue;
673
+ out.set(round, { at: ms(e.ts) ?? 0, head: null });
674
+ }
675
+ // A head the rows do not carry: the round's posted review's commit.
676
+ for (const r of this.store.roundsFor(pr)) {
677
+ const rec = out.get(r.round_no);
678
+ if (rec && !rec.head && r.gh_commit_id) rec.head = r.gh_commit_id;
679
+ }
680
+ return out;
681
+ }
682
+
683
+ /**
684
+ * Link every bot comment of a PR to its finding (see the module header for
685
+ * the order). A marker link is final. Every other comment is resolved
686
+ * AGAIN on every capture, from its facts alone, so the result never
687
+ * depends on the order comments are visited or on what an earlier pass
688
+ * linked.
689
+ *
690
+ * * The round: the one the comment's review id names, else the one the
691
+ * registry (`bot_comments.round_no`) attributes it to. When either is
692
+ * known, ONLY that round's findings are candidates — a comment whose
693
+ * round holds no posted finding at its path and line is unmatched,
694
+ * never linked to another round's finding. Only a comment
695
+ * neither names falls back to the first round recorded after it was
696
+ * posted.
697
+ * * A finding two comments could claim by fallback is NEITHER's, and one
698
+ * a marker claims is the marker's.
699
+ *
700
+ * Returns the counts after.
701
+ */
702
+ linkComments(pr: number): { linked: number; unmatched: number } {
703
+ const rows = this.store.roundFindings(pr);
704
+ const posted = rows.filter((r) => r.disposition === "posted");
705
+ this.ingestLocalFacts(pr, posted);
706
+ const comments = this.store.db
707
+ .prepare("SELECT * FROM finding_comments WHERE pr = ? ORDER BY comment_id")
708
+ .all(pr) as FindingCommentRow[];
709
+ const byKey = new Map(rows.map((r) => [fkey(r.round_no, r.finding_key), r]));
710
+ const recorded = this.recordedRounds(pr, rows);
711
+ const reviewRound = new Map<number, number>();
712
+ for (const r of this.store.roundsFor(pr))
713
+ if (r.gh_review_id !== null) reviewRound.set(r.gh_review_id, r.round_no);
714
+
715
+ const update = this.store.db.prepare(
716
+ "UPDATE finding_comments SET round_no = ?, finding_key = ?, via = ?, linked_at = ? WHERE comment_id = ?",
717
+ );
718
+ const unlink = this.store.db.prepare(
719
+ "UPDATE finding_comments SET round_no = NULL, finding_key = NULL, via = NULL, linked_at = NULL WHERE comment_id = ?",
720
+ );
721
+ const at = this.ts();
722
+ const resolve = (c: FindingCommentRow): { f: RoundFindingRow; via: LinkVia } | null => {
723
+ if (c.marker_round !== null && c.marker_key !== null) {
724
+ const f = byKey.get(fkey(c.marker_round, c.marker_key));
725
+ if (f) return { f, via: "marker" };
726
+ // Its round's record is not parsed yet: wait for it rather than guess.
727
+ if (!recorded.has(c.marker_round)) return null;
728
+ // Parsed, and the key is not in it (the record was rewritten after
729
+ // rendering): the fallback below may still place it.
730
+ }
731
+ const byThread = posted.filter((f) => f.thread_id === c.comment_id);
732
+ if (byThread.length === 1) return { f: byThread[0]!, via: "thread_id" };
733
+ if (c.path === null || c.line === null) return null;
734
+ const here = posted.filter((f) => f.path === c.path && f.line === c.line);
735
+ if (here.length === 0) return null;
736
+ const exactRound = c.review_id !== null ? reviewRound.get(c.review_id) : undefined;
737
+ const named =
738
+ exactRound ?? this.store.botCommentRound(c.comment_id, "review_comment") ?? undefined;
739
+ let pool: RoundFindingRow[];
740
+ if (named !== undefined) pool = here.filter((f) => f.round_no === named);
741
+ else {
742
+ // The comment's round is the first whose record was parsed after the
743
+ // comment was posted (a record is written after posting).
744
+ const rounds = [...new Set(here.map((f) => f.round_no))].sort((a, b) => a - b);
745
+ const created = ms(c.created_at);
746
+ const round =
747
+ created === null
748
+ ? rounds.length === 1
749
+ ? rounds[0]
750
+ : undefined
751
+ : rounds.find((r) => (recorded.get(r)?.at ?? -Infinity) >= created);
752
+ pool = round === undefined ? [] : here.filter((f) => f.round_no === round);
753
+ }
754
+ if (pool.length === 1) return { f: pool[0]!, via: "path_line" };
755
+ if (pool.length === 0 || c.head === null) return null;
756
+ const head = commentHead(c.head);
757
+ const byTitle = pool.filter((f) => f.title && normTitle(f.title) === normTitle(head.title));
758
+ if (byTitle.length === 1) return { f: byTitle[0]!, via: "path_line_title" };
759
+ const tag = head.tags.find((t) => t !== "ask your human" && t !== "deferred") ?? head.tags[0];
760
+ const byTag = pool.filter((f) => f.posted_as && bareTag(f.posted_as) === tag);
761
+ if (byTag.length === 1) return { f: byTag[0]!, via: "path_line_tag" };
762
+ return null;
763
+ };
764
+
765
+ // Pass 1: every comment's own answer, from its facts alone.
766
+ const hits = new Map<number, { f: RoundFindingRow; via: LinkVia } | null>();
767
+ const claims = new Map<string, { marker: number[]; fallback: number[] }>();
768
+ for (const c of comments) {
769
+ const hit =
770
+ c.via === "marker" && c.round_no !== null && c.finding_key !== null
771
+ ? (() => {
772
+ const f = byKey.get(fkey(c.round_no, c.finding_key));
773
+ return f ? { f, via: "marker" as LinkVia } : resolve(c);
774
+ })()
775
+ : resolve(c);
776
+ hits.set(c.comment_id, hit);
777
+ if (!hit) continue;
778
+ const k = fkey(hit.f.round_no, hit.f.finding_key);
779
+ const claim = claims.get(k) ?? { marker: [], fallback: [] };
780
+ (hit.via === "marker" ? claim.marker : claim.fallback).push(c.comment_id);
781
+ claims.set(k, claim);
782
+ }
783
+ // Pass 2: a finding goes to its marker (the earliest comment, when a
784
+ // retried posting stamped one marker twice), else to its one fallback
785
+ // claimant; contested by fallbacks alone, it goes to nobody.
786
+ this.store.db.transaction(() => {
787
+ for (const c of comments) {
788
+ const hit = hits.get(c.comment_id) ?? null;
789
+ let keep: { f: RoundFindingRow; via: LinkVia } | null = null;
790
+ if (hit) {
791
+ const claim = claims.get(fkey(hit.f.round_no, hit.f.finding_key))!;
792
+ if (hit.via === "marker") keep = Math.min(...claim.marker) === c.comment_id ? hit : null;
793
+ else keep = claim.marker.length === 0 && claim.fallback.length === 1 ? hit : null;
794
+ }
795
+ if (!keep) {
796
+ if (c.finding_key !== null) unlink.run(c.comment_id);
797
+ continue;
798
+ }
799
+ if (
800
+ c.finding_key === keep.f.finding_key &&
801
+ c.round_no === keep.f.round_no &&
802
+ c.via === keep.via
803
+ )
804
+ continue;
805
+ update.run(keep.f.round_no, keep.f.finding_key, keep.via, at, c.comment_id);
806
+ }
807
+ })();
808
+ const counts = this.store.db
809
+ .prepare(
810
+ "SELECT COUNT(finding_key) AS linked, COUNT(*) - COUNT(finding_key) AS unmatched FROM finding_comments WHERE pr = ?",
811
+ )
812
+ .get(pr) as { linked: number; unmatched: number };
813
+ return counts;
814
+ }
815
+
816
+ // ---- fates ----------------------------------------------------------------
817
+
818
+ private loadFacts(pr: number): PrFacts {
819
+ const rows = this.store.roundFindings(pr);
820
+ const posted = rows.filter((r) => r.disposition === "posted");
821
+ const byRound = new Map<number, RoundFindingRow[]>();
822
+ for (const r of rows) byRound.set(r.round_no, [...(byRound.get(r.round_no) ?? []), r]);
823
+ const comments = this.commentsFor(pr);
824
+ const links = new Map<string, FindingCommentRow>();
825
+ for (const c of comments)
826
+ if (c.round_no !== null && c.finding_key !== null)
827
+ links.set(fkey(c.round_no, c.finding_key), c);
828
+ const twins = markerTwins(comments);
829
+ const replies = new Map<number, PrCommentRow[]>();
830
+ for (const c of this.store.prComments(pr)) {
831
+ if (c.source !== "review_comment" || c.in_reply_to_id === null) continue;
832
+ if (c.author === this.deps.botLogin && (!this.deps.personMode || carriesOwnMarker(c.body)))
833
+ continue;
834
+ replies.set(c.in_reply_to_id, [...(replies.get(c.in_reply_to_id) ?? []), c]);
835
+ }
836
+ const events = this.store.prEvents(pr, [
837
+ "pingpong_decision_applied",
838
+ "pingpong_thread_resolved",
839
+ "head_push",
840
+ "pr_closed",
841
+ ]);
842
+ const facts: PrFacts = {
843
+ rows,
844
+ priorReports: this.store.priorFindingChecks(pr),
845
+ posted,
846
+ byRound,
847
+ recorded: this.recordedRounds(pr, rows),
848
+ rounds: this.store.roundsFor(pr),
849
+ links,
850
+ twins,
851
+ replies,
852
+ decisions: [],
853
+ resolves: [],
854
+ pushes: [],
855
+ currentHead: null,
856
+ closedAt: null,
857
+ };
858
+ for (const e of events) {
859
+ const at = ms(e.ts) ?? 0;
860
+ const thread = intOf(e.payload.thread_id);
861
+ const comment = intOf(e.payload.comment_id);
862
+ if (e.kind === "pingpong_decision_applied" && thread !== null && comment !== null)
863
+ facts.decisions.push({
864
+ at,
865
+ thread_id: thread,
866
+ comment_id: comment,
867
+ resolution: typeof e.payload.resolution === "string" ? e.payload.resolution : "",
868
+ outcome: typeof e.payload.outcome === "string" ? e.payload.outcome : null,
869
+ });
870
+ else if (e.kind === "pingpong_thread_resolved" && thread !== null && comment !== null)
871
+ facts.resolves.push({
872
+ at,
873
+ thread_id: thread,
874
+ comment_id: comment,
875
+ // A value that is neither null nor a known string is no resolve.
876
+ skipped:
877
+ e.payload.skipped === undefined || e.payload.skipped === null
878
+ ? null
879
+ : typeof e.payload.skipped === "string"
880
+ ? e.payload.skipped
881
+ : "unreadable",
882
+ });
883
+ else if (e.kind === "head_push" && typeof e.payload.head_sha === "string")
884
+ facts.pushes.push({ at, head_sha: e.payload.head_sha });
885
+ else if (e.kind === "pr_closed") facts.closedAt = at;
886
+ }
887
+ // Closed is the PR's state NOW: a PR reopened since its close is open.
888
+ const row = this.store.getPr(pr);
889
+ facts.currentHead = row?.head_sha || null;
890
+ if (row && row.state !== "closed") facts.closedAt = null;
891
+ else if (row && facts.closedAt === null) facts.closedAt = ms(row.updated_at) ?? 0;
892
+ return facts;
893
+ }
894
+
895
+ /** Later-round entries that are this finding again (see the module
896
+ * header). Strong: the entry names its thread or its `settled:` thread, or
897
+ * names its round in `raised_in_round` with the same path and is THIS
898
+ * finding's by key, else by being the one nearest same-lens entry of that
899
+ * round. Weak — the NEXT round only — the same path and lens near the same
900
+ * line, whoever it belongs to. */
901
+ private continuations(
902
+ f: RoundFindingRow,
903
+ facts: PrFacts,
904
+ threads: readonly number[],
905
+ ): { strong: RoundFindingRow[]; next: { round: number; weak: RoundFindingRow[] } | null } {
906
+ const ownRound = (facts.byRound.get(f.round_no) ?? []).filter(isPostedRow);
907
+ const strong: RoundFindingRow[] = [];
908
+ for (const e of facts.rows) {
909
+ if (e.round_no <= f.round_no) continue;
910
+ const word = (e.word ?? "").trim().toLowerCase();
911
+ if (threads.some((t) => e.thread_id === t || word === `settled:${t}`)) {
912
+ strong.push(e);
913
+ continue;
914
+ }
915
+ if (e.raised_in_round !== f.round_no || !f.path || e.path !== f.path) continue;
916
+ if (predecessorOf(ownRound, e) === f.finding_key) strong.push(e);
917
+ }
918
+ const nextRound = [...facts.recorded.keys()]
919
+ .filter((r) => r > f.round_no)
920
+ .sort((a, b) => a - b)[0];
921
+ if (nextRound === undefined) return { strong, next: null };
922
+ const weak = (facts.byRound.get(nextRound) ?? []).filter((e) => sameSite(f, e));
923
+ return { strong, next: { round: nextRound, weak } };
924
+ }
925
+
926
+ private async touched(f: RoundFindingRow, to: string | null): Promise<boolean | null> {
927
+ if (!this.deps.git || !f.path || !f.head_sha || !to) return null;
928
+ try {
929
+ return await this.deps.git.anchorTouched(f.path, f.line, f.head_sha, to);
930
+ } catch {
931
+ return null;
932
+ }
933
+ }
934
+
935
+ /** The heads a fix of `f` can live in: the PR's head now, the heads of the
936
+ * rounds after `f`'s, and every pushed head, newest first — never `f`'s own. */
937
+ private laterHeads(f: RoundFindingRow, facts: PrFacts): string[] {
938
+ const out: string[] = [];
939
+ const add = (h: string | null | undefined) => {
940
+ if (h && h !== f.head_sha && !out.includes(h)) out.push(h);
941
+ };
942
+ add(facts.currentHead);
943
+ for (const r of [...facts.recorded.keys()].sort((a, b) => b - a))
944
+ if (r > f.round_no) add(facts.recorded.get(r)?.head);
945
+ for (const p of [...facts.pushes].reverse()) add(p.head_sha);
946
+ return out.slice(0, MAX_LATER_HEADS);
947
+ }
948
+
949
+ /** Does a human reply cite a commit made after `f`'s head (see `CitedFix`)?
950
+ * Every reply is read, each on its own. */
951
+ private async citedFix(
952
+ f: RoundFindingRow,
953
+ facts: PrFacts,
954
+ replies: readonly PrCommentRow[],
955
+ ): Promise<CitedFixes> {
956
+ const heads = this.laterHeads(f, facts);
957
+ const byReply = new Map<number, CitedFix["state"]>();
958
+ let latest: Extract<CitedFix, { state: "verified" }> | null = null;
959
+ let unverified = false;
960
+ for (const r of replies) {
961
+ const own = await this.replyCitesFix(f, heads, r);
962
+ byReply.set(r.comment_id, own.state);
963
+ if (own.state === "verified") {
964
+ if (latest === null || replyMs(r) >= replyMs(latest.reply)) latest = own;
965
+ } else if (own.state === "unverified") unverified = true;
966
+ }
967
+ const overall: CitedFix = latest ?? (unverified ? { state: "unverified" } : { state: "none" });
968
+ return { overall, byReply };
969
+ }
970
+
971
+ /** One reply's citation: the first of its shas git confirms is a commit
972
+ * after `f`'s head, else unverified when git could not answer for one. */
973
+ private async replyCitesFix(
974
+ f: RoundFindingRow,
975
+ heads: readonly string[],
976
+ r: PrCommentRow,
977
+ ): Promise<CitedFix> {
978
+ let unverified = false;
979
+ for (const sha of citedShas(r.body)) {
980
+ if (!this.deps.git || !f.head_sha || heads.length === 0) {
981
+ unverified = true;
982
+ continue;
983
+ }
984
+ for (const to of heads) {
985
+ let answer: boolean | null;
986
+ try {
987
+ answer = await this.deps.git.introducedBetween(sha, f.head_sha, to);
988
+ } catch {
989
+ answer = null;
990
+ }
991
+ if (answer === true) return { state: "verified", sha, reply: r };
992
+ if (answer === null) unverified = true;
993
+ }
994
+ }
995
+ return unverified ? { state: "unverified" } : { state: "none" };
996
+ }
997
+
998
+ private async evidenceFor(
999
+ f: RoundFindingRow,
1000
+ facts: PrFacts,
1001
+ memo: Map<string, Promise<ChainEvidence>>,
1002
+ ): Promise<ChainEvidence> {
1003
+ const k = fkey(f.round_no, f.finding_key);
1004
+ let p = memo.get(k);
1005
+ if (!p) {
1006
+ p = this.computeEvidence(f, facts, memo);
1007
+ memo.set(k, p);
1008
+ }
1009
+ return p;
1010
+ }
1011
+
1012
+ private async computeEvidence(
1013
+ f: RoundFindingRow,
1014
+ facts: PrFacts,
1015
+ memo: Map<string, Promise<ChainEvidence>>,
1016
+ ): Promise<ChainEvidence> {
1017
+ const evidence: Evidence[] = [];
1018
+ const link = facts.links.get(fkey(f.round_no, f.finding_key)) ?? null;
1019
+ const thread = link?.comment_id ?? null;
1020
+ /** Every thread of this finding: its linked comment's, and its twins'. */
1021
+ const threads = thread === null ? [] : [thread, ...(facts.twins.get(thread) ?? [])];
1022
+ const postedAt =
1023
+ ms(link?.created_at) ?? facts.recorded.get(f.round_no)?.at ?? ms(f.recorded_at) ?? 0;
1024
+ // Backfilled replies and marker twins interleave by when they were written.
1025
+ const replies = threads
1026
+ .flatMap((t) => facts.replies.get(t) ?? [])
1027
+ .sort((a, b) => replyMs(a) - replyMs(b) || a.comment_id - b.comment_id);
1028
+ /** A human reply in the thread, no later than `at`, that accepts the
1029
+ * point (`concedesPoint`) — the latest one, or null. */
1030
+ const humanConceded = (at: number): PrCommentRow | null =>
1031
+ [...replies].reverse().find((r) => replyMs(r) <= at && concedesPoint(r.body)) ?? null;
1032
+
1033
+ // 1. A human reply that cites a commit made after the review — "Fixed in
1034
+ // 3f2a9c1", checked in git. One git cannot check is no evidence, and no
1035
+ // "nothing was cited" either.
1036
+ const cites = await this.citedFix(f, facts, replies);
1037
+ const cited = cites.overall;
1038
+ const fixCited = cited.state === "verified";
1039
+ if (cited.state === "verified")
1040
+ evidence.push({
1041
+ fate: "fixed",
1042
+ basis: "sha_cited",
1043
+ at: replyMs(cited.reply),
1044
+ detail: `reply ${cited.reply.comment_id} cites ${cited.sha}, a commit after the review`,
1045
+ });
1046
+
1047
+ // 2. The session's thread decisions.
1048
+ const decided = new Set<number>();
1049
+ if (thread !== null) {
1050
+ for (const d of facts.decisions.filter((x) => threads.includes(x.thread_id))) {
1051
+ const fate: DecisiveFate | null =
1052
+ d.outcome === "fixed"
1053
+ ? "fixed"
1054
+ : d.outcome === "refuted"
1055
+ ? "refuted_conceded"
1056
+ : d.outcome === "held"
1057
+ ? "upheld_after_reply"
1058
+ : null;
1059
+ if (fate) {
1060
+ decided.add(d.comment_id);
1061
+ evidence.push({
1062
+ fate,
1063
+ basis: "pingpong_outcome",
1064
+ at: d.at,
1065
+ detail: `thread decision on reply ${d.comment_id}: ${d.outcome}`,
1066
+ });
1067
+ } else if (d.resolution === "keep_open") {
1068
+ // Keeping a thread open right after "Fixed in <sha>" is the
1069
+ // session not having re-verified the new head yet — not the bot
1070
+ // holding against a human. When the human reply it answered (the
1071
+ // decision's own comment id) cites a commit: a fix git verified
1072
+ // stands (nothing is added — `sha_cited` is the evidence); one git
1073
+ // cannot check yet leaves the finding undecided. Another reply's
1074
+ // citation decides nothing here.
1075
+ const answered = replies.find((r) => r.comment_id === d.comment_id) ?? null;
1076
+ const citesSha = answered !== null && citedShas(answered.body).length > 0;
1077
+ const answeredCite = answered
1078
+ ? (cites.byReply.get(answered.comment_id) ?? "none")
1079
+ : "none";
1080
+ if (citesSha && answeredCite === "verified") continue;
1081
+ evidence.push(
1082
+ citesSha && answeredCite === "unverified"
1083
+ ? {
1084
+ fate: "unknown",
1085
+ basis: "conflict:kept_open_after_unverified_sha",
1086
+ at: d.at,
1087
+ detail: `kept open after reply ${answered.comment_id} cited a commit git could not check`,
1088
+ }
1089
+ : {
1090
+ fate: "upheld_after_reply",
1091
+ basis: "kept_open",
1092
+ at: d.at,
1093
+ detail: `thread decision on reply ${d.comment_id}: keep_open`,
1094
+ },
1095
+ );
1096
+ }
1097
+ }
1098
+ // 3. The session's resolves, where its decision did not say why. A
1099
+ // thread a human had already resolved is still the session's resolve
1100
+ // (it decided `resolve`), at the latest at the event's time.
1101
+ for (const r of facts.resolves.filter(
1102
+ (x) => threads.includes(x.thread_id) && resolveCounts(x.skipped),
1103
+ )) {
1104
+ if (decided.has(r.comment_id)) continue;
1105
+ const who = r.skipped === "already-resolved" ? " (a human had resolved it first)" : "";
1106
+ if (fixCited) {
1107
+ evidence.push({
1108
+ fate: "fixed",
1109
+ basis: "resolved_after_cited_fix",
1110
+ at: r.at,
1111
+ detail: `resolved after a reply cited a commit${who}`,
1112
+ });
1113
+ continue;
1114
+ }
1115
+ const since = facts.pushes.filter((p) => p.at > postedAt && p.at <= r.at);
1116
+ const head = since[since.length - 1]?.head_sha ?? null;
1117
+ if (head === null) {
1118
+ if (cited.state === "unverified") continue;
1119
+ // Push events carry observation times. A head that differs now may
1120
+ // have landed before this resolve and only been captured afterwards.
1121
+ const observedHead =
1122
+ facts.pushes.filter((p) => p.at <= postedAt).at(-1)?.head_sha ?? f.head_sha;
1123
+ if (
1124
+ !facts.currentHead ||
1125
+ !observedHead ||
1126
+ facts.currentHead.toLowerCase() !== observedHead.toLowerCase()
1127
+ ) {
1128
+ evidence.push({
1129
+ fate: "unknown",
1130
+ basis: "conflict:resolved_unobserved_head",
1131
+ at: r.at,
1132
+ detail: `resolved without a push observed before it${who}, but the current head does not confirm the reviewed head`,
1133
+ });
1134
+ continue;
1135
+ }
1136
+ const accepted = humanConceded(r.at);
1137
+ evidence.push(
1138
+ accepted
1139
+ ? {
1140
+ fate: "unknown",
1141
+ basis: "conflict:resolved_no_change",
1142
+ at: r.at,
1143
+ detail: `resolved with no push since the review${who}, but reply ${accepted.comment_id} accepts the point`,
1144
+ }
1145
+ : {
1146
+ fate: "refuted_conceded",
1147
+ basis: "resolved_no_change",
1148
+ at: r.at,
1149
+ detail: `resolved with no push since the review${who}`,
1150
+ },
1151
+ );
1152
+ continue;
1153
+ }
1154
+ if ((await this.touched(f, head)) === true)
1155
+ evidence.push({
1156
+ fate: "fixed",
1157
+ basis: "resolved_after_push",
1158
+ at: r.at,
1159
+ detail: `resolved after ${head.slice(0, 12)} changed the anchored lines${who}`,
1160
+ });
1161
+ }
1162
+ }
1163
+
1164
+ // 4. Later rounds. Only a strong match is this finding's evidence.
1165
+ const { strong, next } = this.continuations(f, facts, threads);
1166
+ const conceded = strong.filter((e) => e.disposition === "conceded");
1167
+ for (const e of conceded) {
1168
+ const head = facts.recorded.get(e.round_no)?.head ?? e.head_sha ?? null;
1169
+ const changed = await this.touched(f, head);
1170
+ const at = facts.recorded.get(e.round_no)?.at ?? ms(e.recorded_at) ?? 0;
1171
+ if (fixCited || changed === true)
1172
+ evidence.push({
1173
+ fate: "fixed",
1174
+ basis: "conceded_after_fix",
1175
+ at,
1176
+ detail: `round ${e.round_no} recorded it conceded after ${fixCited ? "a cited commit" : "the anchored lines changed"}`,
1177
+ });
1178
+ else if (changed === false && cited.state === "none") {
1179
+ const accepted = humanConceded(at);
1180
+ evidence.push(
1181
+ accepted
1182
+ ? {
1183
+ fate: "unknown",
1184
+ basis: "conflict:conceded_unchanged",
1185
+ at,
1186
+ detail: `round ${e.round_no} recorded it conceded with the anchored lines unchanged, but reply ${accepted.comment_id} accepts the point (a fix may sit away from the anchor)`,
1187
+ }
1188
+ : {
1189
+ fate: "refuted_conceded",
1190
+ basis: "conceded_unchanged",
1191
+ at,
1192
+ detail: `round ${e.round_no} recorded it conceded with the anchored lines unchanged`,
1193
+ },
1194
+ );
1195
+ }
1196
+ }
1197
+
1198
+ // Posted again, provably the same finding: CARRIED — this row inherits the
1199
+ // later one's evidence. Only a strong match carries: a weak one (same
1200
+ // path and lens, nearby) may be a different finding, so it only keeps
1201
+ // this one from reading "gone".
1202
+ const reposted =
1203
+ strong.filter((e) => e.disposition === "posted").sort((a, b) => a.round_no - b.round_no)[0] ??
1204
+ null;
1205
+ let carriedTo: RoundFindingRow | null = null;
1206
+ if (reposted) {
1207
+ carriedTo = reposted;
1208
+ const againAt = facts.recorded.get(reposted.round_no)?.at ?? ms(reposted.recorded_at) ?? 0;
1209
+ if (replies.some((r) => replyMs(r) < againAt))
1210
+ evidence.push({
1211
+ fate: "upheld_after_reply",
1212
+ basis: "reposted_after_reply",
1213
+ at: againAt,
1214
+ detail: `round ${reposted.round_no} posted it again after a human reply`,
1215
+ });
1216
+ } else if (
1217
+ next &&
1218
+ conceded.length === 0 &&
1219
+ next.weak.length === 0 &&
1220
+ !strong.some((e) => e.round_no === next.round) &&
1221
+ !facts.priorReports.some(
1222
+ (report) =>
1223
+ report.round_no === f.round_no &&
1224
+ report.finding_key === f.finding_key &&
1225
+ report.checked_round === next.round &&
1226
+ (report.status === "open" || report.status === "unverifiable"),
1227
+ )
1228
+ ) {
1229
+ // Gone at the next round. Fixed only if the code at its anchor changed.
1230
+ const head = facts.recorded.get(next.round)?.head ?? null;
1231
+ if ((await this.touched(f, head)) === true)
1232
+ evidence.push({
1233
+ fate: "fixed",
1234
+ basis: "gone_after_change",
1235
+ at: facts.recorded.get(next.round)?.at ?? 0,
1236
+ detail: `gone at round ${next.round}, whose head changed the anchored lines`,
1237
+ });
1238
+ }
1239
+ if (carriedTo) {
1240
+ const successor = carriedTo;
1241
+ const later = await this.evidenceFor(successor, facts, memo);
1242
+ evidence.push(
1243
+ ...later.evidence.map((e) => ({
1244
+ ...e,
1245
+ basis: e.basis.startsWith("carried:") ? e.basis : `carried:${e.basis}`,
1246
+ detail: `round ${successor.round_no} ${successor.finding_key}: ${e.detail}`,
1247
+ })),
1248
+ );
1249
+ }
1250
+ return {
1251
+ evidence,
1252
+ comment: thread,
1253
+ replies: replies.length,
1254
+ carriedTo: carriedTo ? carriedToRef(carriedTo.round_no, carriedTo.finding_key) : null,
1255
+ };
1256
+ }
1257
+
1258
+ /** Record the whole-review signals of a PR, one row per signal. */
1259
+ private recordReviewOutcomes(pr: number, rounds: readonly RoundRow[]): void {
1260
+ const upsert = this.store.db.prepare(
1261
+ `INSERT INTO review_outcomes (signal_id, pr, kind, round_no, gh_review_id, at, recorded_at)
1262
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1263
+ ON CONFLICT(signal_id) DO UPDATE SET round_no = excluded.round_no,
1264
+ gh_review_id = excluded.gh_review_id`,
1265
+ );
1266
+ const at = this.ts();
1267
+ for (const s of this.store.feedbackSignalsFor(pr)) {
1268
+ if (s.kind === "review_set_aside" && s.review_id !== null) {
1269
+ const round = rounds.find((r) => r.gh_review_id === s.review_id) ?? null;
1270
+ upsert.run(s.id, pr, "set_aside", round?.round_no ?? null, s.review_id, s.created_at, at);
1271
+ } else if (s.kind === "merged_over_block") {
1272
+ // The review that stood: the latest decided round before the merge,
1273
+ // and only if it decided REQUEST_CHANGES — otherwise unknown.
1274
+ const signalAt = ms(s.created_at) ?? Infinity;
1275
+ const decided = rounds
1276
+ .filter(
1277
+ (r) =>
1278
+ (r.verdict === "APPROVE" || r.verdict === "REQUEST_CHANGES") &&
1279
+ (ms(r.finished_at ?? r.started_at) ?? Infinity) <= signalAt,
1280
+ )
1281
+ .sort((a, b) => a.round_no - b.round_no);
1282
+ const last = decided[decided.length - 1];
1283
+ const stood = last && last.verdict === "REQUEST_CHANGES" ? last : null;
1284
+ upsert.run(
1285
+ s.id,
1286
+ pr,
1287
+ "merged_over_block",
1288
+ stood?.round_no ?? null,
1289
+ stood?.gh_review_id ?? null,
1290
+ s.created_at,
1291
+ at,
1292
+ );
1293
+ }
1294
+ }
1295
+ }
1296
+
1297
+ /** Link, then decide and record the fate of every posted finding of a PR. */
1298
+ async capturePr(pr: number): Promise<CaptureResult> {
1299
+ const link = this.linkComments(pr);
1300
+ const facts = this.loadFacts(pr);
1301
+ this.recordReviewOutcomes(pr, facts.rounds);
1302
+ // The fingerprint of exactly the inputs read above, taken BEFORE the first
1303
+ // await: a reply, push, decision or record that lands while the git reads
1304
+ // below are in flight was not read, so it must move the fingerprint away
1305
+ // from this mark and bring the PR back next pass.
1306
+ const signature = this.signature(pr);
1307
+ const activity = prActivity(this.store, pr);
1308
+ const memo = new Map<string, Promise<ChainEvidence>>();
1309
+ const decided: Array<{ f: RoundFindingRow; chain: ChainEvidence }> = [];
1310
+ for (const f of facts.posted)
1311
+ decided.push({ f, chain: await this.evidenceFor(f, facts, memo) });
1312
+
1313
+ const fates = Object.fromEntries(FINDING_FATES.map((x) => [x, 0])) as Record<
1314
+ FindingFate,
1315
+ number
1316
+ >;
1317
+ const at = this.ts();
1318
+ const existing = new Map(
1319
+ (
1320
+ this.store.db
1321
+ .prepare("SELECT round_no, finding_key, fate FROM finding_outcomes WHERE pr = ?")
1322
+ .all(pr) as Array<{ round_no: number; finding_key: string; fate: string }>
1323
+ ).map((r) => [fkey(r.round_no, r.finding_key), r.fate]),
1324
+ );
1325
+ const upsert = this.store.db.prepare(
1326
+ `INSERT INTO finding_outcomes
1327
+ (pr, round_no, finding_key, comment_id, fate, basis, evidence, decided_at, human_replies,
1328
+ carried_to, fate_changed_at, updated_at)
1329
+ VALUES (@pr, @round_no, @finding_key, @comment_id, @fate, @basis, @evidence, @decided_at,
1330
+ @human_replies, @carried_to, @at, @at)
1331
+ ON CONFLICT(pr, round_no, finding_key) DO UPDATE SET
1332
+ comment_id = excluded.comment_id, fate = excluded.fate, basis = excluded.basis,
1333
+ evidence = excluded.evidence, decided_at = excluded.decided_at,
1334
+ human_replies = excluded.human_replies, carried_to = excluded.carried_to,
1335
+ fate_changed_at = CASE WHEN finding_outcomes.fate = excluded.fate
1336
+ THEN finding_outcomes.fate_changed_at ELSE excluded.fate_changed_at END,
1337
+ updated_at = excluded.updated_at`,
1338
+ );
1339
+ let changed = 0;
1340
+ this.store.db.transaction(() => {
1341
+ for (const { f, chain } of decided) {
1342
+ const best = decideFate(chain.evidence);
1343
+ // A latest piece of evidence that is a CONFLICT reads `unknown`, PR
1344
+ // open or closed — its basis says why — and is decided by nobody.
1345
+ const fate: FindingFate = best
1346
+ ? best.fate
1347
+ : facts.closedAt !== null
1348
+ ? "open_at_close"
1349
+ : "unknown";
1350
+ const decidedAt =
1351
+ best && best.fate !== "unknown"
1352
+ ? new Date(best.at).toISOString()
1353
+ : !best && facts.closedAt !== null
1354
+ ? new Date(facts.closedAt).toISOString()
1355
+ : null;
1356
+ fates[fate]++;
1357
+ if (existing.get(fkey(f.round_no, f.finding_key)) !== fate) changed++;
1358
+ upsert.run({
1359
+ pr,
1360
+ round_no: f.round_no,
1361
+ finding_key: f.finding_key,
1362
+ comment_id: chain.comment,
1363
+ fate,
1364
+ basis: best ? best.basis : fate === "open_at_close" ? "closed" : null,
1365
+ evidence: best ? best.detail.slice(0, 500) : null,
1366
+ decided_at: decidedAt,
1367
+ human_replies: chain.replies,
1368
+ carried_to: chain.carriedTo,
1369
+ at,
1370
+ });
1371
+ }
1372
+ // A row whose finding is no longer posted (a re-recorded round) goes.
1373
+ const live = new Set(decided.map(({ f }) => fkey(f.round_no, f.finding_key)));
1374
+ const drop = this.store.db.prepare(
1375
+ "DELETE FROM finding_outcomes WHERE pr = ? AND round_no = ? AND finding_key = ?",
1376
+ );
1377
+ for (const k of existing.keys())
1378
+ if (!live.has(k)) {
1379
+ const [round, key] = k.split("\u0000");
1380
+ drop.run(pr, Number(round), key);
1381
+ }
1382
+ })();
1383
+ this.markCaptured(pr, signature, activity);
1384
+ return {
1385
+ posted: facts.posted.length,
1386
+ linked: link.linked,
1387
+ unmatched: link.unmatched,
1388
+ changed,
1389
+ fates,
1390
+ };
1391
+ }
1392
+
1393
+ // ---- the bounded pass -----------------------------------------------------
1394
+
1395
+ /** A fingerprint of everything a capture reads, so an unmoved PR is skipped. */
1396
+ signature(pr: number): string {
1397
+ const get = (sql: string) => this.store.db.prepare(sql).get(pr) as Record<string, unknown>;
1398
+ const row = this.store.getPr(pr);
1399
+ return JSON.stringify([
1400
+ OUTCOME_RULES_VERSION,
1401
+ this.deps.git ? 1 : 0,
1402
+ get("SELECT COUNT(*) c, MAX(id) m, MAX(recorded_at) r FROM round_findings WHERE pr = ?"),
1403
+ get("SELECT COUNT(*) c, MAX(id) m FROM pr_comments WHERE pr = ?"),
1404
+ get(
1405
+ "SELECT MAX(id) m FROM events WHERE pr = ? AND kind IN " +
1406
+ "('pingpong_decision_applied','pingpong_thread_resolved','head_push','pr_closed','findings_recorded')",
1407
+ ),
1408
+ get("SELECT COUNT(*) c, MAX(id) m FROM feedback_signals WHERE pr = ?"),
1409
+ this.store.priorFindingChecks(pr),
1410
+ get(
1411
+ // `seen_at` moves when a comment's facts change (its anchor or head
1412
+ // corrected), which can re-link it with every count unchanged.
1413
+ "SELECT COUNT(*) c, COUNT(finding_key) l, COUNT(marker_key) k, COUNT(review_id) v, MAX(seen_at) s FROM finding_comments WHERE pr = ?",
1414
+ ),
1415
+ get("SELECT COUNT(*) c FROM bot_comments WHERE pr = ?"),
1416
+ // a posting receipt landing later (gh_review_id) can settle a link
1417
+ get(
1418
+ "SELECT COUNT(*) c, COUNT(gh_review_id) v, COUNT(gh_commit_id) h, MAX(finished_at) f FROM rounds WHERE pr = ?",
1419
+ ),
1420
+ row?.state ?? null,
1421
+ row?.head_sha ?? null,
1422
+ ]);
1423
+ }
1424
+
1425
+ private markCaptured(pr: number, signature: string, activity: PrActivity): void {
1426
+ const at = this.ts();
1427
+ this.store.db.transaction(() => {
1428
+ this.store.db
1429
+ .prepare(
1430
+ `INSERT INTO outcome_capture_marks (pr, signature, captured_at) VALUES (?, ?, ?)
1431
+ ON CONFLICT(pr) DO UPDATE SET signature=excluded.signature, captured_at=excluded.captured_at`,
1432
+ )
1433
+ .run(pr, signature, at);
1434
+ this.store.db
1435
+ .prepare(
1436
+ `INSERT INTO outcome_capture_activity (pr, captured_at, comments_max, signals_max) VALUES (?, ?, ?, ?)
1437
+ ON CONFLICT(pr) DO UPDATE SET captured_at=excluded.captured_at, comments_max=excluded.comments_max, signals_max=excluded.signals_max`,
1438
+ )
1439
+ .run(pr, at, activity.comments, activity.signals);
1440
+ })();
1441
+ }
1442
+
1443
+ /**
1444
+ * One bounded pass: open PRs and PRs closed within the window, the ones
1445
+ * captured longest ago first, at most `maxPrs` and no new PR once
1446
+ * `budgetMs` of wall time is spent, skipping a PR whose inputs have not
1447
+ * moved (unless its capture is older than the recapture period). Never
1448
+ * throws; a PR that fails is named in `errors` and retried next pass.
1449
+ */
1450
+ async sweep(
1451
+ opts: { maxPrs?: number; force?: boolean; budgetMs?: number } = {},
1452
+ ): Promise<OutcomeSweepReport> {
1453
+ const report: OutcomeSweepReport = {
1454
+ considered: 0,
1455
+ captured: 0,
1456
+ skipped: 0,
1457
+ fates_changed: 0,
1458
+ errors: [],
1459
+ };
1460
+ const nowMs = this.now().getTime();
1461
+ const max = opts.maxPrs ?? OUTCOME_SWEEP_MAX_PRS;
1462
+ const started = performance.now();
1463
+ const budget = opts.budgetMs ?? OUTCOME_SWEEP_BUDGET_MS;
1464
+ try {
1465
+ const marks = new Map(
1466
+ (
1467
+ this.store.db
1468
+ .prepare("SELECT pr, signature, captured_at FROM outcome_capture_marks")
1469
+ .all() as Array<{
1470
+ pr: number;
1471
+ signature: string;
1472
+ captured_at: string;
1473
+ }>
1474
+ ).map((m) => [m.pr, m]),
1475
+ );
1476
+ const candidates = this.store
1477
+ .allPrs()
1478
+ .filter((p) => inCaptureWindow(p, nowMs))
1479
+ .sort((a, b) => {
1480
+ const ma = marks.get(a.number)?.captured_at ?? "";
1481
+ const mb = marks.get(b.number)?.captured_at ?? "";
1482
+ return ma < mb ? -1 : ma > mb ? 1 : a.number - b.number;
1483
+ });
1484
+ // Attempts, not successes: a capture that throws still spent its slot
1485
+ // and its time, so failures cannot run the pass past either limit.
1486
+ let attempted = 0;
1487
+ for (const p of candidates) {
1488
+ if (attempted >= max) break;
1489
+ if (attempted > 0 && performance.now() - started > budget) break;
1490
+ // A real event-loop turn per PR: a capture whose git answers are all
1491
+ // memoized (or with no git at all) never awaits real I/O, so a run of
1492
+ // them would otherwise block the loop as one long synchronous stretch.
1493
+ await yieldToLoop();
1494
+ report.considered++;
1495
+ try {
1496
+ const mark = marks.get(p.number);
1497
+ if (
1498
+ !opts.force &&
1499
+ mark &&
1500
+ nowMs - (ms(mark.captured_at) ?? 0) < OUTCOME_RECAPTURE_MS &&
1501
+ mark.signature === this.signature(p.number)
1502
+ ) {
1503
+ report.skipped++;
1504
+ continue;
1505
+ }
1506
+ attempted++;
1507
+ const r = await this.capturePr(p.number);
1508
+ report.captured++;
1509
+ report.fates_changed += r.changed;
1510
+ } catch (err) {
1511
+ report.errors.push(`outcomes #${p.number}: ${String(err)}`);
1512
+ }
1513
+ }
1514
+ } catch (err) {
1515
+ report.errors.push(`outcomes: ${String(err)}`);
1516
+ }
1517
+ if (report.fates_changed > 0 || report.errors.length > 0)
1518
+ this.store.logEvent("outcomes_captured", null, report);
1519
+ return report;
1520
+ }
1521
+
1522
+ // ---- reading back ---------------------------------------------------------
1523
+
1524
+ outcomesFor(pr: number): FindingOutcomeRow[] {
1525
+ return this.store.db
1526
+ .prepare("SELECT * FROM finding_outcomes WHERE pr = ? ORDER BY round_no, finding_key")
1527
+ .all(pr) as FindingOutcomeRow[];
1528
+ }
1529
+
1530
+ commentsFor(pr: number): FindingCommentRow[] {
1531
+ return this.store.db
1532
+ .prepare("SELECT * FROM finding_comments WHERE pr = ? ORDER BY comment_id")
1533
+ .all(pr) as FindingCommentRow[];
1534
+ }
1535
+
1536
+ reviewOutcomesFor(pr: number): ReviewOutcomeRow[] {
1537
+ return this.store.db
1538
+ .prepare("SELECT * FROM review_outcomes WHERE pr = ? ORDER BY signal_id")
1539
+ .all(pr) as ReviewOutcomeRow[];
1540
+ }
1541
+
1542
+ /**
1543
+ * The per-lens fate table over findings posted since `sinceIso`. A finding
1544
+ * several lenses raised counts once under each of them; one raised outside
1545
+ * any known lens counts under `(no lens)`. Whole-review signals are counted
1546
+ * per review, never per lens.
1547
+ */
1548
+ summary(sinceIso: string): OutcomeSummary {
1549
+ const rows = this.store.db
1550
+ .prepare(
1551
+ `SELECT f.lens_ids, f.lens, o.fate, o.human_replies,
1552
+ EXISTS (SELECT 1 FROM finding_comments c WHERE c.pr = f.pr AND c.round_no = f.round_no
1553
+ AND c.finding_key = f.finding_key) AS linked
1554
+ FROM round_findings f
1555
+ LEFT JOIN finding_outcomes o
1556
+ ON o.pr = f.pr AND o.round_no = f.round_no AND o.finding_key = f.finding_key
1557
+ WHERE f.disposition = 'posted' AND f.recorded_at >= ?`,
1558
+ )
1559
+ .all(sinceIso) as Array<{
1560
+ lens_ids: string | null;
1561
+ lens: string;
1562
+ fate: FindingFate | null;
1563
+ human_replies: number | null;
1564
+ linked: number;
1565
+ }>;
1566
+ const empty = (): LensOutcomeCounts => ({
1567
+ posted: 0,
1568
+ linked: 0,
1569
+ uncaptured: 0,
1570
+ ...(Object.fromEntries(FINDING_FATES.map((x) => [x, 0])) as Record<FindingFate, number>),
1571
+ });
1572
+ const byLens = new Map<string, LensOutcomeCounts>();
1573
+ const total = empty();
1574
+ let replied = 0;
1575
+ let repliedKnown = 0;
1576
+ for (const r of rows) {
1577
+ const ids = storedLensIds(r.lens_ids) ?? [];
1578
+ const count = (c: LensOutcomeCounts) => {
1579
+ c.posted++;
1580
+ if (r.linked) c.linked++;
1581
+ if (r.fate) c[r.fate]++;
1582
+ else c.uncaptured++;
1583
+ };
1584
+ for (const id of ids.length ? ids : ["(no lens)"]) {
1585
+ const c = byLens.get(id) ?? empty();
1586
+ count(c);
1587
+ byLens.set(id, c);
1588
+ }
1589
+ count(total);
1590
+ if ((r.human_replies ?? 0) > 0) {
1591
+ replied++;
1592
+ if (r.fate && r.fate !== "unknown" && r.fate !== "open_at_close") repliedKnown++;
1593
+ }
1594
+ }
1595
+ const reviews = this.store.db
1596
+ .prepare(
1597
+ "SELECT kind, COUNT(*) AS n FROM review_outcomes WHERE at >= ? GROUP BY kind ORDER BY kind",
1598
+ )
1599
+ .all(sinceIso) as Array<{ kind: ReviewOutcomeKind; n: number }>;
1600
+ return {
1601
+ since: sinceIso,
1602
+ total,
1603
+ lenses: [...byLens.entries()]
1604
+ .sort(([a, x], [b, y]) => y.posted - x.posted || a.localeCompare(b))
1605
+ .map(([lens, counts]) => ({ lens, ...counts })),
1606
+ replied_threads: replied,
1607
+ replied_known: repliedKnown,
1608
+ reviews: Object.fromEntries(reviews.map((r) => [r.kind, r.n])),
1609
+ };
1610
+ }
1611
+ }
1612
+
1613
+ interface ChainEvidence {
1614
+ evidence: Evidence[];
1615
+ comment: number | null;
1616
+ replies: number;
1617
+ carriedTo: string | null;
1618
+ }
1619
+
1620
+ export interface LensOutcomeCounts extends Record<FindingFate, number> {
1621
+ posted: number;
1622
+ linked: number;
1623
+ /** Posted findings no capture has reached yet. */
1624
+ uncaptured: number;
1625
+ }
1626
+
1627
+ export interface OutcomeSummary {
1628
+ since: string;
1629
+ total: LensOutcomeCounts;
1630
+ lenses: Array<{ lens: string } & LensOutcomeCounts>;
1631
+ /** Posted findings with at least one human reply in their thread … */
1632
+ replied_threads: number;
1633
+ /** … and how many of them have a fate other than unknown / open at close. */
1634
+ replied_known: number;
1635
+ reviews: Partial<Record<ReviewOutcomeKind, number>>;
1636
+ }
1637
+
1638
+ /** The summary as the operator reads it (`qcr outcomes`). */
1639
+ export function renderOutcomeSummary(s: OutcomeSummary): string {
1640
+ const pct = (n: number, d: number) => (d === 0 ? "—" : `${Math.round((100 * n) / d)}%`);
1641
+ const cols = [
1642
+ "posted",
1643
+ "linked",
1644
+ "fixed",
1645
+ "refuted",
1646
+ "upheld",
1647
+ "open@close",
1648
+ "unknown",
1649
+ "uncaptured",
1650
+ ];
1651
+ const line = (name: string, c: LensOutcomeCounts) =>
1652
+ [
1653
+ name.padEnd(24),
1654
+ ...[
1655
+ c.posted,
1656
+ c.linked,
1657
+ c.fixed,
1658
+ c.refuted_conceded,
1659
+ c.upheld_after_reply,
1660
+ c.open_at_close,
1661
+ c.unknown,
1662
+ c.uncaptured,
1663
+ ].map((n, i) => String(n).padStart(cols[i]!.length + 1)),
1664
+ ].join("");
1665
+ const out = [
1666
+ `Posted findings since ${s.since} — fate per lens (a finding several lenses raised counts under each).`,
1667
+ "",
1668
+ ["lens".padEnd(24), ...cols.map((c) => ` ${c}`)].join(""),
1669
+ ...s.lenses.map((l) => line(l.lens, l)),
1670
+ line("all findings", s.total),
1671
+ "",
1672
+ `linked to their comment: ${s.total.linked}/${s.total.posted} (${pct(s.total.linked, s.total.posted)})`,
1673
+ `human-replied threads with a known fate: ${s.replied_known}/${s.replied_threads} (${pct(s.replied_known, s.replied_threads)})`,
1674
+ `whole-review signals (per review, never per lens): set aside ${s.reviews.set_aside ?? 0}, merged over a block ${s.reviews.merged_over_block ?? 0}`,
1675
+ ];
1676
+ return out.join("\n");
1677
+ }