@quickchatai/quickreview 1.2.0 → 1.3.2

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 (350) hide show
  1. package/README.md +116 -53
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +25 -7
  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/selfimprove.js +18 -0
  11. package/dist/extensions/cli/selfimprove.js.map +1 -0
  12. package/dist/extensions/home.js +350 -0
  13. package/dist/extensions/home.js.map +1 -0
  14. package/dist/extensions/login-route.js +1 -1
  15. package/dist/extensions/login-route.js.map +1 -1
  16. package/dist/extensions/onboarding.js +8 -7
  17. package/dist/extensions/onboarding.js.map +1 -1
  18. package/dist/extensions/selfimprove.js +49 -0
  19. package/dist/extensions/selfimprove.js.map +1 -0
  20. package/dist/extensions/setup-scan.js +23 -592
  21. package/dist/extensions/setup-scan.js.map +1 -1
  22. package/dist/extensions/setup.js +2 -47
  23. package/dist/extensions/setup.js.map +1 -1
  24. package/dist/extensions/vendor/openviking-pi/index.js +4 -0
  25. package/dist/extensions/vendor/openviking-pi/index.js.map +1 -1
  26. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +3 -1
  27. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -1
  28. package/dist/extensions/vendor/openviking-pi/sync.js +81 -0
  29. package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -1
  30. package/dist/src/archive/restore.js +18 -15
  31. package/dist/src/archive/restore.js.map +1 -1
  32. package/dist/src/botworker.js +13 -2
  33. package/dist/src/botworker.js.map +1 -1
  34. package/dist/src/channelfs.js +3 -26
  35. package/dist/src/channelfs.js.map +1 -1
  36. package/dist/src/cli.js +14 -50
  37. package/dist/src/cli.js.map +1 -1
  38. package/dist/src/config.js +16 -0
  39. package/dist/src/config.js.map +1 -1
  40. package/dist/src/daemon.js +33 -4
  41. package/dist/src/daemon.js.map +1 -1
  42. package/dist/src/debuground.js +1 -0
  43. package/dist/src/debuground.js.map +1 -1
  44. package/dist/src/debugroundpi.js +1 -0
  45. package/dist/src/debugroundpi.js.map +1 -1
  46. package/dist/src/engine/caserunner-sdk.js +15 -251
  47. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  48. package/dist/src/home/adapters.js +349 -0
  49. package/dist/src/home/adapters.js.map +1 -0
  50. package/dist/src/home/advanced.js +318 -0
  51. package/dist/src/home/advanced.js.map +1 -0
  52. package/dist/src/home/advancedtools.js +57 -0
  53. package/dist/src/home/advancedtools.js.map +1 -0
  54. package/dist/src/home/autoreview.js +325 -0
  55. package/dist/src/home/autoreview.js.map +1 -0
  56. package/dist/src/home/daemonstatus.js +32 -0
  57. package/dist/src/home/daemonstatus.js.map +1 -0
  58. package/dist/src/home/devicelogin.js +61 -0
  59. package/dist/src/home/devicelogin.js.map +1 -0
  60. package/dist/src/home/launch.js +202 -0
  61. package/dist/src/home/launch.js.map +1 -0
  62. package/dist/src/home/loginowner.js +205 -0
  63. package/dist/src/home/loginowner.js.map +1 -0
  64. package/dist/src/home/memory.js +122 -0
  65. package/dist/src/home/memory.js.map +1 -0
  66. package/dist/src/home/model.js +811 -0
  67. package/dist/src/home/model.js.map +1 -0
  68. package/dist/src/home/panel.js +520 -0
  69. package/dist/src/home/panel.js.map +1 -0
  70. package/dist/src/home/ports.js +19 -0
  71. package/dist/src/home/ports.js.map +1 -0
  72. package/dist/src/home/review.js +163 -0
  73. package/dist/src/home/review.js.map +1 -0
  74. package/dist/src/home/reviews.js +478 -0
  75. package/dist/src/home/reviews.js.map +1 -0
  76. package/dist/src/home/setup.js +507 -0
  77. package/dist/src/home/setup.js.map +1 -0
  78. package/dist/src/home/setupadapters.js +214 -0
  79. package/dist/src/home/setupadapters.js.map +1 -0
  80. package/dist/src/home/setupcard.js +199 -0
  81. package/dist/src/home/setupcard.js.map +1 -0
  82. package/dist/src/home/setuptools.js +45 -0
  83. package/dist/src/home/setuptools.js.map +1 -0
  84. package/dist/src/home/spend.js +160 -0
  85. package/dist/src/home/spend.js.map +1 -0
  86. package/dist/src/home/switches.js +209 -0
  87. package/dist/src/home/switches.js.map +1 -0
  88. package/dist/src/home/switchon.js +439 -0
  89. package/dist/src/home/switchon.js.map +1 -0
  90. package/dist/src/home/toolnames.js +41 -0
  91. package/dist/src/home/toolnames.js.map +1 -0
  92. package/dist/src/home/tune.js +341 -0
  93. package/dist/src/home/tune.js.map +1 -0
  94. package/dist/src/home/words.js +7 -0
  95. package/dist/src/home/words.js.map +1 -0
  96. package/dist/src/homes.js +14 -0
  97. package/dist/src/homes.js.map +1 -1
  98. package/dist/src/install.js +1 -1
  99. package/dist/src/install.js.map +1 -1
  100. package/dist/src/ipc/workerspec.js +8 -0
  101. package/dist/src/ipc/workerspec.js.map +1 -1
  102. package/dist/src/ledgercheck.js +39 -10
  103. package/dist/src/ledgercheck.js.map +1 -1
  104. package/dist/src/live/publish.js +1 -1
  105. package/dist/src/live/publish.js.map +1 -1
  106. package/dist/src/livecli.js +1 -1
  107. package/dist/src/livecli.js.map +1 -1
  108. package/dist/src/loginrefresh/handover.js +121 -0
  109. package/dist/src/loginrefresh/handover.js.map +1 -0
  110. package/dist/src/loginrefresh/lock.js +54 -3
  111. package/dist/src/loginrefresh/lock.js.map +1 -1
  112. package/dist/src/loginrefresh/refresher.js +33 -5
  113. package/dist/src/loginrefresh/refresher.js.map +1 -1
  114. package/dist/src/loginrefresh/supervisor.js +18 -1
  115. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  116. package/dist/src/npmverbs.js +42 -12
  117. package/dist/src/npmverbs.js.map +1 -1
  118. package/dist/src/onboard/choices.js +6 -3
  119. package/dist/src/onboard/choices.js.map +1 -1
  120. package/dist/src/onboard/engine.js +33 -292
  121. package/dist/src/onboard/engine.js.map +1 -1
  122. package/dist/src/onboard/steps/bringup.js +3 -2
  123. package/dist/src/onboard/steps/bringup.js.map +1 -1
  124. package/dist/src/onboard/steps/configfill.js +18 -7
  125. package/dist/src/onboard/steps/configfill.js.map +1 -1
  126. package/dist/src/onboard/steps/loginshare.js +213 -0
  127. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  128. package/dist/src/onboard/steps/memory.js +14 -2
  129. package/dist/src/onboard/steps/memory.js.map +1 -1
  130. package/dist/src/onboard/steps/repoconfig.js +72 -0
  131. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  132. package/dist/src/onboard/toolnames.js +2 -2
  133. package/dist/src/onboard/toolnames.js.map +1 -1
  134. package/dist/src/piagentdir.js +13 -0
  135. package/dist/src/piagentdir.js.map +1 -0
  136. package/dist/src/pilogin.js +31 -16
  137. package/dist/src/pilogin.js.map +1 -1
  138. package/dist/src/qcrbin.js +16 -11
  139. package/dist/src/qcrbin.js.map +1 -1
  140. package/dist/src/reviewargs.js +122 -0
  141. package/dist/src/reviewargs.js.map +1 -0
  142. package/dist/src/reviewcli.js +7 -161
  143. package/dist/src/reviewcli.js.map +1 -1
  144. package/dist/src/reviewdetach.js +76 -11
  145. package/dist/src/reviewdetach.js.map +1 -1
  146. package/dist/src/scancli.js +39 -691
  147. package/dist/src/scancli.js.map +1 -1
  148. package/dist/src/selfimprove.js +66 -0
  149. package/dist/src/selfimprove.js.map +1 -0
  150. package/dist/src/session-command-supervisor.js +38 -9
  151. package/dist/src/session-command-supervisor.js.map +1 -1
  152. package/dist/src/setup/adapters.js +5 -223
  153. package/dist/src/setup/adapters.js.map +1 -1
  154. package/dist/src/setup/caserunner.js +15 -249
  155. package/dist/src/setup/caserunner.js.map +1 -1
  156. package/dist/src/setup/engine.js +32 -341
  157. package/dist/src/setup/engine.js.map +1 -1
  158. package/dist/src/setup/facts.js +3 -592
  159. package/dist/src/setup/facts.js.map +1 -1
  160. package/dist/src/setup/findings.js +3 -947
  161. package/dist/src/setup/findings.js.map +1 -1
  162. package/dist/src/setup/presentation.js +3 -8
  163. package/dist/src/setup/presentation.js.map +1 -1
  164. package/dist/src/setup/scan.js +3 -724
  165. package/dist/src/setup/scan.js.map +1 -1
  166. package/dist/src/setup/scanbuild.js +9 -92
  167. package/dist/src/setup/scanbuild.js.map +1 -1
  168. package/dist/src/setup/scanlease.js +3 -250
  169. package/dist/src/setup/scanlease.js.map +1 -1
  170. package/dist/src/setup/scanreport.js +3 -288
  171. package/dist/src/setup/scanreport.js.map +1 -1
  172. package/dist/src/setup/scanrun.js +3 -1732
  173. package/dist/src/setup/scanrun.js.map +1 -1
  174. package/dist/src/setup/scantree.js +3 -256
  175. package/dist/src/setup/scantree.js.map +1 -1
  176. package/dist/src/setup/stage2request.js +3 -258
  177. package/dist/src/setup/stage2request.js.map +1 -1
  178. package/dist/src/setup/unitcut.js +3 -436
  179. package/dist/src/setup/unitcut.js.map +1 -1
  180. package/dist/src/state.js +75 -9
  181. package/dist/src/state.js.map +1 -1
  182. package/dist/src/takeover.js +1 -0
  183. package/dist/src/takeover.js.map +1 -1
  184. package/dist/src/takeoverpi.js +2 -1
  185. package/dist/src/takeoverpi.js.map +1 -1
  186. package/dist/src/tui/alerts.js +4 -1
  187. package/dist/src/tui/alerts.js.map +1 -1
  188. package/dist/src/tui/board.js +2 -2
  189. package/dist/src/tui/board.js.map +1 -1
  190. package/dist/src/tui/companion.js +2 -1
  191. package/dist/src/tui/companion.js.map +1 -1
  192. package/dist/src/tui/deploytab.js +2 -2
  193. package/dist/src/tui/deploytab.js.map +1 -1
  194. package/dist/src/tui/index.js +9 -6
  195. package/dist/src/tui/index.js.map +1 -1
  196. package/dist/src/tui/memory.js +3 -16
  197. package/dist/src/tui/memory.js.map +1 -1
  198. package/dist/src/tui/memoryurl.js +22 -0
  199. package/dist/src/tui/memoryurl.js.map +1 -0
  200. package/dist/src/tui/remote.js +1 -1
  201. package/dist/src/tui/remote.js.map +1 -1
  202. package/dist/src/tui/route.js +2 -2
  203. package/dist/src/tui/route.js.map +1 -1
  204. package/dist/src/tui/setupstate.js +8 -5
  205. package/dist/src/tui/setupstate.js.map +1 -1
  206. package/dist/src/tui/sshtriage.js +1 -1
  207. package/dist/src/tui/sshtriage.js.map +1 -1
  208. package/dist/src/verdicts.js +12 -0
  209. package/dist/src/verdicts.js.map +1 -1
  210. package/dist/src/wire.js +9 -4
  211. package/dist/src/wire.js.map +1 -1
  212. package/dist/src/workerbreaker.js +32 -5
  213. package/dist/src/workerbreaker.js.map +1 -1
  214. package/dist/src/workerworld.js +4 -0
  215. package/dist/src/workerworld.js.map +1 -1
  216. package/docker/README.md +1 -1
  217. package/docker/entrypoint.sh +40 -6
  218. package/docker-compose.yml +16 -1
  219. package/extensions/cli/home.ts +14 -0
  220. package/extensions/cli/selfimprove.ts +18 -0
  221. package/extensions/home.ts +484 -0
  222. package/extensions/login-route.ts +1 -1
  223. package/extensions/onboarding.ts +8 -7
  224. package/extensions/selfimprove.ts +65 -0
  225. package/extensions/setup-scan.ts +79 -766
  226. package/extensions/setup.ts +14 -70
  227. package/extensions/vendor/openviking-pi/NOTICE +5 -0
  228. package/extensions/vendor/openviking-pi/README.md +39 -5
  229. package/extensions/vendor/openviking-pi/index.ts +4 -0
  230. package/extensions/vendor/openviking-pi/patches/0014-one-turn-sync-in-flight.patch +239 -0
  231. package/extensions/vendor/openviking-pi/qcr-hooks.ts +3 -1
  232. package/extensions/vendor/openviking-pi/sync.ts +80 -0
  233. package/extensions/vendor/openviking-pi/upstream.json +2 -1
  234. package/package.json +6 -4
  235. package/prompts/onboarding.md +8 -7
  236. package/schemas/review-result.v1.json +60 -0
  237. package/scripts/deploy.sh +8 -6
  238. package/scripts/qcr-host.sh +28 -5
  239. package/scripts/vm-ghcr-login.sh +7 -4
  240. package/src/archive/restore.ts +18 -21
  241. package/src/botworker.ts +13 -2
  242. package/src/channelfs.ts +3 -26
  243. package/src/cli.ts +13 -54
  244. package/src/config.ts +38 -0
  245. package/src/daemon.ts +34 -4
  246. package/src/debuground.ts +3 -0
  247. package/src/debugroundpi.ts +1 -0
  248. package/src/engine/caserunner-sdk.ts +29 -314
  249. package/src/home/adapters.ts +418 -0
  250. package/src/home/advanced.ts +421 -0
  251. package/src/home/advancedtools.ts +96 -0
  252. package/src/home/autoreview.ts +385 -0
  253. package/src/home/daemonstatus.ts +55 -0
  254. package/src/home/devicelogin.ts +107 -0
  255. package/src/home/launch.ts +257 -0
  256. package/src/home/loginowner.ts +248 -0
  257. package/src/home/memory.ts +176 -0
  258. package/src/home/model.ts +965 -0
  259. package/src/home/panel.ts +666 -0
  260. package/src/home/ports.ts +338 -0
  261. package/src/home/review.ts +206 -0
  262. package/src/home/reviews.ts +565 -0
  263. package/src/home/setup.ts +689 -0
  264. package/src/home/setupadapters.ts +217 -0
  265. package/src/home/setupcard.ts +288 -0
  266. package/src/home/setuptools.ts +57 -0
  267. package/src/home/spend.ts +194 -0
  268. package/src/home/switches.ts +293 -0
  269. package/src/home/switchon.ts +554 -0
  270. package/src/home/toolnames.ts +46 -0
  271. package/src/home/tune.ts +405 -0
  272. package/src/home/words.ts +8 -0
  273. package/src/homes.ts +17 -0
  274. package/src/install.ts +1 -1
  275. package/src/ipc/workerspec.ts +15 -0
  276. package/src/ledgercheck.ts +45 -9
  277. package/src/live/publish.ts +4 -1
  278. package/src/livecli.ts +6 -1
  279. package/src/loginrefresh/handover.ts +138 -0
  280. package/src/loginrefresh/lock.ts +72 -3
  281. package/src/loginrefresh/refresher.ts +34 -5
  282. package/src/loginrefresh/supervisor.ts +25 -2
  283. package/src/npmverbs.ts +51 -11
  284. package/src/onboard/choices.ts +6 -3
  285. package/src/onboard/engine.ts +64 -434
  286. package/src/onboard/steps/bringup.ts +3 -2
  287. package/src/onboard/steps/configfill.ts +23 -7
  288. package/src/onboard/steps/loginshare.ts +258 -0
  289. package/src/onboard/steps/memory.ts +18 -2
  290. package/src/onboard/steps/repoconfig.ts +85 -0
  291. package/src/onboard/toolnames.ts +2 -2
  292. package/src/piagentdir.ts +12 -0
  293. package/src/pilogin.ts +32 -16
  294. package/src/qcrbin.ts +16 -11
  295. package/src/reviewargs.ts +119 -0
  296. package/src/reviewcli.ts +6 -167
  297. package/src/reviewdetach.ts +107 -13
  298. package/src/scancli.ts +75 -826
  299. package/src/selfimprove.ts +90 -0
  300. package/src/session-command-supervisor.ts +41 -8
  301. package/src/setup/adapters.ts +13 -221
  302. package/src/setup/caserunner.ts +34 -273
  303. package/src/setup/engine.ts +62 -496
  304. package/src/setup/facts.ts +3 -627
  305. package/src/setup/findings.ts +3 -1264
  306. package/src/setup/presentation.ts +3 -18
  307. package/src/setup/scan.ts +3 -1033
  308. package/src/setup/scanbuild.ts +20 -114
  309. package/src/setup/scanlease.ts +3 -297
  310. package/src/setup/scanreport.ts +3 -373
  311. package/src/setup/scanrun.ts +3 -2268
  312. package/src/setup/scantree.ts +3 -259
  313. package/src/setup/stage2request.ts +3 -347
  314. package/src/setup/unitcut.ts +3 -523
  315. package/src/state.ts +126 -11
  316. package/src/takeover.ts +5 -0
  317. package/src/takeoverpi.ts +3 -2
  318. package/src/tui/alerts.ts +9 -2
  319. package/src/tui/board.ts +2 -2
  320. package/src/tui/companion.ts +2 -1
  321. package/src/tui/deploytab.ts +2 -2
  322. package/src/tui/index.ts +9 -6
  323. package/src/tui/memory.ts +3 -15
  324. package/src/tui/memoryurl.ts +22 -0
  325. package/src/tui/remote.ts +1 -1
  326. package/src/tui/route.ts +2 -2
  327. package/src/tui/setupstate.ts +20 -16
  328. package/src/tui/sshtriage.ts +1 -1
  329. package/src/verdicts.ts +13 -0
  330. package/src/wire.ts +9 -4
  331. package/src/workerbreaker.ts +41 -5
  332. package/src/workerworld.ts +9 -1
  333. package/dist/src/live/projection.js +0 -450
  334. package/dist/src/live/projection.js.map +0 -1
  335. package/dist/src/live/read.js +0 -60
  336. package/dist/src/live/read.js.map +0 -1
  337. package/dist/src/onboard/launch.js +0 -75
  338. package/dist/src/onboard/launch.js.map +0 -1
  339. package/dist/src/setup/ledger.js +0 -207
  340. package/dist/src/setup/ledger.js.map +0 -1
  341. package/dist/src/setup/stage2.js +0 -617
  342. package/dist/src/setup/stage2.js.map +0 -1
  343. package/dist/src/setup/textsniff.js +0 -89
  344. package/dist/src/setup/textsniff.js.map +0 -1
  345. package/src/live/projection.ts +0 -491
  346. package/src/live/read.ts +0 -86
  347. package/src/onboard/launch.ts +0 -103
  348. package/src/setup/ledger.ts +0 -324
  349. package/src/setup/stage2.ts +0 -926
  350. package/src/setup/textsniff.ts +0 -95
