@quickchatai/pi-quickreview 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (697) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +75 -2
  3. package/assets/agents/pass-abstraction.md +64 -0
  4. package/assets/agents/pass-ask-human.md +134 -0
  5. package/assets/agents/pass-backcompat.md +67 -0
  6. package/assets/agents/pass-baseline.md +61 -0
  7. package/assets/agents/pass-behaviour-delta.md +64 -0
  8. package/assets/agents/pass-bounds.md +36 -0
  9. package/assets/agents/pass-claims.md +31 -0
  10. package/assets/agents/pass-concurrency.md +168 -0
  11. package/assets/agents/pass-consistency.md +57 -0
  12. package/assets/agents/pass-contracts.md +30 -0
  13. package/assets/agents/pass-data-safety.md +54 -0
  14. package/assets/agents/pass-deferred.md +35 -0
  15. package/assets/agents/pass-delivery.md +37 -0
  16. package/assets/agents/pass-duplication.md +60 -0
  17. package/assets/agents/pass-intent.md +63 -0
  18. package/assets/agents/pass-lint-conformance.md +46 -0
  19. package/assets/agents/pass-maintainability.md +67 -0
  20. package/assets/agents/pass-placement.md +68 -0
  21. package/assets/agents/pass-prod-risk.md +24 -0
  22. package/assets/agents/pass-rollout.md +32 -0
  23. package/assets/agents/pass-roundtrip.md +46 -0
  24. package/assets/agents/pass-security.md +31 -0
  25. package/assets/agents/pass-simplicity.md +68 -0
  26. package/assets/agents/pass-statelessness.md +27 -0
  27. package/assets/agents/pass-style.md +37 -0
  28. package/assets/agents/pass-sweep.md +57 -0
  29. package/assets/agents/pass-tests.md +58 -0
  30. package/assets/agents/pass-ui.md +23 -0
  31. package/assets/agents/pass-validate.md +50 -0
  32. package/assets/agents/pass-widened.md +46 -0
  33. package/assets/agents/scan/pass-scan.md +86 -0
  34. package/assets/config.example.toml +1099 -0
  35. package/assets/deploy/reviewer/AGENTS.override.md +12 -0
  36. package/assets/profiles/default/README.md +21 -0
  37. package/assets/profiles/default/addenda/.gitkeep +0 -0
  38. package/assets/profiles/default/profile.toml +17 -0
  39. package/assets/profiles/example-django/README.md +21 -0
  40. package/assets/profiles/example-django/addenda/10-migration-discipline.md +13 -0
  41. package/assets/profiles/example-django/addenda/20-new-column-nullability.md +12 -0
  42. package/assets/profiles/example-django/addenda/30-test-doctrine.md +12 -0
  43. package/assets/profiles/example-django/addenda/40-orm-usage.md +19 -0
  44. package/assets/profiles/example-django/addenda/50-declared-shape.md +10 -0
  45. package/assets/profiles/example-django/addenda/60-config-placement.md +14 -0
  46. package/assets/profiles/example-django/heuristics.toml +24 -0
  47. package/assets/profiles/example-django/profile.toml +9 -0
  48. package/assets/profiles/fast/README.md +24 -0
  49. package/assets/profiles/fast/profile.toml +59 -0
  50. package/assets/profiles/minimal/README.md +17 -0
  51. package/assets/profiles/minimal/addenda/.gitkeep +0 -0
  52. package/assets/profiles/minimal/profile.toml +8 -0
  53. package/assets/profiles/snappy/README.md +24 -0
  54. package/assets/profiles/snappy/addenda/10-race-as-question.md +6 -0
  55. package/assets/profiles/snappy/profile.toml +46 -0
  56. package/assets/prompts/companion-day-open.md +46 -0
  57. package/assets/prompts/companion-first-run.md +104 -0
  58. package/assets/prompts/companion.md +821 -0
  59. package/assets/prompts/conversation-system.md +117 -0
  60. package/assets/prompts/eod-sweep.md +120 -0
  61. package/assets/prompts/onboarding.md +113 -0
  62. package/assets/prompts/pass-common.md +52 -0
  63. package/assets/prompts/pass-quality-common.md +22 -0
  64. package/assets/prompts/round-first.md +874 -0
  65. package/assets/prompts/round-follow-up.md +991 -0
  66. package/assets/prompts/round-re-review.md +889 -0
  67. package/assets/prompts/round-recheck.md +196 -0
  68. package/assets/prompts/round-staleness.md +856 -0
  69. package/assets/schemas/review-result.v1.json +391 -0
  70. package/assets/schemas/scan-result.v1.json +421 -0
  71. package/assets/schemas/version.v1.json +57 -0
  72. package/assets/seed-manifest.json +19 -0
  73. package/assets/seeds/agents/pass-baseline.md +61 -0
  74. package/assets/seeds/agents/pass-contracts.md +30 -0
  75. package/assets/seeds/agents/pass-data-safety.md +54 -0
  76. package/assets/seeds/agents/pass-delivery.md +37 -0
  77. package/assets/seeds/agents/pass-prod-risk.md +24 -0
  78. package/assets/seeds/agents/pass-security.md +31 -0
  79. package/assets/seeds/agents/pass-statelessness.md +27 -0
  80. package/assets/seeds/agents/pass-ui.md +23 -0
  81. package/assets/seeds/agents/pass-validate.md +50 -0
  82. package/assets/seeds/prompts/conversation-system.md +117 -0
  83. package/assets/seeds/prompts/eod-sweep.md +120 -0
  84. package/assets/seeds/prompts/pass-common.md +52 -0
  85. package/dist/extensions/bash-guard.d.ts +467 -0
  86. package/dist/extensions/bash-guard.d.ts.map +1 -0
  87. package/dist/extensions/bash-guard.js +3058 -0
  88. package/dist/extensions/bash-guard.js.map +1 -0
  89. package/dist/extensions/finding-clusters.d.ts +33 -0
  90. package/dist/extensions/finding-clusters.d.ts.map +1 -0
  91. package/dist/extensions/finding-clusters.js +218 -0
  92. package/dist/extensions/finding-clusters.js.map +1 -0
  93. package/dist/extensions/ledger-pin.d.ts +76 -0
  94. package/dist/extensions/ledger-pin.d.ts.map +1 -0
  95. package/dist/extensions/ledger-pin.js +331 -0
  96. package/dist/extensions/ledger-pin.js.map +1 -0
  97. package/dist/extensions/lens.d.ts +43 -0
  98. package/dist/extensions/lens.d.ts.map +1 -0
  99. package/dist/extensions/lens.js +150 -0
  100. package/dist/extensions/lens.js.map +1 -0
  101. package/dist/extensions/pi-tool.d.ts +22 -0
  102. package/dist/extensions/pi-tool.d.ts.map +1 -0
  103. package/dist/extensions/pi-tool.js +20 -0
  104. package/dist/extensions/pi-tool.js.map +1 -0
  105. package/dist/extensions/quickreview.d.ts +21 -0
  106. package/dist/extensions/quickreview.d.ts.map +1 -0
  107. package/dist/extensions/quickreview.js +132 -0
  108. package/dist/extensions/quickreview.js.map +1 -0
  109. package/dist/src/anchors.d.ts +54 -0
  110. package/dist/src/anchors.d.ts.map +1 -0
  111. package/dist/src/anchors.js +127 -0
  112. package/dist/src/anchors.js.map +1 -0
  113. package/dist/src/assetroot.d.ts +16 -0
  114. package/dist/src/assetroot.d.ts.map +1 -0
  115. package/dist/src/assetroot.js +77 -0
  116. package/dist/src/assetroot.js.map +1 -0
  117. package/dist/src/authwatch.d.ts +61 -0
  118. package/dist/src/authwatch.d.ts.map +1 -0
  119. package/dist/src/authwatch.js +210 -0
  120. package/dist/src/authwatch.js.map +1 -0
  121. package/dist/src/budget.d.ts +15 -0
  122. package/dist/src/budget.d.ts.map +1 -0
  123. package/dist/src/budget.js +32 -0
  124. package/dist/src/budget.js.map +1 -0
  125. package/dist/src/casebudget.d.ts +3 -0
  126. package/dist/src/casebudget.d.ts.map +1 -0
  127. package/dist/src/casebudget.js +3 -0
  128. package/dist/src/casebudget.js.map +1 -0
  129. package/dist/src/cigate.d.ts +116 -0
  130. package/dist/src/cigate.d.ts.map +1 -0
  131. package/dist/src/cigate.js +176 -0
  132. package/dist/src/cigate.js.map +1 -0
  133. package/dist/src/codespan.d.ts +46 -0
  134. package/dist/src/codespan.d.ts.map +1 -0
  135. package/dist/src/codespan.js +75 -0
  136. package/dist/src/codespan.js.map +1 -0
  137. package/dist/src/contextpack.d.ts +56 -0
  138. package/dist/src/contextpack.d.ts.map +1 -0
  139. package/dist/src/contextpack.js +195 -0
  140. package/dist/src/contextpack.js.map +1 -0
  141. package/dist/src/contract.d.ts +8 -0
  142. package/dist/src/contract.d.ts.map +1 -0
  143. package/dist/src/contract.js +21 -0
  144. package/dist/src/contract.js.map +1 -0
  145. package/dist/src/coverage.d.ts +32 -0
  146. package/dist/src/coverage.d.ts.map +1 -0
  147. package/dist/src/coverage.js +85 -0
  148. package/dist/src/coverage.js.map +1 -0
  149. package/dist/src/datafence.d.ts +19 -0
  150. package/dist/src/datafence.d.ts.map +1 -0
  151. package/dist/src/datafence.js +60 -0
  152. package/dist/src/datafence.js.map +1 -0
  153. package/dist/src/dedup.d.ts +65 -0
  154. package/dist/src/dedup.d.ts.map +1 -0
  155. package/dist/src/dedup.js +459 -0
  156. package/dist/src/dedup.js.map +1 -0
  157. package/dist/src/depsources.d.ts +304 -0
  158. package/dist/src/depsources.d.ts.map +1 -0
  159. package/dist/src/depsources.js +3367 -0
  160. package/dist/src/depsources.js.map +1 -0
  161. package/dist/src/diffinput.d.ts +10 -0
  162. package/dist/src/diffinput.d.ts.map +1 -0
  163. package/dist/src/diffinput.js +172 -0
  164. package/dist/src/diffinput.js.map +1 -0
  165. package/dist/src/doubtwords.d.ts +10 -0
  166. package/dist/src/doubtwords.d.ts.map +1 -0
  167. package/dist/src/doubtwords.js +13 -0
  168. package/dist/src/doubtwords.js.map +1 -0
  169. package/dist/src/ecosystem.d.ts +148 -0
  170. package/dist/src/ecosystem.d.ts.map +1 -0
  171. package/dist/src/ecosystem.js +338 -0
  172. package/dist/src/ecosystem.js.map +1 -0
  173. package/dist/src/engine/agentdir.d.ts +5 -0
  174. package/dist/src/engine/agentdir.d.ts.map +1 -0
  175. package/dist/src/engine/agentdir.js +21 -0
  176. package/dist/src/engine/agentdir.js.map +1 -0
  177. package/dist/src/engine/auth.d.ts +214 -0
  178. package/dist/src/engine/auth.d.ts.map +1 -0
  179. package/dist/src/engine/auth.js +374 -0
  180. package/dist/src/engine/auth.js.map +1 -0
  181. package/dist/src/engine/budget.d.ts +60 -0
  182. package/dist/src/engine/budget.d.ts.map +1 -0
  183. package/dist/src/engine/budget.js +113 -0
  184. package/dist/src/engine/budget.js.map +1 -0
  185. package/dist/src/engine/capfd.d.ts +6 -0
  186. package/dist/src/engine/capfd.d.ts.map +1 -0
  187. package/dist/src/engine/capfd.js +21 -0
  188. package/dist/src/engine/capfd.js.map +1 -0
  189. package/dist/src/engine/caseout.d.ts +66 -0
  190. package/dist/src/engine/caseout.d.ts.map +1 -0
  191. package/dist/src/engine/caseout.js +107 -0
  192. package/dist/src/engine/caseout.js.map +1 -0
  193. package/dist/src/engine/context.d.ts +57 -0
  194. package/dist/src/engine/context.d.ts.map +1 -0
  195. package/dist/src/engine/context.js +212 -0
  196. package/dist/src/engine/context.js.map +1 -0
  197. package/dist/src/engine/envmap.d.ts +13 -0
  198. package/dist/src/engine/envmap.d.ts.map +1 -0
  199. package/dist/src/engine/envmap.js +377 -0
  200. package/dist/src/engine/envmap.js.map +1 -0
  201. package/dist/src/engine/extfiles.d.ts +7 -0
  202. package/dist/src/engine/extfiles.d.ts.map +1 -0
  203. package/dist/src/engine/extfiles.js +23 -0
  204. package/dist/src/engine/extfiles.js.map +1 -0
  205. package/dist/src/engine/keyroute.d.ts +21 -0
  206. package/dist/src/engine/keyroute.d.ts.map +1 -0
  207. package/dist/src/engine/keyroute.js +58 -0
  208. package/dist/src/engine/keyroute.js.map +1 -0
  209. package/dist/src/engine/lens/cache-group.d.ts +20 -0
  210. package/dist/src/engine/lens/cache-group.d.ts.map +1 -0
  211. package/dist/src/engine/lens/cache-group.js +31 -0
  212. package/dist/src/engine/lens/cache-group.js.map +1 -0
  213. package/dist/src/engine/lens/cache-run.d.ts +65 -0
  214. package/dist/src/engine/lens/cache-run.d.ts.map +1 -0
  215. package/dist/src/engine/lens/cache-run.js +159 -0
  216. package/dist/src/engine/lens/cache-run.js.map +1 -0
  217. package/dist/src/engine/lens/deadline.d.ts +54 -0
  218. package/dist/src/engine/lens/deadline.d.ts.map +1 -0
  219. package/dist/src/engine/lens/deadline.js +111 -0
  220. package/dist/src/engine/lens/deadline.js.map +1 -0
  221. package/dist/src/engine/lens/engine-tools.d.ts +13 -0
  222. package/dist/src/engine/lens/engine-tools.d.ts.map +1 -0
  223. package/dist/src/engine/lens/engine-tools.js +37 -0
  224. package/dist/src/engine/lens/engine-tools.js.map +1 -0
  225. package/dist/src/engine/lens/env.d.ts +23 -0
  226. package/dist/src/engine/lens/env.d.ts.map +1 -0
  227. package/dist/src/engine/lens/env.js +42 -0
  228. package/dist/src/engine/lens/env.js.map +1 -0
  229. package/dist/src/engine/lens/launch-plan.d.ts +63 -0
  230. package/dist/src/engine/lens/launch-plan.d.ts.map +1 -0
  231. package/dist/src/engine/lens/launch-plan.js +203 -0
  232. package/dist/src/engine/lens/launch-plan.js.map +1 -0
  233. package/dist/src/engine/lens/leaf-cache.d.ts +22 -0
  234. package/dist/src/engine/lens/leaf-cache.d.ts.map +1 -0
  235. package/dist/src/engine/lens/leaf-cache.js +115 -0
  236. package/dist/src/engine/lens/leaf-cache.js.map +1 -0
  237. package/dist/src/engine/lens/permits.d.ts +14 -0
  238. package/dist/src/engine/lens/permits.d.ts.map +1 -0
  239. package/dist/src/engine/lens/permits.js +18 -0
  240. package/dist/src/engine/lens/permits.js.map +1 -0
  241. package/dist/src/engine/lens/registry.d.ts +55 -0
  242. package/dist/src/engine/lens/registry.d.ts.map +1 -0
  243. package/dist/src/engine/lens/registry.js +140 -0
  244. package/dist/src/engine/lens/registry.js.map +1 -0
  245. package/dist/src/engine/lens/round-lens.d.ts +16 -0
  246. package/dist/src/engine/lens/round-lens.d.ts.map +1 -0
  247. package/dist/src/engine/lens/round-lens.js +46 -0
  248. package/dist/src/engine/lens/round-lens.js.map +1 -0
  249. package/dist/src/engine/lens/runlens.d.ts +103 -0
  250. package/dist/src/engine/lens/runlens.d.ts.map +1 -0
  251. package/dist/src/engine/lens/runlens.js +538 -0
  252. package/dist/src/engine/lens/runlens.js.map +1 -0
  253. package/dist/src/engine/lens/section.d.ts +19 -0
  254. package/dist/src/engine/lens/section.d.ts.map +1 -0
  255. package/dist/src/engine/lens/section.js +40 -0
  256. package/dist/src/engine/lens/section.js.map +1 -0
  257. package/dist/src/engine/lens/semaphore.d.ts +13 -0
  258. package/dist/src/engine/lens/semaphore.d.ts.map +1 -0
  259. package/dist/src/engine/lens/semaphore.js +44 -0
  260. package/dist/src/engine/lens/semaphore.js.map +1 -0
  261. package/dist/src/engine/lens/tool.d.ts +34 -0
  262. package/dist/src/engine/lens/tool.d.ts.map +1 -0
  263. package/dist/src/engine/lens/tool.js +345 -0
  264. package/dist/src/engine/lens/tool.js.map +1 -0
  265. package/dist/src/engine/lens/wave.d.ts +63 -0
  266. package/dist/src/engine/lens/wave.d.ts.map +1 -0
  267. package/dist/src/engine/lens/wave.js +95 -0
  268. package/dist/src/engine/lens/wave.js.map +1 -0
  269. package/dist/src/engine/livesessions.d.ts +21 -0
  270. package/dist/src/engine/livesessions.d.ts.map +1 -0
  271. package/dist/src/engine/livesessions.js +57 -0
  272. package/dist/src/engine/livesessions.js.map +1 -0
  273. package/dist/src/engine/livewire.d.ts +18 -0
  274. package/dist/src/engine/livewire.d.ts.map +1 -0
  275. package/dist/src/engine/livewire.js +31 -0
  276. package/dist/src/engine/livewire.js.map +1 -0
  277. package/dist/src/engine/openable.d.ts +37 -0
  278. package/dist/src/engine/openable.d.ts.map +1 -0
  279. package/dist/src/engine/openable.js +120 -0
  280. package/dist/src/engine/openable.js.map +1 -0
  281. package/dist/src/engine/pisdk.d.ts +235 -0
  282. package/dist/src/engine/pisdk.d.ts.map +1 -0
  283. package/dist/src/engine/pisdk.js +577 -0
  284. package/dist/src/engine/pisdk.js.map +1 -0
  285. package/dist/src/engine/port.d.ts +144 -0
  286. package/dist/src/engine/port.d.ts.map +1 -0
  287. package/dist/src/engine/port.js +16 -0
  288. package/dist/src/engine/port.js.map +1 -0
  289. package/dist/src/engine/prompts.d.ts +159 -0
  290. package/dist/src/engine/prompts.d.ts.map +1 -0
  291. package/dist/src/engine/prompts.js +339 -0
  292. package/dist/src/engine/prompts.js.map +1 -0
  293. package/dist/src/engine/render.d.ts +10 -0
  294. package/dist/src/engine/render.d.ts.map +1 -0
  295. package/dist/src/engine/render.js +23 -0
  296. package/dist/src/engine/render.js.map +1 -0
  297. package/dist/src/engine/reviewruntime.d.ts +13 -0
  298. package/dist/src/engine/reviewruntime.d.ts.map +1 -0
  299. package/dist/src/engine/reviewruntime.js +35 -0
  300. package/dist/src/engine/reviewruntime.js.map +1 -0
  301. package/dist/src/engine/reviewsdk.d.ts +75 -0
  302. package/dist/src/engine/reviewsdk.d.ts.map +1 -0
  303. package/dist/src/engine/reviewsdk.js +180 -0
  304. package/dist/src/engine/reviewsdk.js.map +1 -0
  305. package/dist/src/engine/round.d.ts +166 -0
  306. package/dist/src/engine/round.d.ts.map +1 -0
  307. package/dist/src/engine/round.js +543 -0
  308. package/dist/src/engine/round.js.map +1 -0
  309. package/dist/src/engine/roundagents.d.ts +17 -0
  310. package/dist/src/engine/roundagents.d.ts.map +1 -0
  311. package/dist/src/engine/roundagents.js +49 -0
  312. package/dist/src/engine/roundagents.js.map +1 -0
  313. package/dist/src/engine/roundevents.d.ts +110 -0
  314. package/dist/src/engine/roundevents.d.ts.map +1 -0
  315. package/dist/src/engine/roundevents.js +276 -0
  316. package/dist/src/engine/roundevents.js.map +1 -0
  317. package/dist/src/engine/roundsdk.d.ts +33 -0
  318. package/dist/src/engine/roundsdk.d.ts.map +1 -0
  319. package/dist/src/engine/roundsdk.js +76 -0
  320. package/dist/src/engine/roundsdk.js.map +1 -0
  321. package/dist/src/engine/sdkruntime.d.ts +23 -0
  322. package/dist/src/engine/sdkruntime.d.ts.map +1 -0
  323. package/dist/src/engine/sdkruntime.js +340 -0
  324. package/dist/src/engine/sdkruntime.js.map +1 -0
  325. package/dist/src/engine/select.d.ts +16 -0
  326. package/dist/src/engine/select.d.ts.map +1 -0
  327. package/dist/src/engine/select.js +32 -0
  328. package/dist/src/engine/select.js.map +1 -0
  329. package/dist/src/engine/sessionwrap.d.ts +4 -0
  330. package/dist/src/engine/sessionwrap.d.ts.map +1 -0
  331. package/dist/src/engine/sessionwrap.js +31 -0
  332. package/dist/src/engine/sessionwrap.js.map +1 -0
  333. package/dist/src/engine/toolfence.d.ts +90 -0
  334. package/dist/src/engine/toolfence.d.ts.map +1 -0
  335. package/dist/src/engine/toolfence.js +378 -0
  336. package/dist/src/engine/toolfence.js.map +1 -0
  337. package/dist/src/engine/tooluid.d.ts +128 -0
  338. package/dist/src/engine/tooluid.d.ts.map +1 -0
  339. package/dist/src/engine/tooluid.js +401 -0
  340. package/dist/src/engine/tooluid.js.map +1 -0
  341. package/dist/src/envstrip.d.ts +10 -0
  342. package/dist/src/envstrip.d.ts.map +1 -0
  343. package/dist/src/envstrip.js +15 -0
  344. package/dist/src/envstrip.js.map +1 -0
  345. package/dist/src/escalationprompt.d.ts +22 -0
  346. package/dist/src/escalationprompt.d.ts.map +1 -0
  347. package/dist/src/escalationprompt.js +54 -0
  348. package/dist/src/escalationprompt.js.map +1 -0
  349. package/dist/src/execfn.d.ts +29 -0
  350. package/dist/src/execfn.d.ts.map +1 -0
  351. package/dist/src/execfn.js +55 -0
  352. package/dist/src/execfn.js.map +1 -0
  353. package/dist/src/finding-chain.d.ts +40 -0
  354. package/dist/src/finding-chain.d.ts.map +1 -0
  355. package/dist/src/finding-chain.js +76 -0
  356. package/dist/src/finding-chain.js.map +1 -0
  357. package/dist/src/finding-row.d.ts +24 -0
  358. package/dist/src/finding-row.d.ts.map +1 -0
  359. package/dist/src/finding-row.js +26 -0
  360. package/dist/src/finding-row.js.map +1 -0
  361. package/dist/src/findingkey.d.ts +99 -0
  362. package/dist/src/findingkey.d.ts.map +1 -0
  363. package/dist/src/findingkey.js +210 -0
  364. package/dist/src/findingkey.js.map +1 -0
  365. package/dist/src/findingsrecord.d.ts +175 -0
  366. package/dist/src/findingsrecord.d.ts.map +1 -0
  367. package/dist/src/findingsrecord.js +410 -0
  368. package/dist/src/findingsrecord.js.map +1 -0
  369. package/dist/src/followup-mode.d.ts +4 -0
  370. package/dist/src/followup-mode.d.ts.map +1 -0
  371. package/dist/src/followup-mode.js +6 -0
  372. package/dist/src/followup-mode.js.map +1 -0
  373. package/dist/src/gate.d.ts +199 -0
  374. package/dist/src/gate.d.ts.map +1 -0
  375. package/dist/src/gate.js +315 -0
  376. package/dist/src/gate.js.map +1 -0
  377. package/dist/src/ghcred.d.ts +58 -0
  378. package/dist/src/ghcred.d.ts.map +1 -0
  379. package/dist/src/ghcred.js +139 -0
  380. package/dist/src/ghcred.js.map +1 -0
  381. package/dist/src/githubAuth.d.ts +70 -0
  382. package/dist/src/githubAuth.d.ts.map +1 -0
  383. package/dist/src/githubAuth.js +183 -0
  384. package/dist/src/githubAuth.js.map +1 -0
  385. package/dist/src/githubcore.d.ts +20 -0
  386. package/dist/src/githubcore.d.ts.map +1 -0
  387. package/dist/src/githubcore.js +20 -0
  388. package/dist/src/githubcore.js.map +1 -0
  389. package/dist/src/glob.d.ts +11 -0
  390. package/dist/src/glob.d.ts.map +1 -0
  391. package/dist/src/glob.js +69 -0
  392. package/dist/src/glob.js.map +1 -0
  393. package/dist/src/guards.d.ts +106 -0
  394. package/dist/src/guards.d.ts.map +1 -0
  395. package/dist/src/guards.js +365 -0
  396. package/dist/src/guards.js.map +1 -0
  397. package/dist/src/heartbeat.d.ts +29 -0
  398. package/dist/src/heartbeat.d.ts.map +1 -0
  399. package/dist/src/heartbeat.js +87 -0
  400. package/dist/src/heartbeat.js.map +1 -0
  401. package/dist/src/heuristics.d.ts +247 -0
  402. package/dist/src/heuristics.d.ts.map +1 -0
  403. package/dist/src/heuristics.js +927 -0
  404. package/dist/src/heuristics.js.map +1 -0
  405. package/dist/src/homes.d.ts +181 -0
  406. package/dist/src/homes.d.ts.map +1 -0
  407. package/dist/src/homes.js +242 -0
  408. package/dist/src/homes.js.map +1 -0
  409. package/dist/src/hunkheader.d.ts +11 -0
  410. package/dist/src/hunkheader.d.ts.map +1 -0
  411. package/dist/src/hunkheader.js +18 -0
  412. package/dist/src/hunkheader.js.map +1 -0
  413. package/dist/src/identity.d.ts +34 -0
  414. package/dist/src/identity.d.ts.map +1 -0
  415. package/dist/src/identity.js +67 -0
  416. package/dist/src/identity.js.map +1 -0
  417. package/dist/src/include.d.ts +435 -0
  418. package/dist/src/include.d.ts.map +1 -0
  419. package/dist/src/include.js +1692 -0
  420. package/dist/src/include.js.map +1 -0
  421. package/dist/src/launchvocab.d.ts +36 -0
  422. package/dist/src/launchvocab.d.ts.map +1 -0
  423. package/dist/src/launchvocab.js +59 -0
  424. package/dist/src/launchvocab.js.map +1 -0
  425. package/dist/src/ledgercarry.d.ts +24 -0
  426. package/dist/src/ledgercarry.d.ts.map +1 -0
  427. package/dist/src/ledgercarry.js +109 -0
  428. package/dist/src/ledgercarry.js.map +1 -0
  429. package/dist/src/lensids.d.ts +7 -0
  430. package/dist/src/lensids.d.ts.map +1 -0
  431. package/dist/src/lensids.js +53 -0
  432. package/dist/src/lensids.js.map +1 -0
  433. package/dist/src/lensplanning.d.ts +8 -0
  434. package/dist/src/lensplanning.d.ts.map +1 -0
  435. package/dist/src/lensplanning.js +126 -0
  436. package/dist/src/lensplanning.js.map +1 -0
  437. package/dist/src/lensresult.d.ts +94 -0
  438. package/dist/src/lensresult.d.ts.map +1 -0
  439. package/dist/src/lensresult.js +162 -0
  440. package/dist/src/lensresult.js.map +1 -0
  441. package/dist/src/lenstuning.d.ts +97 -0
  442. package/dist/src/lenstuning.d.ts.map +1 -0
  443. package/dist/src/lenstuning.js +326 -0
  444. package/dist/src/lenstuning.js.map +1 -0
  445. package/dist/src/lintconfig.d.ts +31 -0
  446. package/dist/src/lintconfig.d.ts.map +1 -0
  447. package/dist/src/lintconfig.js +543 -0
  448. package/dist/src/lintconfig.js.map +1 -0
  449. package/dist/src/live/events.d.ts +188 -0
  450. package/dist/src/live/events.d.ts.map +1 -0
  451. package/dist/src/live/events.js +136 -0
  452. package/dist/src/live/events.js.map +1 -0
  453. package/dist/src/live/writer.d.ts +54 -0
  454. package/dist/src/live/writer.d.ts.map +1 -0
  455. package/dist/src/live/writer.js +516 -0
  456. package/dist/src/live/writer.js.map +1 -0
  457. package/dist/src/localreview.d.ts +128 -0
  458. package/dist/src/localreview.d.ts.map +1 -0
  459. package/dist/src/localreview.js +426 -0
  460. package/dist/src/localreview.js.map +1 -0
  461. package/dist/src/loosenedtests.d.ts +13 -0
  462. package/dist/src/loosenedtests.d.ts.map +1 -0
  463. package/dist/src/loosenedtests.js +188 -0
  464. package/dist/src/loosenedtests.js.map +1 -0
  465. package/dist/src/memoryport.d.ts +264 -0
  466. package/dist/src/memoryport.d.ts.map +1 -0
  467. package/dist/src/memoryport.js +445 -0
  468. package/dist/src/memoryport.js.map +1 -0
  469. package/dist/src/migrations.d.ts +106 -0
  470. package/dist/src/migrations.d.ts.map +1 -0
  471. package/dist/src/migrations.js +373 -0
  472. package/dist/src/migrations.js.map +1 -0
  473. package/dist/src/modelroute.d.ts +38 -0
  474. package/dist/src/modelroute.d.ts.map +1 -0
  475. package/dist/src/modelroute.js +105 -0
  476. package/dist/src/modelroute.js.map +1 -0
  477. package/dist/src/oneshot.d.ts +12 -0
  478. package/dist/src/oneshot.d.ts.map +1 -0
  479. package/dist/src/oneshot.js +8 -0
  480. package/dist/src/oneshot.js.map +1 -0
  481. package/dist/src/packing.d.ts +11 -0
  482. package/dist/src/packing.d.ts.map +1 -0
  483. package/dist/src/packing.js +31 -0
  484. package/dist/src/packing.js.map +1 -0
  485. package/dist/src/passes.d.ts +16 -0
  486. package/dist/src/passes.d.ts.map +1 -0
  487. package/dist/src/passes.js +104 -0
  488. package/dist/src/passes.js.map +1 -0
  489. package/dist/src/passtasks.d.ts +68 -0
  490. package/dist/src/passtasks.d.ts.map +1 -0
  491. package/dist/src/passtasks.js +360 -0
  492. package/dist/src/passtasks.js.map +1 -0
  493. package/dist/src/pathinside.d.ts +5 -0
  494. package/dist/src/pathinside.d.ts.map +1 -0
  495. package/dist/src/pathinside.js +9 -0
  496. package/dist/src/pathinside.js.map +1 -0
  497. package/dist/src/pibuiltins.d.ts +36 -0
  498. package/dist/src/pibuiltins.d.ts.map +1 -0
  499. package/dist/src/pibuiltins.js +83 -0
  500. package/dist/src/pibuiltins.js.map +1 -0
  501. package/dist/src/plankey.d.ts +4 -0
  502. package/dist/src/plankey.d.ts.map +1 -0
  503. package/dist/src/plankey.js +10 -0
  504. package/dist/src/plankey.js.map +1 -0
  505. package/dist/src/planner.d.ts +375 -0
  506. package/dist/src/planner.d.ts.map +1 -0
  507. package/dist/src/planner.js +722 -0
  508. package/dist/src/planner.js.map +1 -0
  509. package/dist/src/plugin/args.d.ts +19 -0
  510. package/dist/src/plugin/args.d.ts.map +1 -0
  511. package/dist/src/plugin/args.js +107 -0
  512. package/dist/src/plugin/args.js.map +1 -0
  513. package/dist/src/plugin/index.d.ts +3 -0
  514. package/dist/src/plugin/index.d.ts.map +1 -0
  515. package/dist/src/plugin/index.js +14 -0
  516. package/dist/src/plugin/index.js.map +1 -0
  517. package/dist/src/plugin/login.d.ts +42 -0
  518. package/dist/src/plugin/login.d.ts.map +1 -0
  519. package/dist/src/plugin/login.js +82 -0
  520. package/dist/src/plugin/login.js.map +1 -0
  521. package/dist/src/plugin/review.d.ts +27 -0
  522. package/dist/src/plugin/review.d.ts.map +1 -0
  523. package/dist/src/plugin/review.js +302 -0
  524. package/dist/src/plugin/review.js.map +1 -0
  525. package/dist/src/plugin/runner.d.ts +6 -0
  526. package/dist/src/plugin/runner.d.ts.map +1 -0
  527. package/dist/src/plugin/runner.js +65 -0
  528. package/dist/src/plugin/runner.js.map +1 -0
  529. package/dist/src/plugin/scan.d.ts +79 -0
  530. package/dist/src/plugin/scan.d.ts.map +1 -0
  531. package/dist/src/plugin/scan.js +348 -0
  532. package/dist/src/plugin/scan.js.map +1 -0
  533. package/dist/src/plugin/session.d.ts +56 -0
  534. package/dist/src/plugin/session.d.ts.map +1 -0
  535. package/dist/src/plugin/session.js +2 -0
  536. package/dist/src/plugin/session.js.map +1 -0
  537. package/dist/src/plugin/setup.d.ts +19 -0
  538. package/dist/src/plugin/setup.d.ts.map +1 -0
  539. package/dist/src/plugin/setup.js +142 -0
  540. package/dist/src/plugin/setup.js.map +1 -0
  541. package/dist/src/plugin/versiongate.d.ts +26 -0
  542. package/dist/src/plugin/versiongate.d.ts.map +1 -0
  543. package/dist/src/plugin/versiongate.js +83 -0
  544. package/dist/src/plugin/versiongate.js.map +1 -0
  545. package/dist/src/postingkey.d.ts +8 -0
  546. package/dist/src/postingkey.d.ts.map +1 -0
  547. package/dist/src/postingkey.js +25 -0
  548. package/dist/src/postingkey.js.map +1 -0
  549. package/dist/src/postingpolicy.d.ts +108 -0
  550. package/dist/src/postingpolicy.d.ts.map +1 -0
  551. package/dist/src/postingpolicy.js +341 -0
  552. package/dist/src/postingpolicy.js.map +1 -0
  553. package/dist/src/pr-identity.d.ts +3 -0
  554. package/dist/src/pr-identity.d.ts.map +1 -0
  555. package/dist/src/pr-identity.js +9 -0
  556. package/dist/src/pr-identity.js.map +1 -0
  557. package/dist/src/prdelta.d.ts +181 -0
  558. package/dist/src/prdelta.d.ts.map +1 -0
  559. package/dist/src/prdelta.js +501 -0
  560. package/dist/src/prdelta.js.map +1 -0
  561. package/dist/src/pricing.d.ts +94 -0
  562. package/dist/src/pricing.d.ts.map +1 -0
  563. package/dist/src/pricing.js +209 -0
  564. package/dist/src/pricing.js.map +1 -0
  565. package/dist/src/priorfindings.d.ts +167 -0
  566. package/dist/src/priorfindings.d.ts.map +1 -0
  567. package/dist/src/priorfindings.js +445 -0
  568. package/dist/src/priorfindings.js.map +1 -0
  569. package/dist/src/priortasks.d.ts +31 -0
  570. package/dist/src/priortasks.d.ts.map +1 -0
  571. package/dist/src/priortasks.js +119 -0
  572. package/dist/src/priortasks.js.map +1 -0
  573. package/dist/src/profilelookup.d.ts +59 -0
  574. package/dist/src/profilelookup.d.ts.map +1 -0
  575. package/dist/src/profilelookup.js +98 -0
  576. package/dist/src/profilelookup.js.map +1 -0
  577. package/dist/src/profileoverrides.d.ts +91 -0
  578. package/dist/src/profileoverrides.d.ts.map +1 -0
  579. package/dist/src/profileoverrides.js +313 -0
  580. package/dist/src/profileoverrides.js.map +1 -0
  581. package/dist/src/profiles.d.ts +144 -0
  582. package/dist/src/profiles.d.ts.map +1 -0
  583. package/dist/src/profiles.js +604 -0
  584. package/dist/src/profiles.js.map +1 -0
  585. package/dist/src/promptdoc.d.ts +3 -0
  586. package/dist/src/promptdoc.d.ts.map +1 -0
  587. package/dist/src/promptdoc.js +18 -0
  588. package/dist/src/promptdoc.js.map +1 -0
  589. package/dist/src/prtarget.d.ts +70 -0
  590. package/dist/src/prtarget.d.ts.map +1 -0
  591. package/dist/src/prtarget.js +181 -0
  592. package/dist/src/prtarget.js.map +1 -0
  593. package/dist/src/recheckorder.d.ts +32 -0
  594. package/dist/src/recheckorder.d.ts.map +1 -0
  595. package/dist/src/recheckorder.js +5 -0
  596. package/dist/src/recheckorder.js.map +1 -0
  597. package/dist/src/reviewapi.d.ts +207 -0
  598. package/dist/src/reviewapi.d.ts.map +1 -0
  599. package/dist/src/reviewapi.js +939 -0
  600. package/dist/src/reviewapi.js.map +1 -0
  601. package/dist/src/reviewbotfindings.d.ts +33 -0
  602. package/dist/src/reviewbotfindings.d.ts.map +1 -0
  603. package/dist/src/reviewbotfindings.js +80 -0
  604. package/dist/src/reviewbotfindings.js.map +1 -0
  605. package/dist/src/reviewconfig.d.ts +753 -0
  606. package/dist/src/reviewconfig.d.ts.map +1 -0
  607. package/dist/src/reviewconfig.js +805 -0
  608. package/dist/src/reviewconfig.js.map +1 -0
  609. package/dist/src/reviewdelta.d.ts +51 -0
  610. package/dist/src/reviewdelta.d.ts.map +1 -0
  611. package/dist/src/reviewdelta.js +119 -0
  612. package/dist/src/reviewdelta.js.map +1 -0
  613. package/dist/src/reviewdest.d.ts +86 -0
  614. package/dist/src/reviewdest.d.ts.map +1 -0
  615. package/dist/src/reviewdest.js +142 -0
  616. package/dist/src/reviewdest.js.map +1 -0
  617. package/dist/src/reviewerstub.d.ts +12 -0
  618. package/dist/src/reviewerstub.d.ts.map +1 -0
  619. package/dist/src/reviewerstub.js +28 -0
  620. package/dist/src/reviewerstub.js.map +1 -0
  621. package/dist/src/reviewhost.d.ts +58 -0
  622. package/dist/src/reviewhost.d.ts.map +1 -0
  623. package/dist/src/reviewhost.js +135 -0
  624. package/dist/src/reviewhost.js.map +1 -0
  625. package/dist/src/reviewpost.d.ts +79 -0
  626. package/dist/src/reviewpost.d.ts.map +1 -0
  627. package/dist/src/reviewpost.js +233 -0
  628. package/dist/src/reviewpost.js.map +1 -0
  629. package/dist/src/reviewrender.d.ts +154 -0
  630. package/dist/src/reviewrender.d.ts.map +1 -0
  631. package/dist/src/reviewrender.js +480 -0
  632. package/dist/src/reviewrender.js.map +1 -0
  633. package/dist/src/reviewstate.d.ts +62 -0
  634. package/dist/src/reviewstate.d.ts.map +1 -0
  635. package/dist/src/reviewstate.js +167 -0
  636. package/dist/src/reviewstate.js.map +1 -0
  637. package/dist/src/reviewstore.d.ts +17 -0
  638. package/dist/src/reviewstore.d.ts.map +1 -0
  639. package/dist/src/reviewstore.js +105 -0
  640. package/dist/src/reviewstore.js.map +1 -0
  641. package/dist/src/round-progress.d.ts +120 -0
  642. package/dist/src/round-progress.d.ts.map +1 -0
  643. package/dist/src/round-progress.js +172 -0
  644. package/dist/src/round-progress.js.map +1 -0
  645. package/dist/src/round-stage-deadlines.d.ts +12 -0
  646. package/dist/src/round-stage-deadlines.d.ts.map +1 -0
  647. package/dist/src/round-stage-deadlines.js +10 -0
  648. package/dist/src/round-stage-deadlines.js.map +1 -0
  649. package/dist/src/session-command.d.ts +19 -0
  650. package/dist/src/session-command.d.ts.map +1 -0
  651. package/dist/src/session-command.js +20 -0
  652. package/dist/src/session-command.js.map +1 -0
  653. package/dist/src/settledread.d.ts +61 -0
  654. package/dist/src/settledread.d.ts.map +1 -0
  655. package/dist/src/settledread.js +135 -0
  656. package/dist/src/settledread.js.map +1 -0
  657. package/dist/src/severity.d.ts +167 -0
  658. package/dist/src/severity.d.ts.map +1 -0
  659. package/dist/src/severity.js +364 -0
  660. package/dist/src/severity.js.map +1 -0
  661. package/dist/src/severityrank.d.ts +8 -0
  662. package/dist/src/severityrank.d.ts.map +1 -0
  663. package/dist/src/severityrank.js +16 -0
  664. package/dist/src/severityrank.js.map +1 -0
  665. package/dist/src/standing-review.d.ts +45 -0
  666. package/dist/src/standing-review.d.ts.map +1 -0
  667. package/dist/src/standing-review.js +43 -0
  668. package/dist/src/standing-review.js.map +1 -0
  669. package/dist/src/storerows.d.ts +225 -0
  670. package/dist/src/storerows.d.ts.map +1 -0
  671. package/dist/src/storerows.js +7 -0
  672. package/dist/src/storerows.js.map +1 -0
  673. package/dist/src/styleguide.d.ts +25 -0
  674. package/dist/src/styleguide.d.ts.map +1 -0
  675. package/dist/src/styleguide.js +272 -0
  676. package/dist/src/styleguide.js.map +1 -0
  677. package/dist/src/sweepstatus.d.ts +42 -0
  678. package/dist/src/sweepstatus.d.ts.map +1 -0
  679. package/dist/src/sweepstatus.js +97 -0
  680. package/dist/src/sweepstatus.js.map +1 -0
  681. package/dist/src/templates.d.ts +243 -0
  682. package/dist/src/templates.d.ts.map +1 -0
  683. package/dist/src/templates.js +847 -0
  684. package/dist/src/templates.js.map +1 -0
  685. package/dist/src/types.d.ts +639 -0
  686. package/dist/src/types.d.ts.map +1 -0
  687. package/dist/src/types.js +90 -0
  688. package/dist/src/types.js.map +1 -0
  689. package/dist/src/unifieddiff.d.ts +22 -0
  690. package/dist/src/unifieddiff.d.ts.map +1 -0
  691. package/dist/src/unifieddiff.js +176 -0
  692. package/dist/src/unifieddiff.js.map +1 -0
  693. package/dist/src/yieldloop.d.ts +2 -0
  694. package/dist/src/yieldloop.d.ts.map +1 -0
  695. package/dist/src/yieldloop.js +14 -0
  696. package/dist/src/yieldloop.js.map +1 -0
  697. package/package.json +60 -4
