@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,627 +1,3 @@
1
- // Deterministic repo facts: what a repo says about itself in files, read
2
- // without a model — the stack, frameworks, CI checks, linters and formatters,
3
- // the design docs and style guides, the agent-directive files, and a
4
- // stack-aware first guess at the unit cut. Free facts must not cost model
5
- // time, and the survey lens and the portrait start from these rather than
6
- // rediscovering them.
7
- //
8
- // Pure: a listing of repo-relative paths plus a reader that answers null for
9
- // anything it cannot read. Nothing here touches a filesystem, so the engine's
10
- // repo port decides what "the repo" is (git-tracked files of the reviewed
11
- // checkout, in production) and tests hand over a map.
12
- //
13
- // Every detection is evidence-carrying: each fact names the file that proves
14
- // it, so the operator can check the facts and the companion can cite them.
15
- // Nothing is inferred from absence — a repo with no CI config has `ci: []`,
16
- // which says "no config file found", not "no CI".
17
- import { parse as parseToml } from "smol-toml";
18
- import { DEFAULT_DIRECTIVE_VOCABULARY } from "@quickchatai/pi-quickreview/src/severity.js";
19
- import { styleGuideRefsIn } from "@quickchatai/pi-quickreview/src/styleguide.js";
20
- import { escapeControls } from "./textsniff.js";
21
-
22
- export type RepoRead = (rel: string) => string | null;
23
-
24
- export interface Evidence {
25
- name: string;
26
- /** Repo-relative path of the file that shows it. */
27
- evidence: string;
28
- }
29
-
30
- export interface LanguageCount {
31
- language: string;
32
- files: number;
33
- }
34
-
35
- export interface CiFact {
36
- system: string;
37
- path: string;
38
- /** The workflow's `name:` when the file states one. */
39
- name: string | null;
40
- }
41
-
42
- /** Every unit kind — the type and the runtime check (`unitcut.ts`) read this
43
- * one list. */
44
- export const UNIT_KINDS = [
45
- "django-app",
46
- "js-package",
47
- "python-package",
48
- "go-module",
49
- "rust-crate",
50
- "directory",
51
- "rest",
52
- "custom",
53
- ] as const;
54
- export type UnitKind = (typeof UNIT_KINDS)[number];
55
-
56
- /** One candidate unit: a name, the path prefixes it owns, and why. */
57
- export interface UnitCandidate {
58
- name: string;
59
- roots: string[];
60
- kind: UnitKind;
61
- reason: string;
62
- }
63
-
64
- export interface RepoFacts {
65
- files: number;
66
- languages: LanguageCount[];
67
- /** Package-manager manifests, as evidence per ecosystem. */
68
- manifests: Evidence[];
69
- frameworks: Evidence[];
70
- ci: CiFact[];
71
- /** Linters, formatters and type checkers whose config is in the repo. */
72
- linters: Evidence[];
73
- /** Design docs read in full by the survey: README, CONTRIBUTING, DESIGN… */
74
- guides: string[];
75
- /** Written style guides, in `findStyleGuide`'s reference grammar. */
76
- style_guides: string[];
77
- /** First-party agent-directive files (AGENTS.md, CLAUDE.md, rules dirs…). */
78
- agent_files: string[];
79
- /** The deterministic first guess at the unit cut — the companion's input. */
80
- unit_candidates: UnitCandidate[];
81
- }
82
-
83
- /** Stable item text shared by facts and portrait renderers. */
84
- export const evidenceText = (item: Evidence): string => `${item.name} — ${item.evidence}`;
85
- export const ciText = (item: CiFact): string =>
86
- `${item.system} — ${item.path}${item.name ? ` ("${item.name}")` : ""}`;
87
- export const displayRoot = (root: string): string => root || "(repo root)";
88
-
89
- // ---- helpers -----------------------------------------------------------------
90
-
91
- function base(path: string): string {
92
- return path.slice(path.lastIndexOf("/") + 1);
93
- }
94
-
95
- function dirOf(path: string): string {
96
- const i = path.lastIndexOf("/");
97
- return i < 0 ? "" : path.slice(0, i);
98
- }
99
-
100
- /** Migration files, whatever the stack calls their directory. */
101
- const MIGRATION_RE = /(^|\/)(migrations?|alembic\/versions|db\/migrate)\//i;
102
-
103
- export function isMigrationFile(path: string): boolean {
104
- return MIGRATION_RE.test(path);
105
- }
106
-
107
- /** Vendored trees are someone else's repo: never evidence about this one. */
108
- export function isVendored(path: string): boolean {
109
- return path
110
- .split("/")
111
- .some((s) => DEFAULT_DIRECTIVE_VOCABULARY.vendoredSegments.has(s.toLowerCase()));
112
- }
113
-
114
- const LANGUAGE_BY_EXT: Record<string, string> = {
115
- py: "Python",
116
- pyi: "Python",
117
- js: "JavaScript",
118
- jsx: "JavaScript",
119
- mjs: "JavaScript",
120
- cjs: "JavaScript",
121
- ts: "TypeScript",
122
- tsx: "TypeScript",
123
- mts: "TypeScript",
124
- cts: "TypeScript",
125
- go: "Go",
126
- rs: "Rust",
127
- rb: "Ruby",
128
- java: "Java",
129
- kt: "Kotlin",
130
- kts: "Kotlin",
131
- scala: "Scala",
132
- php: "PHP",
133
- cs: "C#",
134
- c: "C",
135
- h: "C",
136
- cc: "C++",
137
- cpp: "C++",
138
- cxx: "C++",
139
- hh: "C++",
140
- hxx: "C++",
141
- hpp: "C++",
142
- swift: "Swift",
143
- m: "Objective-C",
144
- mm: "Objective-C",
145
- dart: "Dart",
146
- ex: "Elixir",
147
- exs: "Elixir",
148
- erl: "Erlang",
149
- hs: "Haskell",
150
- clj: "Clojure",
151
- lua: "Lua",
152
- sh: "Shell",
153
- bash: "Shell",
154
- sql: "SQL",
155
- vue: "Vue",
156
- svelte: "Svelte",
157
- html: "HTML",
158
- css: "CSS",
159
- scss: "CSS",
160
- less: "CSS",
161
- tf: "Terraform",
162
- };
163
-
164
- function languages(files: readonly string[]): LanguageCount[] {
165
- const counts = new Map<string, number>();
166
- for (const f of files) {
167
- const b = base(f);
168
- const dot = b.lastIndexOf(".");
169
- if (dot <= 0) continue;
170
- const lang = LANGUAGE_BY_EXT[b.slice(dot + 1).toLowerCase()];
171
- if (lang) counts.set(lang, (counts.get(lang) ?? 0) + 1);
172
- }
173
- return [...counts.entries()]
174
- .map(([language, n]) => ({ language, files: n }))
175
- .sort((a, b) => b.files - a.files || a.language.localeCompare(b.language));
176
- }
177
-
178
- /** Manifest basename → ecosystem. */
179
- const MANIFESTS: Record<string, string> = {
180
- "package.json": "npm",
181
- "pnpm-workspace.yaml": "pnpm workspace",
182
- "pyproject.toml": "python (pyproject)",
183
- "setup.py": "python (setuptools)",
184
- "setup.cfg": "python (setuptools)",
185
- "requirements.txt": "python (pip)",
186
- Pipfile: "python (pipenv)",
187
- "go.mod": "go modules",
188
- "Cargo.toml": "cargo",
189
- Gemfile: "bundler",
190
- "pom.xml": "maven",
191
- "build.gradle": "gradle",
192
- "build.gradle.kts": "gradle",
193
- "composer.json": "composer",
194
- "mix.exs": "mix",
195
- "pubspec.yaml": "pub",
196
- "Package.swift": "swiftpm",
197
- };
198
-
199
- const isRequirementsName = (name: string): boolean =>
200
- /^requirements([-_.][\w.-]*)?\.txt$/.test(name);
201
-
202
- function manifests(files: readonly string[]): Evidence[] {
203
- const out: Evidence[] = [];
204
- for (const f of files) {
205
- const b = base(f);
206
- const eco = MANIFESTS[b] ?? (isRequirementsName(b) ? "python (pip)" : null);
207
- if (eco) out.push({ name: eco, evidence: f });
208
- }
209
- return out;
210
- }
211
-
212
- /** A Python dependency line naming exactly `dep` (any version spec, extras,
213
- * or a Poetry `dep = "…"` table entry) — `flask-cors` is not Flask. */
214
- function pyDep(dep: string): RegExp {
215
- return new RegExp(`^\\s*${dep}(\\[[^\\]]*\\])?\\s*([<>=~!;,"'@]|$)`, "im");
216
- }
217
-
218
- /** Framework → the dependency spelling that proves it, per manifest family. */
219
- const PY_FRAMEWORKS: Array<[string, RegExp]> = [
220
- ["Django", pyDep("django")],
221
- ["Django REST framework", pyDep("djangorestframework")],
222
- ["Flask", pyDep("flask")],
223
- ["FastAPI", pyDep("fastapi")],
224
- ["Celery", pyDep("celery")],
225
- ["SQLAlchemy", pyDep("sqlalchemy")],
226
- ["pytest", pyDep("pytest")],
227
- ["Pydantic", pyDep("pydantic")],
228
- ];
229
-
230
- const JS_FRAMEWORKS: Array<[string, string]> = [
231
- ["React", "react"],
232
- ["Next.js", "next"],
233
- ["Vue", "vue"],
234
- ["Nuxt", "nuxt"],
235
- ["Svelte", "svelte"],
236
- ["SvelteKit", "@sveltejs/kit"],
237
- ["Angular", "@angular/core"],
238
- ["Express", "express"],
239
- ["NestJS", "@nestjs/core"],
240
- ["Fastify", "fastify"],
241
- ["Tailwind CSS", "tailwindcss"],
242
- ["Vite", "vite"],
243
- ["Jest", "jest"],
244
- ["Vitest", "vitest"],
245
- ["Playwright", "@playwright/test"],
246
- ["Electron", "electron"],
247
- ["React Native", "react-native"],
248
- ];
249
-
250
- const SPRING_SIGNATURE = /spring-boot|springframework/;
251
-
252
- const OTHER_FRAMEWORKS: Array<[string, string, RegExp]> = [
253
- ["Rails", "Gemfile", /^\s*gem\s+["']rails["']/m],
254
- ["Spring", "pom.xml", SPRING_SIGNATURE],
255
- ["Spring", "build.gradle", SPRING_SIGNATURE],
256
- ["Spring", "build.gradle.kts", SPRING_SIGNATURE],
257
- ["Laravel", "composer.json", /"laravel\/framework"/],
258
- ["Phoenix", "mix.exs", /:phoenix\b/],
259
- ["Gin", "go.mod", /github\.com\/gin-gonic\/gin/],
260
- ["Echo", "go.mod", /github\.com\/labstack\/echo/],
261
- ["Actix", "Cargo.toml", /^\s*actix-web\s*=/m],
262
- ["Axum", "Cargo.toml", /^\s*axum\s*=/m],
263
- ];
264
-
265
- function packageDeps(text: string): Set<string> {
266
- try {
267
- const pkg = JSON.parse(text) as Record<string, unknown>;
268
- const deps = new Set<string>();
269
- for (const key of ["dependencies", "devDependencies", "peerDependencies"]) {
270
- const block = pkg[key];
271
- if (block && typeof block === "object") for (const d of Object.keys(block)) deps.add(d);
272
- }
273
- return deps;
274
- } catch {
275
- return new Set();
276
- }
277
- }
278
-
279
- /** Python dependency declarations only; project metadata is never evidence. */
280
- function pythonDependencies(name: string, text: string): string {
281
- if (isRequirementsName(name)) return text;
282
- if (name === "setup.py") {
283
- const declarations = [
284
- ...text.matchAll(
285
- /\b(install_requires|tests_require|extras_require)\s*=\s*(\[[\s\S]*?\]|\{[\s\S]*?\})/g,
286
- ),
287
- ];
288
- return declarations
289
- .flatMap((m) => {
290
- const lists =
291
- m[1] === "extras_require"
292
- ? [...m[2]!.matchAll(/:\s*(\[[\s\S]*?\])/g)].map((v) => v[1]!)
293
- : [m[2]!];
294
- return lists.flatMap((list) => [...list.matchAll(/["']([^"'\n]+)["']/g)].map((v) => v[1]!));
295
- })
296
- .join("\n");
297
- }
298
- if (name === "setup.cfg") {
299
- let section = "";
300
- let collecting = false;
301
- const out: string[] = [];
302
- for (const line of text.split("\n")) {
303
- const header = /^\s*\[([^\]]+)\]/.exec(line);
304
- if (header) {
305
- section = header[1]!;
306
- collecting = false;
307
- continue;
308
- }
309
- const key = /^([\w.-]+)\s*=\s*(.*)$/.exec(line);
310
- if (key) {
311
- collecting =
312
- section === "options.extras_require" ||
313
- (section === "options" && ["install_requires", "tests_require"].includes(key[1]!));
314
- if (collecting) out.push(key[2]!);
315
- } else if (collecting && /^\s+/.test(line)) out.push(line.trim());
316
- }
317
- return out.join("\n");
318
- }
319
- try {
320
- const table = (v: unknown): Record<string, unknown> =>
321
- v !== null && typeof v === "object" && !Array.isArray(v)
322
- ? (v as Record<string, unknown>)
323
- : {};
324
- const doc = table(parseToml(text));
325
- const out: string[] = [];
326
- const strings = (v: unknown): void => {
327
- if (Array.isArray(v)) for (const item of v) if (typeof item === "string") out.push(item);
328
- };
329
- const tableNames = (v: unknown): void => {
330
- out.push(...Object.keys(table(v)));
331
- };
332
- if (name === "Pipfile") {
333
- tableNames(doc.packages);
334
- tableNames(doc["dev-packages"]);
335
- } else {
336
- const project = table(doc.project);
337
- strings(project.dependencies);
338
- for (const values of Object.values(table(project["optional-dependencies"]))) strings(values);
339
- for (const values of Object.values(table(doc["dependency-groups"]))) strings(values);
340
- const poetry = table(table(doc.tool).poetry);
341
- tableNames(poetry.dependencies);
342
- tableNames(poetry["dev-dependencies"]);
343
- for (const group of Object.values(table(poetry.group))) tableNames(table(group).dependencies);
344
- }
345
- return out.join("\n");
346
- } catch {
347
- return "";
348
- }
349
- }
350
-
351
- function frameworks(files: readonly string[], read: RepoRead): Evidence[] {
352
- const seen = new Map<string, string>();
353
- const note = (name: string, evidence: string): void => {
354
- if (!seen.has(name)) seen.set(name, evidence);
355
- };
356
- for (const f of files) {
357
- const b = base(f);
358
- if (b === "manage.py") {
359
- const text = read(f);
360
- if (text !== null && /django/i.test(text)) note("Django", f);
361
- } else if (
362
- b === "pyproject.toml" ||
363
- b === "Pipfile" ||
364
- b === "setup.py" ||
365
- b === "setup.cfg" ||
366
- isRequirementsName(b)
367
- ) {
368
- const text = read(f);
369
- if (text === null) continue;
370
- const lines = pythonDependencies(b, text);
371
- for (const [name, re] of PY_FRAMEWORKS) if (re.test(lines)) note(name, f);
372
- } else if (b === "package.json") {
373
- const text = read(f);
374
- if (text === null) continue;
375
- const deps = packageDeps(text);
376
- for (const [name, dep] of JS_FRAMEWORKS) if (deps.has(dep)) note(name, f);
377
- } else {
378
- for (const [name, file, re] of OTHER_FRAMEWORKS) {
379
- if (b !== file) continue;
380
- const text = read(f);
381
- if (text !== null && re.test(text)) note(name, f);
382
- }
383
- }
384
- }
385
- return [...seen.entries()].map(([name, evidence]) => ({ name, evidence }));
386
- }
387
-
388
- function ciFacts(files: readonly string[], read: RepoRead): CiFact[] {
389
- const out: CiFact[] = [];
390
- for (const f of files) {
391
- let system: string | null = null;
392
- if (/^\.github\/workflows\/[^/]+\.ya?ml$/.test(f)) system = "GitHub Actions";
393
- else if (f === ".gitlab-ci.yml") system = "GitLab CI";
394
- else if (f === ".circleci/config.yml") system = "CircleCI";
395
- else if (f === "Jenkinsfile") system = "Jenkins";
396
- else if (f === "azure-pipelines.yml") system = "Azure Pipelines";
397
- else if (f === ".travis.yml") system = "Travis CI";
398
- else if (f === "bitbucket-pipelines.yml") system = "Bitbucket Pipelines";
399
- else if (/^\.buildkite\/[^/]+\.ya?ml$/.test(f)) system = "Buildkite";
400
- if (!system) continue;
401
- const text = system === "GitHub Actions" ? read(f) : null;
402
- // The name reaches the portrait and the companion's prompt: a control
403
- // byte in the workflow file (a stray NUL is still text) is escaped.
404
- const raw = text === null ? null : (/^name:\s*["']?(.+?)["']?\s*$/m.exec(text)?.[1] ?? null);
405
- const name = raw === null ? null : escapeControls(raw);
406
- out.push({ system, path: f, name });
407
- }
408
- return out;
409
- }
410
-
411
- /** Tool → config basenames that prove it (anywhere in the tree). */
412
- const LINTER_FILES: Array<[string, RegExp]> = [
413
- ["Ruff", /^\.?ruff\.toml$/],
414
- ["Flake8", /^\.flake8$/],
415
- ["Pylint", /^\.?pylintrc$/],
416
- ["mypy", /^\.?mypy\.ini$/],
417
- ["Black", /^\.black$/],
418
- ["isort", /^\.isort\.cfg$/],
419
- ["ESLint", /^(\.eslintrc(\.(js|cjs|json|ya?ml))?|eslint\.config\.(js|mjs|cjs|ts))$/],
420
- ["Prettier", /^(\.prettierrc(\.(js|cjs|json|ya?ml|toml))?|prettier\.config\.(js|mjs|cjs))$/],
421
- ["Biome", /^biome\.jsonc?$/],
422
- ["Stylelint", /^(\.stylelintrc(\.(js|json|ya?ml))?|stylelint\.config\.(js|cjs|mjs))$/],
423
- ["TypeScript compiler", /^tsconfig\.json$/],
424
- ["golangci-lint", /^\.golangci\.ya?ml$/],
425
- ["rustfmt", /^\.?rustfmt\.toml$/],
426
- ["Clippy", /^\.?clippy\.toml$/],
427
- ["RuboCop", /^\.rubocop\.yml$/],
428
- ["EditorConfig", /^\.editorconfig$/],
429
- ["pre-commit", /^\.pre-commit-config\.yaml$/],
430
- ["Checkstyle", /^checkstyle\.xml$/],
431
- ["Spotless", /^\.spotless$/],
432
- ];
433
-
434
- /** `[tool.X]` sections in a pyproject.toml / setup.cfg. */
435
- const PY_TOOL_SECTIONS: Array<[string, RegExp]> = [
436
- ["Ruff", /^\[tool\.ruff[\].]/m],
437
- ["Black", /^\[tool\.black\]/m],
438
- ["isort", /^\[(tool\.)?isort\]/m],
439
- ["mypy", /^\[(tool\.)?mypy[\].-]/m],
440
- ["Pylint", /^\[tool\.pylint[\].]/m],
441
- ["Flake8", /^\[flake8\]/m],
442
- ["pyright", /^\[tool\.pyright\]/m],
443
- ];
444
-
445
- function linters(files: readonly string[], read: RepoRead): Evidence[] {
446
- const seen = new Map<string, string>();
447
- const note = (name: string, evidence: string): void => {
448
- if (!seen.has(name)) seen.set(name, evidence);
449
- };
450
- for (const f of files) {
451
- const b = base(f);
452
- for (const [name, re] of LINTER_FILES) if (re.test(b)) note(name, f);
453
- if (b === "pyproject.toml" || b === "setup.cfg" || b === "tox.ini") {
454
- const text = read(f);
455
- if (text !== null)
456
- for (const [name, re] of PY_TOOL_SECTIONS) if (re.test(text)) note(name, f);
457
- } else if (b === "package.json") {
458
- const text = read(f);
459
- if (text === null) continue;
460
- try {
461
- const pkg = JSON.parse(text) as Record<string, unknown>;
462
- if (pkg.eslintConfig) note("ESLint", f);
463
- if (pkg.prettier) note("Prettier", f);
464
- } catch {
465
- // Not JSON: nothing to read, and no reason to fail the whole facts call.
466
- }
467
- }
468
- }
469
- return [...seen.entries()].map(([name, evidence]) => ({ name, evidence }));
470
- }
471
-
472
- /** Design docs the survey reads in full — the repo's stated intent. */
473
- const GUIDE_RE =
474
- /^(readme|contributing|design|product|architecture|overview|development|hacking|security|conventions|styleguide|style[-_]guide)(\.[a-z]+)?$/i;
475
-
476
- function guides(files: readonly string[]): string[] {
477
- return files
478
- .filter((f) => {
479
- const d = dirOf(f);
480
- return (d === "" || d === "docs" || d === "doc" || d === ".github") && GUIDE_RE.test(base(f));
481
- })
482
- .sort();
483
- }
484
-
485
- /** Agent-directive files: what the repo's team already told agents. Broader
486
- * than the review's `isFirstPartyDirectivePath` on purpose — that predicate
487
- * decides a finding's provenance and keeps its shipped vocabulary; this one is
488
- * a reading list, so it also takes the other tools' rule files. */
489
- const AGENT_BASENAMES = new Set([
490
- "AGENTS.md",
491
- "AGENT.md",
492
- "CLAUDE.md",
493
- "GEMINI.md",
494
- ".cursorrules",
495
- ".windsurfrules",
496
- ".clinerules",
497
- ]);
498
-
499
- export function isAgentFile(path: string): boolean {
500
- if (isVendored(path)) return false;
501
- if (AGENT_BASENAMES.has(base(path))) return true;
502
- if (path === ".github/copilot-instructions.md") return true;
503
- if (/^\.github\/instructions\/.+\.md$/.test(path)) return true;
504
- if (/(^|\/)\.cursor\/rules\//.test(path)) return true;
505
- if (/(^|\/)\.clinerules\//.test(path)) return true;
506
- if (/(^|\/)\.claude\/.+\.md$/.test(path)) return true;
507
- return false;
508
- }
509
-
510
- /** The shallowest vendored directories in the listing (`web/node_modules`,
511
- * `third_party`) — what a proposed cut ignores by default. */
512
- export function vendoredRoots(files: readonly string[]): string[] {
513
- const roots = new Set<string>();
514
- for (const f of files) {
515
- const segs = f.split("/");
516
- const i = segs.findIndex((s) =>
517
- DEFAULT_DIRECTIVE_VOCABULARY.vendoredSegments.has(s.toLowerCase()),
518
- );
519
- if (i >= 0 && i < segs.length - 1) roots.add(segs.slice(0, i + 1).join("/"));
520
- }
521
- return [...roots].filter((r) => ![...roots].some((o) => o !== r && r.startsWith(`${o}/`))).sort();
522
- }
523
-
524
- // ---- unit candidates ---------------------------------------------------------
525
-
526
- /** The longest unit name. */
527
- export const UNIT_NAME_MAX = 64;
528
-
529
- /** A unit name: one plain segment (the state files and reports key on it). */
530
- export const UNIT_NAME_RE = new RegExp(`^[A-Za-z0-9][A-Za-z0-9._-]{0,${UNIT_NAME_MAX - 1}}$`);
531
-
532
- function slug(path: string): string {
533
- const s = path
534
- .replace(/[^A-Za-z0-9._-]+/g, "-")
535
- .replace(/\.{2,}/g, "-")
536
- .replace(/^[-._]+|-+$/g, "");
537
- return (s || "root").slice(0, UNIT_NAME_MAX);
538
- }
539
-
540
- /** Directories never offered as units of their own (they ride "rest"). */
541
- const NON_UNIT_TOP =
542
- /^(\..*|docs?|scripts?|tools?|bin|ci|examples?|fixtures|assets|static|public)$/i;
543
-
544
- /**
545
- * The stack-aware first guess at the unit cut. Markers, most specific first:
546
- * a Django app (`apps.py`), a JS package (a nested `package.json`), a Python
547
- * package with its own `pyproject.toml`/`setup.py`, a Go module, a Rust crate.
548
- * Top-level directories holding code that no marker claimed become
549
- * `directory` units, and a `rest` unit at the repo root catches the remainder
550
- * so the guess is always a complete partition. The companion reshapes this;
551
- * it is evidence, not the answer.
552
- */
553
- export function unitCandidates(files: readonly string[]): UnitCandidate[] {
554
- const out: UnitCandidate[] = [];
555
- const roots = new Set<string>();
556
- const add = (root: string, kind: UnitKind, reason: string): void => {
557
- if (roots.has(root)) return;
558
- roots.add(root);
559
- out.push({ name: "", roots: [root], kind, reason });
560
- };
561
- const sorted = [...files].filter((f) => !isVendored(f)).sort();
562
- for (const f of sorted) {
563
- const b = base(f);
564
- const d = dirOf(f);
565
- if (d === "") continue;
566
- if (b === "apps.py") add(d, "django-app", `${f} declares a Django app`);
567
- else if (b === "package.json") add(d, "js-package", `${f} declares a package`);
568
- else if (b === "pyproject.toml" || b === "setup.py")
569
- add(d, "python-package", `${f} declares a Python package`);
570
- else if (b === "go.mod") add(d, "go-module", `${f} declares a Go module`);
571
- else if (b === "Cargo.toml") add(d, "rust-crate", `${f} declares a crate`);
572
- }
573
- const covered = (f: string): boolean => [...roots].some((r) => f === r || f.startsWith(`${r}/`));
574
- const tops = new Map<string, number>();
575
- for (const f of sorted) {
576
- const i = f.indexOf("/");
577
- if (i < 0 || covered(f) || !/\.[A-Za-z0-9]+$/.test(f)) continue;
578
- const top = f.slice(0, i);
579
- if (NON_UNIT_TOP.test(top)) continue;
580
- const ext = f.slice(f.lastIndexOf(".") + 1).toLowerCase();
581
- if (LANGUAGE_BY_EXT[ext]) tops.set(top, (tops.get(top) ?? 0) + 1);
582
- }
583
- for (const [top, n] of [...tops.entries()].sort()) {
584
- add(
585
- top,
586
- "directory",
587
- `top-level directory with ${n} code file${n === 1 ? "" : "s"} no package marker claims`,
588
- );
589
- }
590
- // Names: the last segment, widened with its parents only where two collide.
591
- const byLast = new Map<string, number>();
592
- for (const c of out) {
593
- const last = base(c.roots[0]!);
594
- byLast.set(last, (byLast.get(last) ?? 0) + 1);
595
- }
596
- const taken = new Set<string>(["rest"]);
597
- for (const c of out) {
598
- const root = c.roots[0]!;
599
- let name = slug(byLast.get(base(root))! > 1 ? root : base(root));
600
- // The suffix must fit UNIT_NAME_MAX too, or the whole candidate cut is
601
- // refused for one long colliding path.
602
- for (let i = 2; taken.has(name); i++)
603
- name = `${slug(root).slice(0, UNIT_NAME_MAX - `-${i}`.length)}-${i}`;
604
- taken.add(name);
605
- c.name = name;
606
- }
607
- out.push({ name: "rest", roots: [""], kind: "rest", reason: "everything no other unit claims" });
608
- return out;
609
- }
610
-
611
- // ---- the whole read ----------------------------------------------------------
612
-
613
- export function repoFacts(files: readonly string[], read: RepoRead): RepoFacts {
614
- const own = files.filter((f) => !isVendored(f));
615
- return {
616
- files: files.length,
617
- languages: languages(own),
618
- manifests: manifests(own),
619
- frameworks: frameworks(own, read),
620
- ci: ciFacts(own, read),
621
- linters: linters(own, read),
622
- guides: guides(own),
623
- style_guides: styleGuideRefsIn(own, read),
624
- agent_files: own.filter(isAgentFile).sort(),
625
- unit_candidates: unitCandidates(own),
626
- };
627
- }
1
+ // Moved to the plugin (packages/pi-plugin/src/scan/facts.ts), the scan engine's
2
+ // home: this name stays for the app's importers.
3
+ export * from "@quickchatai/pi-quickreview/src/scan/facts.js";