oc-pilot 0.5.1 → 0.88.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/CHANGELOG.md +683 -0
  2. package/CONTRIBUTING.md +4 -2
  3. package/README.md +588 -20
  4. package/README.ru.md +572 -10
  5. package/dist/cli.js +185 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cli.test.js +1211 -1
  8. package/dist/cli.test.js.map +1 -1
  9. package/dist/commands/anti-regression.js +97 -0
  10. package/dist/commands/anti-regression.js.map +1 -0
  11. package/dist/commands/arch-diff.js +80 -0
  12. package/dist/commands/arch-diff.js.map +1 -0
  13. package/dist/commands/authz.js +167 -0
  14. package/dist/commands/authz.js.map +1 -0
  15. package/dist/commands/autopsy.js +64 -0
  16. package/dist/commands/autopsy.js.map +1 -0
  17. package/dist/commands/bankruptcy.js +60 -0
  18. package/dist/commands/bankruptcy.js.map +1 -0
  19. package/dist/commands/battle.js +119 -0
  20. package/dist/commands/battle.js.map +1 -0
  21. package/dist/commands/boundaries.js +114 -0
  22. package/dist/commands/boundaries.js.map +1 -0
  23. package/dist/commands/bounty.js +56 -0
  24. package/dist/commands/bounty.js.map +1 -0
  25. package/dist/commands/brag.js +62 -0
  26. package/dist/commands/brag.js.map +1 -0
  27. package/dist/commands/cache.js +62 -0
  28. package/dist/commands/cache.js.map +1 -0
  29. package/dist/commands/cinema.js +61 -0
  30. package/dist/commands/cinema.js.map +1 -0
  31. package/dist/commands/cleanup.js +110 -0
  32. package/dist/commands/cleanup.js.map +1 -0
  33. package/dist/commands/commit.js +177 -0
  34. package/dist/commands/commit.js.map +1 -0
  35. package/dist/commands/commit.test.js +160 -0
  36. package/dist/commands/commit.test.js.map +1 -0
  37. package/dist/commands/compliance.js +121 -0
  38. package/dist/commands/compliance.js.map +1 -0
  39. package/dist/commands/contract-drift.js +97 -0
  40. package/dist/commands/contract-drift.js.map +1 -0
  41. package/dist/commands/coverage.js +137 -0
  42. package/dist/commands/coverage.js.map +1 -0
  43. package/dist/commands/day.js +116 -0
  44. package/dist/commands/day.js.map +1 -0
  45. package/dist/commands/day.test.js +45 -0
  46. package/dist/commands/day.test.js.map +1 -0
  47. package/dist/commands/deps.js +245 -0
  48. package/dist/commands/deps.js.map +1 -0
  49. package/dist/commands/docgen.js +4 -2
  50. package/dist/commands/docgen.js.map +1 -1
  51. package/dist/commands/doctor.js +146 -2
  52. package/dist/commands/doctor.js.map +1 -1
  53. package/dist/commands/doctor.test.js +69 -0
  54. package/dist/commands/doctor.test.js.map +1 -0
  55. package/dist/commands/echo.js +78 -0
  56. package/dist/commands/echo.js.map +1 -0
  57. package/dist/commands/ecosystem-verify.js +113 -0
  58. package/dist/commands/ecosystem-verify.js.map +1 -0
  59. package/dist/commands/env-sync.js +102 -0
  60. package/dist/commands/env-sync.js.map +1 -0
  61. package/dist/commands/explain.js +115 -0
  62. package/dist/commands/explain.js.map +1 -0
  63. package/dist/commands/fix.js +145 -3
  64. package/dist/commands/fix.js.map +1 -1
  65. package/dist/commands/flaky.js +111 -0
  66. package/dist/commands/flaky.js.map +1 -0
  67. package/dist/commands/fuzz.js +119 -0
  68. package/dist/commands/fuzz.js.map +1 -0
  69. package/dist/commands/hivemind.js +124 -0
  70. package/dist/commands/hivemind.js.map +1 -0
  71. package/dist/commands/impact.js +73 -0
  72. package/dist/commands/impact.js.map +1 -0
  73. package/dist/commands/init.js +73 -11
  74. package/dist/commands/init.js.map +1 -1
  75. package/dist/commands/init.test.js +138 -0
  76. package/dist/commands/init.test.js.map +1 -0
  77. package/dist/commands/install-hook.js +161 -0
  78. package/dist/commands/install-hook.js.map +1 -0
  79. package/dist/commands/install-hook.test.js +100 -0
  80. package/dist/commands/install-hook.test.js.map +1 -0
  81. package/dist/commands/invariants.js +103 -0
  82. package/dist/commands/invariants.js.map +1 -0
  83. package/dist/commands/mock-leak.js +97 -0
  84. package/dist/commands/mock-leak.js.map +1 -0
  85. package/dist/commands/mutate.js +99 -0
  86. package/dist/commands/mutate.js.map +1 -0
  87. package/dist/commands/pack.js +144 -0
  88. package/dist/commands/pack.js.map +1 -0
  89. package/dist/commands/preflight.js +137 -0
  90. package/dist/commands/preflight.js.map +1 -0
  91. package/dist/commands/query.js +57 -0
  92. package/dist/commands/query.js.map +1 -0
  93. package/dist/commands/ready.js +152 -0
  94. package/dist/commands/ready.js.map +1 -0
  95. package/dist/commands/receipt.js +289 -0
  96. package/dist/commands/receipt.js.map +1 -0
  97. package/dist/commands/replay.js +112 -0
  98. package/dist/commands/replay.js.map +1 -0
  99. package/dist/commands/replay.test.js +125 -0
  100. package/dist/commands/replay.test.js.map +1 -0
  101. package/dist/commands/restore.js +54 -0
  102. package/dist/commands/restore.js.map +1 -0
  103. package/dist/commands/review.js +263 -4
  104. package/dist/commands/review.js.map +1 -1
  105. package/dist/commands/review.test.js +160 -0
  106. package/dist/commands/review.test.js.map +1 -0
  107. package/dist/commands/reviewers.js +86 -0
  108. package/dist/commands/reviewers.js.map +1 -0
  109. package/dist/commands/roast.js +102 -0
  110. package/dist/commands/roast.js.map +1 -0
  111. package/dist/commands/sandbox.js +82 -0
  112. package/dist/commands/sandbox.js.map +1 -0
  113. package/dist/commands/sast.js +162 -0
  114. package/dist/commands/sast.js.map +1 -0
  115. package/dist/commands/scan.js +47 -12
  116. package/dist/commands/scan.js.map +1 -1
  117. package/dist/commands/slop.js +145 -0
  118. package/dist/commands/slop.js.map +1 -0
  119. package/dist/commands/slop.test.js +135 -0
  120. package/dist/commands/slop.test.js.map +1 -0
  121. package/dist/commands/stats.js +61 -0
  122. package/dist/commands/stats.js.map +1 -0
  123. package/dist/commands/test-gen.js +372 -0
  124. package/dist/commands/test-gen.js.map +1 -0
  125. package/dist/commands/test-guard.js +89 -0
  126. package/dist/commands/test-guard.js.map +1 -0
  127. package/dist/commands/test-plan.js +133 -0
  128. package/dist/commands/test-plan.js.map +1 -0
  129. package/dist/commands/zoom-symbol.js +52 -0
  130. package/dist/commands/zoom-symbol.js.map +1 -0
  131. package/dist/core/agent-rules.js +35 -0
  132. package/dist/core/agent-rules.js.map +1 -0
  133. package/dist/core/anthropic.test.js +116 -0
  134. package/dist/core/anthropic.test.js.map +1 -0
  135. package/dist/core/anti-regression.js +161 -0
  136. package/dist/core/anti-regression.js.map +1 -0
  137. package/dist/core/anti-regression.test.js +154 -0
  138. package/dist/core/anti-regression.test.js.map +1 -0
  139. package/dist/core/aql.js +220 -0
  140. package/dist/core/aql.js.map +1 -0
  141. package/dist/core/aql.test.js +64 -0
  142. package/dist/core/aql.test.js.map +1 -0
  143. package/dist/core/arch-diff.js +122 -0
  144. package/dist/core/arch-diff.js.map +1 -0
  145. package/dist/core/arch-diff.test.js +93 -0
  146. package/dist/core/arch-diff.test.js.map +1 -0
  147. package/dist/core/authz.js +292 -0
  148. package/dist/core/authz.js.map +1 -0
  149. package/dist/core/authz.test.js +115 -0
  150. package/dist/core/authz.test.js.map +1 -0
  151. package/dist/core/autopsy.js +230 -0
  152. package/dist/core/autopsy.js.map +1 -0
  153. package/dist/core/autopsy.test.js +106 -0
  154. package/dist/core/autopsy.test.js.map +1 -0
  155. package/dist/core/bankruptcy.js +232 -0
  156. package/dist/core/bankruptcy.js.map +1 -0
  157. package/dist/core/bankruptcy.test.js +91 -0
  158. package/dist/core/bankruptcy.test.js.map +1 -0
  159. package/dist/core/baseline.js +131 -0
  160. package/dist/core/baseline.js.map +1 -0
  161. package/dist/core/baseline.test.js +91 -0
  162. package/dist/core/baseline.test.js.map +1 -0
  163. package/dist/core/battle.js +213 -0
  164. package/dist/core/battle.js.map +1 -0
  165. package/dist/core/battle.test.js +116 -0
  166. package/dist/core/battle.test.js.map +1 -0
  167. package/dist/core/boundaries.js +200 -0
  168. package/dist/core/boundaries.js.map +1 -0
  169. package/dist/core/boundaries.test.js +62 -0
  170. package/dist/core/boundaries.test.js.map +1 -0
  171. package/dist/core/bounty.js +128 -0
  172. package/dist/core/bounty.js.map +1 -0
  173. package/dist/core/bounty.test.js +82 -0
  174. package/dist/core/bounty.test.js.map +1 -0
  175. package/dist/core/brag.js +174 -0
  176. package/dist/core/brag.js.map +1 -0
  177. package/dist/core/brag.test.js +43 -0
  178. package/dist/core/brag.test.js.map +1 -0
  179. package/dist/core/cache.js +189 -0
  180. package/dist/core/cache.js.map +1 -0
  181. package/dist/core/cache.test.js +159 -0
  182. package/dist/core/cache.test.js.map +1 -0
  183. package/dist/core/cinema.js +242 -0
  184. package/dist/core/cinema.js.map +1 -0
  185. package/dist/core/cinema.test.js +73 -0
  186. package/dist/core/cinema.test.js.map +1 -0
  187. package/dist/core/cleanup.js +107 -0
  188. package/dist/core/cleanup.js.map +1 -0
  189. package/dist/core/compliance.js +193 -0
  190. package/dist/core/compliance.js.map +1 -0
  191. package/dist/core/compliance.test.js +123 -0
  192. package/dist/core/compliance.test.js.map +1 -0
  193. package/dist/core/config.js +78 -0
  194. package/dist/core/config.js.map +1 -1
  195. package/dist/core/config.test.js +66 -0
  196. package/dist/core/config.test.js.map +1 -1
  197. package/dist/core/contract-drift.js +296 -0
  198. package/dist/core/contract-drift.js.map +1 -0
  199. package/dist/core/contract-drift.test.js +166 -0
  200. package/dist/core/contract-drift.test.js.map +1 -0
  201. package/dist/core/cost.test.js +103 -0
  202. package/dist/core/cost.test.js.map +1 -0
  203. package/dist/core/coverage.js +335 -0
  204. package/dist/core/coverage.js.map +1 -0
  205. package/dist/core/coverage.test.js +190 -0
  206. package/dist/core/coverage.test.js.map +1 -0
  207. package/dist/core/deps-fix.js +169 -0
  208. package/dist/core/deps-fix.js.map +1 -0
  209. package/dist/core/deps-fix.test.js +176 -0
  210. package/dist/core/deps-fix.test.js.map +1 -0
  211. package/dist/core/deps-health.js +353 -0
  212. package/dist/core/deps-health.js.map +1 -0
  213. package/dist/core/deps-health.test.js +230 -0
  214. package/dist/core/deps-health.test.js.map +1 -0
  215. package/dist/core/deps-why.js +260 -0
  216. package/dist/core/deps-why.js.map +1 -0
  217. package/dist/core/deps-why.test.js +175 -0
  218. package/dist/core/deps-why.test.js.map +1 -0
  219. package/dist/core/deps.js +179 -0
  220. package/dist/core/deps.js.map +1 -0
  221. package/dist/core/deps.test.js +88 -0
  222. package/dist/core/deps.test.js.map +1 -0
  223. package/dist/core/diff-source.js +4 -2
  224. package/dist/core/diff-source.js.map +1 -1
  225. package/dist/core/echo.js +151 -0
  226. package/dist/core/echo.js.map +1 -0
  227. package/dist/core/echo.test.js +90 -0
  228. package/dist/core/echo.test.js.map +1 -0
  229. package/dist/core/ecosystem-verify.js +331 -0
  230. package/dist/core/ecosystem-verify.js.map +1 -0
  231. package/dist/core/ecosystem-verify.test.js +158 -0
  232. package/dist/core/ecosystem-verify.test.js.map +1 -0
  233. package/dist/core/engine-preset.test.js +60 -0
  234. package/dist/core/engine-preset.test.js.map +1 -0
  235. package/dist/core/engine.js +278 -32
  236. package/dist/core/engine.js.map +1 -1
  237. package/dist/core/engine.test.js +63 -0
  238. package/dist/core/engine.test.js.map +1 -1
  239. package/dist/core/env-sync.js +122 -0
  240. package/dist/core/env-sync.js.map +1 -0
  241. package/dist/core/env-sync.test.js +99 -0
  242. package/dist/core/env-sync.test.js.map +1 -0
  243. package/dist/core/file-context.js +4 -2
  244. package/dist/core/file-context.js.map +1 -1
  245. package/dist/core/findings-baseline.js +60 -0
  246. package/dist/core/findings-baseline.js.map +1 -0
  247. package/dist/core/findings-baseline.test.js +77 -0
  248. package/dist/core/findings-baseline.test.js.map +1 -0
  249. package/dist/core/findings.js +68 -21
  250. package/dist/core/findings.js.map +1 -1
  251. package/dist/core/flaky.js +137 -0
  252. package/dist/core/flaky.js.map +1 -0
  253. package/dist/core/flaky.test.js +97 -0
  254. package/dist/core/flaky.test.js.map +1 -0
  255. package/dist/core/format.js +5 -3
  256. package/dist/core/format.js.map +1 -1
  257. package/dist/core/fuzz.js +306 -0
  258. package/dist/core/fuzz.js.map +1 -0
  259. package/dist/core/fuzz.test.js +121 -0
  260. package/dist/core/fuzz.test.js.map +1 -0
  261. package/dist/core/git.js +22 -0
  262. package/dist/core/git.js.map +1 -1
  263. package/dist/core/gitlab.js +176 -0
  264. package/dist/core/gitlab.js.map +1 -0
  265. package/dist/core/gitlab.test.js +199 -0
  266. package/dist/core/gitlab.test.js.map +1 -0
  267. package/dist/core/hivemind.js +250 -0
  268. package/dist/core/hivemind.js.map +1 -0
  269. package/dist/core/hivemind.test.js +79 -0
  270. package/dist/core/hivemind.test.js.map +1 -0
  271. package/dist/core/hologram.js +463 -0
  272. package/dist/core/hologram.js.map +1 -0
  273. package/dist/core/hologram.test.js +124 -0
  274. package/dist/core/hologram.test.js.map +1 -0
  275. package/dist/core/impact.js +240 -0
  276. package/dist/core/impact.js.map +1 -0
  277. package/dist/core/impact.test.js +105 -0
  278. package/dist/core/impact.test.js.map +1 -0
  279. package/dist/core/import-graph.js +170 -0
  280. package/dist/core/import-graph.js.map +1 -0
  281. package/dist/core/import-graph.test.js +52 -0
  282. package/dist/core/import-graph.test.js.map +1 -0
  283. package/dist/core/injection.js +55 -0
  284. package/dist/core/injection.js.map +1 -0
  285. package/dist/core/injection.test.js +45 -0
  286. package/dist/core/injection.test.js.map +1 -0
  287. package/dist/core/invariants.js +286 -0
  288. package/dist/core/invariants.js.map +1 -0
  289. package/dist/core/invariants.test.js +87 -0
  290. package/dist/core/invariants.test.js.map +1 -0
  291. package/dist/core/last-review.js +92 -0
  292. package/dist/core/last-review.js.map +1 -0
  293. package/dist/core/last-review.test.js +97 -0
  294. package/dist/core/last-review.test.js.map +1 -0
  295. package/dist/core/lockfile-support.test.js +112 -0
  296. package/dist/core/lockfile-support.test.js.map +1 -0
  297. package/dist/core/lockfile.js +284 -0
  298. package/dist/core/lockfile.js.map +1 -0
  299. package/dist/core/lockfile.test.js +203 -0
  300. package/dist/core/lockfile.test.js.map +1 -0
  301. package/dist/core/mock-leak.js +120 -0
  302. package/dist/core/mock-leak.js.map +1 -0
  303. package/dist/core/mock-leak.test.js +79 -0
  304. package/dist/core/mock-leak.test.js.map +1 -0
  305. package/dist/core/mutation.js +206 -0
  306. package/dist/core/mutation.js.map +1 -0
  307. package/dist/core/mutation.test.js +104 -0
  308. package/dist/core/mutation.test.js.map +1 -0
  309. package/dist/core/opencode.js +15 -2
  310. package/dist/core/opencode.js.map +1 -1
  311. package/dist/core/opencode.test.js +10 -1
  312. package/dist/core/opencode.test.js.map +1 -1
  313. package/dist/core/options.js +51 -3
  314. package/dist/core/options.js.map +1 -1
  315. package/dist/core/osv.js +202 -0
  316. package/dist/core/osv.js.map +1 -0
  317. package/dist/core/osv.test.js +173 -0
  318. package/dist/core/osv.test.js.map +1 -0
  319. package/dist/core/output-format.test.js +102 -0
  320. package/dist/core/output-format.test.js.map +1 -0
  321. package/dist/core/pack.js +212 -0
  322. package/dist/core/pack.js.map +1 -0
  323. package/dist/core/pack.test.js +129 -0
  324. package/dist/core/pack.test.js.map +1 -0
  325. package/dist/core/phantom.js +155 -0
  326. package/dist/core/phantom.js.map +1 -0
  327. package/dist/core/phantom.test.js +58 -0
  328. package/dist/core/phantom.test.js.map +1 -0
  329. package/dist/core/pr-comment.js +116 -0
  330. package/dist/core/pr-comment.js.map +1 -0
  331. package/dist/core/pr-comment.test.js +53 -0
  332. package/dist/core/pr-comment.test.js.map +1 -0
  333. package/dist/core/pr-review.js +160 -0
  334. package/dist/core/pr-review.js.map +1 -0
  335. package/dist/core/pr-review.test.js +62 -0
  336. package/dist/core/pr-review.test.js.map +1 -0
  337. package/dist/core/preflight-run.js +106 -0
  338. package/dist/core/preflight-run.js.map +1 -0
  339. package/dist/core/preflight.js +173 -0
  340. package/dist/core/preflight.js.map +1 -0
  341. package/dist/core/preflight.test.js +187 -0
  342. package/dist/core/preflight.test.js.map +1 -0
  343. package/dist/core/presets.js +408 -0
  344. package/dist/core/presets.js.map +1 -0
  345. package/dist/core/presets.test.js +79 -0
  346. package/dist/core/presets.test.js.map +1 -0
  347. package/dist/core/prompt.golden.test.js +74 -0
  348. package/dist/core/prompt.golden.test.js.map +1 -0
  349. package/dist/core/prompt.js +43 -4
  350. package/dist/core/prompt.js.map +1 -1
  351. package/dist/core/prompt.test.js +16 -0
  352. package/dist/core/prompt.test.js.map +1 -1
  353. package/dist/core/readiness-fix.js +111 -0
  354. package/dist/core/readiness-fix.js.map +1 -0
  355. package/dist/core/readiness-fix.test.js +99 -0
  356. package/dist/core/readiness-fix.test.js.map +1 -0
  357. package/dist/core/readiness.js +189 -0
  358. package/dist/core/readiness.js.map +1 -0
  359. package/dist/core/readiness.test.js +199 -0
  360. package/dist/core/readiness.test.js.map +1 -0
  361. package/dist/core/receipt.js +193 -0
  362. package/dist/core/receipt.js.map +1 -0
  363. package/dist/core/receipt.test.js +193 -0
  364. package/dist/core/receipt.test.js.map +1 -0
  365. package/dist/core/reviewers.js +179 -0
  366. package/dist/core/reviewers.js.map +1 -0
  367. package/dist/core/reviewers.test.js +105 -0
  368. package/dist/core/reviewers.test.js.map +1 -0
  369. package/dist/core/roast.js +27 -0
  370. package/dist/core/roast.js.map +1 -0
  371. package/dist/core/roast.test.js +19 -0
  372. package/dist/core/roast.test.js.map +1 -0
  373. package/dist/core/router.js +8 -0
  374. package/dist/core/router.js.map +1 -0
  375. package/dist/core/router.test.js +157 -0
  376. package/dist/core/router.test.js.map +1 -0
  377. package/dist/core/rules.js +2 -2
  378. package/dist/core/rules.js.map +1 -1
  379. package/dist/core/run.js +322 -20
  380. package/dist/core/run.js.map +1 -1
  381. package/dist/core/sarif.js +125 -0
  382. package/dist/core/sarif.js.map +1 -0
  383. package/dist/core/sarif.test.js +60 -0
  384. package/dist/core/sarif.test.js.map +1 -0
  385. package/dist/core/sast.js +324 -0
  386. package/dist/core/sast.js.map +1 -0
  387. package/dist/core/sast.test.js +105 -0
  388. package/dist/core/sast.test.js.map +1 -0
  389. package/dist/core/sbom.js +121 -0
  390. package/dist/core/sbom.js.map +1 -0
  391. package/dist/core/sbom.test.js +81 -0
  392. package/dist/core/sbom.test.js.map +1 -0
  393. package/dist/core/second-opinion.js +88 -0
  394. package/dist/core/second-opinion.js.map +1 -0
  395. package/dist/core/second-opinion.test.js +89 -0
  396. package/dist/core/second-opinion.test.js.map +1 -0
  397. package/dist/core/secrets.js +120 -5
  398. package/dist/core/secrets.js.map +1 -1
  399. package/dist/core/secrets.test.js +74 -1
  400. package/dist/core/secrets.test.js.map +1 -1
  401. package/dist/core/shadow.js +94 -0
  402. package/dist/core/shadow.js.map +1 -0
  403. package/dist/core/shadow.test.js +50 -0
  404. package/dist/core/shadow.test.js.map +1 -0
  405. package/dist/core/slicing.js +110 -0
  406. package/dist/core/slicing.js.map +1 -0
  407. package/dist/core/slicing.test.js +287 -0
  408. package/dist/core/slicing.test.js.map +1 -0
  409. package/dist/core/slop/churn.js +49 -0
  410. package/dist/core/slop/churn.js.map +1 -0
  411. package/dist/core/slop/collect.js +186 -0
  412. package/dist/core/slop/collect.js.map +1 -0
  413. package/dist/core/slop/dead-exports.js +101 -0
  414. package/dist/core/slop/dead-exports.js.map +1 -0
  415. package/dist/core/slop/duplicates.js +155 -0
  416. package/dist/core/slop/duplicates.js.map +1 -0
  417. package/dist/core/slop/index.js +72 -0
  418. package/dist/core/slop/index.js.map +1 -0
  419. package/dist/core/slop/model.js +2 -0
  420. package/dist/core/slop/model.js.map +1 -0
  421. package/dist/core/slop/score.js +185 -0
  422. package/dist/core/slop/score.js.map +1 -0
  423. package/dist/core/slop/slop.test.js +160 -0
  424. package/dist/core/slop/slop.test.js.map +1 -0
  425. package/dist/core/slop/source.js +129 -0
  426. package/dist/core/slop/source.js.map +1 -0
  427. package/dist/core/slop/test-debt.js +178 -0
  428. package/dist/core/slop/test-debt.js.map +1 -0
  429. package/dist/core/slop/test-debt.test.js +114 -0
  430. package/dist/core/slop/test-debt.test.js.map +1 -0
  431. package/dist/core/slop/todos.js +117 -0
  432. package/dist/core/slop/todos.js.map +1 -0
  433. package/dist/core/snapshot.js +214 -0
  434. package/dist/core/snapshot.js.map +1 -0
  435. package/dist/core/snapshot.test.js +101 -0
  436. package/dist/core/snapshot.test.js.map +1 -0
  437. package/dist/core/stats.js +119 -0
  438. package/dist/core/stats.js.map +1 -0
  439. package/dist/core/stats.test.js +86 -0
  440. package/dist/core/stats.test.js.map +1 -0
  441. package/dist/core/suppressions.js +96 -0
  442. package/dist/core/suppressions.js.map +1 -0
  443. package/dist/core/suppressions.test.js +90 -0
  444. package/dist/core/suppressions.test.js.map +1 -0
  445. package/dist/core/test-delta.js +53 -0
  446. package/dist/core/test-delta.js.map +1 -0
  447. package/dist/core/test-delta.test.js +41 -0
  448. package/dist/core/test-delta.test.js.map +1 -0
  449. package/dist/core/test-plan.js +212 -0
  450. package/dist/core/test-plan.js.map +1 -0
  451. package/dist/core/test-plan.test.js +94 -0
  452. package/dist/core/test-plan.test.js.map +1 -0
  453. package/dist/core/test-weakening.js +191 -0
  454. package/dist/core/test-weakening.js.map +1 -0
  455. package/dist/core/test-weakening.test.js +114 -0
  456. package/dist/core/test-weakening.test.js.map +1 -0
  457. package/dist/core/tree-scan.js +77 -0
  458. package/dist/core/tree-scan.js.map +1 -0
  459. package/dist/core/tree-scan.test.js +58 -0
  460. package/dist/core/tree-scan.test.js.map +1 -0
  461. package/dist/mcp-prompts.test.js +133 -0
  462. package/dist/mcp-prompts.test.js.map +1 -0
  463. package/dist/mcp.js +3303 -240
  464. package/dist/mcp.js.map +1 -1
  465. package/dist/mcp.test.js +1739 -1
  466. package/dist/mcp.test.js.map +1 -1
  467. package/dist/skills.test.js +221 -0
  468. package/dist/skills.test.js.map +1 -0
  469. package/dist/version.js +1 -1
  470. package/dist/version.js.map +1 -1
  471. package/docs/integrations.md +151 -7
  472. package/package.json +2 -1
  473. package/prompts/oc-pilot.en.md +8 -1
  474. package/prompts/oc-pilot.ru.md +8 -1
  475. package/server.json +2 -2
  476. package/skills/agent-self-review/SKILL.md +87 -0
  477. package/skills/commit-discipline/SKILL.md +86 -0
  478. package/skills/oc-pilot/SKILL.md +86 -0
  479. package/skills/secure-diff-gate/SKILL.md +101 -0
  480. package/skills/stack-review-packs/SKILL.md +76 -0
  481. package/skills/test-failure-triage/SKILL.md +88 -0