@@ -1,4 +1,3 @@
1
- import { usd, findingLocation } from "../src/setup/presentation.js";
2
1
  // setup-scan — the scan half of the companion's setup tool belt. Not an
3
2
  // extension of its own: extensions/setup.ts (the one setup & scan extension)
4
3
  // registers these beside the setup_* tools. The companion judges
@@ -34,8 +33,6 @@ import { usd, findingLocation } from "../src/setup/presentation.js";
34
33
  //
35
34
  // Convention (setup.ts, onboarding.ts): every tool answers a refusal as its
36
35
  // result, never a throw.
37
- import { FINDING_STATES } from "../src/setup/findings.js";
38
- import { join } from "node:path";
39
36
  import {
40
37
  textResult as text,
41
38
  refusedResult as refused,
@@ -46,708 +43,69 @@ import { homeProfileLookup, stateHome } from "../src/install.js";
46
43
  import { scanCasesForEnv } from "@quickchatai/pi-quickreview/src/engine/auth.js";
47
44
  import { piCaseRunner } from "../src/setup/caserunner.js";
48
45
  import type { CaseRunner, SetupEngine } from "../src/setup/engine.js";
49
- import type {
50
- FindingsFilter,
51
- FindingState,
52
- MatchQuestionView,
53
- ScanFinding,
54
- ValidityView,
55
- } from "../src/setup/findings.js";
56
- import {
57
- VALIDITY_ORDER,
58
- clickStopScan,
59
- readStopResult,
60
- scanStopOutcome,
61
- validityOf,
62
- type ScanStopRequest,
63
- } from "../src/setup/scanreport.js";
64
- import { FindingsStore, dismissedLookalike, lookalikeRank } from "../src/setup/findings.js";
46
+ import type { FindingsStore } from "../src/setup/findings.js";
65
47
  import { stateHomeFs } from "../src/setup/adapters.js";
66
- import { SEVERITIES, type ScanSeverity } from "../src/setup/scan.js";
67
- import { lensTiers, stage2Lenses, MAX_STAGE2_ROWS } from "../src/setup/stage2.js";
68
- import {
69
- priceIsMeasured,
70
- priceLabel,
71
- type StoredStage2Proposal,
72
- } from "../src/setup/stage2request.js";
48
+ import { loadScanConfig } from "../src/setup/scanbuild.js";
49
+ import { assembleScan, scanRuns } from "@quickchatai/pi-quickreview/src/scan/scanassemble.js";
73
50
  import {
74
- buildScanAgents,
75
- loadScanConfig,
76
- scanSettings,
77
- SCAN_AGENTS_DIR,
78
- } from "../src/setup/scanbuild.js";
79
- import { escapeControls } from "../src/setup/textsniff.js";
80
- import {
81
- SCAN_TRANSCRIPTS_DIR,
82
- ScanRunner,
83
- type ScanPreview,
84
- type ScanRun,
85
- type ScanSettings,
86
- type ScanStatus,
87
- type StoredSuggestion,
88
- } from "../src/setup/scanrun.js";
89
- import { gitScanSource } from "../src/setup/scantree.js";
51
+ FINDINGS_LIMIT,
52
+ FINDINGS_LIMIT_MAX,
53
+ answerMatch,
54
+ findingsFilter,
55
+ listFindings,
56
+ previewDetails,
57
+ proposalDetails,
58
+ renderFindings,
59
+ renderPreview,
60
+ renderProposal,
61
+ renderQuestion,
62
+ renderStatus,
63
+ renderSuggestions,
64
+ stage2StartedLine,
65
+ startedDetails,
66
+ startedLine,
67
+ stopScan,
68
+ type StopChannel,
69
+ } from "@quickchatai/pi-quickreview/src/scan/scanrender.js";
70
+ import type { ScanRunner, ScanSettings } from "../src/setup/scanrun.js";
90
71
  import { ScanTriage, type CloseFixedResult } from "../src/setup/triage.js";
