@quickchatai/quickreview 1.1.0 → 1.3.1

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 (471) hide show
  1. package/README.md +137 -57
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +60 -14
  4. package/deploy/ansible/README.md +20 -12
  5. package/deploy/ansible/inventory.example.ini +1 -1
  6. package/deploy/collect-secrets.sh +1 -1
  7. package/deploy/qcr-update.sh +1 -1
  8. package/dist/extensions/cli/home.js +14 -0
  9. package/dist/extensions/cli/home.js.map +1 -0
  10. package/dist/extensions/cli/login-route.js +5 -0
  11. package/dist/extensions/cli/login-route.js.map +1 -0
  12. package/dist/extensions/cli/selfimprove.js +18 -0
  13. package/dist/extensions/cli/selfimprove.js.map +1 -0
  14. package/dist/extensions/home.js +350 -0
  15. package/dist/extensions/home.js.map +1 -0
  16. package/dist/extensions/login-route.js +180 -0
  17. package/dist/extensions/login-route.js.map +1 -0
  18. package/dist/extensions/onboarding.js +35 -11
  19. package/dist/extensions/onboarding.js.map +1 -1
  20. package/dist/extensions/selfimprove.js +49 -0
  21. package/dist/extensions/selfimprove.js.map +1 -0
  22. package/dist/extensions/setup-scan.js +23 -592
  23. package/dist/extensions/setup-scan.js.map +1 -1
  24. package/dist/extensions/setup.js +2 -47
  25. package/dist/extensions/setup.js.map +1 -1
  26. package/dist/src/archive/restore.js +18 -15
  27. package/dist/src/archive/restore.js.map +1 -1
  28. package/dist/src/botworker.js +13 -2
  29. package/dist/src/botworker.js.map +1 -1
  30. package/dist/src/channelfs.js +3 -26
  31. package/dist/src/channelfs.js.map +1 -1
  32. package/dist/src/ciretry.js +2 -1
  33. package/dist/src/ciretry.js.map +1 -1
  34. package/dist/src/cli.js +54 -61
  35. package/dist/src/cli.js.map +1 -1
  36. package/dist/src/cliops.js +3 -2
  37. package/dist/src/cliops.js.map +1 -1
  38. package/dist/src/commands.js +25 -2
  39. package/dist/src/commands.js.map +1 -1
  40. package/dist/src/config.js +32 -2
  41. package/dist/src/config.js.map +1 -1
  42. package/dist/src/daemon.js +50 -8
  43. package/dist/src/daemon.js.map +1 -1
  44. package/dist/src/debuground.js +1 -0
  45. package/dist/src/debuground.js.map +1 -1
  46. package/dist/src/debugroundpi.js +6 -1
  47. package/dist/src/debugroundpi.js.map +1 -1
  48. package/dist/src/dispatch.js +5 -4
  49. package/dist/src/dispatch.js.map +1 -1
  50. package/dist/src/engine/caserunner-sdk.js +15 -251
  51. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  52. package/dist/src/engine/debugpi.js +17 -5
  53. package/dist/src/engine/debugpi.js.map +1 -1
  54. package/dist/src/execboundary.js +10 -3
  55. package/dist/src/execboundary.js.map +1 -1
  56. package/dist/src/github.js +51 -16
  57. package/dist/src/github.js.map +1 -1
  58. package/dist/src/githubtoken.js +346 -0
  59. package/dist/src/githubtoken.js.map +1 -0
  60. package/dist/src/home/adapters.js +349 -0
  61. package/dist/src/home/adapters.js.map +1 -0
  62. package/dist/src/home/advanced.js +318 -0
  63. package/dist/src/home/advanced.js.map +1 -0
  64. package/dist/src/home/advancedtools.js +57 -0
  65. package/dist/src/home/advancedtools.js.map +1 -0
  66. package/dist/src/home/autoreview.js +325 -0
  67. package/dist/src/home/autoreview.js.map +1 -0
  68. package/dist/src/home/daemonstatus.js +32 -0
  69. package/dist/src/home/daemonstatus.js.map +1 -0
  70. package/dist/src/home/devicelogin.js +61 -0
  71. package/dist/src/home/devicelogin.js.map +1 -0
  72. package/dist/src/home/launch.js +202 -0
  73. package/dist/src/home/launch.js.map +1 -0
  74. package/dist/src/home/loginowner.js +205 -0
  75. package/dist/src/home/loginowner.js.map +1 -0
  76. package/dist/src/home/memory.js +122 -0
  77. package/dist/src/home/memory.js.map +1 -0
  78. package/dist/src/home/model.js +811 -0
  79. package/dist/src/home/model.js.map +1 -0
  80. package/dist/src/home/panel.js +520 -0
  81. package/dist/src/home/panel.js.map +1 -0
  82. package/dist/src/home/ports.js +19 -0
  83. package/dist/src/home/ports.js.map +1 -0
  84. package/dist/src/home/review.js +163 -0
  85. package/dist/src/home/review.js.map +1 -0
  86. package/dist/src/home/reviews.js +478 -0
  87. package/dist/src/home/reviews.js.map +1 -0
  88. package/dist/src/home/setup.js +507 -0
  89. package/dist/src/home/setup.js.map +1 -0
  90. package/dist/src/home/setupadapters.js +214 -0
  91. package/dist/src/home/setupadapters.js.map +1 -0
  92. package/dist/src/home/setupcard.js +199 -0
  93. package/dist/src/home/setupcard.js.map +1 -0
  94. package/dist/src/home/setuptools.js +45 -0
  95. package/dist/src/home/setuptools.js.map +1 -0
  96. package/dist/src/home/spend.js +160 -0
  97. package/dist/src/home/spend.js.map +1 -0
  98. package/dist/src/home/switches.js +209 -0
  99. package/dist/src/home/switches.js.map +1 -0
  100. package/dist/src/home/switchon.js +439 -0
  101. package/dist/src/home/switchon.js.map +1 -0
  102. package/dist/src/home/toolnames.js +41 -0
  103. package/dist/src/home/toolnames.js.map +1 -0
  104. package/dist/src/home/tune.js +341 -0
  105. package/dist/src/home/tune.js.map +1 -0
  106. package/dist/src/home/words.js +7 -0
  107. package/dist/src/home/words.js.map +1 -0
  108. package/dist/src/homes.js +14 -0
  109. package/dist/src/homes.js.map +1 -1
  110. package/dist/src/install.js +1 -1
  111. package/dist/src/install.js.map +1 -1
  112. package/dist/src/ipc/workerspec.js +8 -0
  113. package/dist/src/ipc/workerspec.js.map +1 -1
  114. package/dist/src/live/publish.js +1 -1
  115. package/dist/src/live/publish.js.map +1 -1
  116. package/dist/src/livecli.js +1 -1
  117. package/dist/src/livecli.js.map +1 -1
  118. package/dist/src/loginhint.js +8 -3
  119. package/dist/src/loginhint.js.map +1 -1
  120. package/dist/src/loginrefresh/handover.js +121 -0
  121. package/dist/src/loginrefresh/handover.js.map +1 -0
  122. package/dist/src/loginrefresh/lock.js +54 -3
  123. package/dist/src/loginrefresh/lock.js.map +1 -1
  124. package/dist/src/loginrefresh/pirefresh.js +3 -0
  125. package/dist/src/loginrefresh/pirefresh.js.map +1 -1
  126. package/dist/src/loginrefresh/refresher.js +33 -5
  127. package/dist/src/loginrefresh/refresher.js.map +1 -1
  128. package/dist/src/loginrefresh/supervisor.js +18 -1
  129. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  130. package/dist/src/npmverbs.js +46 -14
  131. package/dist/src/npmverbs.js.map +1 -1
  132. package/dist/src/onboard/choices.js +35 -4
  133. package/dist/src/onboard/choices.js.map +1 -1
  134. package/dist/src/onboard/engine.js +32 -306
  135. package/dist/src/onboard/engine.js.map +1 -1
  136. package/dist/src/onboard/pipin.js +22 -0
  137. package/dist/src/onboard/pipin.js.map +1 -0
  138. package/dist/src/onboard/stepforward.js +41 -0
  139. package/dist/src/onboard/stepforward.js.map +1 -0
  140. package/dist/src/onboard/steps/appslug.js +4 -1
  141. package/dist/src/onboard/steps/appslug.js.map +1 -1
  142. package/dist/src/onboard/steps/bringup.js +3 -2
  143. package/dist/src/onboard/steps/bringup.js.map +1 -1
  144. package/dist/src/onboard/steps/compose.js +29 -0
  145. package/dist/src/onboard/steps/compose.js.map +1 -1
  146. package/dist/src/onboard/steps/configfill.js +120 -39
  147. package/dist/src/onboard/steps/configfill.js.map +1 -1
  148. package/dist/src/onboard/steps/githubaccount.js +595 -0
  149. package/dist/src/onboard/steps/githubaccount.js.map +1 -0
  150. package/dist/src/onboard/steps/githubapp.js +65 -7
  151. package/dist/src/onboard/steps/githubapp.js.map +1 -1
  152. package/dist/src/onboard/steps/golive.js +2 -0
  153. package/dist/src/onboard/steps/golive.js.map +1 -1
  154. package/dist/src/onboard/steps/index.js +11 -0
  155. package/dist/src/onboard/steps/index.js.map +1 -1
  156. package/dist/src/onboard/steps/ingress.js +80 -25
  157. package/dist/src/onboard/steps/ingress.js.map +1 -1
  158. package/dist/src/onboard/steps/llmauth.js +103 -30
  159. package/dist/src/onboard/steps/llmauth.js.map +1 -1
  160. package/dist/src/onboard/steps/loginshare.js +213 -0
  161. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  162. package/dist/src/onboard/steps/memory.js +67 -3
  163. package/dist/src/onboard/steps/memory.js.map +1 -1
  164. package/dist/src/onboard/steps/proof.js +366 -56
  165. package/dist/src/onboard/steps/proof.js.map +1 -1
  166. package/dist/src/onboard/steps/repoconfig.js +72 -0
  167. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  168. package/dist/src/onboard/steps/topology.js +6 -6
  169. package/dist/src/onboard/steps/topology.js.map +1 -1
  170. package/dist/src/onboard/steps/transport.js +9 -1
  171. package/dist/src/onboard/steps/transport.js.map +1 -1
  172. package/dist/src/onboard/steps/tuiroute.js +102 -0
  173. package/dist/src/onboard/steps/tuiroute.js.map +1 -0
  174. package/dist/src/onboard/steps/verifyup.js +17 -1
  175. package/dist/src/onboard/steps/verifyup.js.map +1 -1
  176. package/dist/src/onboard/toolnames.js +30 -0
  177. package/dist/src/onboard/toolnames.js.map +1 -0
  178. package/dist/src/operatorwrite.js +23 -0
  179. package/dist/src/operatorwrite.js.map +1 -0
  180. package/dist/src/outcomes.js +4 -1
  181. package/dist/src/outcomes.js.map +1 -1
  182. package/dist/src/piagentdir.js +13 -0
  183. package/dist/src/piagentdir.js.map +1 -0
  184. package/dist/src/pilogin.js +31 -16
  185. package/dist/src/pilogin.js.map +1 -1
  186. package/dist/src/posting-journal.js +2 -1
  187. package/dist/src/posting-journal.js.map +1 -1
  188. package/dist/src/posting-outbox.js +6 -2
  189. package/dist/src/posting-outbox.js.map +1 -1
  190. package/dist/src/posting-receipt.js +14 -1
  191. package/dist/src/posting-receipt.js.map +1 -1
  192. package/dist/src/qcrbin.js +16 -11
  193. package/dist/src/qcrbin.js.map +1 -1
  194. package/dist/src/recheck.js +2 -1
  195. package/dist/src/recheck.js.map +1 -1
  196. package/dist/src/reconcile.js +22 -2
  197. package/dist/src/reconcile.js.map +1 -1
  198. package/dist/src/reviewargs.js +122 -0
  199. package/dist/src/reviewargs.js.map +1 -0
  200. package/dist/src/reviewcli.js +7 -161
  201. package/dist/src/reviewcli.js.map +1 -1
  202. package/dist/src/reviewdetach.js +76 -11
  203. package/dist/src/reviewdetach.js.map +1 -1
  204. package/dist/src/reviewhistory.js +32 -6
  205. package/dist/src/reviewhistory.js.map +1 -1
  206. package/dist/src/reviewnow.js +45 -0
  207. package/dist/src/reviewnow.js.map +1 -0
  208. package/dist/src/scancli.js +39 -691
  209. package/dist/src/scancli.js.map +1 -1
  210. package/dist/src/selfimprove.js +66 -0
  211. package/dist/src/selfimprove.js.map +1 -0
  212. package/dist/src/setup/adapters.js +5 -223
  213. package/dist/src/setup/adapters.js.map +1 -1
  214. package/dist/src/setup/caserunner.js +15 -249
  215. package/dist/src/setup/caserunner.js.map +1 -1
  216. package/dist/src/setup/engine.js +32 -341
  217. package/dist/src/setup/engine.js.map +1 -1
  218. package/dist/src/setup/facts.js +3 -592
  219. package/dist/src/setup/facts.js.map +1 -1
  220. package/dist/src/setup/findings.js +3 -947
  221. package/dist/src/setup/findings.js.map +1 -1
  222. package/dist/src/setup/presentation.js +3 -8
  223. package/dist/src/setup/presentation.js.map +1 -1
  224. package/dist/src/setup/scan.js +3 -724
  225. package/dist/src/setup/scan.js.map +1 -1
  226. package/dist/src/setup/scanbuild.js +9 -92
  227. package/dist/src/setup/scanbuild.js.map +1 -1
  228. package/dist/src/setup/scanlease.js +3 -250
  229. package/dist/src/setup/scanlease.js.map +1 -1
  230. package/dist/src/setup/scanreport.js +3 -288
  231. package/dist/src/setup/scanreport.js.map +1 -1
  232. package/dist/src/setup/scanrun.js +3 -1732
  233. package/dist/src/setup/scanrun.js.map +1 -1
  234. package/dist/src/setup/scantree.js +3 -256
  235. package/dist/src/setup/scantree.js.map +1 -1
  236. package/dist/src/setup/stage2request.js +3 -258
  237. package/dist/src/setup/stage2request.js.map +1 -1
  238. package/dist/src/setup/unitcut.js +3 -436
  239. package/dist/src/setup/unitcut.js.map +1 -1
  240. package/dist/src/state.js +2 -0
  241. package/dist/src/state.js.map +1 -1
  242. package/dist/src/statemachine.js +4 -1
  243. package/dist/src/statemachine.js.map +1 -1
  244. package/dist/src/takeover.js +1 -0
  245. package/dist/src/takeover.js.map +1 -1
  246. package/dist/src/takeoverpi.js +15 -1
  247. package/dist/src/takeoverpi.js.map +1 -1
  248. package/dist/src/tui/app.js +49 -0
  249. package/dist/src/tui/app.js.map +1 -1
  250. package/dist/src/tui/board.js +88 -12
  251. package/dist/src/tui/board.js.map +1 -1
  252. package/dist/src/tui/cmdbar.js +4 -1
  253. package/dist/src/tui/cmdbar.js.map +1 -1
  254. package/dist/src/tui/companion.js +76 -11
  255. package/dist/src/tui/companion.js.map +1 -1
  256. package/dist/src/tui/connect.js +7 -1
  257. package/dist/src/tui/connect.js.map +1 -1
  258. package/dist/src/tui/deploytab.js +2 -2
  259. package/dist/src/tui/deploytab.js.map +1 -1
  260. package/dist/src/tui/filters.js +5 -2
  261. package/dist/src/tui/filters.js.map +1 -1
  262. package/dist/src/tui/ghread.js +61 -5
  263. package/dist/src/tui/ghread.js.map +1 -1
  264. package/dist/src/tui/index.js +109 -20
  265. package/dist/src/tui/index.js.map +1 -1
  266. package/dist/src/tui/memforward.js +39 -2
  267. package/dist/src/tui/memforward.js.map +1 -1
  268. package/dist/src/tui/memory.js +3 -16
  269. package/dist/src/tui/memory.js.map +1 -1
  270. package/dist/src/tui/memoryurl.js +22 -0
  271. package/dist/src/tui/memoryurl.js.map +1 -0
  272. package/dist/src/tui/model.js +25 -0
  273. package/dist/src/tui/model.js.map +1 -1
  274. package/dist/src/tui/prpause.js +7 -0
  275. package/dist/src/tui/prpause.js.map +1 -1
  276. package/dist/src/tui/remote.js +7 -1
  277. package/dist/src/tui/remote.js.map +1 -1
  278. package/dist/src/tui/route.js +143 -0
  279. package/dist/src/tui/route.js.map +1 -0
  280. package/dist/src/tui/setupstate.js +32 -0
  281. package/dist/src/tui/setupstate.js.map +1 -0
  282. package/dist/src/tui/sshtriage.js +102 -5
  283. package/dist/src/tui/sshtriage.js.map +1 -1
  284. package/dist/src/tui/theme.js +2 -1
  285. package/dist/src/tui/theme.js.map +1 -1
  286. package/dist/src/tui/unwedge.js +2 -12
  287. package/dist/src/tui/unwedge.js.map +1 -1
  288. package/dist/src/verdicts.js +17 -5
  289. package/dist/src/verdicts.js.map +1 -1
  290. package/dist/src/version.js +1 -1
  291. package/dist/src/version.js.map +1 -1
  292. package/dist/src/webhook.js +12 -1
  293. package/dist/src/webhook.js.map +1 -1
  294. package/dist/src/wire.js +34 -8
  295. package/dist/src/wire.js.map +1 -1
  296. package/dist/src/workerworld.js +11 -1
  297. package/dist/src/workerworld.js.map +1 -1
  298. package/docker/README.md +1 -1
  299. package/docker/companion/entrypoint.sh +20 -1
  300. package/docker/entrypoint.sh +57 -6
  301. package/docker-compose.yml +16 -1
  302. package/extensions/cli/home.ts +14 -0
  303. package/extensions/cli/login-route.ts +10 -0
  304. package/extensions/cli/selfimprove.ts +18 -0
  305. package/extensions/home.ts +484 -0
  306. package/extensions/login-route.ts +220 -0
  307. package/extensions/onboarding.ts +41 -11
  308. package/extensions/selfimprove.ts +65 -0
  309. package/extensions/setup-scan.ts +79 -766
  310. package/extensions/setup.ts +14 -70
  311. package/package.json +6 -4
  312. package/prompts/onboarding.md +58 -18
  313. package/schemas/review-result.v1.json +60 -0
  314. package/scripts/deploy.sh +8 -6
  315. package/scripts/mint-token.sh +25 -4
  316. package/scripts/qcr-host.sh +28 -5
  317. package/scripts/vm-ghcr-login.sh +7 -4
  318. package/src/archive/restore.ts +18 -21
  319. package/src/botworker.ts +13 -2
  320. package/src/channelfs.ts +3 -26
  321. package/src/ciretry.ts +2 -1
  322. package/src/cli.ts +59 -67
  323. package/src/cliops.ts +3 -2
  324. package/src/commands.ts +28 -2
  325. package/src/config.ts +62 -4
  326. package/src/daemon.ts +58 -7
  327. package/src/debuground.ts +3 -0
  328. package/src/debugroundpi.ts +12 -1
  329. package/src/dispatch.ts +5 -4
  330. package/src/engine/caserunner-sdk.ts +29 -314
  331. package/src/engine/debugpi.ts +17 -5
  332. package/src/execboundary.ts +16 -4
  333. package/src/github.ts +57 -15
  334. package/src/githubtoken.ts +435 -0
  335. package/src/home/adapters.ts +418 -0
  336. package/src/home/advanced.ts +421 -0
  337. package/src/home/advancedtools.ts +96 -0
  338. package/src/home/autoreview.ts +385 -0
  339. package/src/home/daemonstatus.ts +55 -0
  340. package/src/home/devicelogin.ts +107 -0
  341. package/src/home/launch.ts +257 -0
  342. package/src/home/loginowner.ts +248 -0
  343. package/src/home/memory.ts +176 -0
  344. package/src/home/model.ts +965 -0
  345. package/src/home/panel.ts +666 -0
  346. package/src/home/ports.ts +338 -0
  347. package/src/home/review.ts +206 -0
  348. package/src/home/reviews.ts +565 -0
  349. package/src/home/setup.ts +689 -0
  350. package/src/home/setupadapters.ts +217 -0
  351. package/src/home/setupcard.ts +288 -0
  352. package/src/home/setuptools.ts +57 -0
  353. package/src/home/spend.ts +194 -0
  354. package/src/home/switches.ts +293 -0
  355. package/src/home/switchon.ts +554 -0
  356. package/src/home/toolnames.ts +46 -0
  357. package/src/home/tune.ts +405 -0
  358. package/src/home/words.ts +8 -0
  359. package/src/homes.ts +17 -0
  360. package/src/install.ts +1 -1
  361. package/src/ipc/workerspec.ts +15 -0
  362. package/src/live/publish.ts +4 -1
  363. package/src/livecli.ts +6 -1
  364. package/src/loginhint.ts +8 -3
  365. package/src/loginrefresh/handover.ts +138 -0
  366. package/src/loginrefresh/lock.ts +72 -3
  367. package/src/loginrefresh/pirefresh.ts +3 -0
  368. package/src/loginrefresh/refresher.ts +34 -5
  369. package/src/loginrefresh/supervisor.ts +25 -2
  370. package/src/npmverbs.ts +55 -13
  371. package/src/onboard/choices.ts +40 -4
  372. package/src/onboard/engine.ts +70 -421
  373. package/src/onboard/pipin.ts +31 -0
  374. package/src/onboard/stepforward.ts +61 -0
  375. package/src/onboard/steps/appslug.ts +4 -1
  376. package/src/onboard/steps/bringup.ts +3 -2
  377. package/src/onboard/steps/compose.ts +31 -0
  378. package/src/onboard/steps/configfill.ts +144 -40
  379. package/src/onboard/steps/githubaccount.ts +716 -0
  380. package/src/onboard/steps/githubapp.ts +92 -8
  381. package/src/onboard/steps/golive.ts +2 -0
  382. package/src/onboard/steps/index.ts +11 -0
  383. package/src/onboard/steps/ingress.ts +98 -31
  384. package/src/onboard/steps/llmauth.ts +114 -29
  385. package/src/onboard/steps/loginshare.ts +258 -0
  386. package/src/onboard/steps/memory.ts +76 -3
  387. package/src/onboard/steps/proof.ts +473 -55
  388. package/src/onboard/steps/repoconfig.ts +85 -0
  389. package/src/onboard/steps/topology.ts +7 -6
  390. package/src/onboard/steps/transport.ts +15 -0
  391. package/src/onboard/steps/tuiroute.ts +119 -0
  392. package/src/onboard/steps/verifyup.ts +20 -1
  393. package/src/onboard/toolnames.ts +34 -0
  394. package/src/operatorwrite.ts +25 -0
  395. package/src/outcomes.ts +8 -1
  396. package/src/piagentdir.ts +12 -0
  397. package/src/pilogin.ts +32 -16
  398. package/src/posting-journal.ts +6 -1
  399. package/src/posting-outbox.ts +10 -2
  400. package/src/posting-receipt.ts +17 -0
  401. package/src/qcrbin.ts +16 -11
  402. package/src/recheck.ts +2 -1
  403. package/src/reconcile.ts +29 -3
  404. package/src/reviewargs.ts +119 -0
  405. package/src/reviewcli.ts +6 -167
  406. package/src/reviewdetach.ts +107 -13
  407. package/src/reviewhistory.ts +20 -6
  408. package/src/reviewnow.ts +76 -0
  409. package/src/scancli.ts +75 -826
  410. package/src/selfimprove.ts +90 -0
  411. package/src/setup/adapters.ts +13 -221
  412. package/src/setup/caserunner.ts +34 -273
  413. package/src/setup/engine.ts +62 -496
  414. package/src/setup/facts.ts +3 -627
  415. package/src/setup/findings.ts +3 -1264
  416. package/src/setup/presentation.ts +3 -18
  417. package/src/setup/scan.ts +3 -1033
  418. package/src/setup/scanbuild.ts +20 -114
  419. package/src/setup/scanlease.ts +3 -297
  420. package/src/setup/scanreport.ts +3 -373
  421. package/src/setup/scanrun.ts +3 -2268
  422. package/src/setup/scantree.ts +3 -259
  423. package/src/setup/stage2request.ts +3 -347
  424. package/src/setup/unitcut.ts +3 -523
  425. package/src/state.ts +43 -0
  426. package/src/statemachine.ts +4 -1
  427. package/src/takeover.ts +5 -0
  428. package/src/takeoverpi.ts +16 -2
  429. package/src/tui/app.ts +47 -0
  430. package/src/tui/board.ts +111 -14
  431. package/src/tui/cmdbar.ts +4 -1
  432. package/src/tui/companion.ts +80 -11
  433. package/src/tui/connect.ts +7 -1
  434. package/src/tui/deploytab.ts +2 -2
  435. package/src/tui/filters.ts +7 -3
  436. package/src/tui/ghread.ts +74 -7
  437. package/src/tui/index.ts +117 -23
  438. package/src/tui/memforward.ts +52 -3
  439. package/src/tui/memory.ts +3 -15
  440. package/src/tui/memoryurl.ts +22 -0
  441. package/src/tui/model.ts +36 -2
  442. package/src/tui/prpause.ts +11 -2
  443. package/src/tui/remote.ts +9 -1
  444. package/src/tui/route.ts +173 -0
  445. package/src/tui/setupstate.ts +56 -0
  446. package/src/tui/sshtriage.ts +127 -6
  447. package/src/tui/theme.ts +2 -1
  448. package/src/tui/unwedge.ts +2 -14
  449. package/src/verdicts.ts +23 -5
  450. package/src/version.ts +1 -1
  451. package/src/webhook.ts +10 -1
  452. package/src/wire.ts +42 -9
  453. package/src/workerworld.ts +16 -2
  454. package/dist/src/live/projection.js +0 -450
  455. package/dist/src/live/projection.js.map +0 -1
  456. package/dist/src/live/read.js +0 -60
  457. package/dist/src/live/read.js.map +0 -1
  458. package/dist/src/onboard/launch.js +0 -83
  459. package/dist/src/onboard/launch.js.map +0 -1
  460. package/dist/src/setup/ledger.js +0 -207
  461. package/dist/src/setup/ledger.js.map +0 -1
  462. package/dist/src/setup/stage2.js +0 -617
  463. package/dist/src/setup/stage2.js.map +0 -1
  464. package/dist/src/setup/textsniff.js +0 -89
  465. package/dist/src/setup/textsniff.js.map +0 -1
  466. package/src/live/projection.ts +0 -491
  467. package/src/live/read.ts +0 -86
  468. package/src/onboard/launch.ts +0 -115
  469. package/src/setup/ledger.ts +0 -324
  470. package/src/setup/stage2.ts +0 -926
  471. package/src/setup/textsniff.ts +0 -95