@@ -0,0 +1,178 @@
1
+ import { analyzeChurn } from "./churn.js";
2
+ import { collectCommits, readRepository } from "./collect.js";
3
+ import { isCodeFile, isGeneratedPath, isTestFile } from "./source.js";
4
+ import { findDuplicates } from "./duplicates.js";
5
+ import { findTodos } from "./todos.js";
6
+ const DEFAULT_LIMIT = 3;
7
+ const DEFAULT_MAX_CHARS = 24_000;
8
+ const SINCE_DAYS = 30;
9
+ function stemOf(path) {
10
+ const normalized = path.replace(/\\/g, "/");
11
+ const stem = normalized.replace(/\.[^./]+$/, "");
12
+ const base = stem.slice(stem.lastIndexOf("/") + 1);
13
+ return { stem, base };
14
+ }
15
+ /** Maps a test file path to the lowercase stem of the module it tests. */
16
+ function testedStemOf(path) {
17
+ if (!isTestFile(path))
18
+ return undefined;
19
+ const { stem, base } = stemOf(path);
20
+ let cleaned = base.replace(/\.(test|spec)$/, "");
21
+ cleaned = cleaned.replace(/_test$/, "");
22
+ if (!/\.(test|spec|snap)$/.test(base) && !/_test$/.test(base)) {
23
+ cleaned = cleaned.replace(/Test$/, "");
24
+ }
25
+ if (cleaned.startsWith("test_") && cleaned.length > "test_".length)
26
+ cleaned = cleaned.slice("test_".length);
27
+ const dir = stem.slice(0, stem.length - base.length);
28
+ return `${dir}${cleaned}`.toLowerCase();
29
+ }
30
+ /** Stems of every module that some test file already covers. */
31
+ export function buildTestIndex(paths) {
32
+ const index = new Set();
33
+ for (const path of paths) {
34
+ const stem = testedStemOf(path);
35
+ if (stem !== undefined)
36
+ index.add(stem);
37
+ }
38
+ return index;
39
+ }
40
+ /** Best-effort check: does any test file in the index cover `file`? */
41
+ export function hasTestFile(file, testedStems) {
42
+ const { stem, base } = stemOf(file);
43
+ return testedStems.has(stem.toLowerCase()) || testedStems.has(base.toLowerCase());
44
+ }
45
+ /**
46
+ * Pure ranking: given repository files, slop signals and the test index,
47
+ * returns the files most worth writing tests for.
48
+ */
49
+ export function rankTestTargets(files, signals, testedStems, options = {}) {
50
+ const limit = options.limit ?? DEFAULT_LIMIT;
51
+ const maxCharsPerFile = options.maxCharsPerFile ?? DEFAULT_MAX_CHARS;
52
+ const churn = new Map(signals.churn.map((entry) => [entry.file, entry]));
53
+ const duplicates = new Map();
54
+ for (const pair of signals.duplicates) {
55
+ for (const side of [pair.first, pair.second]) {
56
+ const entry = duplicates.get(side.file) ?? { blocks: 0, tokens: 0 };
57
+ entry.blocks += 1;
58
+ entry.tokens += pair.tokens;
59
+ duplicates.set(side.file, entry);
60
+ }
61
+ }
62
+ const todos = new Map();
63
+ for (const item of signals.todos) {
64
+ todos.set(item.file, (todos.get(item.file) ?? 0) + 1);
65
+ }
66
+ const targets = [];
67
+ for (const file of files) {
68
+ const path = file.path.replace(/\\/g, "/");
69
+ if (!isCodeFile(path) || isTestFile(path) || isGeneratedPath(path))
70
+ continue;
71
+ if (hasTestFile(path, testedStems))
72
+ continue;
73
+ if (file.text.trim().length === 0 || file.text.length > maxCharsPerFile)
74
+ continue;
75
+ const lines = file.text.split("\n");
76
+ while (lines.length > 0 && lines[lines.length - 1] === "")
77
+ lines.pop();
78
+ const reasons = [];
79
+ let score = 0;
80
+ const churnEntry = churn.get(path);
81
+ if (churnEntry) {
82
+ score += churnEntry.commits + (churnEntry.testCommits === 0 ? 4 : 0);
83
+ reasons.push(`churn: ${String(churnEntry.commits)} commits over ${String(churnEntry.days)} days, test files changed ${String(churnEntry.testCommits)} times`);
84
+ }
85
+ const duplicateEntry = duplicates.get(path);
86
+ if (duplicateEntry) {
87
+ score += duplicateEntry.blocks + duplicateEntry.tokens / 200;
88
+ reasons.push(`duplicates: ${String(duplicateEntry.blocks)} blocks (~${String(duplicateEntry.tokens)} tokens)`);
89
+ }
90
+ const todoCount = todos.get(path);
91
+ if (todoCount) {
92
+ score += todoCount * 0.8;
93
+ reasons.push(`todos: ${String(todoCount)}`);
94
+ }
95
+ if (reasons.length === 0) {
96
+ score = Math.min(lines.length / 200, 5);
97
+ reasons.push(`no entropy signal yet: untested file, ${String(lines.length)} lines`);
98
+ }
99
+ targets.push({
100
+ file: path,
101
+ lines: lines.length,
102
+ score: Math.round(score * 100) / 100,
103
+ reasons,
104
+ commits: churnEntry?.commits,
105
+ testCommits: churnEntry?.testCommits,
106
+ days: churnEntry?.days,
107
+ duplicateBlocks: duplicateEntry?.blocks,
108
+ duplicateTokens: duplicateEntry?.tokens,
109
+ todos: todoCount,
110
+ });
111
+ }
112
+ targets.sort((x, y) => y.score - x.score || y.lines - x.lines || x.file.localeCompare(y.file));
113
+ return targets.slice(0, limit);
114
+ }
115
+ /** Unified diff that introduces each target file, so the model sees the code. */
116
+ export function syntheticTargetDiff(files) {
117
+ const blocks = [];
118
+ for (const file of files) {
119
+ const lines = file.text.split("\n");
120
+ while (lines.length > 0 && lines[lines.length - 1] === "")
121
+ lines.pop();
122
+ if (lines.length === 0)
123
+ continue;
124
+ blocks.push([
125
+ `diff --git a/${file.path} b/${file.path}`,
126
+ "new file mode 100644",
127
+ "--- /dev/null",
128
+ `+++ b/${file.path}`,
129
+ `@@ -0,0 +1,${String(lines.length)} @@`,
130
+ lines.map((line) => `+${line}`).join("\n"),
131
+ ].join("\n"));
132
+ }
133
+ return blocks.length > 0 ? `${blocks.join("\n")}\n` : "";
134
+ }
135
+ /** Scans the repository and picks the test targets with the highest debt. */
136
+ export function selectTestTargets(cwd, options = {}) {
137
+ const loaded = readRepository(cwd, { sinceDays: options.sinceDays });
138
+ const commits = collectCommits(cwd, { sinceDays: options.sinceDays ?? SINCE_DAYS });
139
+ const churn = analyzeChurn(commits, { minCommits: 5, limit: 500 });
140
+ const duplicatePairs = findDuplicates(loaded.files);
141
+ const todoItems = findTodos(loaded.files);
142
+ const testedStems = buildTestIndex(loaded.files.map((file) => file.path));
143
+ const codeFiles = loaded.files.filter((file) => isCodeFile(file.path) && !isTestFile(file.path) && !isGeneratedPath(file.path));
144
+ const untested = codeFiles.filter((file) => !hasTestFile(file.path, testedStems)).length;
145
+ const targets = rankTestTargets(loaded.files, { churn, duplicates: duplicatePairs, todos: todoItems }, testedStems, {
146
+ limit: options.limit,
147
+ maxCharsPerFile: options.maxCharsPerFile,
148
+ });
149
+ const byPath = new Map(loaded.files.map((file) => [file.path, file]));
150
+ return {
151
+ targets,
152
+ filesScanned: codeFiles.length,
153
+ untested,
154
+ payloadDiff: syntheticTargetDiff(targets.map((target) => byPath.get(target.file)).filter((file) => file !== undefined)),
155
+ };
156
+ }
157
+ /** English prompt block describing the selected targets. */
158
+ export function formatTestTargets(targets) {
159
+ if (targets.length === 0)
160
+ return "";
161
+ const lines = targets.map((target) => `- ${target.file} ${target.reasons.join("; ")}`);
162
+ return [
163
+ "TEST_DEBT_TARGETS: untested files chosen by the slop scan (churn, duplicates, TODOs). Write tests for these files:",
164
+ ...lines,
165
+ ].join("\n");
166
+ }
167
+ /** Short Russian summary of one target, for the CLI log. */
168
+ export function describeTarget(target) {
169
+ if (target.commits !== undefined) {
170
+ return `${String(target.commits)} коммитов / ${String(target.testCommits)} с тестами`;
171
+ }
172
+ if (target.duplicateBlocks)
173
+ return `${String(target.duplicateBlocks)} дублей`;
174
+ if (target.todos)
175
+ return `${String(target.todos)} TODO`;
176
+ return `${String(target.lines)} строк без тестов`;
177
+ }
178
+ //# sourceMappingURL=test-debt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-debt.js","sourceRoot":"","sources":["../../../src/core/slop/test-debt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { buildTestIndex, describeTarget, formatTestTargets, hasTestFile, rankTestTargets, syntheticTargetDiff, } from "./test-debt.js";
4
+ test("buildTestIndex maps every test naming convention to its module stem", () => {
5
+ const index = buildTestIndex([
6
+ "src/foo.test.ts",
7
+ "lib/wallet_test.go",
8
+ "app/test_parser.py",
9
+ "src/FooTest.java",
10
+ "src/view.spec.tsx",
11
+ "src/bar.ts",
12
+ ]);
13
+ assert.deepEqual([...index].sort(), ["app/parser", "lib/wallet", "src/foo", "src/view"]);
14
+ assert.equal(hasTestFile("src/foo.ts", index), true);
15
+ assert.equal(hasTestFile("src/view.tsx", index), true);
16
+ assert.equal(hasTestFile("lib/wallet.go", index), true);
17
+ assert.equal(hasTestFile("app/parser.py", index), true);
18
+ assert.equal(hasTestFile("src/bar.ts", index), false);
19
+ });
20
+ test("hasTestFile matches a test that sits at the repository root", () => {
21
+ const index = buildTestIndex(["foo.test.ts"]);
22
+ assert.equal(hasTestFile("src/foo.ts", index), true);
23
+ });
24
+ test("rankTestTargets prefers churn and duplicate signals over file size", () => {
25
+ const targets = rankTestTargets([
26
+ { path: "src/wallet.ts", text: "line\n".repeat(10) },
27
+ { path: "src/big.ts", text: "line\n".repeat(400) },
28
+ { path: "src/small.ts", text: "line\n".repeat(20) },
29
+ ], {
30
+ churn: [{ file: "src/wallet.ts", commits: 7, days: 30, testCommits: 0 }],
31
+ duplicates: [
32
+ {
33
+ first: { file: "src/wallet.ts", line: 1 },
34
+ second: { file: "src/big.ts", line: 1 },
35
+ tokens: 400,
36
+ },
37
+ ],
38
+ todos: [{ file: "src/wallet.ts", line: 2, text: "TODO: audit" }],
39
+ }, new Set());
40
+ assert.deepEqual(targets.map((target) => target.file), ["src/wallet.ts", "src/big.ts", "src/small.ts"]);
41
+ const [wallet, big, small] = targets;
42
+ assert.equal(wallet.score, 14.8);
43
+ assert.equal(wallet.lines, 10);
44
+ assert.deepEqual(wallet.reasons, [
45
+ "churn: 7 commits over 30 days, test files changed 0 times",
46
+ "duplicates: 1 blocks (~400 tokens)",
47
+ "todos: 1",
48
+ ]);
49
+ assert.equal(big.score, 3);
50
+ assert.equal(small.score, 0.1);
51
+ assert.match(small.reasons[0], /no entropy signal yet: untested file, 20 lines/);
52
+ });
53
+ test("rankTestTargets skips tested, generated, empty and oversized files", () => {
54
+ const targets = rankTestTargets([
55
+ { path: "src/covered.ts", text: "code\n" },
56
+ { path: "src/thing.test.ts", text: "test\n" },
57
+ { path: "dist/bundle.js", text: "built\n" },
58
+ { path: "src/empty.ts", text: " \n" },
59
+ { path: "src/huge.ts", text: "x".repeat(25_000) },
60
+ { path: "src/kept.ts", text: "code\n" },
61
+ ], { churn: [], duplicates: [], todos: [] }, buildTestIndex(["src/covered.test.ts"]));
62
+ assert.deepEqual(targets.map((target) => target.file), ["src/kept.ts"]);
63
+ });
64
+ test("rankTestTargets honours the limit and sorts ties by lines then path", () => {
65
+ const targets = rankTestTargets([
66
+ { path: "src/b.ts", text: "line\n".repeat(10) },
67
+ { path: "src/a.ts", text: "line\n".repeat(10) },
68
+ { path: "src/c.ts", text: "line\n".repeat(50) },
69
+ ], { churn: [], duplicates: [], todos: [] }, new Set(), { limit: 2 });
70
+ assert.deepEqual(targets.map((target) => target.file), ["src/c.ts", "src/a.ts"]);
71
+ });
72
+ test("syntheticTargetDiff renders each target as a new file block", () => {
73
+ const diff = syntheticTargetDiff([
74
+ { path: "src/a.ts", text: "const a = 1;\n\n" },
75
+ { path: "src/b.ts", text: "one\ntwo" },
76
+ ]);
77
+ assert.equal(diff, [
78
+ "diff --git a/src/a.ts b/src/a.ts",
79
+ "new file mode 100644",
80
+ "--- /dev/null",
81
+ "+++ b/src/a.ts",
82
+ "@@ -0,0 +1,1 @@",
83
+ "+const a = 1;",
84
+ "diff --git a/src/b.ts b/src/b.ts",
85
+ "new file mode 100644",
86
+ "--- /dev/null",
87
+ "+++ b/src/b.ts",
88
+ "@@ -0,0 +1,2 @@",
89
+ "+one",
90
+ "+two",
91
+ "",
92
+ ].join("\n"));
93
+ assert.equal(syntheticTargetDiff([{ path: "src/blank.ts", text: "\n\n" }]), "");
94
+ });
95
+ test("formatTestTargets lists the reasons of every target", () => {
96
+ const targets = [
97
+ { file: "src/wallet.ts", lines: 10, score: 11, reasons: ["churn: 7 commits over 30 days, test files changed 0 times"] },
98
+ { file: "src/api.ts", lines: 40, score: 2, reasons: ["no entropy signal yet: untested file, 40 lines"] },
99
+ ];
100
+ assert.equal(formatTestTargets(targets), [
101
+ "TEST_DEBT_TARGETS: untested files chosen by the slop scan (churn, duplicates, TODOs). Write tests for these files:",
102
+ "- src/wallet.ts churn: 7 commits over 30 days, test files changed 0 times",
103
+ "- src/api.ts no entropy signal yet: untested file, 40 lines",
104
+ ].join("\n"));
105
+ assert.equal(formatTestTargets([]), "");
106
+ });
107
+ test("describeTarget explains the strongest signal in Russian", () => {
108
+ const base = { file: "x.ts", lines: 12, score: 1, reasons: [] };
109
+ assert.equal(describeTarget({ ...base, commits: 7, testCommits: 0 }), "7 коммитов / 0 с тестами");
110
+ assert.equal(describeTarget({ ...base, duplicateBlocks: 3 }), "3 дублей");
111
+ assert.equal(describeTarget({ ...base, todos: 2 }), "2 TODO");
112
+ assert.equal(describeTarget(base), "12 строк без тестов");
113
+ });
114
+ //# sourceMappingURL=test-debt.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-debt.test.js","sourceRoot":"","sources":["../../../src/core/slop/test-debt.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ import { extname } from "node:path";
2
+ import { isCodeFile, isGeneratedPath, isTestFile } from "./source.js";
3
+ const MARKER = /\b(TODO|FIXME|HACK|XXX)\b(?!\s*\|)(?:\s*\(([^)]{0,40})\))?\s*[:\-–—]?\s*(.{0,160})/;
4
+ const MAX_ITEMS = 2_000;
5
+ const STRING_LITERAL = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g;
6
+ /** A JSDoc block line: it explains a marker rather than tracking real work. */
7
+ const DOC_COMMENT = /^\s*(?:\/\*\*|\*)/;
8
+ /**
9
+ * Markers inside string literals are prose, help text or a regex, not a note
10
+ * left for a human. Blank the literals out before matching.
11
+ */
12
+ function withoutStrings(line) {
13
+ return line.replace(STRING_LITERAL, " ");
14
+ }
15
+ /**
16
+ * Template literals span lines, so a help text or a regex can smuggle the word
17
+ * TODO into the report. Mark every line that sits inside one.
18
+ */
19
+ function insideTemplate(lines) {
20
+ const inside = new Array(lines.length).fill(false);
21
+ let open = false;
22
+ for (let index = 0; index < lines.length; index += 1) {
23
+ if (open)
24
+ inside[index] = true;
25
+ const ticks = lines[index].replace(/\\`/g, "").match(/`/g)?.length ?? 0;
26
+ if (ticks % 2 === 1)
27
+ open = !open;
28
+ }
29
+ return inside;
30
+ }
31
+ /** Languages where a hash starts a comment instead of a private field. */
32
+ const HASH_COMMENT = new Set([".py", ".rb", ".sh", ".ps1", ".lua", ".ex", ".exs", ".pl", ".r", ".yml", ".yaml", ".toml"]);
33
+ /**
34
+ * Returns the comment part of a line. A marker only counts inside a comment:
35
+ * strings, regexes and code identifiers are not notes for a human.
36
+ */
37
+ function commentOf(line, hashComments, state) {
38
+ let start = -1;
39
+ if (state.block) {
40
+ start = 0;
41
+ }
42
+ else {
43
+ for (const opener of ["//", "/*"]) {
44
+ const at = line.indexOf(opener);
45
+ if (at >= 0 && (start < 0 || at < start))
46
+ start = at;
47
+ }
48
+ if (hashComments) {
49
+ const at = line.indexOf("#");
50
+ if (at >= 0 && (start < 0 || at < start))
51
+ start = at;
52
+ }
53
+ }
54
+ if (start < 0)
55
+ return "";
56
+ const close = line.indexOf("*/", start);
57
+ const text = line.slice(start, state.block && close >= 0 ? close + 2 : line.length);
58
+ if (state.block) {
59
+ if (close >= 0)
60
+ state.block = false;
61
+ }
62
+ else {
63
+ const open = line.indexOf("/*", start);
64
+ if (open >= 0 && line.indexOf("*/", open) < 0)
65
+ state.block = true;
66
+ }
67
+ return text;
68
+ }
69
+ export function isOldTodo(item, thresholdDays = 90) {
70
+ return item.ageDays !== undefined && item.ageDays >= thresholdDays;
71
+ }
72
+ /**
73
+ * Collects TODO-like markers with their age. Markers in tests and generated
74
+ * files are skipped: fixtures are full of intentional TODOs.
75
+ */
76
+ export function findTodos(files, options = {}) {
77
+ const items = [];
78
+ const maxItems = options.maxItems ?? MAX_ITEMS;
79
+ for (const file of files) {
80
+ const path = file.path.replace(/\\/g, "/");
81
+ if (!isCodeFile(path) || isGeneratedPath(path) || isTestFile(path))
82
+ continue;
83
+ const lines = file.text.split("\n");
84
+ const quoted = insideTemplate(lines);
85
+ const hashComments = HASH_COMMENT.has(extname(path).toLowerCase());
86
+ const state = { block: false };
87
+ for (let index = 0; index < lines.length; index += 1) {
88
+ if (quoted[index])
89
+ continue;
90
+ const comment = commentOf(lines[index], hashComments, state);
91
+ if (!comment)
92
+ continue;
93
+ // Documentation blocks describe a marker instead of tracking work.
94
+ if (DOC_COMMENT.test(comment))
95
+ continue;
96
+ const match = MARKER.exec(withoutStrings(comment));
97
+ if (!match)
98
+ continue;
99
+ const owner = match[2]?.trim();
100
+ const note = (match[3] ?? "").trim().replace(/\*\/\s*$/, "");
101
+ const ageDays = options.lineAge?.(path, index + 1);
102
+ items.push({
103
+ file: path,
104
+ line: index + 1,
105
+ text: `${match[1]}${owner ? `(${owner})` : ""}${note ? `: ${note}` : ""}`.slice(0, 180),
106
+ ...(ageDays !== undefined ? { ageDays } : {}),
107
+ });
108
+ if (items.length >= maxItems)
109
+ break;
110
+ }
111
+ if (items.length >= maxItems)
112
+ break;
113
+ }
114
+ items.sort((x, y) => (y.ageDays ?? -1) - (x.ageDays ?? -1) || x.file.localeCompare(y.file));
115
+ return items;
116
+ }
117
+ //# sourceMappingURL=todos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todos.js","sourceRoot":"","sources":["../../../src/core/slop/todos.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,214 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { PilotError } from "./errors.js";
4
+ import { getRepoRoot, runGit } from "./git.js";
5
+ export const SNAPSHOT_VERSION = 1;
6
+ const MAX_UNTRACKED_BYTES = 8 * 1024 * 1024;
7
+ function gitDir(repoRoot) {
8
+ const result = runGit(["rev-parse", "--git-dir"], repoRoot);
9
+ if (result.status !== 0) {
10
+ throw new PilotError("Не удалось определить git-dir репозитория", { hint: result.stderr });
11
+ }
12
+ const value = result.stdout.trim();
13
+ return isAbsoluteLike(value) ? value : resolve(repoRoot, value);
14
+ }
15
+ function isAbsoluteLike(value) {
16
+ return /^(?:[A-Za-z]:[\\/]|\/|\\\\)/.test(value);
17
+ }
18
+ function hasHead(repoRoot) {
19
+ return runGit(["rev-parse", "--verify", "HEAD"], repoRoot).status === 0;
20
+ }
21
+ function snapshotsRoot(repoRoot) {
22
+ return join(gitDir(repoRoot), "oc-pilot", "snapshots");
23
+ }
24
+ function newSnapshotId() {
25
+ const now = new Date();
26
+ const pad = (value) => String(value).padStart(2, "0");
27
+ const stamp = [
28
+ now.getFullYear(),
29
+ pad(now.getMonth() + 1),
30
+ pad(now.getDate()),
31
+ ].join("") + "-" + [pad(now.getHours()), pad(now.getMinutes()), pad(now.getSeconds())].join("");
32
+ const random = Math.random().toString(16).slice(2, 6);
33
+ return `${stamp}-${random}`;
34
+ }
35
+ /**
36
+ * Creates a local working-tree snapshot under .git/oc-pilot/snapshots/<id>/:
37
+ * a binary patch of tracked changes versus HEAD plus copies of untracked files.
38
+ * Nothing is committed and nothing leaves the repository.
39
+ */
40
+ export function createSnapshot(cwd) {
41
+ const repoRoot = getRepoRoot(cwd);
42
+ if (!hasHead(repoRoot)) {
43
+ throw new PilotError("Снимок требует хотя бы одного коммита (HEAD отсутствует)", {
44
+ hint: "Сделайте первый коммит или запускайте fix без --apply.",
45
+ exitCode: 2,
46
+ });
47
+ }
48
+ const head = runGit(["rev-parse", "HEAD"], repoRoot).stdout.trim();
49
+ const patchResult = runGit(["diff", "--binary", "HEAD"], repoRoot);
50
+ if (patchResult.status !== 0) {
51
+ throw new PilotError("Не удалось снять diff рабочего дерева", { hint: patchResult.stderr });
52
+ }
53
+ const trackedPatch = patchResult.stdout;
54
+ const list = runGit(["ls-files", "--others", "--exclude-standard", "--full-name", "-z"], repoRoot);
55
+ if (list.status !== 0) {
56
+ throw new PilotError("Не удалось получить список untracked-файлов", { hint: list.stderr });
57
+ }
58
+ const untracked = list.stdout.split("\0").filter(Boolean).map((file) => file.replace(/\\/g, "/"));
59
+ const id = newSnapshotId();
60
+ const dir = join(snapshotsRoot(repoRoot), id);
61
+ const untrackedDir = join(dir, "untracked");
62
+ mkdirSync(untrackedDir, { recursive: true });
63
+ writeFileSync(join(dir, "tracked.patch"), trackedPatch, "utf8");
64
+ let copied = 0;
65
+ for (const file of untracked) {
66
+ const absolute = join(repoRoot, file);
67
+ if (!existsSync(absolute) || !statSync(absolute).isFile())
68
+ continue;
69
+ if (statSync(absolute).size > MAX_UNTRACKED_BYTES)
70
+ continue;
71
+ const target = join(untrackedDir, file);
72
+ mkdirSync(dirname(target), { recursive: true });
73
+ copyFileSync(absolute, target);
74
+ copied += 1;
75
+ }
76
+ const meta = {
77
+ version: SNAPSHOT_VERSION,
78
+ id,
79
+ createdAt: new Date().toISOString(),
80
+ head,
81
+ repoRoot,
82
+ untracked,
83
+ };
84
+ writeFileSync(join(dir, "meta.json"), JSON.stringify(meta, null, 2), "utf8");
85
+ return {
86
+ id,
87
+ path: dir,
88
+ head,
89
+ createdAt: meta.createdAt,
90
+ untrackedCount: copied,
91
+ tracked: trackedPatch.trim().length > 0,
92
+ };
93
+ }
94
+ function readMeta(dir) {
95
+ const metaPath = join(dir, "meta.json");
96
+ if (!existsSync(metaPath)) {
97
+ throw new PilotError(`Повреждённый снимок: нет meta.json (${dir})`);
98
+ }
99
+ try {
100
+ return JSON.parse(readFileSync(metaPath, "utf8"));
101
+ }
102
+ catch {
103
+ throw new PilotError(`Не удалось прочитать meta.json снимка (${dir})`);
104
+ }
105
+ }
106
+ export function listSnapshots(cwd) {
107
+ const repoRoot = getRepoRoot(cwd);
108
+ const root = snapshotsRoot(repoRoot);
109
+ if (!existsSync(root))
110
+ return [];
111
+ const entries = readdirSync(root, { withFileTypes: true })
112
+ .filter((entry) => entry.isDirectory())
113
+ .map((entry) => entry.name)
114
+ .sort();
115
+ const snapshots = [];
116
+ for (const id of entries) {
117
+ const dir = join(root, id);
118
+ try {
119
+ const meta = readMeta(dir);
120
+ snapshots.push({
121
+ id: meta.id,
122
+ path: dir,
123
+ head: meta.head,
124
+ createdAt: meta.createdAt,
125
+ untrackedCount: meta.untracked.length,
126
+ tracked: existsSync(join(dir, "tracked.patch")),
127
+ });
128
+ }
129
+ catch {
130
+ // Skip damaged snapshot directories when listing.
131
+ }
132
+ }
133
+ return snapshots;
134
+ }
135
+ /**
136
+ * Restores the working tree from a snapshot: resets tracked files to the
137
+ * snapshotted state (HEAD + saved binary patch), then copies back saved
138
+ * untracked files. The staged/unstaged split is not preserved. Files created
139
+ * after the snapshot are never deleted.
140
+ */
141
+ export function restoreSnapshot(cwd, id) {
142
+ const repoRoot = getRepoRoot(cwd);
143
+ const root = snapshotsRoot(repoRoot);
144
+ if (!existsSync(root)) {
145
+ throw new PilotError("Снимков нет", { hint: "Снимок создаётся автоматически при oc-pilot fix --apply." });
146
+ }
147
+ let dir;
148
+ if (id) {
149
+ dir = join(root, id);
150
+ if (!existsSync(dir)) {
151
+ throw new PilotError(`Снимок не найден: ${id}`, {
152
+ hint: "Список снимков: oc-pilot restore --list",
153
+ exitCode: 2,
154
+ });
155
+ }
156
+ }
157
+ else {
158
+ const snapshots = listSnapshots(cwd);
159
+ if (snapshots.length === 0) {
160
+ throw new PilotError("Снимков нет", { hint: "Снимок создаётся автоматически при oc-pilot fix --apply." });
161
+ }
162
+ dir = snapshots[snapshots.length - 1].path;
163
+ }
164
+ const meta = readMeta(dir);
165
+ const currentHead = runGit(["rev-parse", "HEAD"], repoRoot).stdout.trim();
166
+ if (currentHead !== meta.head) {
167
+ throw new PilotError("HEAD сместился после снимка; восстановление небезопасно", {
168
+ hint: "Закоммитьте или сделайте stash текущих изменений, затем повторите.",
169
+ exitCode: 2,
170
+ });
171
+ }
172
+ const reset = runGit(["reset", "--hard", "HEAD"], repoRoot);
173
+ if (reset.status !== 0) {
174
+ throw new PilotError("git reset --hard завершился с ошибкой", { hint: reset.stderr });
175
+ }
176
+ const patchPath = join(dir, "tracked.patch");
177
+ const patch = existsSync(patchPath) ? readFileSync(patchPath, "utf8") : "";
178
+ if (patch.trim()) {
179
+ const applied = runGit(["apply", "--binary", "--whitespace=nowarn"], repoRoot, patch);
180
+ if (applied.status !== 0) {
181
+ throw new PilotError("Не удалось применить снимок к рабочему дереву", {
182
+ hint: applied.stderr || "Рабочее дерево сброшено к HEAD; снимок сохранён.",
183
+ });
184
+ }
185
+ }
186
+ let restoredUntracked = 0;
187
+ for (const file of meta.untracked) {
188
+ const source = join(dir, "untracked", file);
189
+ if (!existsSync(source))
190
+ continue;
191
+ const target = join(repoRoot, file);
192
+ mkdirSync(dirname(target), { recursive: true });
193
+ copyFileSync(source, target);
194
+ restoredUntracked += 1;
195
+ }
196
+ return {
197
+ id: meta.id,
198
+ path: dir,
199
+ head: meta.head,
200
+ createdAt: meta.createdAt,
201
+ untrackedCount: restoredUntracked,
202
+ tracked: patch.trim().length > 0,
203
+ };
204
+ }
205
+ /** Removes snapshots older than keepLatest. Returns the number removed. */
206
+ export function pruneSnapshots(cwd, keepLatest) {
207
+ const snapshots = listSnapshots(cwd);
208
+ const removable = snapshots.slice(0, Math.max(0, snapshots.length - keepLatest));
209
+ for (const snapshot of removable) {
210
+ rmSync(snapshot.path, { recursive: true, force: true });
211
+ }
212
+ return removable.length;
213
+ }
214
+ //# sourceMappingURL=snapshot.js.map