91
72
  import type { MemoryStore } from "../src/memory/store.js";
92
73
  import type { TrackerSink } from "../src/setup/engine.js";
93
- import type { MatchResult } from "../src/setup/findings.js";
94
- import { fileLease, type ScanLease } from "../src/setup/scanlease.js";
74
+ import type { ScanLease } from "../src/setup/scanlease.js";
95
75
 
96
76
  const NO_PARAMS = { type: "object", properties: {} } as const;
97
77
 
98
- // ---- rendering ---------------------------------------------------------------
99
-
100
- /** How a rendering names the next step: the companion's tools, or the
101
- * `qcr scan` CLI's subcommands — the same words around either. */
102
- export interface ScanVerbs {
103
- /** How to start the previewed run. */
104
- start(p: Pick<ScanPreview, "key" | "sha" | "mode">): string;
105
- /** Where the pending match question is shown. */
106
- matches: string;
107
- /** How to answer the match question `key`: what precedes "that are the
108
- * SAME finding". */
109
- match(key: string): string;
110
- /** How to start the stored stage-2 proposal `key`, and only some of its rows. */
111
- stage2Start(key: string): string;
112
- /** The progress and stop verbs a start names. */
113
- status: string;
114
- stop: string;
115
- }
116
-
117
- export const TOOL_VERBS: ScanVerbs = {
118
- start: (p) =>
119
- `scan_start with key ${p.key} and sha ${p.sha}${p.mode === "delta" ? " and delta true" : ""}`,
120
- matches: "scan_matches",
121
- match: (key) => `scan_match: key ${key} and the pairs`,
122
- stage2Start: (key) =>
123
- `scan_stage2_start with key ${key} — only after the operator agreed to the cost ("do it"); the TUI's start click starts the same proposal. ` +
124
- `To run only some rows: add rows (their ids, e.g. ["row-1"]) — only runnable rows, and the run stops hard at their total.`,
125
- status: "scan_status",
126
- stop: "scan_stop",
127
- };
128
-
129
- export const CLI_VERBS: ScanVerbs = {
130
- start: (p) =>
131
- `qcr scan start --key ${p.key} --sha ${p.sha}${p.mode === "delta" ? " --delta" : ""}`,
132
- matches: "qcr scan matches",
133
- match: (key) => `qcr scan match --key ${key} OLD=NEW… — the pairs`,
134
- stage2Start: (key) =>
135
- `qcr scan stage2 start --key ${key} — only after the operator agreed to the cost; the TUI's start click starts the same proposal. ` +
136
- "To run only some rows: add --rows with their ids (e.g. --rows row-1,row-3) — only runnable rows, and the run stops hard at their total.",
137
- status: "qcr scan status",
138
- stop: "qcr scan stop",
139
- };
140
-
141
- /** A delta's unchanged-but-owed files: those no reading covers, and — on a
142
- * deep delta — those a standard reading covers but no deep one does yet. */
143
- function owedClause(d: ScanPreview["delta"]): string {
144
- if (d === null || d.owed <= 0) return "";
145
- const shallow = Math.min(d.owed_shallow ?? 0, d.owed);
146
- const unread = d.owed - shallow;
147
- const parts: string[] = [];
148
- if (unread > 0)
149
- parts.push(
150
- `${unread} of the files are unchanged but no reading covers them yet (never read, cut short or unreadable before)`,
151
- );
152
- if (shallow > 0)
153
- parts.push(`${shallow} of the files are unchanged but not yet read at deep depth`);
154
- return `; ${parts.join("; ")} — read first`;
155
- }
156
-
157
- export function renderPreview(p: ScanPreview, verbs: ScanVerbs = TOOL_VERBS): string {
158
- const out: string[] = [];
159
- const deep = p.depth === "deep";
160
- const head = `${deep ? "(deep) " : ""}at ${p.sha.slice(0, 12)} — ${p.settings.tier} at ${p.settings.effort} effort, skeptic ${p.settings.validator ? `on (at the lens's tier, ${p.settings.validate_tier}; its cost is in the price)` : "off (findings stay unjudged — validity unknown)"}`;
161
- // A rate measured at the other depth stands in until this depth is priced.
162
- const across = p.rate_across_depth
163
- ? ` — from readings at the other depth: nothing at ${p.depth} depth is priced at these settings yet`
164
- : "";
165
- if (p.mode === "delta") {
166
- out.push(
167
- `DELTA re-scan ${head}, against the last scan at ${p.delta?.since.slice(0, 12) ?? "?"}.`,
168
- `Files whose content changed since, and any not yet read whole: ${p.files} files, ${p.lines} lines in ${p.cases.length} case(s) — ` +
169
- `${p.covered} already read at this content` +
170
- owedClause(p.delta) +
171
- `. Estimate ${usd(p.estimate_usd)}, measured at ` +
172
- `${usd(p.rate?.usd_per_kline ?? null)}/kline over ${p.rate?.cases ?? 0} finished case(s)${across}. The run stops hard at the estimate. ` +
173
- "No deep pass runs on a delta.",
174
- );
175
- } else if (p.mode === "calibration") {
176
- out.push(
177
- `CALIBRATION run ${head}.`,
178
- `No measured price for stage 1 at these settings yet (${p.calibration.measured} of ${p.calibration.wanted} priced cases). ` +
179
- `This run reads ${p.cases.length} case(s) from units spread across the size range and measures what they cost; ` +
180
- `it stops hard at ${usd(p.cap_usd)}. Its findings are real stage-1 findings. Preview again afterwards for the full estimate` +
181
- (p.remaining_cases > 0 ? ` (${p.remaining_cases} more case(s) after it).` : "."),
182
- );
183
- } else {
184
- out.push(
185
- `STAGE 1 ${head}.`,
186
- `Estimate ${usd(p.estimate_usd)} for ${p.cases.length} case(s), ${p.files} files, ${p.lines} lines — measured at ` +
187
- `${usd(p.rate?.usd_per_kline ?? null)}/kline over ${p.rate?.cases ?? 0} finished case(s)${across}. The run stops hard at the estimate.`,
188
- );
189
- }
190
- if (deep) {
191
- out.push(
192
- "DEEP: pass-scan names a deep lens for every focus file, and a file the ledger holds only a standard reading of is read again. " +
193
- "A deep stage 2 proposal then adds the floor — the [scan] deep_lenses over every file this stage reads.",
194
- );
195
- }
196
- const rows = p.cases
197
- .slice(0, 40)
198
- .map(
199
- (c) =>
200
- ` - ${c.id}: ${c.files.length} files, ${c.lines} lines${c.estimate_usd !== null ? `, ~${usd(c.estimate_usd)}` : ""}`,
201
- );
202
- if (p.cases.length > 40) rows.push(` … and ${p.cases.length - 40} more`);
203
- out.push(rows.join("\n"));
204
- const skipped = Object.entries(p.excluded)
205
- .filter(([, n]) => n > 0)
206
- .map(([why, n]) => `${n} ${why}`);
207
- out.push(
208
- `Not read: ${skipped.length ? skipped.join(", ") : "nothing excluded"}; ${p.covered} already in the ledger at this content; ` +
209
- `${p.unreadable} unreadable${p.settings.exclude.length ? `; exclude globs ${p.settings.exclude.join(", ")}` : ""}.`,
210
- );
211
- // What the tree lists but no lens can read, by reason and by name — a
212
- // binary is REPORTED as skipped, never silently absent from coverage.
213
- const skippedBy = new Map<string, string[]>();
214
- for (const f of p.skipped ?? []) skippedBy.set(f.why, [...(skippedBy.get(f.why) ?? []), f.path]);
215
- for (const [why, paths] of skippedBy) {
216
- const label =
217
- why === "binary" ? "binary" : why === "too-big" ? "over the size cap" : "unreadable";
218
- out.push(
219
- `skipped: ${label} — ${paths.length} file(s): ${paths.slice(0, 5).map(escapeControls).join(", ")}${paths.length > 5 ? ", …" : ""}; not read by any lens.`,
220
- );
221
- }
222
- if (p.unassigned.length > 0) {
223
- out.push(
224
- `! ${p.unassigned.length} file(s) no unit owns at this sha (e.g. ${p.unassigned.slice(0, 3).join(", ")}) — the unit cut may need adjusting; they are not read.`,
225
- );
226
- }
227
- if (p.reconcile_only) {
228
- out.push(
229
- `Nothing to read, but ${p.reconcile_only}: the run reads no case and only reconciles the findings (no cost).`,
230
- );
231
- }
232
- for (const b of p.blockers) out.push(`✗ ${b}`);
233
- if (p.blockers.length === 0) {
234
- out.push(`To run it: ${verbs.start(p)} — only after the operator agreed to the cost.`);
235
- }
236
- return out.join("\n\n");
237
- }
238
-
239
- /** A run's findings by validity tier — `(2 confirmed, 1 likely, 3 doubtful)`
240
- * — from the per-case counts; `unknown` for what no skeptic judged (the
241
- * validator off or failed, or a case recorded before the tiers). */
242
- function tierCounts(r: NonNullable<ScanStatus["run"]>, findings: number, doubted: number): string {
243
- if (!r.settings.validator)
244
- return doubted ? ` (${doubted} may not be valid)` : " (skeptic off — validity unknown)";
245
- const confirmed = r.cases.reduce((n, c) => n + (c.confirmed ?? 0), 0);
246
- const likely = r.cases.reduce((n, c) => n + (c.likely ?? 0), 0);
247
- const unknown = findings - confirmed - likely - doubted;
248
- if (findings === 0) return "";
249
- return ` (${confirmed} confirmed, ${likely} likely, ${doubted} doubtful${unknown > 0 ? `, ${unknown} unknown` : ""})`;
250
- }
251
-
252
- export function renderStatus(s: ScanStatus, verbs: ScanVerbs = TOOL_VERBS): string {
253
- const out: string[] = [];
254
- for (const p of s.problems) out.push(`✗ ${p}`);
255
- const r = s.run;
256
- if (r === null) {
257
- out.push(s.problems.length > 0 ? "Latest scan: UNKNOWN — see above." : "No scan has run yet.");
258
- } else {
259
- const done = r.cases.filter((c) => c.status === "done").length;
260
- const findings = r.cases.reduce((n, c) => n + c.findings, 0);
261
- const doubted = r.cases.reduce((n, c) => n + c.doubted, 0);
262
- out.push(
263
- `${r.id} (${r.mode === "stage2" ? "stage 2" : r.mode}${r.settings.depth === "deep" ? ", deep" : ""}) at ${r.sha.slice(0, 12)}: ${r.display.toUpperCase()} — ${done}/${r.cases.length} cases, ` +
264
- `${findings} finding(s)${tierCounts(r, findings, doubted)}, spent ${usd(r.spent_usd)} of ${usd(r.cap_usd)}` +
265
- (r.unpriced_cases
266
- ? ` (${r.unpriced_cases} unpriced case(s) counted at their estimate)`
267
- : ""),
268
- );
269
- // Stage 2 is re-entered by a new proposal (the rows already read are
270
- // covered, free); stage 1 by a new preview.
271
- const again = r.mode === "stage2" ? "Propose stage 2 again" : "Preview";
272
- if (r.display === "interrupted")
273
- out.push(
274
- `It was interrupted (the process driving it went away — the companion restarted, or a \`qcr scan\` driver died). ${again} and start again — the ledger resumes it.`,
275
- );
276
- if (r.display === "capped")
277
- out.push(`It stopped at its hard cap. ${again} for the price of what is left.`);
278
- if (r.mode === "stage2") {
279
- for (const c of r.cases.slice(0, 40)) {
280
- out.push(
281
- ` - ${c.id} ${c.lens ?? "?"} at ${c.tier ?? "?"} (${(c.units ?? [c.unit]).join(", ")}): ${c.status}, ${c.findings} finding(s), ${usd(c.cost_usd)}` +
282
- (c.reads !== undefined ? `, ${c.reads} read(s)` : "") +
283
- (c.unopened?.length ? `, ${c.unopened.length} focus file(s) not opened` : ""),
284
- );
285
- }
286
- }
287
- // Where to read back what a lens did, when the runner kept it.
288
- if (r.cases.some((c) => c.transcript !== undefined))
289
- out.push(
290
- `Transcripts: ${SCAN_TRANSCRIPTS_DIR}/${r.id}/ under the state home (one .jsonl per case, .validate.jsonl for its skeptic).`,
291
- );
292
- const failed = r.cases.filter((c) => c.status === "failed");
293
- for (const c of failed.slice(0, 10)) out.push(` ✗ ${c.id}: ${c.error}`);
294
- for (const p of r.problems.slice(-5)) out.push(` ! ${p}`);
295
- if (r.tickets_closed)
296
- out.push(`Tickets closed as fixed at ${r.sha.slice(0, 12)}: ${r.tickets_closed}.`);
297
- if (r.matching && (r.matching.fixed > 0 || r.matching.pending > 0)) {
298
- out.push(
299
- `Findings no longer reported: ${r.matching.fixed} fixed` +
300
- (r.matching.pending > 0
301
- ? `, ${r.matching.pending} waiting on your match (${verbs.matches})`
302
- : ""),
303
- );
304
- }
305
- // A dismissal is sticky: what would have been fixed is only stamped.
306
- const dismissedGone = r.matching?.dismissed_unreported ?? 0;
307
- if (dismissedGone > 0) {
308
- out.push(
309
- `Dismissed and no longer reported: ${dismissedGone} — kept dismissed (a scan never overwrites the operator's triage), stamped for audit.`,
310
- );
311
- }
312
- // A record written before the stamp existed has no count: nothing to say.
313
- const notReproduced = r.matching?.not_reproduced ?? 0;
314
- if (notReproduced > 0) {
315
- out.push(
316
- `Not reproduced: ${notReproduced} open finding(s) re-read at UNCHANGED content without being reported again — ` +
317
- "kept open (only changed code is fixed); dismiss any that were noise.",
318
- );
319
- }
320
- }
321
- if (s.ledger) {
322
- out.push(
323
- `Ledger: ${s.ledger.files} file(s) read by stage 1` +
324
- (s.ledger.deep_files ? `, ${s.ledger.deep_files} file × lens read by stage 2` : "") +
325
- `, ${usd(s.ledger.cost_usd)} recorded.`,
326
- );
327
- }
328
- return out.join("\n");
329
- }
330
-
331
- /** Findings in validity order — confirmed, likely, doubtful, unknown — the
332
- * store's order (most severe first) kept inside each tier, a finding that
333
- * looks like a dismissed one below the rest of its tier. */
334
- export function byValidity(fs: readonly ScanFinding[]): ScanFinding[] {
335
- return VALIDITY_ORDER.flatMap((v) => {
336
- const tier = fs.filter((f) => validityOf(f) === v);
337
- return [
338
- ...tier.filter((f) => lookalikeRank(f) === 0),
339
- ...tier.filter((f) => lookalikeRank(f) === 1),
340
- ];
341
- });
342
- }
343
-
344
- /** A new finding that resembles one the operator dismissed: which, and why
345
- * — flagged for triage, never dismissed by the scan. */
346
- function lookalikeNote(f: ScanFinding): string {
347
- const d = dismissedLookalike(f);
348
- return d
349
- ? `, LOOKS LIKE DISMISSED ${d.fingerprint.slice(0, 12)}: ${d.reason} — not auto-dismissed`
350
- : "";
351
- }
352
-
353
- function validityNote(f: ScanFinding): string {
354
- const v = validityOf(f);
355
- if (v === "doubtful")
356
- return `, DOUBTFUL — ${f.doubt?.word ?? "?"}${f.doubt?.class ? ` [${f.doubt.class}]` : ""}`;
357
- if (v === "unknown")
358
- return f.doubt
359
- ? `, MAY NOT BE VALID — ${f.doubt.word}`
360
- : f.validated
361
- ? ", validity unknown"
362
- : ", unvalidated";
363
- return `, ${v.toUpperCase()}`;
364
- }
365
-
366
- export function renderFindings(fs: readonly ScanFinding[], total: number): string {
367
- if (fs.length === 0) return "No stored scan findings match.";
368
- const rows = fs.map(
369
- (f) =>
370
- `- [${f.severity}] ${findingLocation(f)} (${f.unit}, ${f.kind}, ${f.state}` +
371
- validityNote(f) +
372
- `${f.capped_from ? `, ${f.capped_from}→${f.severity}: ${f.reachability}` : ""}` +
373
- `${f.anchor_quoted === false ? ", ANCHOR NOT QUOTED — the lines may not be the defect's" : ""}` +
374
- `${f.fixed ? ` at ${f.fixed.sha.slice(0, 12)}, was ${f.fixed.from}` : ""}` +
375
- `${f.state === "dismissed" && f.unreported ? `, no longer reported at ${f.unreported.sha.slice(0, 12)} — kept dismissed` : ""}` +
376
- `${f.not_reproduced ? `, NOT REPRODUCED by ${f.not_reproduced.count} re-read(s) of unchanged code, latest ${f.not_reproduced.run}` : ""}` +
377
- `${lookalikeNote(f)}) ` +
378
- `${f.claim} [${f.fingerprint.slice(0, 12)}]`,
379
- );
380
- if (total > fs.length) rows.push(`… ${total - fs.length} more — narrow the filter.`);
381
- return rows.join("\n");
382
- }
383
-
384
- export function renderQuestion(q: MatchQuestionView | null, verbs: ScanVerbs = TOOL_VERBS): string {
385
- if (q === null)
386
- return "No match question is pending: every finding a re-scan stopped reporting has been settled.";
387
- const row = (f: ScanFinding): string =>
388
- ` - ${f.fingerprint} [${f.severity}] ${findingLocation(f)} (${f.unit}, ${f.lens}, ${f.kind}` +
389
- `${f.state === "new" ? "" : `, ${f.state}`}${lookalikeNote(f)}) ${f.claim}`;
390
- return [
391
- `MATCH QUESTION ${q.key} (re-scan ${q.run} at ${q.sha.slice(0, 12)}).`,
392
- `No longer reported exactly (${q.unreported.length}):`,
393
- ...q.unreported.map(row),
394
- `Fresh — first seen by the re-scan, possibly one of the above moved or reworded (${q.fresh.length}):`,
395
- ...q.fresh.map(row),
396
- `Answer with ${verbs.match(q.key)} that are the SAME finding. Every unpaired old one becomes fixed (a dismissed one stays dismissed); every unpaired fresh one stays new.`,
397
- ].join("\n");
398
- }
399
-
400
- export function renderSuggestions(
401
- sugs: readonly StoredSuggestion[],
402
- settings: ScanSettings | null,
403
- ): string {
404
- const out: string[] = [];
405
- if (sugs.length === 0)
406
- out.push("No deep-lens suggestions stored — stage 1 has not suggested any yet.");
407
- else {
408
- const byLens = new Map<string, StoredSuggestion[]>();
409
- for (const s of sugs) byLens.set(s.lens, [...(byLens.get(s.lens) ?? []), s]);
410
- for (const [lens, ss] of [...byLens.entries()].sort(
411
- (a, b) => b[1].length - a[1].length || (a[0] < b[0] ? -1 : 1),
412
- )) {
413
- out.push(`${lens} (${ss.length}):`);
414
- for (const s of ss.slice(0, 30))
415
- out.push(` - [${s.unit}] ${s.files.join(", ")} — ${s.reason} (at ${s.sha.slice(0, 12)})`);
416
- if (ss.length > 30) out.push(` … and ${ss.length - 30} more`);
417
- }
418
- }
419
- if (settings) {
420
- const lenses = stage2Lenses(settings.lenses);
421
- out.push(
422
- "",
423
- `Stage 2 may run: ${lenses.map((l) => `${l} (${lensTiers(settings.lenses, l, settings.riskPasses ?? []).join("|")})`).join(", ")}.`,
424
- `One row reads at most ${settings.scan.case_max_files} files / ${settings.scan.case_max_lines} lines, paired tests included; at most ${MAX_STAGE2_ROWS} rows.`,
425
- settings.scan.depth === "deep"
426
- ? `Depth: deep — a proposal adds the floor after your rows (${settings.scan.deep_lenses.filter((l) => lenses.includes(l)).join(", ") || "no lens: every deep_lenses lens is switched off"} over every file stage 1 reads; not counted in the ${MAX_STAGE2_ROWS}), so your rows may be none. deep: false proposes a standard plan.`
427
- : "Depth: standard — deep: true on a proposal makes it deep (the floor, every lens told to dig).",
428
- );
429
- }
430
- return out.join("\n");
431
- }
432
-
433
- /** The floor's rows, one line per lens: its row ids, files, cost and what
434
- * the budget left out — the floor is bounded by the tree, so its rows are
435
- * summed, never listed one by one. */
436
- function renderFloor(p: StoredStage2Proposal): string[] {
437
- const floor = p.rows.filter((r) => r.floor);
438
- const out: string[] = [];
439
- const lenses = [...new Set(floor.map((r) => r.lens))];
440
- for (const lens of lenses) {
441
- const rows = floor.filter((r) => r.lens === lens);
442
- const run = rows.filter((r) => r.status === "run");
443
- const over = rows.filter((r) => r.status === "over-budget");
444
- const covered = rows.filter((r) => r.status === "covered");
445
- const ids = rows.length === 1 ? rows[0]!.id : `${rows[0]!.id}…${rows[rows.length - 1]!.id}`;
446
- out.push(
447
- ` ${lens} (floor ${ids}) at ${rows[0]!.tier}: ${rows.length} row(s) over ${[...new Set(rows.flatMap((r) => r.units))].length} unit(s), ` +
448
- `${rows.reduce((n, r) => n + r.files.length, 0)} file(s), ${rows.reduce((n, r) => n + r.lines, 0)} lines — ` +
449
- `${run.length} to run, ~${usd(run.reduce((n, r) => n + r.estimate_usd, 0))} (${priceLabel(rows[0]!.price)})` +
450
- (covered.length ? `; ${covered.length} covered (free)` : "") +
451
- (over.length
452
- ? `; OVER BUDGET, left out: ${over.map((r) => r.id).join(", ")} (${usd(over.reduce((n, r) => n + r.estimate_usd, 0))})`
453
- : ""),
454
- );
455
- }
456
- const f = p.floor;
457
- if (f) {
458
- if (f.lenses.length === 0)
459
- out.push(" The floor has no lens: every [scan] deep_lenses lens is switched off.");
460
- if (f.skipped.length > 0)
461
- out.push(` Switched off by the profile, no floor rows: ${f.skipped.join(", ")}.`);
462
- if (f.covered > 0 || f.named > 0)
463
- out.push(
464
- ` Left out of the floor: ${f.covered} file × lens already read deep at this content` +
465
- (f.named > 0 ? `, ${f.named} named by your rows for the same lens` : "") +
466
- ".",
467
- );
468
- }
469
- return out;
470
- }
471
-
472
- export function renderProposal(p: StoredStage2Proposal, verbs: ScanVerbs = TOOL_VERBS): string {
473
- const out: string[] = [];
474
- const run = p.rows.filter((r) => r.status === "run");
475
- const deep = p.depth === "deep";
476
- out.push(
477
- `STAGE 2 proposal ${deep ? "(deep) " : ""}at ${p.sha.slice(0, 12)} — ${run.length} row(s) to run, estimate ${usd(p.cap_usd)} (the run stops hard there)` +
478
- (p.budget_usd !== null ? `, fitted to a ${usd(p.budget_usd)} budget` : "") +
479
- `; validator ${p.validator ? "on" : "off"}.`,
480
- );
481
- if (deep) {
482
- const own = run.filter((r) => !r.floor);
483
- const floor = run.filter((r) => r.floor);
484
- const sum = (rs: readonly { estimate_usd: number }[]): number =>
485
- rs.reduce((n, r) => n + r.estimate_usd, 0);
486
- out.push(
487
- `DEEP: your rows ${own.length} to run, ~${usd(sum(own))}; the floor ${floor.length} to run, ~${usd(sum(floor))}. ` +
488
- "Every lens is told to dig past its row's files; a standard reading covers nothing here, and the floor runs after your rows (the budget fits them first).",
489
- );
490
- }
491
- const rows = p.rows
492
- .filter((r) => !r.floor)
493
- .map((r) => {
494
- const mark =
495
- r.status === "run"
496
- ? ""
497
- : r.status === "covered"
498
- ? " — COVERED (already read at this content, free)"
499
- : ` — OVER BUDGET (${usd(r.estimate_usd)}, left out)`;
500
- const price = priceLabel(r.price);
501
- return [
502
- ` ${r.id}: ${r.lens} at ${r.tier} over ${r.units.join(" + ")}${r.units.length > 1 ? " (cross-module)" : ""} — ${r.files.length} file(s)` +
503
- (r.tests.length ? ` + ${r.tests.length} paired test(s)` : "") +
504
- `, ${r.lines} lines, ~${usd(r.estimate_usd)} (${price})${mark}`,
505
- ` ${r.reason}`,
506
- ` files: ${r.files.map((f) => f.path).join(", ")}`,
507
- ...(r.tests.length ? [` tests: ${r.tests.map((f) => f.path).join(", ")}`] : []),
508
- ...(r.unpaired.length ? [` not paired (row full): ${r.unpaired.join(", ")}`] : []),
509
- ].join("\n");
510
- });
511
- if (rows.length > 0) out.push(rows.join("\n"));
512
- if (deep) out.push(["The deep floor:", ...renderFloor(p)].join("\n"));
513
- const priced = p.rows.filter((r) => r.status !== "covered");
514
- const measured = priced.filter((r) => priceIsMeasured(r.price)).length;
515
- if (priced.length > 0) {
516
- out.push(
517
- `Priced by measurement: ${measured} row(s); by estimate: ${priced.length - measured} row(s).` +
518
- (measured < priced.length
519
- ? " An estimate stands for this run once approved; the next proposal is priced from what the run measures."
520
- : ""),
521
- );
522
- }
523
- if (p.over_budget_usd > 0) out.push(`Left out by the budget: ${usd(p.over_budget_usd)} of rows.`);
524
- if (p.runnable > 0) {
525
- out.push(`To run it: ${verbs.stage2Start(p.key)}`);
526
- } else out.push("✗ nothing to run: every row is covered or over the budget.");
527
- return out.join("\n\n");
528
- }
529
-
530
- // ---- shared verbs ------------------------------------------------------------
531
- //
532
- // What a tool and its `qcr scan` subcommand (src/scancli.ts) both answer:
533
- // the checks, the orders and the structured results. Each surface renders
534
- // them its own way; neither carries a rule of its own.
535
-
536
- /** A preview's structured result: its cases summarised (counts, not files). */
537
- export function previewDetails(p: ScanPreview): Record<string, unknown> {
538
- return {
539
- ...p,
540
- cases: p.cases.map((c) => ({
541
- id: c.id,
542
- files: c.files.length,
543
- lines: c.lines,
544
- estimate_usd: c.estimate_usd,
545
- })),
546
- };
547
- }
548
-
549
- /** A started run, as a start answers it. */
550
- export function startedDetails(run: ScanRun): {
551
- id: string;
552
- mode: ScanRun["mode"];
553
- sha: string;
554
- cases: number;
555
- cap_usd: number;
556
- } {
557
- return {
558
- id: run.id,
559
- mode: run.mode,
560
- sha: run.sha,
561
- cases: run.cases.length,
562
- cap_usd: run.cap_usd,
563
- };
564
- }
565
-
566
- export { FINDING_STATES } from "../src/setup/findings.js";
567
- /** How many findings a listing shows unless told otherwise, and at most. */
568
- export const FINDINGS_LIMIT = 50;
569
- export const FINDINGS_LIMIT_MAX = 200;
570
-
571
- /** A findings filter from loose parameters (a tool call's, the CLI's flags),
572
- * or what is wrong with one. */
573
- export function findingsFilter(
574
- p: Record<string, unknown>,
575
- ): { ok: true; filter: FindingsFilter } | { ok: false; error: string } {
576
- const filter: FindingsFilter = {};
577
- if (typeof p.state === "string") {
578
- if (!(FINDING_STATES as readonly string[]).includes(p.state))
579
- return { ok: false, error: `Unknown state ${JSON.stringify(p.state)}.` };
580
- filter.state = p.state as FindingState;
581
- }
582
- if (typeof p.severity === "string") {
583
- if (!(SEVERITIES as readonly string[]).includes(p.severity))
584
- return { ok: false, error: `Unknown severity ${JSON.stringify(p.severity)}.` };
585
- filter.severity = p.severity as ScanSeverity;
586
- }
587
- if (typeof p.lens === "string") filter.lens = p.lens;
588
- if (typeof p.unit === "string") filter.unit = p.unit;
589
- if (typeof p.doubted === "boolean") filter.doubted = p.doubted;
590
- if (typeof p.validity === "string") {
591
- if (!(VALIDITY_ORDER as readonly string[]).includes(p.validity))
592
- return {
593
- ok: false,
594
- error: `Unknown validity ${JSON.stringify(p.validity)} — confirmed, likely, doubtful or unknown.`,
595
- };
596
- filter.validity = p.validity as ValidityView;
597
- }
598
- return { ok: true, filter };
599
- }
600
-
601
- /** The stored findings a filter keeps, in validity order (`byValidity`),
602
- * with the count in each tier — or why the store cannot be read. */
603
- export function listFindings(
604
- findings: FindingsStore,
605
- filter: FindingsFilter,
606
- ):
607
- | { ok: true; findings: ScanFinding[]; validity: Record<ValidityView, number> }
608
- | { ok: false; error: string } {
609
- const { findings: listed, error } = findings.list(filter);
610
- if (error !== null) return { ok: false, error };
611
- const fs = byValidity(listed);
612
- const validity = Object.fromEntries(
613
- VALIDITY_ORDER.map((v) => [v, fs.filter((f) => validityOf(f) === v).length]),
614
- ) as Record<ValidityView, number>;
615
- return { ok: true, findings: fs, validity };
616
- }
617
-
618
- /** Answer the match question whole, then close the tickets of the promoted
619
- * findings it fixed — and the lines that say what was settled. */
620
- export async function answerMatch(
621
- findings: FindingsStore,
622
- args: { key?: unknown; pairs?: unknown },
623
- afterMatch: (() => Promise<CloseFixedResult>) | null,
624
- ): Promise<
625
- | Extract<MatchResult, { ok: false }>
626
- | (Extract<MatchResult, { ok: true }> & { tickets: CloseFixedResult | null; lines: string[] })
627
- > {
628
- const r = findings.match({ key: args.key, pairs: args.pairs ?? [] });
629
- if (!r.ok) return r;
630
- const lines = [
631
- `Settled: ${r.matched} matched (state kept), ${r.fixed} fixed, ${r.new} new` +
632
- (r.dismissed_unreported > 0
633
- ? `, ${r.dismissed_unreported} dismissed kept dismissed (stamped no longer reported)`
634
- : "") +
635
- ".",
636
- ];
637
- let tickets: CloseFixedResult | null = null;
638
- if (afterMatch && r.fixed > 0) {
639
- tickets = await afterMatch();
640
- if (tickets.closed.length > 0)
641
- lines.push(`Closed as fixed: ${tickets.closed.map((c) => c.ref).join(", ")}.`);
642
- for (const p of tickets.problems) lines.push(`! ticket not closed: ${p}`);
643
- }
644
- return { ...r, tickets, lines };
645
- }
646
-
647
- /** A stored proposal's summary, as a propose answers it. */
648
- export function proposalDetails(p: StoredStage2Proposal): {
649
- key: string;
650
- sha: string;
651
- cap_usd: number;
652
- runnable: number;
653
- rows: number;
654
- depth: "standard" | "deep";
655
- floor_rows: number;
656
- floor_usd: number;
657
- } {
658
- const floor = p.rows.filter((x) => x.floor);
659
- return {
660
- key: p.key,
661
- sha: p.sha,
662
- cap_usd: p.cap_usd,
663
- runnable: p.runnable,
664
- rows: p.rows.length,
665
- depth: p.depth ?? "standard",
666
- floor_rows: floor.length,
667
- floor_usd: floor.filter((x) => x.status === "run").reduce((n, x) => n + x.estimate_usd, 0),
668
- };
669
- }
670
-
671
- /** The line a stage-2 start answers: how many rows run (of how many, when
672
- * narrowed), and the hard stop. */
673
- export function stage2StartedLine(
674
- run: ScanRun,
675
- runnable: number | null,
676
- narrowed: boolean,
677
- verbs: ScanVerbs = TOOL_VERBS,
678
- ): string {
679
- const count =
680
- narrowed && runnable !== null
681
- ? `${run.cases.length} row(s) of ${runnable} (${run.cases.map((c) => c.id).join(", ")})`
682
- : `${run.cases.length} row(s)`;
683
- return (
684
- `Stage 2 ${run.id} started at ${run.sha.slice(0, 12)}: ${count}, hard stop ${usd(run.cap_usd)}. ` +
685
- `It runs in the background — ${verbs.status} shows progress, ${verbs.stop} stops it.`
686
- );
687
- }
688
-
689
- /** The line a stage-1 start answers. */
690
- export function startedLine(run: ScanRun, verbs: ScanVerbs = TOOL_VERBS): string {
691
- return (
692
- `Scan ${run.id} (${run.mode}) started at ${run.sha.slice(0, 12)}: ${run.cases.length} case(s), hard stop ${usd(run.cap_usd)}. ` +
693
- `It runs in the background — ${verbs.status} shows progress, ${verbs.stop} stops it.`
694
- );
695
- }
696
-
697
- /** How often a stop through the channel looks for its answer, and how long
698
- * it waits for one. */
699
- export const STOP_POLL_MS = 2000;
700
- export const STOP_WAIT_MS = 15_000;
701
-
702
- /** Where a stop of a run another process drives goes: the stop channel
703
- * under the state home, answered by whichever process holds the run. */
704
- export interface StopChannel {
705
- home: string;
706
- by?: ScanStopRequest["by"];
707
- sleep?: (ms: number) => Promise<void>;
708
- now?: () => number;
709
- }
710
-
711
- /** Stop the recorded run: here when this runner drives it, else through the
712
- * stop channel the driving process answers — the companion, or a
713
- * `qcr scan` driver. No answer within STOP_WAIT_MS: nothing drives it. */
714
- export async function stopScan(
715
- scan: ScanRunner,
716
- ch: StopChannel,
717
- ): Promise<{ ok: true; run: string; answer: string } | { ok: false; errors: string[] }> {
718
- const sleep = ch.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
719
- const now = ch.now ?? (() => Date.now());
720
- const s = scan.status();
721
- const run = s.run;
722
- if (run === null || run.state !== "running")
723
- return { ok: false, errors: ["no scan is recorded as running"] };
724
- if (s.active) {
725
- const r = scan.stop();
726
- if (!r.ok) return r;
727
- return { ok: true, run: r.id, answer: `scan ${r.id} stopped — what finished is in the ledger` };
728
- }
729
- const asked = clickStopScan(ch.home, run.id, new Date(now()), ch.by ?? "companion");
730
- if (!asked.ok) return { ok: false, errors: [asked.line] };
731
- const t0 = now();
732
- for (;;) {
733
- const answer = scanStopOutcome(ch.home, asked.request);
734
- if (answer !== null) {
735
- const read = readStopResult(ch.home);
736
- const ok =
737
- read.kind === "read" && read.result.request === asked.request && read.result.ok === true;
738
- return ok ? { ok: true, run: run.id, answer } : { ok: false, errors: [answer] };
739
- }
740
- if (now() - t0 >= STOP_WAIT_MS)
741
- return {
742
- ok: false,
743
- errors: [
744
- `no process answered the stop of ${run.id} within ${STOP_WAIT_MS / 1000}s — nothing is driving it ` +
745
- "(it reads as interrupted); preview and start again to resume",
746
- ],
747
- };
748
- await sleep(STOP_POLL_MS);
749
- }
750
- }
78
+ // The rendering and the shared verbs are
79
+ // packages/pi-plugin/src/scan/scanrender.ts's, shared with `qcr scan` and the
80
+ // pi plugin's /qcr-scan.
81
+ export {
82
+ CLI_VERBS,
83
+ FINDINGS_LIMIT,
84
+ FINDINGS_LIMIT_MAX,
85
+ FINDING_STATES,
86
+ STOP_POLL_MS,
87
+ STOP_WAIT_MS,
88
+ TOOL_VERBS,
89
+ answerMatch,
90
+ byValidity,
91
+ findingsFilter,
92
+ listFindings,
93
+ previewDetails,
94
+ proposalDetails,
95
+ renderFindings,
96
+ renderPreview,
97
+ renderProposal,
98
+ renderQuestion,
99
+ renderStatus,
100
+ renderSuggestions,
101
+ stage2StartedLine,
102
+ startedDetails,
103
+ startedLine,
104
+ stopScan,
105
+ type ScanVerbs,
106
+ type StopChannel,
107
+ type TicketsClosed,
108
+ } from "@quickchatai/pi-quickreview/src/scan/scanrender.js";
751
109
 
752
110
  // ---- tool belt ---------------------------------------------------------------
753
111
 
@@ -1119,11 +477,12 @@ export function scanLeafRunner(
1119
477
  };
1120
478
  }