package/src/setup/scan.ts CHANGED
@@ -1,1033 +1,3 @@
1
- // Scan stage 1's rules, pure:
2
- // what a stage-1 case reads, what it is expected to cost, what the lens is
3
- // told, how its answer is read back, and a scan finding's identity. The
4
- // stateful half — the run, the ledger, the findings store — is scanrun.ts,
5
- // ledger.ts and findings.ts, every effect through a port.
6
- //
7
- // Stage 1 is `pass-scan` over every unit of the saved unit cut, one case per
8
- // unit (an oversized unit splits, its name kept; a single file over one case
9
- // is read in overlapping line ranges, one case each — `cutRanges`), at the
10
- // `[scan]` tier and effort. It never reads tests or migrations (they enter
11
- // only in stage 2, paired with their code), nor what no pass reads (lockfiles, generated
12
- // output, binaries, prose — `isSweepSkipped`), nor the operator's `[scan]
13
- // exclude` globs. A file the coverage ledger already holds at the same
14
- // content hash for `pass-scan` is not read again: that is how an interrupted
15
- // scan resumes, and why a finished one costs nothing to "re-run". Coverage is
16
- // read at the run's depth (`[scan] depth`): a deep run reads again a file only
17
- // a standard reading covers, and a deep reading covers both (ledger.ts).
18
- //
19
- // A delta re-scan is stage 1 over the files whose content changed since the
20
- // last scan (read by the ledger at another content, or new or different
21
- // against the tree the last scan read) and every file no complete reading
22
- // covers at its content — never read (a capped or stopped run's tail;
23
- // unreadable then, readable now) or cut short between ranges. Those are
24
- // owed, not skipped: a delta that left them out would leave a file no
25
- // lens ever read out of every delta for as long as it stays unchanged.
26
- import { planKey } from "@quickchatai/pi-quickreview/src/plankey.js";
27
- import { createHash } from "node:crypto";
28
- import { posix } from "node:path";
29
- import { globMatcher } from "@quickchatai/pi-quickreview/src/glob.js";
30
- import { isSweepSkipped, isTestFile } from "@quickchatai/pi-quickreview/src/heuristics.js";
31
- import {
32
- SCAN_DEEP_MAX_SUGGESTIONS,
33
- SCAN_FALSE_POSITIVE_CLASSES,
34
- SCAN_PASS,
35
- VALIDATE_PASS,
36
- } from "@quickchatai/pi-quickreview/src/include.js";
37
- import { SCAN_DEEP_LENSES } from "@quickchatai/pi-quickreview/src/passes.js";
38
- import {
39
- REACHABILITY_VALUES,
40
- reachabilityCappedSeverity,
41
- type Reachability,
42
- } from "@quickchatai/pi-quickreview/src/severity.js";
43
- import { isMigrationFile } from "./facts.js";
44
- import { assign, splitUnit, type CaseBudget, type UnitCut } from "./unitcut.js";
45
-
46
- /** The stage-1 lens (agents/scan/pass-scan.md). */
47
- export { SCAN_PASS };
48
-
49
- /** Lenses a stage-1 suggestion may NOT name (passes.ts), and the deep lenses
50
- * stage 1 may point stage 2 at. */
51
- export { DIFF_ONLY_LENSES, SCAN_DEEP_LENSES } from "@quickchatai/pi-quickreview/src/passes.js";
52
-
53
- export { isMigrationFile };
54
-
55
- export type ExclusionReason = "test" | "migration" | "unreadable-kind" | "excluded-glob";
56
-
57
- /** Why stage 1 does not read `path`, or null when it does. */
58
- export function stage1Exclusion(
59
- path: string,
60
- excluded: (p: string) => boolean,
61
- ): ExclusionReason | null {
62
- if (isSweepSkipped(path)) return "unreadable-kind";
63
- if (isTestFile(path)) return "test";
64
- if (isMigrationFile(path)) return "migration";
65
- if (excluded(path)) return "excluded-glob";
66
- return null;
67
- }
68
-
69
- // ---- the plan ----------------------------------------------------------------
70
-
71
- export interface TreeFile {
72
- path: string;
73
- /** The git blob hash — the content hash the ledger keys on. */
74
- blob: string;
75
- }
76
-
77
- export interface ScanCaseFile {
78
- path: string;
79
- blob: string;
80
- /** Lines this case READS of the file for sizing and price: the whole file,
81
- * or its `range`'s length. */
82
- lines: number;
83
- /** A file over one case is cut into line ranges (`cutRanges`): this case
84
- * reads the whole file for context but reports only inside `range`. */
85
- range?: FileRange;
86
- }
87
-
88
- /** One line range of a file too big for one case — 1-based, inclusive, and
89
- * `total` the file's own line count (what the ledger's coverage needs). */
90
- export interface FileRange {
91
- start: number;
92
- end: number;
93
- total: number;
94
- }
95
-
96
- /** Neighbouring ranges overlap by this many lines at most (a tenth of the
97
- * window below that), so a finding straddling a cut sits whole in one. */
98
- export const RANGE_OVERLAP_MAX = 40;
99
-
100
- /** A top-level declaration: a line a cut may fall just before. */
101
- const DECL_RE =
102
- /^(export\s+(default\s+)?)?(declare\s+)?(abstract\s+)?(async\s+)?(pub(\([a-z]+\))?\s+)?(def|class|function\*?|interface|type|enum|const|let|var|namespace|module|fn|func|impl|struct|trait|object)\b/;
103
- /** Lines that belong to the declaration below them (decorators, comments). */
104
- const LEAD_RE = /^(@\w|#|\/\/|\/\*|\s\*)/;
105
-
106
- /**
107
- * Cut a file of `total` lines into windows of at most `maxLines`: each
108
- * window ends just before a top-level declaration found in its second half
109
- * when the text is at hand (pulled above that declaration's decorators and
110
- * comments), else at the limit; the next starts a few lines earlier
111
- * (`RANGE_OVERLAP_MAX`), so neighbours overlap and nothing falls between
112
- * them. A file that fits is one window. Deterministic.
113
- */
114
- export function cutRanges(total: number, maxLines: number, text: string | null): FileRange[] {
115
- const n = Math.max(0, Math.floor(total));
116
- const w = Math.max(1, Math.floor(maxLines));
117
- if (n <= w) return [{ start: 1, end: Math.max(n, 1), total: n }];
118
- const overlap = Math.min(RANGE_OVERLAP_MAX, Math.floor(w / 10));
119
- const lines = text === null ? null : text.split("\n");
120
- const at = (l: number): string => (lines === null ? "" : (lines[l - 1] ?? ""));
121
- const out: FileRange[] = [];
122
- let s = 1;
123
- for (;;) {
124
- if (n - s + 1 <= w) {
125
- out.push({ start: s, end: n, total: n });
126
- return out;
127
- }
128
- let end = s + w - 1;
129
- if (lines !== null) {
130
- const floor = s + Math.ceil(w / 2);
131
- for (let l = end + 1; l > floor; l--) {
132
- if (!DECL_RE.test(at(l))) continue;
133
- let first = l;
134
- while (first - 1 > floor && LEAD_RE.test(at(first - 1))) first--;
135
- end = first - 1;
136
- break;
137
- }
138
- }
139
- out.push({ start: s, end, total: n });
140
- s = Math.max(end + 1 - overlap, s + 1);
141
- }
142
- }
143
-
144
- /** Does the union of `ranges` cover every line of `start..end`? */
145
- export function rangesCover(
146
- ranges: readonly { start: number; end: number }[],
147
- start: number,
148
- end: number,
149
- ): boolean {
150
- let reach = start - 1;
151
- for (const r of [...ranges].sort((a, b) => a.start - b.start)) {
152
- if (r.start > reach + 1) break;
153
- reach = Math.max(reach, r.end);
154
- if (reach >= end) return true;
155
- }
156
- return reach >= end;
157
- }
158
-
159
- export interface ScanCasePlan {
160
- /** The UNIT's name, never the part's. */
161
- unit: string;
162
- /** `<unit>` or `<unit>#<i>` — unitcut.ts's spelling. */
163
- id: string;
164
- part: number;
165
- parts: number;
166
- files: ScanCaseFile[];
167
- lines: number;
168
- }
169
-
170
- export interface Stage1Plan {
171
- cases: ScanCasePlan[];
172
- /** Files stage 1 skips, by reason. */
173
- excluded: Record<ExclusionReason, number>;
174
- /** Readable files the ledger already covers at this content. */
175
- covered: number;
176
- /** Files at this sha no unit of the saved cut owns (the repo outgrew it). */
177
- unassigned: string[];
178
- /** Files the tree lists but whose text cannot be read (binary, too big). */
179
- unreadable: number;
180
- /** Each of those files and why (`binary`, `too-big`, `unreadable` when the
181
- * tree cannot say) — reported, never silently absent from coverage. */
182
- skipped: SkippedFile[];
183
- /** Delta only: planned files whose content has NOT changed since the last
184
- * scan but that no complete reading covers at it — never read (the rest of
185
- * an unfinished scan, or unreadable then and readable now), or read only in
186
- * part. A delta owes them as much as the changed ones (0 for a full plan). */
187
- owed: number;
188
- }
189
-
190
- /** What a delta re-scan compares against. */
191
- export interface DeltaBasis {
192
- /** Each path's newest content hash in the ledger for `pass-scan`. */
193
- read: ReadonlyMap<string, string>;
194
- /** The tree the last scan read: path → blob. */
195
- baseline: ReadonlyMap<string, string>;
196
- }
197
-
198
- /** Has `f`'s content changed since the last scan? Newest read by the ledger
199
- * at another content, or absent from / different in the last scan's tree.
200
- * Not what a delta reads by: an unchanged file with no complete reading at
201
- * this content is owed too (planStage1). */
202
- export function changedSince(f: TreeFile, basis: DeltaBasis): boolean {
203
- const read = basis.read.get(f.path);
204
- if (read !== undefined && read !== f.blob) return true;
205
- const was = basis.baseline.get(f.path);
206
- return was === undefined || was !== f.blob;
207
- }
208
-
209
- /** Why a listed file has no text for a lens. */
210
- export type UnreadableReason = "binary" | "too-big" | "unreadable";
211
-
212
- export interface SkippedFile {
213
- path: string;
214
- why: UnreadableReason;
215
- }
216
-
217
- export interface PlanInput {
218
- cut: UnitCut;
219
- files: readonly TreeFile[];
220
- linesOf: (path: string) => number | null;
221
- /** Why `linesOf` answered null; absent = `unreadable` (the reason unknown). */
222
- whyUnreadable?: (path: string) => UnreadableReason | null;
223
- /** `path\0blob` of each file's newest ledger reading for `pass-scan`. */
224
- covered: ReadonlySet<string>;
225
- exclude: readonly string[];
226
- budget: CaseBudget;
227
- /** A delta re-scan: files changed since the last scan, and every file no
228
- * complete reading covers at its content (`covered`, `history`). */
229
- delta?: DeltaBasis;
230
- /** A file's text, where a range cut looks for declaration boundaries;
231
- * absent (or null) = fixed windows. */
232
- textOf?: (path: string) => string | null;
233
- /** `path\0blob` → the line ranges the ledger's newest, still-incomplete
234
- * reading holds for `pass-scan`: a range inside them is not read again. */
235
- partial?: ReadonlyMap<string, readonly { start: number; end: number }[]>;
236
- /** path → the ledger's newest `pass-scan` reading of it (any content):
237
- * what orders the cases. Absent: the plan's own (unit) order stands. */
238
- history?: ReadonlyMap<string, PriorReading>;
239
- }
240
-
241
- /** A file's newest reading as the plan orders by it (ledger.ts `readings`). */
242
- export interface PriorReading {
243
- blob: string;
244
- /** Whole file, or ranges covering every line. */
245
- complete: boolean;
246
- /** When that reading's newest row was written (ISO): among changed files
247
- * the stalest reading runs first. */
248
- at: string;
249
- }
250
-
251
- /**
252
- * Coverage first. The hard stop ends a run whose actuals outrun its estimate
253
- * before the plan's tail, so the tail must not be the same cases every run:
254
- * a file the ledger never read, or whose newest reading was cut short
255
- * between ranges, is tier 0; a file read before at other content is tier 1
256
- * (changed); anything else is tier 2. A case takes its best file's
257
- * tier; a tier keeps the plan's order.
258
- */
259
- export function readTier(
260
- f: { path: string; blob: string },
261
- history: ReadonlyMap<string, PriorReading>,
262
- ): 0 | 1 | 2 {
263
- const r = history.get(f.path);
264
- if (r === undefined || !r.complete) return 0;
265
- return r.blob !== f.blob ? 1 : 2;
266
- }
267
-
268
- export const coverKey = (path: string, blob: string): string => `${path}\0${blob}`;
269
-
270
- /** Stage 1 over the saved cut at one tree: every readable, uncovered,
271
- * non-excluded file (a delta the same set, counting what it owes unchanged
272
- * as `owed`), one case per
273
- * unit, split to fit — ordered coverage-first by `history` (readTier).
274
- * Deterministic. */
275
- export function planStage1(input: PlanInput): Stage1Plan {
276
- const excludedGlob = globMatcher(input.exclude);
277
- const excluded: Record<ExclusionReason, number> = {
278
- test: 0,
279
- migration: 0,
280
- "unreadable-kind": 0,
281
- "excluded-glob": 0,
282
- };
283
- const blobs = new Map(input.files.map((f) => [f.path, f.blob]));
284
- const lines = new Map<string, number>();
285
- let covered = 0;
286
- let unreadable = 0;
287
- const skipped: SkippedFile[] = [];
288
- const unchangedPaths = new Set<string>();
289
- const readable: string[] = [];
290
- // Files over one case: the ranges still to read, cut up front.
291
- const ranged = new Map<string, FileRange[]>();
292
- for (const f of [...input.files].sort((a, b) =>
293
- a.path < b.path ? -1 : a.path > b.path ? 1 : 0,
294
- )) {
295
- const why = stage1Exclusion(f.path, excludedGlob);
296
- if (why !== null) {
297
- excluded[why]++;
298
- continue;
299
- }
300
- const n = input.linesOf(f.path);
301
- if (n === null) {
302
- unreadable++;
303
- skipped.push({ path: f.path, why: input.whyUnreadable?.(f.path) ?? "unreadable" });
304
- continue;
305
- }
306
- if (input.covered.has(coverKey(f.path, f.blob))) {
307
- covered++;
308
- continue;
309
- }
310
- // A delta reads what changed since the last scan AND every file no
311
- // complete reading covers at this content — never read (the rest of an
312
- // unfinished scan; unreadable then, readable now), or cut short between
313
- // ranges. Past `covered`, that is every file left: a delta leaves out only
314
- // what an ordinary plan leaves out, and says how much it owes unchanged.
315
- // An unchanged file that was never read is owed, never skipped as
316
- // "unchanged".
317
- if (input.delta !== undefined && !changedSince(f, input.delta)) unchangedPaths.add(f.path);
318
- if (n > input.budget.maxLines) {
319
- const done = input.partial?.get(coverKey(f.path, f.blob)) ?? [];
320
- const todo = cutRanges(n, input.budget.maxLines, input.textOf?.(f.path) ?? null).filter(
321
- (r) => !rangesCover(done, r.start, r.end),
322
- );
323
- if (todo.length === 0) {
324
- covered++;
325
- continue;
326
- }
327
- ranged.set(f.path, todo);
328
- }
329
- lines.set(f.path, n);
330
- readable.push(f.path);
331
- }
332
- const a = assign(input.cut, readable);
333
- const cases: ScanCasePlan[] = [];
334
- for (const u of input.cut.units) {
335
- const owned = a.byUnit.get(u.name) ?? [];
336
- if (owned.length === 0) continue;
337
- // A file over one case is alone in its split case; it becomes one case
338
- // per range. Parts are numbered over the unit's final list.
339
- const parts: Array<{ files: ScanCaseFile[]; lines: number }> = [];
340
- for (const c of splitUnit(u.name, owned, (p) => lines.get(p) ?? 0, input.budget)) {
341
- const only = c.files.length === 1 ? ranged.get(c.files[0]!) : undefined;
342
- if (only !== undefined) {
343
- const p = c.files[0]!;
344
- for (const r of only) {
345
- parts.push({
346
- files: [{ path: p, blob: blobs.get(p)!, lines: r.end - r.start + 1, range: r }],
347
- lines: r.end - r.start + 1,
348
- });
349
- }
350
- continue;
351
- }
352
- parts.push({
353
- files: c.files.map((p) => ({ path: p, blob: blobs.get(p)!, lines: lines.get(p) ?? 0 })),
354
- lines: c.lines,
355
- });
356
- }
357
- parts.forEach((p, i) => {
358
- cases.push({
359
- unit: u.name,
360
- id: parts.length === 1 ? u.name : `${u.name}#${i + 1}`,
361
- part: i + 1,
362
- parts: parts.length,
363
- files: p.files,
364
- lines: p.lines,
365
- });
366
- });
367
- }
368
- const history = input.history;
369
- if (history !== undefined) {
370
- const rank = (c: ScanCasePlan, i: number) => {
371
- const tiers = c.files.map((f) => readTier(f, history));
372
- const t = Math.min(...tiers);
373
- // The stalest tier-1 reading of the case ("" sorts first; tier 0/2 unused).
374
- let at = "";
375
- if (t === 1) {
376
- const stamps = c.files.filter((_, k) => tiers[k] === 1).map((f) => history.get(f.path)!.at);
377
- at = stamps.reduce((m, x) => (x < m ? x : m));
378
- }
379
- return { c, i, t, at };
380
- };
381
- const ranked = cases.map(rank);
382
- ranked.sort((x, y) => x.t - y.t || (x.at < y.at ? -1 : x.at > y.at ? 1 : 0) || x.i - y.i);
383
- cases.splice(0, cases.length, ...ranked.map((r) => r.c));
384
- }
385
- const owed = new Set(
386
- cases.flatMap((c) => c.files.map((f) => f.path)).filter((p) => unchangedPaths.has(p)),
387
- ).size;
388
- return { cases, excluded, covered, unassigned: a.unassigned, unreadable, skipped, owed };
389
- }
390
-
391
- /**
392
- * The calibration pick: one case from each of up to `n` units spread across
393
- * the size range (by the unit's planned lines), so the measured rate is not
394
- * the rate of the smallest or the largest code alone. Deterministic.
395
- */
396
- export function calibrationCases(plan: readonly ScanCasePlan[], n: number): ScanCasePlan[] {
397
- const byUnit = new Map<string, ScanCasePlan[]>();
398
- for (const c of plan) byUnit.set(c.unit, [...(byUnit.get(c.unit) ?? []), c]);
399
- const units = [...byUnit.entries()]
400
- .map(([unit, cs]) => ({ unit, lines: cs.reduce((s, c) => s + c.lines, 0), first: cs[0]! }))
401
- .sort((a, b) => a.lines - b.lines || (a.unit < b.unit ? -1 : 1));
402
- if (units.length <= n) return units.map((u) => u.first);
403
- const picked = new Set<number>();
404
- for (let i = 0; i < n; i++) picked.add(Math.floor(((i + 1) * units.length) / (n + 1)));
405
- return [...picked].sort((a, b) => a - b).map((i) => units[i]!.first);
406
- }
407
-
408
- // ---- the price -----------------------------------------------------------------
409
-
410
- /** A measured stage-1 price: dollars per thousand lines read, from finished
411
- * cases of the SAME lens, tier, effort and validator setting. */
412
- export interface ScanRate {
413
- usd_per_kline: number;
414
- /** Priced cases it was measured over, and their lines and cost. */
415
- cases: number;
416
- lines: number;
417
- cost_usd: number;
418
- }
419
-
420
- export function caseEstimate(lines: number, rate: Pick<ScanRate, "usd_per_kline">): number {
421
- return (Math.max(lines, 1) / 1000) * rate.usd_per_kline;
422
- }
423
-
424
- /**
425
- * Run `lanes` copies of `lane` to the end. One that throws aborts the rest
426
- * and the throw is raised only once EVERY lane has stopped — never while a
427
- * sibling is still in a paid case, which would otherwise go on spending and
428
- * writing the run record after the run is settled and its slot released.
429
- */
430
- export async function drainLanes(
431
- lanes: number,
432
- lane: () => Promise<void>,
433
- abort: AbortController,
434
- ): Promise<void> {
435
- let failed: { err: unknown } | null = null;
436
- await Promise.all(
437
- Array.from({ length: lanes }, async () => {
438
- try {
439
- await lane();
440
- } catch (err) {
441
- failed ??= { err };
442
- abort.abort();
443
- }
444
- }),
445
- );
446
- if (failed !== null) throw (failed as { err: unknown }).err;
447
- }
448
-
449
- /** What a preview's key covers (packages/pi-plugin/src/plankey.ts, shared with `qcr review`). */
450
- export { planKey };
451
-
452
- // ---- the prompts -----------------------------------------------------------------
453
-
454
- export interface PromptContext {
455
- /** Absolute path of the tree at the scan sha. */
456
- root: string;
457
- sha: string;
458
- caseId: string;
459
- }
460
-
461
- /** Where a lens opens a tree file: its absolute path under the root. */
462
- export function treePath(root: string, path: string): string {
463
- return posix.join(root, path);
464
- }
465
-
466
- /** The line every scan task carries about paths: the lens runs outside the
467
- * tree (first-party cwd — the case runner), so a relative path it opens is
468
- * some other checkout's file, never the scanned one. */
469
- export const TREE_PATHS_NOTE =
470
- "Your working directory is NOT the tree: a relative path opens some other checkout's file. Open every file — focus files, callers, tests — by its absolute path under the tree root (each focus file below says where). Paths in your answer are relative to the tree root.";
471
-
472
- /** One focus file as a task names it: its repo-relative path (what an
473
- * answer anchors to), its size or range, and — given the tree root — the
474
- * absolute path to open it by. */
475
- export function focusLine(f: ScanCaseFile, root?: string): string {
476
- const open = root === undefined ? "" : ` — open ${treePath(root, f.path)}`;
477
- return f.range
478
- ? `- ${f.path} (lines ${f.range.start}-${f.range.end} of ${f.range.total} — read the whole file, report only inside lines ${f.range.start}-${f.range.end})${open}`
479
- : `- ${f.path} (${f.lines} lines)${open}`;
480
- }
481
-
482
- /** The note a case with a ranged file carries, or none. */
483
- export function rangeNote(files: readonly ScanCaseFile[]): string[] {
484
- const ranged = files.filter((f) => f.range);
485
- if (ranged.length === 0) return [];
486
- return [
487
- "",
488
- ...ranged.map(
489
- (f) =>
490
- `Focus range: ${f.path} lines ${f.range!.start}-${f.range!.end} of ${f.range!.total}. The file is larger than one case, so it is cut into line ranges and the other ranges are other cases: read the whole file for context, but report only findings anchored inside lines ${f.range!.start}-${f.range!.end} — anything outside it is dropped.`,
491
- ),
492
- ];
493
- }
494
-
495
- function fileList(c: ScanCasePlan, root: string): string {
496
- return c.files.map((f) => focusLine(f, root)).join("\n");
497
- }
498
-
499
- /** The stage-1 task string for one case. */
500
- export function scanCasePrompt(
501
- c: ScanCasePlan,
502
- ctx: PromptContext,
503
- deepLenses: readonly string[] = SCAN_DEEP_LENSES,
504
- ): string {
505
- const part = c.parts > 1 ? ` (part ${c.part} of ${c.parts})` : "";
506
- return [
507
- `SCAN CASE ${ctx.caseId} — stage 1, unit ${c.unit}${part}.`,
508
- `Scan sha: ${ctx.sha}`,
509
- `Tree root (the repository at the scan sha, read-only): ${ctx.root}`,
510
- TREE_PATHS_NOTE,
511
- "",
512
- `Focus files (${c.files.length}, ${c.lines} lines — read each in full):`,
513
- fileList(c, ctx.root),
514
- ...rangeNote(c.files),
515
- "",
516
- `Deep lenses you may name in SUGGEST: ${deepLenses.join(", ")}.`,
517
- "",
518
- `Answer with the findings block headed \`FINDINGS ${SCAN_PASS} SCAN ${ctx.caseId} @${ctx.sha}\` (or NONE), then the SUGGEST block.`,
519
- ].join("\n");
520
- }
521
-
522
- /** One candidate as a validator reads it, in the findings grammar — the
523
- * scan's skeptic and the replay's validator alike. */
524
- export function candidateItem(f: ScanCandidate): string {
525
- return [
526
- `- path: ${f.path}`,
527
- ` line: ${f.start === f.end ? f.end : `${f.start}-${f.end}`}`,
528
- ` kind: ${f.kind}`,
529
- // The severity the LENS wrote, not the capped one: the validator audits
530
- // reachability, and a corrected `routine` must be able to restore it —
531
- // its survivors are re-read, so the cap applies again on the way out.
532
- ` severity: ${f.capped_from ?? f.severity}`,
533
- ...(f.confidence !== null ? [` confidence: ${f.confidence}`] : []),
534
- ...(f.reachability !== null ? [` reachability: ${f.reachability}`] : []),
535
- ` claim: ${f.claim}`,
536
- ...(f.evidence ? [` evidence: ${f.evidence}`] : []),
537
- // The failure mechanism: the validator judges with it, and a survivor
538
- // is re-read from the validator's answer, so it must travel both ways.
539
- ...(f.detail ? [` detail: ${f.detail}`] : []),
540
- ...(f.fix ? [` fix: ${f.fix}`] : []),
541
- ].join("\n");
542
- }
543
-
544
- /** The validator's task string over one case's stage-1 candidates. */
545
- export function scanValidatorPrompt(
546
- c: ScanCasePlan,
547
- candidates: readonly ScanCandidate[],
548
- ctx: PromptContext,
549
- lens: string = SCAN_PASS,
550
- label = `stage 1, unit ${c.unit}`,
551
- ): string {
552
- const block = candidates.map(candidateItem).join("\n");
553
- return [
554
- `SCAN VALIDATION ${ctx.caseId} — ${label}.`,
555
- `Scan sha: ${ctx.sha}`,
556
- `Tree root (read-only): ${ctx.root}`,
557
- TREE_PATHS_NOTE,
558
- "",
559
- `The ${lens} lens read these files:`,
560
- fileList(c, ctx.root),
561
- "",
562
- `Its ${candidates.length} candidate finding(s):`,
563
- `FINDINGS ${lens} SCAN ${ctx.caseId} @${ctx.sha}`,
564
- block,
565
- "",
566
- `Judge every candidate against the files under the tree root, as the scan's skeptic: try to refute each one. Return the survivors as a findings block headed \`FINDINGS ${VALIDATE_PASS} SCAN ${ctx.caseId} @${ctx.sha}\` (keep each survivor's path, line, kind and detail, and add its \`validity:\` — confirmed or likely — and its \`scenario:\`; a confirmed scenario cites a \`path:line\` for every hop it traced, or it is read as likely), then your DOUBTED block for every other candidate. Nothing is killed in a scan: write no KILLED block.`,
567
- ].join("\n");
568
- }
569
-
570
- // ---- reading the answer ----------------------------------------------------------
571
-
572
- export const SEVERITIES = ["P0", "P1", "P2", "P3", "nit"] as const;
573
- export type ScanSeverity = (typeof SEVERITIES)[number];
574
-
575
- /** A finding's place: its file, its line range and its kind. */
576
- interface Placed {
577
- path: string;
578
- start: number;
579
- end: number;
580
- kind: string;
581
- }
582
-
583
- /**
584
- * The candidate a validator's survivor stands for: one not yet `used`, on the
585
- * same file with overlapping lines — the same kind first. One survivor stands
586
- * for at most one candidate, so a duplicate on the same lines or a neighbour
587
- * of another kind is never silently claimed by a survivor that did not mean
588
- * it. The scan and the replay attribute survivors through this one rule.
589
- */
590
- export function survivorSource<T extends Placed>(
591
- survivor: Placed,
592
- candidates: readonly T[],
593
- used: ReadonlySet<T>,
594
- ): T | undefined {
595
- const open = candidates.filter(
596
- (c) =>
597
- !used.has(c) &&
598
- c.path === survivor.path &&
599
- c.start <= survivor.end &&
600
- survivor.start <= c.end,
601
- );
602
- return open.find((c) => c.kind === survivor.kind) ?? open[0];
603
- }
604
-
605
- /** A stage-1 candidate as read back, anchored or not yet checked. */
606
- export interface ScanCandidate {
607
- path: string;
608
- start: number;
609
- end: number;
610
- kind: string;
611
- /** After the reachability cap (`capped_from` holds what the lens said). */
612
- severity: ScanSeverity;
613
- confidence: number | null;
614
- /** The lens's `reachability:` field, or null when it wrote none (read as
615
- * `routine`, exactly as a PR round reads it). */
616
- reachability: Reachability | null;
617
- /** The severity the lens wrote, when the reachability axis capped it —
618
- * the posting map's gate (`reachabilityCappedSeverity`): contrived never
619
- * blocks, plausible P2 is a question. Null when nothing was capped. */
620
- capped_from: ScanSeverity | null;
621
- claim: string;
622
- evidence: string | null;
623
- fix: string | null;
624
- detail: string | null;
625
- }
626
-
627
- export interface ScanSuggestion {
628
- lens: string;
629
- files: string[];
630
- reason: string;
631
- }
632
-
633
- export interface ParsedScanAnswer {
634
- candidates: ScanCandidate[];
635
- /** Items with no path, claim or line — never repaired. */
636
- dropped: number;
637
- suggestions: ScanSuggestion[];
638
- /** Suggestions naming no allowed lens or no file, or past the cap. */
639
- suggestions_dropped: number;
640
- /** The findings block said NONE. */
641
- none: boolean;
642
- }
643
-
644
- /** At most this many suggestions per case are kept — `SCAN_DEEP_MAX_SUGGESTIONS`
645
- * in a deep scan, where every focus file is to be named in one. */
646
- export const MAX_SUGGESTIONS = 5;
647
- export { SCAN_DEEP_MAX_SUGGESTIONS };
648
-
649
- const ITEM_FIELD = /^(\s*)(-\s+)?([a-z][a-z0-9_-]{0,40}):(?:\s(.*)|$)/i;
650
-
651
- /** Parse the `- key: value` items of one block (continuation lines join the
652
- * previous field; the first occurrence of a key wins). */
653
- function parseItems(text: string): Array<Map<string, string>> {
654
- const items: Array<Map<string, string>> = [];
655
- let cur: Map<string, string> | null = null;
656
- let last = "";
657
- let itemIndent = -1;
658
- for (const raw of text.split("\n")) {
659
- const line = raw.replace(/\s+$/, "");
660
- if (/^\s*```/.test(line) || line.trim() === "") continue;
661
- const indent = /^\s*/.exec(line)![0].length;
662
- const m = ITEM_FIELD.exec(line);
663
- if (m && m[2] !== undefined && (itemIndent < 0 || indent <= itemIndent)) {
664
- cur = new Map();
665
- items.push(cur);
666
- itemIndent = indent;
667
- last = m[3]!.toLowerCase();
668
- cur.set(last, (m[4] ?? "").trim());
669
- continue;
670
- }
671
- if (!cur || indent <= itemIndent) {
672
- cur = null;
673
- continue;
674
- }
675
- if (m) {
676
- last = m[3]!.toLowerCase();
677
- if (!cur.has(last)) cur.set(last, (m[4] ?? "").trim());
678
- else last = "";
679
- continue;
680
- }
681
- if (last) cur.set(last, `${cur.get(last) ?? ""} ${line.trim()}`.trim());
682
- }
683
- return items;
684
- }
685
-
686
- function lineRange(v: string | undefined): { start: number; end: number } | null {
687
- const m = /^\s*`?L?(\d+)(?:\s*[-–]\s*L?(\d+))?/.exec(v ?? "");
688
- if (!m) return null;
689
- const a = Number(m[1]);
690
- const b = m[2] !== undefined ? Number(m[2]) : a;
691
- if (!(a > 0) || b < a) return null;
692
- return { start: a, end: b };
693
- }
694
-
695
- function cleanPath(v: string, root: string | null): string {
696
- let p = v.trim().replace(/^`+|`+$/g, "");
697
- if (root && p.startsWith(`${root.replace(/\/+$/, "")}/`))
698
- p = p.slice(root.replace(/\/+$/, "").length + 1);
699
- return p.replace(/^\.?\/+/, "").replace(/:(\d+)(?:-\d+)?$/, "");
700
- }
701
-
702
- function severityOf(v: string | undefined): ScanSeverity {
703
- const s = (v ?? "").trim().replace(/[`*]/g, "");
704
- const m = /^(p[0-3])\b/i.exec(s);
705
- if (m) return m[1]!.toUpperCase() as ScanSeverity;
706
- return /^nit\b/i.test(s) ? "nit" : "P3";
707
- }
708
-
709
- function reachabilityOf(v: string | undefined): Reachability | null {
710
- const r =
711
- (v ?? "")
712
- .trim()
713
- .replace(/[`*]/g, "")
714
- .toLowerCase()
715
- .split(/[\s,;.(—–-]/)[0] ?? "";
716
- return (REACHABILITY_VALUES as readonly string[]).includes(r) ? (r as Reachability) : null;
717
- }
718
-
719
- /** One lowercase word naming the class — the identity half of a kind. */
720
- export function normalizeKind(v: string | undefined): string {
721
- const k = (v ?? "")
722
- .toLowerCase()
723
- .replace(/[`*]/g, "")
724
- .trim()
725
- .replace(/[^a-z0-9]+/g, "-")
726
- .replace(/^-+|-+$/g, "");
727
- return k === "" ? "unclassified" : k.slice(0, 40);
728
- }
729
-
730
- /** The findings of one block of text (the part before SUGGEST / DOUBTED /
731
- * KILLED). */
732
- function readFindings(
733
- text: string,
734
- root: string | null,
735
- ): { candidates: ScanCandidate[]; dropped: number } {
736
- const { read, dropped } = readFindingItems(text, root);
737
- return { candidates: read.map((r) => r.candidate), dropped };
738
- }
739
-
740
- /** `readFindings`, each candidate beside the raw fields it was read from. */
741
- function readFindingItems(
742
- text: string,
743
- root: string | null,
744
- ): { read: Array<{ candidate: ScanCandidate; fields: Map<string, string> }>; dropped: number } {
745
- const read: Array<{ candidate: ScanCandidate; fields: Map<string, string> }> = [];
746
- let dropped = 0;
747
- for (const f of parseItems(text)) {
748
- const path = cleanPath(f.get("path") ?? f.get("file") ?? "", root);
749
- const claim = (f.get("claim") ?? "").trim();
750
- const range =
751
- lineRange(f.get("line")) ?? lineRange(/:(\d+(?:-\d+)?)`?$/.exec(f.get("path") ?? "")?.[1]);
752
- if (path === "" || claim === "" || range === null) {
753
- dropped++;
754
- continue;
755
- }
756
- const conf = Number((f.get("confidence") ?? "").trim());
757
- const opt = (k: string): string | null => {
758
- const v = f.get(k)?.trim();
759
- return v ? v : null;
760
- };
761
- const said = severityOf(f.get("severity"));
762
- const reachability = reachabilityOf(f.get("reachability"));
763
- const severity = reachabilityCappedSeverity(said, reachability);
764
- const candidate: ScanCandidate = {
765
- path,
766
- ...range,
767
- kind: normalizeKind(f.get("kind")),
768
- severity,
769
- confidence: (f.get("confidence") ?? "").trim() !== "" && Number.isFinite(conf) ? conf : null,
770
- reachability,
771
- capped_from: severity === said ? null : said,
772
- claim,
773
- evidence: opt("evidence"),
774
- // A nit's remedy rides `request:` (the findings contract); it is the
775
- // candidate's fix when no `fix:` is written.
776
- fix: opt("fix") ?? opt("request"),
777
- detail: opt("detail"),
778
- };
779
- read.push({ candidate, fields: f });
780
- }
781
- return { read, dropped };
782
- }
783
-
784
- const BLOCK_RE = /^\s*(SUGGEST|DOUBTED|KILLED)\b.*$/m;
785
-
786
- /** The text before the first trailing block (SUGGEST / DOUBTED / KILLED). */
787
- function findingsPart(text: string): string {
788
- const m = BLOCK_RE.exec(text);
789
- return m ? text.slice(0, m.index) : text;
790
- }
791
-
792
- /** The body of one named trailing block, up to the next one. */
793
- function blockBody(text: string, name: string): string | null {
794
- const re = new RegExp(`^\\s*${name}\\b(.*)$`, "m");
795
- const m = re.exec(text);
796
- if (!m) return null;
797
- const rest = text.slice(m.index + m[0].length);
798
- const next = BLOCK_RE.exec(rest);
799
- return `${m[1] ?? ""}\n${next ? rest.slice(0, next.index) : rest}`;
800
- }
801
-
802
- /**
803
- * Read a `pass-scan` answer: its findings (line anchors required — a scan
804
- * finding without lines is dropped, per the scan-mode anchor rule) and its
805
- * deep-lens suggestions (lenses from `deepLenses` only, files as given — the
806
- * caller checks they exist; at most `maxSuggestions`, default
807
- * `MAX_SUGGESTIONS`). `root` strips an absolute tree prefix a lens wrote by
808
- * mistake.
809
- */
810
- export function parseScanAnswer(
811
- text: string,
812
- opts: { root?: string | null; deepLenses?: readonly string[]; maxSuggestions?: number } = {},
813
- ): ParsedScanAnswer {
814
- const root = opts.root ?? null;
815
- const lenses = opts.deepLenses ?? SCAN_DEEP_LENSES;
816
- const cap = opts.maxSuggestions ?? MAX_SUGGESTIONS;
817
- const head = findingsPart(text);
818
- const { candidates, dropped } = readFindings(head, root);
819
- const none = candidates.length === 0 && /^\s*NONE\s*$/m.test(head);
820
- const suggestions: ScanSuggestion[] = [];
821
- let suggestionsDropped = 0;
822
- const body = blockBody(text, "SUGGEST");
823
- if (body !== null && !/^\s*NONE\b/.test(body)) {
824
- for (const s of parseItems(body)) {
825
- const lens = (s.get("lens") ?? "").trim().replace(/[`*]/g, "");
826
- const files = (s.get("files") ?? s.get("file") ?? "")
827
- .split(",")
828
- .map((p) => cleanPath(p, root))
829
- .filter((p) => p !== "");
830
- const reason = (s.get("reason") ?? "").trim();
831
- if (
832
- !lenses.includes(lens) ||
833
- files.length === 0 ||
834
- reason === "" ||
835
- suggestions.length >= cap
836
- ) {
837
- suggestionsDropped++;
838
- continue;
839
- }
840
- suggestions.push({ lens, files: [...new Set(files)], reason });
841
- }
842
- }
843
- return { candidates, dropped, suggestions, suggestions_dropped: suggestionsDropped, none };
844
- }
845
-
846
- export interface DoubtLine {
847
- /** The lens the validator named on the line. */
848
- lens: string;
849
- path: string;
850
- line: number | null;
851
- word: string;
852
- reason: string;
853
- /** The false-positive class the skeptic named (`[<slug>]` leading the
854
- * reason — a SCAN_FALSE_POSITIVE_CLASSES slug), or null. */
855
- class: string | null;
856
- }
857
-
858
- /** The slugs a doubt may name as its class — the ONE class list. */
859
- export const SCAN_FALSE_POSITIVE_SLUGS: readonly string[] = SCAN_FALSE_POSITIVE_CLASSES.map(
860
- (c) => c.slug,
861
- );
862
-
863
- /** A `path:line` reference — a file with an extension, a colon, a line
864
- * (`src/tui/app.ts:369`, `webhook.ts:88`, `a.py:3-7`); or an extensionless
865
- * file named with its directory (`scripts/qcr:12`) or by one of the common
866
- * extensionless build/CI names (`Dockerfile:5`, `Makefile:30`) — a scan
867
- * reviews any repo, and those files are anchored like any other. */
868
- const PATH_LINE =
869
- /(?:[A-Za-z0-9_][\w./-]*\.[A-Za-z][A-Za-z0-9]*|[A-Za-z0-9_.][\w.-]*(?:\/[\w.-]+)+|\b(?:Dockerfile|Containerfile|Makefile|GNUmakefile|Jenkinsfile|Procfile|Gemfile|Rakefile|Vagrantfile|Brewfile|Justfile|justfile|Earthfile|Tiltfile))(?:\.[\w-]+)?:\d+/;
870
-
871
- /**
872
- * Whether a skeptic's scenario is anchored: it cites at least one `path:line`
873
- * of the path it traced. A `confirmed` scenario told as a prose chain of
874
- * names and arrows with no `path:line` hides the hop the skeptic did not
875
- * read — that hop is exactly the one it cannot cite. The engine checks that
876
- * structure; the rest of the tightening lives in SCAN_SKEPTIC_ADDENDUM.
877
- */
878
- export function scenarioIsAnchored(scenario: string): boolean {
879
- return PATH_LINE.test(scenario);
880
- }
881
-
882
- /** A survivor as the scan's skeptic sorted it. */
883
- export interface SkepticSurvivor extends ScanCandidate {
884
- /** `confirmed` only with a written `scenario:` that cites a `path:line`
885
- * (scenarioIsAnchored) — a demonstration not written down, or not anchored
886
- * in the code it traced, is not one; anything else that survived is
887
- * `likely` (the scenario kept, so the reader sees what was traced). */
888
- validity: "confirmed" | "likely";
889
- scenario: string | null;
890
- }
891
-
892
- /** One `- <lens> <path:line> — <word>: [<class>] <reason>` line per entry of
893
- * a DOUBTED or KILLED block; a line that does not parse is skipped. */
894
- function judgementLines(body: string | null, root: string | null): DoubtLine[] {
895
- const out: DoubtLine[] = [];
896
- if (body === null) return out;
897
- for (const raw of body.split("\n")) {
898
- const m = /^\s*-\s+(\S+)\s+(\S+?)(?::(\d+)(?:-\d+)?)?\s+[—–-]+\s+([a-z-]+):?\s*(.*)$/i.exec(
899
- raw,
900
- );
901
- if (!m) continue;
902
- let reason = (m[5] ?? "").trim();
903
- let cls: string | null = null;
904
- const c = /^\[\s*`?([a-z-]+)`?\s*\]\s*(.*)$/i.exec(reason);
905
- if (c && SCAN_FALSE_POSITIVE_SLUGS.includes(c[1]!.toLowerCase())) {
906
- cls = c[1]!.toLowerCase();
907
- reason = (c[2] ?? "").trim();
908
- }
909
- out.push({
910
- lens: m[1]!,
911
- path: cleanPath(m[2]!, root),
912
- line: m[3] !== undefined ? Number(m[3]) : null,
913
- word: m[4]!.toLowerCase(),
914
- reason,
915
- class: cls,
916
- });
917
- }
918
- return out;
919
- }
920
-
921
- /**
922
- * Read the validator's answer over a scan case — the scan's skeptic: the
923
- * survivors (its findings block — they carry the lens's anchors), each with
924
- * its tier (`validity: confirmed` with a `scenario:` citing a `path:line`,
925
- * else `likely`), its DOUBTED lines,
926
- * `- <lens> <path:line> — <word>: [<class>] <reason>`, and its KILLED lines
927
- * in the same shape. In a scan nothing is killed: the caller stores a KILLED
928
- * line as a doubt exactly like a DOUBTED one (the frozen validator's kill
929
- * paths — below-bar, restated, duplicate, a confidence under 0.5 — become a
930
- * doubtful mark, never a deletion).
931
- */
932
- export function parseValidatorAnswer(
933
- text: string,
934
- root: string | null,
935
- ): {
936
- survivors: SkepticSurvivor[];
937
- dropped: number;
938
- doubted: DoubtLine[];
939
- killed: DoubtLine[];
940
- answered: boolean;
941
- } {
942
- const head = findingsPart(text);
943
- const { read, dropped } = readFindingItems(head, root);
944
- const survivors = read.map(({ candidate, fields }): SkepticSurvivor => {
945
- const said = (fields.get("validity") ?? "").trim().replace(/[`*]/g, "").toLowerCase();
946
- const scenario = fields.get("scenario")?.trim() || null;
947
- const confirmed =
948
- said.startsWith("confirmed") && scenario !== null && scenarioIsAnchored(scenario);
949
- return { ...candidate, validity: confirmed ? "confirmed" : "likely", scenario };
950
- });
951
- const body = blockBody(text, "DOUBTED");
952
- const killedBody = blockBody(text, "KILLED");
953
- const doubted = judgementLines(body, root);
954
- const killed = judgementLines(killedBody, root);
955
- // A validator's answer: survivors, a NONE, or a DOUBTED/KILLED block.
956
- // Prose with none of them (a tool error told in words) judges nothing.
957
- const answered =
958
- survivors.length > 0 ||
959
- doubted.length > 0 ||
960
- /^\s*NONE\s*$/m.test(head) ||
961
- killedBody !== null ||
962
- body !== null;
963
- return { survivors, dropped, doubted, killed, answered };
964
- }
965
-
966
- // ---- identity ----------------------------------------------------------------------
967
-
968
- /** The anchored code, whitespace-normalised: each line's runs of whitespace
969
- * collapsed, blank lines dropped — so a re-indent or a reflow is the same
970
- * code, and a changed token is not. */
971
- export function normalizeCode(lines: readonly string[]): string {
972
- return lines
973
- .map((l) => l.trim().replace(/\s+/g, " "))
974
- .filter((l) => l !== "")
975
- .join("\n");
976
- }
977
-
978
- /** Lines `start..end` (1-based, inclusive) of `text`, or null when the range
979
- * is not inside the file. */
980
- export function anchoredLines(text: string, start: number, end: number): string[] | null {
981
- const lines = text.split("\n");
982
- if (text.endsWith("\n")) lines.pop();
983
- if (start < 1 || end < start || end > lines.length) return null;
984
- return lines.slice(start - 1, end);
985
- }
986
-
987
- /** Whitespace collapsed and trimmed, for the quote check. */
988
- const squash = (t: string): string => t.replace(/\s+/g, " ").trim();
989
-
990
- /** A fragment long enough to mean something: four characters or more with a
991
- * letter or digit in it (never a lone `}` or `)`). */
992
- const quotable = (t: string): boolean => t.length >= 4 && /[A-Za-z0-9]/.test(t);
993
-
994
- /**
995
- * Does a finding's evidence quote its anchored code? True when the evidence
996
- * contains a whole anchored line (whitespace-normalised), or a quoted
997
- * fragment of it — a `backticked` or "double-quoted" run that sits inside one
998
- * of the anchored lines. An anchor with nothing quotable in it (braces, blank
999
- * lines) cannot be checked and is not held against the finding. A finding
1000
- * whose evidence quotes only code ABOVE or BELOW its anchor — the function
1001
- * signature for a dereference twelve lines down — is exactly the misanchor
1002
- * this catches.
1003
- */
1004
- export function evidenceQuotesAnchor(
1005
- evidence: string | null,
1006
- anchored: readonly string[],
1007
- ): boolean {
1008
- const lines = anchored.map(squash).filter(quotable);
1009
- if (lines.length === 0) return true;
1010
- if (evidence === null) return false;
1011
- const ev = squash(evidence);
1012
- if (lines.some((l) => ev.includes(l))) return true;
1013
- for (const m of evidence.matchAll(/`([^`]+)`|"([^"]+)"/g)) {
1014
- const frag = squash(m[1] ?? m[2] ?? "");
1015
- if (quotable(frag) && lines.some((l) => l.includes(frag))) return true;
1016
- }
1017
- return false;
1018
- }
1019
-
1020
- /**
1021
- * A scan finding's fingerprint: file + lens + kind + a hash of the
1022
- * anchored code, whitespace-normalised. Stable across a re-scan that only
1023
- * moved the code or re-indented it; different when the code itself changed.
1024
- */
1025
- export function findingFingerprint(f: {
1026
- path: string;
1027
- lens: string;
1028
- kind: string;
1029
- code: string;
1030
- }): string {
1031
- const code = createHash("sha256").update(f.code).digest("hex");
1032
- return createHash("sha256").update([f.path, f.lens, f.kind, code].join("\0")).digest("hex");
1033
- }
1
+ // Moved to the plugin (packages/pi-plugin/src/scan/scan.ts), the scan engine's
2
+ // home: this name stays for the app's importers.
3
+ export * from "@quickchatai/pi-quickreview/src/scan/scan.js";