@@ -0,0 +1,927 @@
1
+ // Pure dispatch heuristics: test-file exclusion, the migration gate, lens
2
+ // selection, sub-lens triggers, sweep / behaviour-delta / baseline grouping
3
+ // and the pass tier. Consumed by dispatch; unit-tested directly.
4
+ import { packByBudget } from "./packing.js";
5
+ // A file is a test file iff any of these match.
6
+ const TEST_PATTERNS = [
7
+ /(^|\/)tests?\//,
8
+ /(^|\/)__tests__\//,
9
+ /(^|\/)test_[^/]+$/,
10
+ /_test\.[a-z0-9]+$/,
11
+ /\.(test|spec)\.[a-z0-9]+$/i,
12
+ /(^|\/)conftest\.py$/,
13
+ /(^|\/)fixtures\//,
14
+ ];
15
+ export function isTestFile(filename) {
16
+ return TEST_PATTERNS.some((re) => re.test(filename));
17
+ }
18
+ export function prodStats(files) {
19
+ const prod = files.filter((f) => !isTestFile(f.filename));
20
+ return {
21
+ prod_files: prod.length,
22
+ prod_lines: prod.reduce((n, f) => n + f.additions + f.deletions, 0),
23
+ };
24
+ }
25
+ // There is no escalation-worthiness score, and none may be reintroduced under
26
+ // another name (the never-hand-to-a-human rule): the tier ladder below keys on size and the
27
+ // migration gate alone.
28
+ /** A path under a `migrations/` directory — the one spelling of "this file is
29
+ * a migration" shared by the migration gate, lens selection, the ecosystem
30
+ * pre-check and the recheck's renumber classifier. */
31
+ export const MIGRATION_DIR_RE = /(^|\/)migrations\//;
32
+ export function isMigrationPath(path) {
33
+ return MIGRATION_DIR_RE.test(path);
34
+ }
35
+ export function migrationGate(files) {
36
+ return files.some((f) => isMigrationPath(f.filename));
37
+ }
38
+ /** Pass model tier for a round: small quiet diffs start at terra-med; a
39
+ * migration or a size at/over `smallDiffLines` starts at sol-med. (Risk passes
40
+ * and pass-validate have fixed tiers in config — this function only picks the
41
+ * base tier for the raisable passes.) */
42
+ export function passTier(files, opts) {
43
+ const total = files.reduce((n, f) => n + f.additions + f.deletions, 0);
44
+ if (total < opts.smallDiffLines && !opts.migrationGate)
45
+ return "terra-med";
46
+ return "sol-med";
47
+ }
48
+ // ---- pass-intent -----------------------------------------------------------
49
+ //
50
+ // Does the PR do what its author said it would? Its intent
51
+ // artifact is the PR TITLE + BODY and nothing else (intent-as-artifact — what
52
+ // the author told other humans; the implementing conversation/prompt is
53
+ // rejected as an echo chamber and is never fetched). Selection keys on the
54
+ // body clearing a minimum substance bar: dispatching a pass whose first act
55
+ // is to return NONE is pure spend, and the lens's own load-bearing rule is
56
+ // that a thin body supports no finding. The bar lives HERE (daemon-side, not
57
+ // in the lens text) so a thin-body PR never costs a subagent at all.
58
+ export const INTENT_PASS = "pass-intent";
59
+ /** Words of author prose a body must carry before pass-intent is worth a
60
+ * dispatch. Below this, "the body says X" has no X to check — the lens's own
61
+ * contract says NONE is the only honest output. */
62
+ export const INTENT_MIN_SUBSTANCE_WORDS = 60;
63
+ /**
64
+ * Words of actual author prose in a PR body, after stripping the parts that
65
+ * carry no intent: PR-template HTML comments, fenced/inline code, bare URLs,
66
+ * template scaffolding headings and empty checkboxes. Deliberately crude —
67
+ * this is a dispatch-cost gate, not comprehension.
68
+ */
69
+ export function intentSubstanceWords(body) {
70
+ if (!body)
71
+ return 0;
72
+ const prose = body
73
+ .replace(/<!--[\s\S]*?-->/g, " ") // PR-template comments
74
+ .replace(/```[\s\S]*?(```|$)/g, " ") // fenced code blocks
75
+ .replace(/`[^`\n]*`/g, " ") // inline code
76
+ .replace(/https?:\/\/\S+/g, " ") // bare links carry no prose
77
+ .replace(/^#{1,6}\s.*$/gm, " ") // headings = template scaffolding
78
+ .replace(/^\s*[-*]\s*\[[ xX]?\]\s*$/gm, " "); // empty checkbox rows
79
+ return prose.split(/\s+/).filter((w) => /[A-Za-z]/.test(w)).length;
80
+ }
81
+ /** The substance bar: run pass-intent iff the body clears it. */
82
+ export function meetsIntentBar(body, minWords = INTENT_MIN_SUBSTANCE_WORDS) {
83
+ return intentSubstanceWords(body) >= minWords;
84
+ }
85
+ // ---- placement / simplicity / backcompat -----------------------------------
86
+ //
87
+ // Three judgment lenses. Selection is deterministic like everything above;
88
+ // the caps (placement and simplicity post at P2 max) are enforced at the
89
+ // validator via PASS_ADDENDA, not here.
90
+ export const PLACEMENT_PASS = "pass-placement";
91
+ export const SIMPLICITY_PASS = "pass-simplicity";
92
+ export const BACKCOMPAT_PASS = "pass-backcompat";
93
+ /** A public or exported declaration on a removed or changed line: TS/JS
94
+ * `export`, Go exported funcs/types, Rust `pub`, Java/Kotlin/C#/Swift `public`,
95
+ * Python names in `__all__` or a top-level `def`/`class` without a leading
96
+ * underscore. */
97
+ export const PUBLIC_SIGNATURE_RE = /^(?:\s*(?:export\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|interface|type|enum)\b|func\s+(?:\([^)]*\)\s*)?[A-Z]\w*\s*\(|type\s+[A-Z]\w*\s|pub(?:\([^)]*\))?\s+(?:async\s+)?(?:fn|struct|enum|trait|type|const|mod)\b|public\s+(?:static\s+)?(?:final\s+)?(?:abstract\s+)?[\w<>[\],\s]+\s+\w+\s*\(|public\s+(?:class|interface|enum|record)\b|__all__\s*=)|(?:def|class)\s+[A-Za-z]\w*)/;
98
+ /** A wire-serialization site on a changed line: field names and order that
99
+ * a peer or a stored payload agrees with. */
100
+ export const WIRE_FORMAT_RE = /(\bwriteTo\s*\(|\breadFrom\s*\(|@JsonProperty|@SerializedName|@JsonKey|\bserde\s*\(|#\[serde\(|\bStreamOutput\b|\bStreamInput\b|\bjson:"|\bprotobuf\b|\bMarshal\w*\(|\bUnmarshal\w*\()/;
101
+ /** CODE files — the unit placement/simplicity judge. A new README, lockfile
102
+ * or asset is "prod" by the test filter but has no placement or complexity
103
+ * to judge: dispatching on it is the pure-spend case the intent bar above
104
+ * exists to avoid. */
105
+ export const CODE_FILE_RE = /\.(py|pyi|ts|tsx|mts|cts|js|jsx|mjs|cjs|go|rb|java|kt|kts|rs|c|cc|cpp|cxx|h|hh|hpp|hxx|cs|php|swift|scala|dart|ex|exs|vue|svelte)$/i;
106
+ /** New-code statuses that pose the "does this live in the right place"
107
+ * question — a move IS the purest placement change. */
108
+ const PLACEMENT_STATUSES = new Set(["added", "renamed", "copied"]);
109
+ /** Placement also fires on cross-cutting diffs: at least this many code files. */
110
+ export const PLACEMENT_MIN_PROD_FILES = 8;
111
+ /** …or on a new code file of at least this many added lines. The two
112
+ * placement thresholds and the intent bar above are the `default` review
113
+ * preset's (config.ts REVIEW_PRESETS, held equal by tests/review-levers.test.ts):
114
+ * the lean plan, measured on the defect benchmark to keep recall at ~15% less
115
+ * spend. */
116
+ export const PLACEMENT_MIN_NEW_FILE_LINES = 30;
117
+ /** Simplicity fires on diffs big enough to hide complexity — aligned with
118
+ * `small_diff_lines` (150) so a "small quiet diff" round stays lean… */
119
+ export const SIMPLICITY_MIN_PROD_LINES = 150;
120
+ /** …or on a file gaining this many branches (`+ if/elif/else if/case`). */
121
+ export const SIMPLICITY_MIN_ADDED_BRANCHES = 5;
122
+ /** …or on added lines nested this deep (5 levels at 4-space, or tabs). */
123
+ const SIMPLICITY_DEEP_INDENT_RE = /^\+(?: {20,}|\t{5,})\S/m;
124
+ /** Added-branch counter: indent must not cross lines (`\s` matches
125
+ * newlines), and brace languages open rungs with `} else if`. */
126
+ const SIMPLICITY_BRANCH_RE = /^\+[ \t]*[)\]}]*[ \t]*(if|elif|else[ \t]+if|case|when)\b/gm;
127
+ // Dependency-manifest / lockfile shapes — a security surface (supply chain).
128
+ const DEPS_MANIFEST_RE = /(^|\/)(package(-lock)?\.json|yarn\.lock|pnpm-lock\.yaml|requirements[^/]*\.txt|poetry\.lock|uv\.lock|pyproject\.toml|Pipfile(\.lock)?|composer\.(json|lock)|go\.(mod|sum)|Gemfile(\.lock)?|Cargo\.(toml|lock))$/;
129
+ export const EMPTY_HEURISTICS = { paths: {}, addedLines: {} };
130
+ /** The lenses a profile may extend: exactly the shape-selected conditionals.
131
+ * Deliberately NOT extendable: `pass-baseline`/`pass-validate` (always in the
132
+ * plan already), `pass-intent` (keyed on the PR body, never the file shape),
133
+ * `pass-placement`/`pass-simplicity` (computed from code-file status/size
134
+ * signals, not path membership), and `pass-concurrency`/`pass-ask-human`
135
+ * (never selected by path: a race or an unwritten answer has no filename, so a
136
+ * profile path predicate would be exactly the selection that rule refuses). */
137
+ export const PROFILE_EXTENDABLE_LENSES = [
138
+ "pass-ui",
139
+ "pass-contracts",
140
+ "pass-data-safety",
141
+ "pass-security",
142
+ "pass-statelessness",
143
+ "pass-prod-risk",
144
+ "pass-delivery",
145
+ "pass-backcompat",
146
+ ];
147
+ // Deterministic lens selection: baseline always; conditional lenses
148
+ // from the changed-file shape (path + patch-content signals), each tied to
149
+ // the seeded lens's stated focus; validator always runs last (added by
150
+ // dispatch, never selected here). `ext` is the selected profile's
151
+ // extension; each conditional below ORs in its extra predicates AT ITS CORE
152
+ // POSITION, so the emitted lens order is identical with and without a profile.
153
+ export function selectLenses(files, intent, ext = EMPTY_HEURISTICS, tuning = {},
154
+ /** Filled with why each lens was planned: the plan explains every lens it
155
+ * chose. */
156
+ reasons) {
157
+ const lenses = new Set(["pass-baseline"]);
158
+ reasons?.set("pass-baseline", "every round: the PR against its own claims");
159
+ const names = files.map((f) => f.filename);
160
+ // The signal that fired last — the conditions below are OR-chains, so when
161
+ // one is true this names the arm that made it true.
162
+ let lastHit = "";
163
+ const any = (re) => {
164
+ const n = names.find((x) => re.test(x));
165
+ if (n !== undefined)
166
+ lastHit = `the diff touches ${n}`;
167
+ return n !== undefined;
168
+ };
169
+ /** Added-line content signal — only fires when a patch is present. */
170
+ const added = (re) => {
171
+ const f = files.find((x) => x.patch !== undefined && re.test(x.patch));
172
+ if (f !== undefined)
173
+ lastHit = `an added line in ${f.filename} matches its signal`;
174
+ return f !== undefined;
175
+ };
176
+ const pick = (lens, why) => {
177
+ lenses.add(lens);
178
+ reasons?.set(lens, why ?? lastHit);
179
+ };
180
+ /** Profile-extension signal for one lens: extra path regexes ∪ extra
181
+ * added-line probes. */
182
+ const extHit = (lens) => (ext.paths[lens] ?? []).some((re) => any(re)) ||
183
+ (ext.addedLines[lens] ?? []).some((re) => added(re));
184
+ // UI: frontend files only — extensions or an explicit frontend/ path.
185
+ if (any(/\.(tsx|jsx|vue|svelte|css|scss|less|html)$/) ||
186
+ any(/(^|\/)frontend\//) ||
187
+ extHit("pass-ui"))
188
+ pick("pass-ui");
189
+ // External contracts: provider payload surfaces — schema/serializer dirs,
190
+ // wire-format files, integration/provider client dirs.
191
+ if (any(/(^|\/)(serializers?|schemas?|integrations?|providers?|connectors?)\//) ||
192
+ any(/\.(proto|graphql)$/) ||
193
+ extHit("pass-contracts"))
194
+ pick("pass-contracts");
195
+ // Data safety: migrations DDL, raw SQL, backfills (bulk per-row work) — not
196
+ // every models/ touch. Framework-specific command dirs (Django's
197
+ // management/commands/) come from the profile.
198
+ if (any(MIGRATION_DIR_RE) ||
199
+ any(/\.sql$/) ||
200
+ any(/(^|\/)backfills?\//) ||
201
+ extHit("pass-data-safety"))
202
+ pick("pass-data-safety");
203
+ // Security & tenancy: auth/permission surfaces, secret/crypto-shaped names,
204
+ // env files, dependency manifests. Framework settings-file globs
205
+ // (settings*.py) come from the profile. A manifest/lockfile change counts
206
+ // only when it adds lines: a deletion-only hunk introduces no dependency. A
207
+ // lockfile that adds lines with no package.json change still fires — that is
208
+ // the poisoning shape.
209
+ if (any(/(^|\/)(auth|authn|authz|security|permissions|middleware|throttling)\//) ||
210
+ any(/(secret|token|passw|crypt|oauth|jwt|csrf|cors)/i) ||
211
+ any(/(^|\/)\.env(\.[^/]+)?$/) ||
212
+ files.some((f) => {
213
+ const hit = DEPS_MANIFEST_RE.test(f.filename) && f.additions > 0;
214
+ if (hit)
215
+ lastHit = `the diff adds lines to the dependency manifest ${f.filename}`;
216
+ return hit;
217
+ }) ||
218
+ extHit("pass-security"))
219
+ pick("pass-security");
220
+ // Statelessness: in-process state signals in the added lines (module-level
221
+ // mutable collections, lru_cache/threading), or cache/singleton/registry
222
+ // shaped filenames — not every tasks/ touch. The content probes carry
223
+ // Python spellings but stay compiled in as multi-language intent
224
+ // (docs/profiles.md): they detect real cross-language
225
+ // smells — memoization on mutable state, in-process locks, module-level
226
+ // mutable collections — and a profile may ADD probes for other spellings,
227
+ // never remove these.
228
+ if (added(/^\+.*(@lru_cache|functools\.(cache|lru_cache))/m) ||
229
+ added(/^\+.*threading\.(Lock|local)\(/m) ||
230
+ added(/^\+[A-Za-z_][A-Za-z0-9_]*\s*(?::[^=\n]+)?=\s*(\{\}|\[\]|set\(\)|dict\(\)|defaultdict\()/m) ||
231
+ any(/(^|\/)[^/]*(cache|singleton|registry)[^/]*\.[a-z]+$/i) ||
232
+ extHit("pass-statelessness"))
233
+ pick("pass-statelessness");
234
+ // Production risk: infra-shaped paths — IaC, CI workflows, container files.
235
+ if (any(/(^|\/)(infra|ansible|terraform|helm|k8s|kubernetes)\//) ||
236
+ any(/(^|\/)\.github\/workflows\//) ||
237
+ any(/\.tf$/) ||
238
+ any(/(^|\/)(Dockerfile[^/]*|docker-compose[^/]*\.ya?ml)$/) ||
239
+ extHit("pass-prod-risk"))
240
+ pick("pass-prod-risk");
241
+ // Delivery semantics: at-least-once handler surfaces — webhooks, background
242
+ // tasks/workers/crons, queue consumers (not email/notification content
243
+ // dirs). Product-local handler markers (e.g. a repo-specific @background_task)
244
+ // come from the profile.
245
+ if (any(/(^|\/)(webhooks?|tasks|jobs|workers?|crons?|consumers?|subscribers?)\//) ||
246
+ any(/(pubsub|pub_sub|cloud_tasks|celery)/i) ||
247
+ extHit("pass-delivery"))
248
+ pick("pass-delivery");
249
+ // Placement: new (or moved — the purest placement change) CODE
250
+ // needs a home judgment, and a cross-cutting code diff is where
251
+ // wrong-layer changes hide. Docs/config/asset adds, test-only PRs and
252
+ // small edits-in-place skip it — pure spend otherwise.
253
+ const code = files.filter((f) => !isTestFile(f.filename) && CODE_FILE_RE.test(f.filename));
254
+ const crossCutting = code.length >= (tuning.placementMinCodeFiles ?? PLACEMENT_MIN_PROD_FILES);
255
+ if (code.some((f) => PLACEMENT_STATUSES.has(f.status ?? "") &&
256
+ f.additions >= (tuning.placementMinNewFileLines ?? PLACEMENT_MIN_NEW_FILE_LINES)) ||
257
+ crossCutting)
258
+ pick(PLACEMENT_PASS, crossCutting
259
+ ? `the diff touches ${code.length} code files`
260
+ : "the diff adds or moves a code file that needs a home");
261
+ // Simplicity: complexity signals — a large code diff, deep
262
+ // nesting in added lines, or a file gaining a pile of branches.
263
+ const codeLines = code.reduce((n, f) => n + f.additions + f.deletions, 0);
264
+ const largeCodeDiff = codeLines >= SIMPLICITY_MIN_PROD_LINES;
265
+ if (largeCodeDiff ||
266
+ code.some((f) => f.patch !== undefined && SIMPLICITY_DEEP_INDENT_RE.test(f.patch)) ||
267
+ code.some((f) => (f.patch?.match(SIMPLICITY_BRANCH_RE)?.length ?? 0) >= SIMPLICITY_MIN_ADDED_BRANCHES))
268
+ pick(SIMPLICITY_PASS, largeCodeDiff
269
+ ? `the code diff is ${codeLines} lines`
270
+ : "added code nests deeply or piles up branches");
271
+ // Backcompat: deploy-window skew surfaces — wire formats, API
272
+ // modules, queue/task payloads, migrations, feature flags. The dir arm
273
+ // requires a CODE file so a docs/api/ page never buys the lens. The
274
+ // framework-shaped FILENAME arms (serializers/views/urls .py modules,
275
+ // settings*.py) and framework flag-library names (django-waffle) come from
276
+ // the profile; the flag probe here keeps the language-agnostic
277
+ // spellings (feature flag itself, plus the cross-language LaunchDarkly /
278
+ // Unleash services).
279
+ const codeIn = (re) => {
280
+ const f = code.find((x) => re.test(x.filename));
281
+ if (f !== undefined)
282
+ lastHit = `the diff touches ${f.filename}`;
283
+ return f !== undefined;
284
+ };
285
+ // Content, not paths: a changed or removed public signature or a
286
+ // wire-serialization change breaks callers and peers that were built against
287
+ // the old one, wherever the file lives — a library's exported API, a
288
+ // mixed-version cluster's wire format.
289
+ const publicSurface = () => {
290
+ for (const f of code) {
291
+ if (f.patch === undefined)
292
+ continue;
293
+ const removed = f.patch
294
+ .split("\n")
295
+ .find((l) => l.startsWith("-") && !l.startsWith("---") && PUBLIC_SIGNATURE_RE.test(l.slice(1)));
296
+ if (removed !== undefined) {
297
+ lastHit = `a public signature changes in ${f.filename}`;
298
+ return true;
299
+ }
300
+ if (/^[+-](?![+-]).*/m.test(f.patch) &&
301
+ f.patch.split("\n").some((l) => /^[+-](?![+-])/.test(l) && WIRE_FORMAT_RE.test(l))) {
302
+ lastHit = `wire serialization changes in ${f.filename}`;
303
+ return true;
304
+ }
305
+ }
306
+ return false;
307
+ };
308
+ if (codeIn(/(^|\/)(serializers?|schemas?|apis?|views?|endpoints?|routes?|controllers?|handlers?)\//) ||
309
+ any(/\.(proto|graphql)$/) ||
310
+ any(MIGRATION_DIR_RE) ||
311
+ codeIn(/(^|\/)(tasks|jobs|workers?|consumers?|queues?)\//) ||
312
+ added(/^\+.*(feature.?flag|launch.?darkly|unleash)/im) ||
313
+ publicSurface() ||
314
+ extHit(BACKCOMPAT_PASS))
315
+ pick(BACKCOMPAT_PASS);
316
+ // Not selected here, deliberately: `pass-concurrency` and `pass-ask-human`.
317
+ // Neither has a path predicate — a
318
+ // read-modify-write race has no filename, and neither does a question whose
319
+ // answer was never written down. pass-ask-human reaches a round through the
320
+ // add protocol only. pass-concurrency also does, unless the diff's CONTENT
321
+ // touches synchronisation or the PR text says it is about a race or a lock:
322
+ // then SUBLENS_TRIGGERS plans it. Adding a path predicate for either is a
323
+ // decision, not a tidy-up.
324
+ // Intent: keyed on the BODY, not the file shape — the artifact it
325
+ // judges against is the author's own description, so a body below the
326
+ // substance bar means the pass's only honest output would be NONE. Skip it.
327
+ if (meetsIntentBar(intent?.body, tuning.intentMinWords))
328
+ pick(INTENT_PASS, "the PR body is long enough to state intent the diff can be checked against");
329
+ return [...lenses];
330
+ }
331
+ /** A non-test CODE file whose patch the content triggers can read — the
332
+ * one population both `addedCode` and `changedCode` scan. */
333
+ function isPatchedSourceCode(f) {
334
+ return f.patch !== undefined && CODE_FILE_RE.test(f.filename) && !isTestFile(f.filename);
335
+ }
336
+ /** Added lines of CODE files only (a README that says "encoding" is not a
337
+ * round trip). */
338
+ function addedCode(files) {
339
+ return files.filter(isPatchedSourceCode).map((f) => ({
340
+ file: f,
341
+ added: f.patch
342
+ .split("\n")
343
+ .filter((l) => l.startsWith("+") && !l.startsWith("+++"))
344
+ .join("\n"),
345
+ }));
346
+ }
347
+ /** Changed (added or removed) lines of code files. */
348
+ function changedCode(files) {
349
+ return files.filter(isPatchedSourceCode).map((f) => ({
350
+ file: f,
351
+ changed: f.patch
352
+ .split("\n")
353
+ .filter((l) => /^[+-]/.test(l) && !/^(\+\+\+|---)/.test(l))
354
+ .join("\n"),
355
+ }));
356
+ }
357
+ /** A predicate's name: `isBinary(`, `is_supported(`, and the prefixed C
358
+ * style `SDL_IsJoystick8BitDo(` — the verb may follow a `Prefix_`. */
359
+ const PREDICATE_CHANGE_RE = /(?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]*_)?(?:[iI]s|[hH]as|[cC]an|[sS]hould|[sS]upports?|[aA]llows?|[aA]ccepts?|[nN]eeds?)(?:_[a-z]|[A-Z])\w*\s*\(/;
360
+ const LITERAL_GUARD_RE = /(===|!==|==|!=)\s*(['"][^'"\n]{1,40}['"]|0x[0-9a-fA-F]+)/;
361
+ const HEX_ID_RE = /\b0x[0-9a-fA-F]{3,}\b/g;
362
+ const CASE_LABEL_RE = /^\+\s*case\s+[^:\n]+:/gm;
363
+ const ENCODING_RE = /\b(encoding|charset|chardet|jschardet|iconv|TextDecoder|TextEncoder|codecs?|BOM|byte.?order|utf-?(8|16)|latin-?1|windows-125\d|\\r\\n|CRLF|EOL|line.?endings?)\b|\.(decode|encode)\(/i;
364
+ const WRITE_RE = /\b(writeFile\w*|createWriteStream|fs\.write\w*|\.write\(|\.save\(|saveDocument|applyEdit|open\([^)\n]*['"][wa]b?\+?['"]|write_text|write_bytes)/;
365
+ /** Work that is recorded now and done later: queues, timers, coalescing, and
366
+ * the drain step on the other side ("run the pending callbacks", flush). Case
367
+ * does not matter — a comment saying "Coalesce" counts as much as the call —
368
+ * and a snake_case prefix does not hide a term (`task_enqueue`). */
369
+ const DEFER_RE = /(?:^|[^A-Za-z0-9])(?:[A-Za-z0-9]*_)?(enqueue\w*|dequeue\w*|queue\w*|setTimeout|setImmediate|nextTick|requestAnimationFrame|debounce\w*|throttle\w*|coalesc\w*|defer\w*|schedule\w*|apply_async|delay\(|create_task|call_soon\w*|run_in_executor|go func|spawn\w*|dispatch_async|post_task|wakeup\w*|pending\w*|flush\w*|drain\w*|\w*run_\w*callbacks)/gi;
370
+ const LOOP_RE = /^\+.*\b(while\s*\(|while\s+\w|loop\s*\{|for\s*\(\s*;\s*;|for\s*\{)/m;
371
+ const REMOTE_RE = /\b(fetch|request|axios|http\w*|grpc|rpc|client\.\w+|recv\w*|page|offset|cursor|next_?page|has_?more|continuation)\b/i;
372
+ const READ_ALL_RE = /\b(readFile\w*|read_to_end|read_to_string|ReadAll|readAll|\.read\(\)|arrayBuffer\(\)|\.text\(\))/;
373
+ const LIMIT_RE = /\b(MAX_\w+|max\w*Size|size\w*Limit|limit|LIMIT\w*)\b/;
374
+ const UNCHECKED_CAST_RE = /(\.json\(\)\s*\)?\s*as\s+[A-Z]|as\s+[A-Z]\w*\[\]|JSON\.parse\([^)\n]*\)\s*as\s+)/;
375
+ const LEAK_RE = /\b(os\.Hostname|hostname\(\)|gethostname|socket\.gethostname|os\.environ|process\.env)\b/;
376
+ const CLAIM_RE = /\b(silenc\w*|suppress\w*|warnings?|C\d{4}|-W[a-z-]+|clippy|eslint|lint|support(s|ed)?\s+(for\s+)?\S+|compatib\w*|prevent\w*|no longer|firmware|only\s+(for|when|if|on|with)|require[sd]?\s+(version|firmware|v?\d))\b/i;
377
+ /** A stated behaviour: "outputs the last lines that fit",
378
+ * "keeps only the newest", "returns all …" — a one-line PR body below the
379
+ * intent bar can still make a claim the code contradicts, and the claim check
380
+ * is cheap. */
381
+ const BEHAVIOUR_CLAIM_RE = /\b(outputs?|returns?|renders?|displays?|shows?|keeps?|preserves?|ensures?|guarantees?|prints?|emits?)\s+(the|only|all|every|a|an|last|first|newest|latest|oldest)\b/i;
382
+ const ROLLOUT_TEXT_RE = /\b(hotfix|backport|sdk|protocol|wire format|older (clients?|versions?|sdks?|nodes?)|backward|compat\w*|roll-?out|mixed.?version|persist\w*|migrat\w*|validators?|client and server|server and client)\b/i;
383
+ const ROLLOUT_CODE_RE = /\b(persistReducer|redux-persist|createMigrate|localStorage|sessionStorage|MAX_[A-Z_]*(ENTRIES|PAGE|ITEMS|SIZE|LEN)\w*|protocol_version|api_version|schema_version|version:\s*\d)\b/;
384
+ // ---- the concurrency trigger ----------------------------------------------
385
+ //
386
+ // A read-modify-write race has no filename, so no path predicate may plan
387
+ // pass-concurrency. Content may: left to the add protocol alone, the lens went
388
+ // unplanned even on a PR titled "fix data race". So a diff that
389
+ // touches synchronisation itself — a mutex, an atomic, a thread or goroutine,
390
+ // a channel, a lock held across an await — or a PR whose text says it is about
391
+ // a race, a lock or thread safety, plans the lens through this registry. The
392
+ // add protocol is unchanged for every other PR, and a profile still cannot
393
+ // give the lens a path predicate (PROFILE_EXTENDABLE_LENSES above).
394
+ //
395
+ // The probes are per language, keyed on the file's extension, so a word that
396
+ // is synchronisation in one language (`volatile`, `chan`, `<-`, `lock`) cannot
397
+ // fire on another. They test changed lines (added or removed — dropping a lock
398
+ // is as much a concurrency change as adding one) of non-test code files.
399
+ /** The JS/TS family's extensions — one test for both of its signals below. */
400
+ const JS_FILE_RE = /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)$/i;
401
+ /** Per-language synchronisation spellings: [extension test, probe]. */
402
+ const SYNC_PROBES = [
403
+ // Go: sync/atomic, Lock/Unlock calls, goroutines, channels.
404
+ [
405
+ /\.go$/i,
406
+ /\bsync\.(?:Mutex|RWMutex|WaitGroup|Once|Map|Cond)\b|\bsync\/atomic\b|\batomic\.(?:Add|Load|Store|Swap|CompareAndSwap|Int32|Int64|Uint32|Uint64|Bool|Value|Pointer)\w*|\.(?:R?Lock|R?Unlock|TryR?Lock)\(\)|\bgo\s+(?:func\b|[\w.]+\()|\bchan\b|<-/,
407
+ ],
408
+ // Java / Kotlin / Scala: monitors, j.u.c locks and collections, atomics.
409
+ [
410
+ /\.(?:java|kt|kts|scala)$/i,
411
+ /\bsynchronized\b|@Synchronized\b|\bvolatile\b|@Volatile\b|\b(?:ReentrantLock|ReentrantReadWriteLock|ReadWriteLock|StampedLock|Semaphore|CountDownLatch|CyclicBarrier|ExecutorService|CompletableFuture|ThreadLocal)\b|\bConcurrent[A-Z]\w*|\bAtomic(?:Integer|Long|Boolean|Reference)\w*|\.(?:lock|unlock|tryLock)\(\)|\bnew\s+Thread\(|\bMutex\(\)/,
412
+ ],
413
+ // Rust: std/tokio sync, atomics, shared ownership across threads.
414
+ [
415
+ /\.rs$/i,
416
+ /\b(?:Mutex|RwLock|Condvar|Barrier)\s*(?:<|::new)|\bArc\s*<|\bAtomic(?:Bool|Usize|Isize|U\d+|I\d+|Ptr)\b|\b(?:std|tokio|parking_lot)::sync\b|\.(?:lock|try_lock)\(\)|\b(?:thread|tokio)::spawn\b|\bOrdering::(?:SeqCst|Acquire|Release|AcqRel|Relaxed)\b|\bmpsc\b|\bunsafe\s+impl\s+(?:Send|Sync)\b/,
417
+ ],
418
+ // C / C++: std::mutex and friends, std::atomic, pthreads, C11 atomics.
419
+ [
420
+ /\.(?:c|cc|cpp|cxx|h|hh|hpp|hxx)$/i,
421
+ /\bstd::(?:mutex|shared_mutex|recursive_mutex|timed_mutex|atomic\w*|lock_guard|unique_lock|scoped_lock|shared_lock|thread|jthread|condition_variable\w*|memory_order\w*)\b|\bpthread_\w+|\batomic_\w+\s*\(|\b_Atomic\b|\bvolatile\b|\b__(?:sync|atomic)_\w+/,
422
+ ],
423
+ // C#: lock statements, Interlocked, concurrent collections.
424
+ [
425
+ /\.cs$/i,
426
+ /\block\s*\(|\bInterlocked\.\w+|\b(?:SemaphoreSlim|Monitor|Mutex|ReaderWriterLockSlim)\b|\bConcurrent(?:Dictionary|Queue|Bag|Stack)\b|\bvolatile\b/,
427
+ ],
428
+ // Python: threading, asyncio primitives, process pools, row locks.
429
+ [
430
+ /\.pyi?$/i,
431
+ /\bthreading\.\w+|\bfrom\s+threading\s+import\b|\basyncio\.(?:Lock|Semaphore|BoundedSemaphore|Event|Condition|Queue|gather|create_task|TaskGroup)\b|\bmultiprocessing\b|\bconcurrent\.futures\b|\b(?:ThreadPoolExecutor|ProcessPoolExecutor)\b|\bselect_for_update\(|\bwith\s+(?:self\.)?_*\w*lock\w*\s*:|\bR?Lock\(\)/i,
432
+ ],
433
+ // Ruby: Mutex, threads, concurrent-ruby.
434
+ [/\.rb$/i, /\bMutex\.new\b|\bThread\.new\b|\bConcurrent::\w+|\.synchronize\b/],
435
+ // Swift: dispatch queues, locks, actors — and Swift concurrency's isolation
436
+ // annotations, whose addition or removal changes which thread a closure may
437
+ // run on (a dropped @Sendable/@MainActor on a callback, `nonisolated(unsafe)`,
438
+ // an unisolated `Task {}`).
439
+ [
440
+ /\.swift$/i,
441
+ /\bDispatchQueue\b|\bNSLock\b|\bos_unfair_lock\b|\bactor\s+[A-Z]\w*|\bOSAllocatedUnfairLock\b|@Sendable\b|@MainActor\b|\bnonisolated\b|@preconcurrency\b|\bTask(?:\.detached)?\s*(?:\(\s*priority:[^)]*\))?\s*\{/,
442
+ ],
443
+ // JS / TS: real shared memory and workers, explicit lock libraries, and the
444
+ // one shared-state race the added lines can show — a member written after an
445
+ // await, or bumped in place, on an object other callers share.
446
+ [
447
+ JS_FILE_RE,
448
+ /\bAtomics\.\w+|\bSharedArrayBuffer\b|\bnew\s+Worker\(|\bworker_threads\b|\bnavigator\.locks\b|\b(?:Mutex|Semaphore)\b|\bthis\.[\w.]+\s*=\s*await\b/,
449
+ ],
450
+ ];
451
+ /** JS/TS: an in-place read-modify-write of a shared member, counted only in a
452
+ * file whose changed lines also await — the shape where two callers interleave. */
453
+ const JS_SHARED_RMW_RE = /\bthis\.[\w.]+(?:\s*(?:\+\+|--|[-+*]=)|\[[^\]\n]+\]\s*(?:\+\+|--|[-+*]=))/;
454
+ const JS_AWAIT_RE = /\bawait\b/;
455
+ /** The PR text says the change is about concurrency. `lock` counts unless it
456
+ * is a lockfile (`package-lock`, `lock file`). */
457
+ const CONCURRENCY_TEXT_RE = /\b(?:data[- ]races?|race[- ]conditions?|races?|racy|concurren(?:t|tly|cy)|thread[- ]?safe(?:ty)?|deadlocks?|mutex(?:es)?|(?<![-./])lock(?:s|ing|ed)?(?![- ]?files?\b))\b/i;
458
+ /** The changed line a sync probe fired on, trimmed and capped, for the plan's
459
+ * reason — the lens gets the code, not one token of it. */
460
+ function syncHit(filename, changed) {
461
+ const shown = (line) => {
462
+ const t = line.slice(1).trim().replace(/`/g, "'");
463
+ return t.length > 70 ? `${t.slice(0, 67)}…` : t;
464
+ };
465
+ const lines = changed.split("\n");
466
+ for (const [ext, probe] of SYNC_PROBES) {
467
+ if (!ext.test(filename))
468
+ continue;
469
+ const hit = lines.find((l) => probe.test(l.slice(1)));
470
+ if (hit !== undefined)
471
+ return shown(hit);
472
+ }
473
+ if (JS_FILE_RE.test(filename) && JS_AWAIT_RE.test(changed)) {
474
+ const hit = lines.find((l) => JS_SHARED_RMW_RE.test(l.slice(1)));
475
+ if (hit !== undefined)
476
+ return shown(hit);
477
+ }
478
+ return null;
479
+ }
480
+ /** The phrase a text trigger fired on: from the match to the end of its
481
+ * line or sentence, capped — the pass gets the claim, not one word of it. */
482
+ function phraseAt(text, at) {
483
+ const rest = text.slice(at).split(/\n|(?<=[.!?])\s/)[0] ?? "";
484
+ const clean = rest.replace(/[`"]/g, "'").trim();
485
+ return clean.length > 80 ? `${clean.slice(0, 77)}…` : clean;
486
+ }
487
+ export const SUBLENS_TRIGGERS = [
488
+ // The concurrency trigger (see above): first in the registry so the order
489
+ // matches ALL_PASSES.
490
+ {
491
+ lens: "pass-concurrency",
492
+ family: "pass-statelessness",
493
+ fires: (files, text) => {
494
+ for (const { file, changed } of changedCode(files)) {
495
+ const hit = syncHit(file.filename, changed);
496
+ if (hit !== null)
497
+ return `the diff touches synchronisation in ${file.filename} (\`${hit}\`)`;
498
+ }
499
+ const m = CONCURRENCY_TEXT_RE.exec(text);
500
+ return m ? `the PR text says "${phraseAt(text, m.index)}"` : null;
501
+ },
502
+ },
503
+ {
504
+ lens: "pass-widened",
505
+ family: "pass-baseline",
506
+ fires: (files) => {
507
+ for (const { file, changed } of changedCode(files)) {
508
+ if (/^[+-].*(function|def|func|bool|boolean|static)\b[^\n]*/m.test(changed) &&
509
+ changed.split("\n").some((l) => PREDICATE_CHANGE_RE.test(l) && /^[+-]/.test(l)))
510
+ return `a predicate changes in ${file.filename}`;
511
+ if (changed.split("\n").some((l) => LITERAL_GUARD_RE.test(l)))
512
+ return `a guard compares against a literal in ${file.filename}`;
513
+ }
514
+ for (const { file, added } of addedCode(files)) {
515
+ const ids = new Set(added.match(HEX_ID_RE) ?? []);
516
+ const cases = (`\n${added}`.match(CASE_LABEL_RE) ?? []).length;
517
+ if (ids.size >= 2 || cases >= 3)
518
+ return `accepted values are added in ${file.filename}`;
519
+ }
520
+ return null;
521
+ },
522
+ },
523
+ {
524
+ lens: "pass-roundtrip",
525
+ family: "pass-data-safety",
526
+ fires: (files) => {
527
+ const code = addedCode(files);
528
+ const enc = code.find((c) => ENCODING_RE.test(c.added));
529
+ if (!enc)
530
+ return null;
531
+ const write = code.find((c) => WRITE_RE.test(c.added));
532
+ // Encoding work in one file and the write in another is still a round trip.
533
+ return write ? `encoding handling and a write-back in ${enc.file.filename}` : null;
534
+ },
535
+ },
536
+ {
537
+ lens: "pass-deferred",
538
+ family: "pass-concurrency",
539
+ fires: (files) => {
540
+ for (const { file, added } of addedCode(files)) {
541
+ // Distinct TERMS (the capture), not distinct spellings around them.
542
+ const hits = new Set([...added.matchAll(DEFER_RE)].map((m) => (m[1] ?? "").toLowerCase()));
543
+ if (hits.size >= 2)
544
+ return `work is queued or deferred in ${file.filename}`;
545
+ }
546
+ return null;
547
+ },
548
+ },
549
+ {
550
+ lens: "pass-bounds",
551
+ family: "pass-security",
552
+ fires: (files) => {
553
+ for (const { file, added } of addedCode(files)) {
554
+ if (LOOP_RE.test(added) && REMOTE_RE.test(added))
555
+ return `a loop over another party's data in ${file.filename}`;
556
+ if (READ_ALL_RE.test(added) && LIMIT_RE.test(added))
557
+ return `a read with a size limit in ${file.filename}`;
558
+ if (UNCHECKED_CAST_RE.test(added))
559
+ return `fetched data is cast to a type in ${file.filename}`;
560
+ if (LEAK_RE.test(added))
561
+ return `host or environment facts are read in ${file.filename}`;
562
+ }
563
+ return null;
564
+ },
565
+ },
566
+ {
567
+ lens: "pass-claims",
568
+ family: "pass-intent",
569
+ fires: (_files, text) => {
570
+ const m = CLAIM_RE.exec(text) ?? BEHAVIOUR_CLAIM_RE.exec(text);
571
+ return m ? `the PR text claims "${phraseAt(text, m.index)}"` : null;
572
+ },
573
+ },
574
+ {
575
+ lens: "pass-rollout",
576
+ family: "pass-backcompat",
577
+ fires: (files, text) => {
578
+ const m = ROLLOUT_TEXT_RE.exec(text);
579
+ if (m)
580
+ return `the PR text mentions "${phraseAt(text, m.index)}"`;
581
+ for (const { file, added } of addedCode(files))
582
+ if (ROLLOUT_CODE_RE.test(added))
583
+ return `persisted or versioned state changes in ${file.filename}`;
584
+ return null;
585
+ },
586
+ },
587
+ ];
588
+ /** Every sub-lens pass name, in registry order. */
589
+ export const SUBLENS_PASSES = SUBLENS_TRIGGERS.map((t) => t.lens);
590
+ /** The sub-lenses this PR fires, with the reason each fired. Pure. */
591
+ export function selectSubLenses(files, intent) {
592
+ const text = `${intent?.title ?? ""}\n${intent?.body ?? ""}`;
593
+ const out = [];
594
+ for (const t of SUBLENS_TRIGGERS) {
595
+ const why = t.fires(files, text);
596
+ if (why)
597
+ out.push({ lens: t.lens, why });
598
+ }
599
+ return out;
600
+ }
601
+ // ---- the per-file sweep ----------------------------------------------------
602
+ //
603
+ // Some misses are attention: the pass had the code in front of it and never
604
+ // looked (a misspelled name quoted twice, an ID never checked). Reviewing a
605
+ // few files at a time catches that class. The sweep is that shape: the
606
+ // changed files, cut into a few small groups, one line-by-line pass per group.
607
+ export const SWEEP_PASS = "pass-sweep";
608
+ /** The behaviour-delta lens (agents/pass-behaviour-delta.md): base versus
609
+ * head on every modified, rewrapped, replaced or deleted line of non-test
610
+ * code — a safeguard made conditional, a value changed in a new `else`,
611
+ * deleted handling, nil turned into an empty map. No other lens reads base and
612
+ * head side by side (the sweep reads added lines only and skips removed
613
+ * files). */
614
+ export const BEHAVIOUR_DELTA_PASS = "pass-behaviour-delta";
615
+ // Behaviour-delta is planned only where base behaviour is at risk, not on
616
+ // every modified line (planned everywhere, it was nearly all noise). A file is
617
+ // at risk when its patch removes or replaces a line that carries behaviour — a
618
+ // guard or condition, a predicate or comparison, a call, a return / raise /
619
+ // default / error path, an assignment — rather than a comment, an import, a
620
+ // log line or a re-indented copy of itself (a statement re-added at the same
621
+ // indent elsewhere is a reorder, and counts); or when it wraps existing code in a NEW
622
+ // branch (a guard rewrapped, a value changed in a new `else`). A deleted file,
623
+ // and a file whose patch is not available, stay at risk: no signal is never
624
+ // read as no risk.
625
+ const DELTA_RISK_RE = /\b(if|elif|else|unless|guard|while|for|foreach|switch|case|match|when|try|catch|except|finally|rescue|ensure|defer|raise|throw|throws|return|yield|await|assert|require|default|break|continue|goto|null|nil|None|undefined)\b|&&|\|\||\?\?|[!=]==?|[<>]=|\w\s*[<>]\s*\w|(?:[+\-*/%&|^]|<<|>>)=|[^=!<>:+\-*/%&|^]=[^=>]|\w\s*\(/;
626
+ const DELTA_NEW_BRANCH_RE = /^\s*(\}\s*)?(if|elif|else|unless|guard|switch|case|match|try|catch|except|rescue|when)\b/;
627
+ const DELTA_INERT_RE = /^\s*(\/\/|#(?!include|define|if|else|endif)|\*|\/\*|--|"""|'''|<!--)|^\s*(import|from\s+\S+\s+import|#include|use\s|using\s|package\s|export\s+\{[^}]*\}\s+from)\b|^\s*[)\]};,]*\s*$/;
628
+ const DELTA_LOG_ONLY_RE = /^\s*(console\.(log|debug|info|warn|error)|(self\.|this\.)?(log|logger|logging|LOG|Log)\.(trace|debug|info|warn|warning|error|fatal|Debugf|Infof|Warnf|Errorf)|print(ln|f)?|fmt\.Print(ln|f)?|System\.out\.print(ln)?|puts|echo)\s*\(/;
629
+ const norm = (l) => l.replace(/\s+/g, " ").trim();
630
+ /** Does this file's change put base behaviour at risk? (see DELTA_RISK_RE). */
631
+ export function behaviourDeltaRisk(f) {
632
+ if (f.status === "removed" || f.status === "deleted")
633
+ return true;
634
+ if (f.patch === undefined || f.patch === "")
635
+ return true;
636
+ const removed = [];
637
+ const added = [];
638
+ for (const line of f.patch.split("\n")) {
639
+ if (line.startsWith("---") || line.startsWith("+++"))
640
+ continue;
641
+ if (line.startsWith("-"))
642
+ removed.push(line.slice(1));
643
+ else if (line.startsWith("+"))
644
+ added.push(line.slice(1));
645
+ }
646
+ if (removed.length === 0)
647
+ return false;
648
+ const addedExact = new Set(added);
649
+ const addedNorm = new Set(added.map(norm));
650
+ for (const r of removed) {
651
+ const n = norm(r);
652
+ if (n === "")
653
+ continue;
654
+ // The same statement re-indented (a rewrap) is judged by the new-branch
655
+ // check below; the same statement at the same indent re-added elsewhere is
656
+ // a reorder, which changes behaviour, so it falls through to the risk test.
657
+ if (addedNorm.has(n) && !addedExact.has(r))
658
+ continue;
659
+ // A leading star can also dereference a C/C++ pointer for a write.
660
+ const pointerWrite = /^\s*\*\s*[A-Za-z_$][\w$]*(?:\s*(?:\.[\w$]+|->\s*[\w$]+|\[[^\]]+\]))*\s*(?:[+\-*/%&|^]|<<|>>)?=(?!=)/.test(r);
661
+ if ((!pointerWrite && DELTA_INERT_RE.test(r)) || DELTA_LOG_ONLY_RE.test(r))
662
+ continue;
663
+ if (DELTA_RISK_RE.test(r))
664
+ return true;
665
+ }
666
+ // existing code wrapped in a NEW branch: a guard rewrapped, a value changed in a new else
667
+ return added.some((a) => DELTA_NEW_BRANCH_RE.test(a) && !addedSetHasRemoved(a, removed));
668
+ }
669
+ function addedSetHasRemoved(a, removed) {
670
+ const n = norm(a);
671
+ return removed.some((r) => norm(r) === n);
672
+ }
673
+ /**
674
+ * Cut the PR's MODIFYING changes into behaviour-delta groups: non-test code
675
+ * files that delete or replace lines AND put base behaviour at risk
676
+ * (`behaviourDeltaRisk`; a file the PR removes included; pure additions are not
677
+ * this lens's), diff order, packed by changed lines (at most `maxLines` per
678
+ * group unless one file alone exceeds it), at most `maxGroups` groups. Files
679
+ * past the last group come back in `unread` — a silent cap reads as covered.
680
+ */
681
+ export function behaviourDeltaGroups(files, maxGroups, maxLines) {
682
+ const eligible = files.filter((f) => f.deletions > 0 &&
683
+ CODE_FILE_RE.test(f.filename) &&
684
+ !isTestFile(f.filename) &&
685
+ !SWEEP_SKIP_RE.test(f.filename) &&
686
+ behaviourDeltaRisk(f));
687
+ const { groups, left: unread } = packGroups(eligible, (f) => f.additions + f.deletions, maxGroups, Number.POSITIVE_INFINITY, maxLines);
688
+ return { groups, unread };
689
+ }
690
+ /**
691
+ * Greedy packing in the given order: a group closes when the next file would
692
+ * take it past `maxFiles` files or `maxLines` lines (a file alone may exceed
693
+ * `maxLines`), and at most `maxGroups` groups are made. Files past the last
694
+ * group come back in `left`, never dropped.
695
+ */
696
+ function packGroups(files, size, maxGroups, maxFiles, maxLines) {
697
+ return packByBudget(files, { maxGroups, maxFiles, maxLines }, (file) => file.filename, size);
698
+ }
699
+ /** The tests lens (agents/pass-tests.md). */
700
+ export const TESTS_PASS = "pass-tests";
701
+ // The code-quality lenses (`[review] quality_lenses`): the other half of a
702
+ // review — will the next person understand and safely change this — beside the
703
+ // defect-finding lenses. Three lenses, each one question: maintainability
704
+ // (readers and future edits), duplication (the same knowledge written twice),
705
+ // abstraction (boundaries and contracts at the right level). Style is
706
+ // deliberately none of them: every repo has its own, so it has its own lens,
707
+ // planned only where the repo writes its style down.
708
+ export const MAINTAINABILITY_PASS = "pass-maintainability";
709
+ export const DUPLICATION_PASS = "pass-duplication";
710
+ export const ABSTRACTION_PASS = "pass-abstraction";
711
+ /** Is the change consistent with itself and with the code around it? An
712
+ * outlier sibling is often a defect, so this one is not capped when it shows
713
+ * a failure. */
714
+ export const CONSISTENCY_PASS = "pass-consistency";
715
+ export const QUALITY_PASSES = [
716
+ MAINTAINABILITY_PASS,
717
+ DUPLICATION_PASS,
718
+ ABSTRACTION_PASS,
719
+ CONSISTENCY_PASS,
720
+ ];
721
+ /** Added non-test code lines below which a change is too small to carry a
722
+ * maintainability, duplication or abstraction question worth a pass. */
723
+ export const QUALITY_MIN_ADDED_LINES = 20;
724
+ /** The style lens (agents/pass-style.md): planned only when the repo has a
725
+ * written style guide (packages/pi-plugin/src/styleguide.ts). */
726
+ export const STYLE_PASS = "pass-style";
727
+ /** The lint-conformance sub-lens (agents/pass-lint-conformance.md): planned
728
+ * only when a CI job runs a linter whose rules cover a changed file. */
729
+ export const LINT_CONFORMANCE_PASS = "pass-lint-conformance";
730
+ /** The quality lenses a change warrants: all of them once its non-test code
731
+ * adds `minAdded` lines — or, on a test-only change (a test file is code a
732
+ * reader maintains too), once its test code adds that many —
733
+ * none below. */
734
+ export function selectQualityLenses(files, minAdded) {
735
+ const { added } = qualityCount(files);
736
+ return added >= minAdded && added > 0 ? [...QUALITY_PASSES] : [];
737
+ }
738
+ /** What the quality lenses count: the added lines of non-test code — or, on a
739
+ * test-only change, of its test code. One count for the selection and the
740
+ * reason the plan shows. */
741
+ function qualityCount(files) {
742
+ const code = files.filter((f) => CODE_FILE_RE.test(f.filename));
743
+ const testOnly = code.length > 0 && code.every((f) => isTestFile(f.filename));
744
+ const added = code
745
+ .filter((f) => isTestFile(f.filename) === testOnly)
746
+ .reduce((n, f) => n + f.additions, 0);
747
+ return { testOnly, added };
748
+ }
749
+ /** Why the quality lenses were planned, for the plan. */
750
+ export function qualityReason(files, minAdded) {
751
+ const { testOnly, added: n } = qualityCount(files);
752
+ return testOnly
753
+ ? `a test-only change adds ${n} lines of test code (at least ${minAdded})`
754
+ : `the change adds ${n} lines of non-test code (at least ${minAdded})`;
755
+ }
756
+ /** Files the sweep never reads: lockfiles, generated or minified output,
757
+ * vendored trees, snapshots, binaries and prose — and translation catalogs
758
+ * (`TRANSLATION_FILE_RE`). */
759
+ const SWEEP_SKIP_RE = /((^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|go\.sum|poetry\.lock|uv\.lock|composer\.lock|Gemfile\.lock)$|\.(min\.js|map|snap|lock|svg|png|jpe?g|gif|ico|pdf|woff2?|ttf|mdx?|rst|adoc|txt)$|(^|\/)(vendor|third_party|node_modules|dist|build|generated)\/)/i;
760
+ /** Translation catalogs: a data file under a locale /
761
+ * translation directory, or a gettext / XLIFF / ARB catalog anywhere. A
762
+ * line-by-line sweep of one is a row spent reading strings: one
763
+ * per-language config change can touch dozens of these at once. The PR's other
764
+ * lenses still see them (pass-ui owns translations where a profile says so),
765
+ * and they still count toward the change — only the sweep skips them. */
766
+ const TRANSLATION_FILE_RE = /((^|\/)(locales?|i18n|l10n|translations?|langs?)\/(.+\/)?[^/]+\.(json|ya?ml|po|pot|xliff|xlf|arb|strings|properties|toml)$|\.(po|pot|xliff|xlf|arb)$)/i;
767
+ /** Is `path` a translation catalog the sweep skips (`TRANSLATION_FILE_RE`)? */
768
+ export function isTranslationFile(path) {
769
+ return TRANSLATION_FILE_RE.test(path);
770
+ }
771
+ /** Config and data files: what a "light" sweep row may hold besides tests and
772
+ * prose. Source in a config-shaped language (a Dockerfile, a shell script, a
773
+ * CI workflow's embedded script) is NOT light — it runs. */
774
+ const LIGHT_DATA_FILE_RE = /\.(json|jsonc|ya?ml|toml|ini|cfg|conf|properties|xml|csv|tsv|env)$/i;
775
+ /**
776
+ * Is `path` a file a sweep row can read at a cheaper tier
777
+ * (`[review] sweep_light_tier`)? Tests, prose and config/data files — the
778
+ * rows that hold no production code. A CI workflow (`.github/workflows/`)
779
+ * is not: its steps run.
780
+ */
781
+ export function isLightSweepFile(path) {
782
+ if (/(^|\/)\.github\/workflows\//.test(path))
783
+ return false;
784
+ return isTestFile(path) || SWEEP_DOC_RE.test(path) || LIGHT_DATA_FILE_RE.test(path);
785
+ }
786
+ /** Prose the sweep reads when the PR also changes code. */
787
+ const SWEEP_DOC_RE = /\.(md|mdx|rst|adoc)$/i;
788
+ /** Is `path` a file the sweep never reads? Exported for the setup engine's
789
+ * case sizing: a unit is sized by what a pass would actually read. Prose is
790
+ * judged by path alone here (README.md reads as skipped); `sweepGroups` still
791
+ * reads prose when the PR also changes code, so this is the code-less answer. */
792
+ export function isSweepSkipped(path) {
793
+ return SWEEP_SKIP_RE.test(path) || TRANSLATION_FILE_RE.test(path);
794
+ }
795
+ /** A group stays under both caps unless one file alone exceeds them. The
796
+ * defaults are sized to hold a typical small PR in one or two passes, so no
797
+ * pass is spent on a handful of lines. */
798
+ export const SWEEP_MAX_FILES_PER_GROUP = 8;
799
+ export const SWEEP_MAX_LINES_PER_GROUP = 500;
800
+ /** A WIDE change — more eligible files than `maxFiles × maxGroups`, so the
801
+ * file cap would leave files unswept — is packed by its changed-line budget
802
+ * instead, at most this many files per group: cost tracks the number of
803
+ * passes, and a wide, shallow change would otherwise run every sweep pass and
804
+ * still leave most files unswept. Every non-wide plan is unchanged by it.
805
+ * 0 turns the rule off. */
806
+ export const SWEEP_WIDE_FILES_PER_GROUP = 40;
807
+ /**
808
+ * Cut the PR's changed files into sweep groups: diff order, greedy, at most
809
+ * `SWEEP_MAX_FILES_PER_GROUP` files and `SWEEP_MAX_LINES_PER_GROUP` changed
810
+ * lines per group, at most `maxGroups` groups. Files past the last group come
811
+ * back in `unswept` so the plan can say so — a silent cap reads as "covered".
812
+ */
813
+ export function sweepGroups(files, maxGroups, maxFiles = SWEEP_MAX_FILES_PER_GROUP, maxLines = SWEEP_MAX_LINES_PER_GROUP, wideFiles = SWEEP_WIDE_FILES_PER_GROUP) {
814
+ // Changed prose (docs, READMEs) is swept too when the PR
815
+ // also changes code — its commands, API names and examples can go stale with
816
+ // the code — and every group is filled source-first: code, then tests and
817
+ // fixtures, then prose, so a group cap starves the least likely place for a
818
+ // defect, never a source file. A prose file counts half its lines.
819
+ const changesCode = files.some((f) => f.status !== "removed" && CODE_FILE_RE.test(f.filename) && !isTestFile(f.filename));
820
+ const isDoc = (name) => SWEEP_DOC_RE.test(name);
821
+ const rank = (name) => (isDoc(name) ? 2 : isTestFile(name) ? 1 : 0);
822
+ const eligible = files
823
+ .filter((f) => f.status !== "removed" &&
824
+ f.additions > 0 &&
825
+ !TRANSLATION_FILE_RE.test(f.filename) &&
826
+ (!SWEEP_SKIP_RE.test(f.filename) || (changesCode && isDoc(f.filename))))
827
+ .map((f, i) => ({ f, i }))
828
+ .sort((a, b) => rank(a.f.filename) - rank(b.f.filename) || a.i - b.i)
829
+ .map((x) => x.f);
830
+ // A wide, shallow change: pack by changed lines, not by file count, so the
831
+ // budget of passes covers every file instead of running out of groups.
832
+ if (wideFiles > maxFiles && eligible.length > maxFiles * maxGroups)
833
+ maxFiles = wideFiles;
834
+ const { groups, left: unswept } = packGroups(eligible, sweepSize, maxGroups, maxFiles, maxLines);
835
+ return { groups, unswept };
836
+ }
837
+ /** A file's changed lines by the sweep's sizing rule: prose counts half. */
838
+ function sweepSize(f) {
839
+ const lines = f.additions + f.deletions;
840
+ return SWEEP_DOC_RE.test(f.filename) ? Math.ceil(lines / 2) : lines;
841
+ }
842
+ /** At most this many pass-baseline rows when a very large change splits it:
843
+ * each row is a whole baseline reading, so more rows buy a shorter
844
+ * critical path at the price of more parallel full readings. */
845
+ export const BASELINE_SPLIT_MAX_ROWS = 3;
846
+ /** The changed lines a reader of `f` reads, by the sweep's own sizing rule:
847
+ * prose half, lockfiles / generated / vendored / binary nothing, the rest
848
+ * additions + deletions. */
849
+ function readLines(f) {
850
+ if (SWEEP_DOC_RE.test(f.filename))
851
+ return sweepSize(f);
852
+ return SWEEP_SKIP_RE.test(f.filename) ? 0 : sweepSize(f);
853
+ }
854
+ /** The changed lines `baselineGroups` measures a change by. */
855
+ export function baselineSplitLines(files) {
856
+ return files.reduce((n, f) => n + readLines(f), 0);
857
+ }
858
+ /**
859
+ * Split pass-baseline by file group on a very large change
860
+ * (`[review] baseline_split_lines`). pass-baseline is one row and, once the
861
+ * fan-out launches in one turn, the round's critical path on the largest
862
+ * PRs.
863
+ *
864
+ * Over `splitLines` changed lines (`baselineSplitLines`; 0 = never) the
865
+ * change's files — every one the PR does not remove — are cut, in diff order
866
+ * (directories stay together), into `ceil(lines / splitLines)` groups, at
867
+ * most `maxRows`, balanced by those lines: each file goes to the group its
868
+ * line midpoint falls in, so no file is split and every file has exactly one
869
+ * group. A group under a quarter of its share joins its neighbour, so a
870
+ * file that weighs nothing, or a few lines beside one file that is nearly
871
+ * the whole change, never buys a row. Each group is where one baseline row
872
+ * STARTS; the row's charter stays the whole PR.
873
+ *
874
+ * Returns `[]` — one ordinary row — at or under the
875
+ * threshold, and when the cut leaves fewer than two groups.
876
+ */
877
+ export function baselineGroups(files, splitLines, maxRows = BASELINE_SPLIT_MAX_ROWS) {
878
+ if (!(splitLines > 0))
879
+ return [];
880
+ const total = baselineSplitLines(files);
881
+ if (total <= splitLines)
882
+ return [];
883
+ const n = Math.min(Math.max(maxRows, 1), Math.ceil(total / splitLines));
884
+ if (n < 2)
885
+ return [];
886
+ const target = total / n;
887
+ const groups = Array.from({ length: n }, () => ({ files: [], lines: 0 }));
888
+ let before = 0;
889
+ for (const f of files) {
890
+ if (f.status === "removed")
891
+ continue;
892
+ const w = readLines(f);
893
+ const g = groups[Math.min(n - 1, Math.floor((before + w / 2) / target))];
894
+ g.files.push(f.filename);
895
+ g.lines += w;
896
+ before += w;
897
+ }
898
+ // A group under a quarter of its share — weightless files past the last
899
+ // real one, or the crumbs beside one file that is nearly the whole change —
900
+ // is not worth a whole baseline reading: its files join the group before
901
+ // it, or the one after when it is first.
902
+ const kept = [];
903
+ let carry = [];
904
+ for (const g of groups) {
905
+ if (g.lines >= target / 4) {
906
+ kept.push([...carry, ...g.files]);
907
+ carry = [];
908
+ }
909
+ else if (kept.length > 0)
910
+ kept.at(-1).push(...g.files);
911
+ else
912
+ carry.push(...g.files);
913
+ }
914
+ if (carry.length > 0) {
915
+ if (kept.length === 0)
916
+ return [];
917
+ kept.at(-1).push(...carry);
918
+ }
919
+ return kept.length >= 2 ? kept : [];
920
+ }
921
+ /** A pass-baseline row of a split baseline (`baselineGroups`): its `focus`
922
+ * is where it STARTS, never what it covers — unlike a sweep or
923
+ * behaviour-delta row, whose focus is exactly its files. */
924
+ export function isSplitBaselineRow(p) {
925
+ return p.lens === "pass-baseline" && p.focus !== undefined;
926
+ }
927
+ //# sourceMappingURL=heuristics.js.map