1121
479
 
1122
- /** The scan runner the companion's belt drives: the reviewed checkout as a
480
+ /** The scan runner the companion's belt drives
481
+ * (packages/pi-plugin/src/scan/scanassemble.ts): the reviewed checkout as a
1123
482
  * source of trees, the state home for every record, the saved unit cut from
1124
483
  * the setup engine, the live config under the profile, and a case runner
1125
- * over the scan agent build — and the triage over its findings:
1126
- * reviewer memory and the tracker sink are the engine's own, handed in. */
484
+ * over the scan agent build — and the triage over its findings: reviewer
485
+ * memory and the tracker sink are the engine's own, handed in. */
1127
486
  export function buildScan(
1128
487
  engine: SetupEngine,
1129
488
  reviewedRepo: string,
@@ -1145,75 +504,38 @@ export function buildScan(
1145
504
  } {
1146
505
  const root = env.QCR_ROOT?.trim() || sandboxRoot();
1147
506
  const home = stateHome(env);
1148
- const state = stateHomeFs(home);
1149
- const agentsDir = join(home, SCAN_AGENTS_DIR);
1150
- const settingsOf = (): ScanSettings | null => {
1151
- const loaded = loadScanConfig(root, env);
1152
- return loaded.ok ? scanSettings(loaded.cfg) : null;
1153
- };
1154
- const findings = new FindingsStore(state);
1155
- const transcriptDir = join(home, SCAN_TRANSCRIPTS_DIR);
1156
- // A token a case would not outlive stops the run, on either host.
1157
- const stopOnExpired = (reason: string): void => {
1158
- console.error(`scan: stopping — ${reason}`);
1159
- scan.stop();
1160
- };
1161
507
  // Assigned below: the triage reads the runner's runs, the runner closes
1162
508
  // the triage's tickets at every run's end.
1163
509
  let triage: ScanTriage | null = null;
1164
- const scan = new ScanRunner({
1165
- state,
1166
- clock: () => new Date(),
510
+ const { scan, findings, settingsOf } = assembleScan({
511
+ home,
512
+ reviewedRepo,
513
+ root,
514
+ config: () => loadScanConfig(root, env),
515
+ profiles: homeProfileLookup(env),
516
+ unitCut: engine,
1167
517
  // In the scan container: each case on a fresh refresh-disabled copy of
1168
518
  // the companion's login; a token a case would not outlive stops the run.
1169
- cases: scanCasesForEnv(
1170
- scanLeafRunner(
1171
- piCaseRunner({ agentsDir, cwd: root, transcriptDir }),
1172
- root,
519
+ cases: ({ agentsDir, transcriptDir, stop }) =>
520
+ scanCasesForEnv(
521
+ scanLeafRunner(
522
+ piCaseRunner({ agentsDir, cwd: root, transcriptDir }),
523
+ root,
524
+ env,
525
+ sdkLeaves &&
526
+ (() => sdkLeaves({ agentsDir, cwd: root, transcriptDir, env, onExpired: stop })),
527
+ ),
1173
528
  env,
1174
- sdkLeaves &&
1175
- (() => sdkLeaves({ agentsDir, cwd: root, transcriptDir, env, onExpired: stopOnExpired })),
529
+ stop,
1176
530
  ),
1177
- env,
1178
- stopOnExpired,
1179
- ),
1180
- source: gitScanSource(reviewedRepo, home),
1181
- savedCut: () => {
1182
- const cut = engine.savedCut();
1183
- if (cut) return { ok: true, cut };
1184
- const status = engine.status();
1185
- return {
1186
- ok: false,
1187
- errors: status.cut_file_unreadable
1188
- ? status.problems
1189
- : ["no saved unit cut — a scan reads by unit; propose, adjust and save one first"],
1190
- };
1191
- },
1192
- settings: () => {
1193
- const loaded = loadScanConfig(root, env);
1194
- return loaded.ok ? { ok: true, settings: scanSettings(loaded.cfg) } : loaded;
1195
- },
1196
- // At the depth the runner planned at — a stage-2 proposal's own, which
1197
- // its `deep` may have set apart from `[scan] depth`.
1198
- buildAgents: (settings) => {
1199
- const loaded = loadScanConfig(root, env);
1200
- if (!loaded.ok) return loaded;
1201
- const cfg = { ...loaded.cfg, scan: { ...loaded.cfg.scan, depth: settings.scan.depth } };
1202
- try {
1203
- buildScanAgents(root, agentsDir, cfg, homeProfileLookup(env));
1204
- return { ok: true };
1205
- } catch (err) {
1206
- return { ok: false, errors: [err instanceof Error ? err.message : String(err)] };
1207
- }
1208
- },
1209
531
  closeFixedTickets: async () => (triage ? triage.closeFixed() : { closed: [], problems: [] }),
1210
- lease: lease ?? fileLease(home),
532
+ lease,
1211
533
  log: (line) => console.error(line),
1212
534
  });
1213
535
  if (shared) {
1214
536
  triage = new ScanTriage({
1215
537
  findings,
1216
- state,
538
+ state: stateHomeFs(home),
1217
539
  memory: shared.memory,
1218
540
  tracker: shared.tracker,
1219
541
  clock: () => new Date(),
@@ -1224,13 +546,4 @@ export function buildScan(
1224
546
  return { scan, findings, settingsOf, triage, home };
1225
547
  }
1226
548
 
1227
- /** The runner's runs, newest first, as the triage sees them: a run left
1228
- * `running` by a companion that is gone reads as interrupted. */
1229
- export function scanRuns(scan: ScanRunner): Array<{ id: string; sha: string; state: string }> {
1230
- const s = scan.status();
1231
- const out: Array<{ id: string; sha: string; state: string }> = [];
1232
- if (s.run) out.push({ id: s.run.id, sha: s.run.sha, state: s.run.display });
1233
- for (const h of s.history)
1234
- if (!out.some((r) => r.id === h.id)) out.push({ id: h.id, sha: h.sha, state: h.state });
1235
- return out;
1236
- }
549
+ export { scanRuns };