pipcode 0.0.0-stage → 2.1.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 (401) hide show
  1. package/LICENSE +47 -0
  2. package/README.md +44 -2
  3. package/auto.js +40 -0
  4. package/bin/setup.js +31 -0
  5. package/bin/teardown.js +18 -0
  6. package/grammars/tree-sitter-javascript.wasm +0 -0
  7. package/grammars/tree-sitter-python.wasm +0 -0
  8. package/grammars/tree-sitter-tsx.wasm +0 -0
  9. package/grammars/tree-sitter-typescript.wasm +0 -0
  10. package/index.js +39 -0
  11. package/package.json +45 -4
  12. package/src/auth/auth-email.js +32 -0
  13. package/src/auth/trusted-key.js +13 -0
  14. package/src/auto/adjust.js +41 -0
  15. package/src/auto/amnesia.js +37 -0
  16. package/src/auto/anonymize-tasks.js +13 -0
  17. package/src/auto/apply-diff-backed.js +10 -0
  18. package/src/auto/apply-diff.js +60 -0
  19. package/src/auto/ask-guard.js +16 -0
  20. package/src/auto/ask-pause.js +36 -0
  21. package/src/auto/ask-protocol.js +6 -0
  22. package/src/auto/ask-user.js +29 -0
  23. package/src/auto/backup-touched.js +30 -0
  24. package/src/auto/budget.js +52 -0
  25. package/src/auto/cancel.js +20 -0
  26. package/src/auto/constants.js +30 -0
  27. package/src/auto/crypto-util.js +17 -0
  28. package/src/auto/decompose.js +47 -0
  29. package/src/auto/diff.js +19 -0
  30. package/src/auto/fs-list.js +34 -0
  31. package/src/auto/git-base.js +16 -0
  32. package/src/auto/git-init.js +39 -0
  33. package/src/auto/git-restore.js +34 -0
  34. package/src/auto/git-scope.js +10 -0
  35. package/src/auto/git.js +38 -0
  36. package/src/auto/mission-docs.js +29 -0
  37. package/src/auto/mission-line.js +22 -0
  38. package/src/auto/node-check.js +30 -0
  39. package/src/auto/observe.js +23 -0
  40. package/src/auto/orchestrator.js +30 -0
  41. package/src/auto/patch-ask.js +16 -0
  42. package/src/auto/paths.js +18 -0
  43. package/src/auto/plan.js +42 -0
  44. package/src/auto/pricing.js +49 -0
  45. package/src/auto/project-files.js +12 -0
  46. package/src/auto/prompts.js +24 -0
  47. package/src/auto/quote-format.js +33 -0
  48. package/src/auto/retry.js +40 -0
  49. package/src/auto/run-lock.js +40 -0
  50. package/src/auto/run-loop.js +38 -0
  51. package/src/auto/run.js +49 -0
  52. package/src/auto/snapshot-gate.js +51 -0
  53. package/src/auto/snapshot-paths.js +33 -0
  54. package/src/auto/snapshot-restore.js +59 -0
  55. package/src/auto/snapshot-take.js +30 -0
  56. package/src/auto/state.js +52 -0
  57. package/src/auto/step-commit.js +46 -0
  58. package/src/auto/step-green.js +38 -0
  59. package/src/auto/step-loop.js +56 -0
  60. package/src/auto/step-prep.js +60 -0
  61. package/src/auto/step-qa.js +53 -0
  62. package/src/auto/step.js +30 -0
  63. package/src/auto/stop-options.js +34 -0
  64. package/src/auto/stop-report.js +17 -0
  65. package/src/auto/tasks.js +44 -0
  66. package/src/cli/apply-loop.js +29 -0
  67. package/src/cli/apply-toml.js +26 -0
  68. package/src/cli/apply.js +45 -0
  69. package/src/cli/ask-prepare.js +37 -0
  70. package/src/cli/auth-branches.js +53 -0
  71. package/src/cli/auth-interactive.js +46 -0
  72. package/src/cli/auto-ask.js +33 -0
  73. package/src/cli/auto-audit.js +16 -0
  74. package/src/cli/auto-cli.js +46 -0
  75. package/src/cli/auto-consent.js +22 -0
  76. package/src/cli/auto-flags.js +39 -0
  77. package/src/cli/auto-gate-session.js +27 -0
  78. package/src/cli/auto-gate.js +34 -0
  79. package/src/cli/auto-loader.js +17 -0
  80. package/src/cli/auto-plan.js +36 -0
  81. package/src/cli/auto-preflight.js +35 -0
  82. package/src/cli/auto-quote.js +28 -0
  83. package/src/cli/auto-report.js +36 -0
  84. package/src/cli/auto-resume.js +28 -0
  85. package/src/cli/auto-rules.js +20 -0
  86. package/src/cli/auto-run.js +26 -0
  87. package/src/cli/auto-task.js +26 -0
  88. package/src/cli/banner.js +20 -0
  89. package/src/cli/centralize.js +23 -0
  90. package/src/cli/collect-step.js +25 -0
  91. package/src/cli/config-pairs.js +46 -0
  92. package/src/cli/config.js +51 -0
  93. package/src/cli/detect-os.js +11 -0
  94. package/src/cli/dispatch.js +36 -0
  95. package/src/cli/env-file.js +44 -0
  96. package/src/cli/env-load.js +28 -0
  97. package/src/cli/env-parse.js +39 -0
  98. package/src/cli/env-rebuild.js +33 -0
  99. package/src/cli/gitignore-env.js +46 -0
  100. package/src/cli/help.js +28 -0
  101. package/src/cli/ide-entries.js +42 -0
  102. package/src/cli/ide-paths.js +37 -0
  103. package/src/cli/ide-roots.js +25 -0
  104. package/src/cli/interactive.js +26 -0
  105. package/src/cli/jetbrains.js +40 -0
  106. package/src/cli/legacy-entry.js +17 -0
  107. package/src/cli/main.js +42 -0
  108. package/src/cli/memory-cli.js +15 -0
  109. package/src/cli/prompts.js +24 -0
  110. package/src/cli/server-entry.js +42 -0
  111. package/src/cli/setup-args.js +39 -0
  112. package/src/cli/target-manual.js +24 -0
  113. package/src/cli/target-select.js +51 -0
  114. package/src/cli/toml-merge.js +56 -0
  115. package/src/cli/trial-check.js +15 -0
  116. package/src/cli/welcome.js +7 -0
  117. package/src/core/abort-signal.js +38 -0
  118. package/src/core/auto-deps.js +29 -0
  119. package/src/core/cache-ttl.js +21 -0
  120. package/src/core/config-base.js +38 -0
  121. package/src/core/config-cache.js +50 -0
  122. package/src/core/config.js +58 -0
  123. package/src/core/constants.js +31 -0
  124. package/src/core/crypto.js +45 -0
  125. package/src/core/dlp.js +51 -0
  126. package/src/core/doctrine-all.js +31 -0
  127. package/src/core/doctrine-caches.js +10 -0
  128. package/src/core/doctrine-fetch.js +49 -0
  129. package/src/core/doctrine-one.js +18 -0
  130. package/src/core/doctrine.js +39 -0
  131. package/src/core/dual-stream-parser.js +55 -0
  132. package/src/core/exports.js +28 -0
  133. package/src/core/google-route.js +17 -0
  134. package/src/core/identity.js +23 -0
  135. package/src/core/lang.js +19 -0
  136. package/src/core/leak-guard.js +40 -0
  137. package/src/core/llm-builder-anthropic.js +26 -0
  138. package/src/core/llm-builder-gemini.js +22 -0
  139. package/src/core/llm-builder-openai.js +26 -0
  140. package/src/core/llm-call.js +53 -0
  141. package/src/core/llm-config-secondary.js +28 -0
  142. package/src/core/llm-config.js +32 -0
  143. package/src/core/llm-errors.js +25 -0
  144. package/src/core/llm-exchange.js +27 -0
  145. package/src/core/llm-extract.js +52 -0
  146. package/src/core/llm-label.js +33 -0
  147. package/src/core/llm-ollama-request.js +32 -0
  148. package/src/core/llm-ollama.js +42 -0
  149. package/src/core/llm-prompts.js +14 -0
  150. package/src/core/llm-providers.js +26 -0
  151. package/src/core/llm-request.js +17 -0
  152. package/src/core/llm-retry-429.js +23 -0
  153. package/src/core/llm-stream-events.js +29 -0
  154. package/src/core/llm-stream.js +42 -0
  155. package/src/core/llm-unbounded.js +15 -0
  156. package/src/core/llm-warmup.js +44 -0
  157. package/src/core/manifest.js +42 -0
  158. package/src/core/master-prompt.js +11 -0
  159. package/src/core/model-id.js +19 -0
  160. package/src/core/ollama-lines.js +48 -0
  161. package/src/core/ollama.js +43 -0
  162. package/src/core/pkg-manager.js +35 -0
  163. package/src/core/process-guards.js +47 -0
  164. package/src/core/progress-emit.js +25 -0
  165. package/src/core/purge-key.js +20 -0
  166. package/src/core/reasoning-cut.js +19 -0
  167. package/src/core/reasoning-models.js +8 -0
  168. package/src/core/session-net.js +60 -0
  169. package/src/core/session-resilient.js +44 -0
  170. package/src/core/session.js +49 -0
  171. package/src/core/sse-parser.js +25 -0
  172. package/src/core/stdout-guard.js +11 -0
  173. package/src/core/tier.js +48 -0
  174. package/src/core/trial-resolve.js +22 -0
  175. package/src/core/trial-session.js +60 -0
  176. package/src/core/trial-status.js +35 -0
  177. package/src/core/usage-cost.js +25 -0
  178. package/src/core/usage-extract.js +28 -0
  179. package/src/core/usage-meter.js +41 -0
  180. package/src/engine/analyze-need.js +24 -0
  181. package/src/engine/analyze-ux.js +42 -0
  182. package/src/engine/ask-history.js +43 -0
  183. package/src/engine/ask-llm.js +40 -0
  184. package/src/engine/ask-prompt.js +48 -0
  185. package/src/engine/ask-rules.js +13 -0
  186. package/src/engine/ast-call-rules-danger.js +24 -0
  187. package/src/engine/ast-call-rules-inject.js +29 -0
  188. package/src/engine/ast-context.js +28 -0
  189. package/src/engine/ast-import-helpers.js +46 -0
  190. package/src/engine/ast-lang.js +24 -0
  191. package/src/engine/ast-node-helpers.js +51 -0
  192. package/src/engine/ast-rules-calls.js +16 -0
  193. package/src/engine/ast-rules-graph.js +58 -0
  194. package/src/engine/ast-rules-js.js +30 -0
  195. package/src/engine/ast-rules-jsx-py.js +26 -0
  196. package/src/engine/ast-runtime.js +60 -0
  197. package/src/engine/ast-scan.js +48 -0
  198. package/src/engine/audit-citations.js +28 -0
  199. package/src/engine/audit-facts.js +47 -0
  200. package/src/engine/audit-report.js +40 -0
  201. package/src/engine/audit-task.js +37 -0
  202. package/src/engine/cap-cache.js +50 -0
  203. package/src/engine/cap-eval.js +49 -0
  204. package/src/engine/cap-http.js +32 -0
  205. package/src/engine/cap-meta-anthropic.js +15 -0
  206. package/src/engine/cap-meta-ollama.js +15 -0
  207. package/src/engine/cap-meta-openrouter.js +19 -0
  208. package/src/engine/cap-meta.js +19 -0
  209. package/src/engine/cap-probe.js +34 -0
  210. package/src/engine/cap-types.js +29 -0
  211. package/src/engine/chat-doctrine.js +31 -0
  212. package/src/engine/checkpoint-control.js +23 -0
  213. package/src/engine/checkpoint-detect.js +19 -0
  214. package/src/engine/checkpoint-rules.js +19 -0
  215. package/src/engine/code-decls.js +12 -0
  216. package/src/engine/code-graph.js +52 -0
  217. package/src/engine/coerce.js +8 -0
  218. package/src/engine/collect.js +21 -0
  219. package/src/engine/compute-impact.js +17 -0
  220. package/src/engine/context-files.js +42 -0
  221. package/src/engine/context-fit.js +28 -0
  222. package/src/engine/context-margin.js +37 -0
  223. package/src/engine/context-relay.js +60 -0
  224. package/src/engine/control-duo.js +38 -0
  225. package/src/engine/control-phase.js +32 -0
  226. package/src/engine/control-prompt.js +25 -0
  227. package/src/engine/control-verdict.js +36 -0
  228. package/src/engine/default-steps.js +7 -0
  229. package/src/engine/deps-parse-js.js +49 -0
  230. package/src/engine/deps-parse-more.js +30 -0
  231. package/src/engine/deps-parse-other.js +41 -0
  232. package/src/engine/deps-parse.js +31 -0
  233. package/src/engine/det-builders-1.js +38 -0
  234. package/src/engine/det-builders-2.js +50 -0
  235. package/src/engine/detect-domains.js +27 -0
  236. package/src/engine/diff-files.js +35 -0
  237. package/src/engine/doc-suite.js +39 -0
  238. package/src/engine/doctrine-prefix.js +13 -0
  239. package/src/engine/domain-regex.js +11 -0
  240. package/src/engine/exports.js +35 -0
  241. package/src/engine/extract-deterministic.js +33 -0
  242. package/src/engine/extract-llm.js +48 -0
  243. package/src/engine/facts-binary.js +12 -0
  244. package/src/engine/facts-embedded.js +18 -0
  245. package/src/engine/facts-functions.js +21 -0
  246. package/src/engine/facts-html.js +22 -0
  247. package/src/engine/facts-inventory.js +33 -0
  248. package/src/engine/facts-lang.js +34 -0
  249. package/src/engine/facts-measure.js +43 -0
  250. package/src/engine/facts-patterns.js +28 -0
  251. package/src/engine/facts-project.js +26 -0
  252. package/src/engine/facts-size.js +26 -0
  253. package/src/engine/facts-wordpress.js +36 -0
  254. package/src/engine/generate-architecture-guide.js +46 -0
  255. package/src/engine/generate-readme.js +38 -0
  256. package/src/engine/generate-runbook.js +47 -0
  257. package/src/engine/git-facts.js +16 -0
  258. package/src/engine/history-purge.js +16 -0
  259. package/src/engine/json-balance.js +37 -0
  260. package/src/engine/json-safe.js +13 -0
  261. package/src/engine/line-numbers.js +16 -0
  262. package/src/engine/md-build-mid.js +35 -0
  263. package/src/engine/md-build-top.js +58 -0
  264. package/src/engine/md-build.js +36 -0
  265. package/src/engine/md-parse-helpers.js +7 -0
  266. package/src/engine/md-parse-pillars.js +38 -0
  267. package/src/engine/md-parse-ux-design.js +31 -0
  268. package/src/engine/md-parse.js +33 -0
  269. package/src/engine/mission-context-prefix.js +30 -0
  270. package/src/engine/mission-guard.js +36 -0
  271. package/src/engine/mission-helpers.js +23 -0
  272. package/src/engine/mission-log.js +56 -0
  273. package/src/engine/mission-paths.js +7 -0
  274. package/src/engine/mission-store.js +51 -0
  275. package/src/engine/normalize-extraction.js +35 -0
  276. package/src/engine/normalize-impact-design.js +39 -0
  277. package/src/engine/normalize-pillars.js +36 -0
  278. package/src/engine/orchestrate-route.js +39 -0
  279. package/src/engine/orchestrator-build.js +37 -0
  280. package/src/engine/orchestrator-phases.js +55 -0
  281. package/src/engine/orchestrator.js +40 -0
  282. package/src/engine/osv.js +43 -0
  283. package/src/engine/parse-json-loose.js +36 -0
  284. package/src/engine/patch-file-sections.js +28 -0
  285. package/src/engine/patch-scan-cheat.js +52 -0
  286. package/src/engine/patch-scan-prep.js +37 -0
  287. package/src/engine/patch-scan-rules.js +51 -0
  288. package/src/engine/patch-scan.js +38 -0
  289. package/src/engine/patch-secret-guard.js +50 -0
  290. package/src/engine/patch-ui-guard.js +49 -0
  291. package/src/engine/pattern-sanitize.js +23 -0
  292. package/src/engine/plan-draft.js +37 -0
  293. package/src/engine/plan-mission.js +60 -0
  294. package/src/engine/plan-reply-legacy.js +34 -0
  295. package/src/engine/plan-reply-pillars.js +34 -0
  296. package/src/engine/plan-reply-ux.js +17 -0
  297. package/src/engine/plan-reply.js +28 -0
  298. package/src/engine/plan-summary.js +16 -0
  299. package/src/engine/preflight-text.js +27 -0
  300. package/src/engine/preflight.js +27 -0
  301. package/src/engine/project-brief.js +41 -0
  302. package/src/engine/project-context.js +49 -0
  303. package/src/engine/project-instructions.js +19 -0
  304. package/src/engine/py-imports.js +31 -0
  305. package/src/engine/relay-analysis.js +44 -0
  306. package/src/engine/relay-digest.js +42 -0
  307. package/src/engine/route-llm.js +25 -0
  308. package/src/engine/rule-text.js +12 -0
  309. package/src/engine/self-assessment.js +33 -0
  310. package/src/engine/steps-block.js +19 -0
  311. package/src/engine/ux-violations-leak.js +43 -0
  312. package/src/engine/vuln-facts.js +37 -0
  313. package/src/fs/atomic-text.js +45 -0
  314. package/src/fs/atomic-write.js +21 -0
  315. package/src/fs/backup-once.js +27 -0
  316. package/src/fs/config-path.js +39 -0
  317. package/src/fs/config-read.js +23 -0
  318. package/src/fs/config-write.js +60 -0
  319. package/src/fs/home-root.js +55 -0
  320. package/src/fs/load-config.js +36 -0
  321. package/src/fs/os-errors.js +26 -0
  322. package/src/fs/permissions.js +48 -0
  323. package/src/mcp/boot-warmup.js +9 -0
  324. package/src/mcp/call-tool.js +20 -0
  325. package/src/mcp/client-info.js +35 -0
  326. package/src/mcp/engine-rules.js +16 -0
  327. package/src/mcp/handle-dlp.js +37 -0
  328. package/src/mcp/handle-licence.js +46 -0
  329. package/src/mcp/handle-list.js +15 -0
  330. package/src/mcp/handle-memory.js +37 -0
  331. package/src/mcp/handle-skill-fetch.js +50 -0
  332. package/src/mcp/handle-skill.js +38 -0
  333. package/src/mcp/licence-text.js +29 -0
  334. package/src/mcp/prompt-licence.js +11 -0
  335. package/src/mcp/server-create.js +33 -0
  336. package/src/mcp/server-main.js +39 -0
  337. package/src/mcp/tool-dispatch.js +28 -0
  338. package/src/mcp/tool-dlp.js +20 -0
  339. package/src/mcp/tool-licence.js +17 -0
  340. package/src/mcp/tool-memory.js +32 -0
  341. package/src/mcp/tool-skill.js +24 -0
  342. package/src/mcp/tools-list.js +13 -0
  343. package/src/memory/exports.js +5 -0
  344. package/src/memory/memory-filter.js +32 -0
  345. package/src/memory/memory-gate.js +21 -0
  346. package/src/memory/memory-store.js +44 -0
  347. package/src/net/auth-browser.js +49 -0
  348. package/src/net/auth-endpoints.js +33 -0
  349. package/src/net/local-fetch.js +31 -0
  350. package/src/net/loopback.js +8 -0
  351. package/src/net/proxy-env.js +59 -0
  352. package/src/project-memory/batches.js +17 -0
  353. package/src/project-memory/cahiers.js +42 -0
  354. package/src/project-memory/decision-settle.js +15 -0
  355. package/src/project-memory/decisions.js +36 -0
  356. package/src/project-memory/index-build.js +35 -0
  357. package/src/project-memory/memory-build.js +16 -0
  358. package/src/project-memory/memory-tags.js +40 -0
  359. package/src/project-memory/pending.js +24 -0
  360. package/src/project-memory/qa-apply.js +39 -0
  361. package/src/project-memory/qa-tags.js +28 -0
  362. package/src/project-memory/recall.js +38 -0
  363. package/src/project-memory/store.js +37 -0
  364. package/src/project-memory/window.js +13 -0
  365. package/src/rules/rules-advisor.js +17 -0
  366. package/src/rules/rules-frugality.js +17 -0
  367. package/src/rules/rules-load.js +56 -0
  368. package/src/rules/rules-project.js +16 -0
  369. package/src/rules/rules-registry.js +32 -0
  370. package/src/rules/rules-validate-pricing.js +22 -0
  371. package/src/rules/rules-validate-scan.js +45 -0
  372. package/src/rules/rules-validate.js +53 -0
  373. package/src/teardown/consent.js +31 -0
  374. package/src/teardown/flags.js +39 -0
  375. package/src/teardown/main.js +41 -0
  376. package/src/teardown/plan.js +31 -0
  377. package/src/teardown/remove-entry.js +39 -0
  378. package/src/teardown/remove-file.js +37 -0
  379. package/src/teardown/remove-toml.js +33 -0
  380. package/src/teardown/report.js +34 -0
  381. package/src/types/ast.d.ts +40 -0
  382. package/src/types/auto-ask.d.ts +6 -0
  383. package/src/types/auto-orch.d.ts +59 -0
  384. package/src/types/auto.d.ts +45 -0
  385. package/src/types/capability.d.ts +12 -0
  386. package/src/types/cli-auto-gate.d.ts +45 -0
  387. package/src/types/cli-auto.d.ts +42 -0
  388. package/src/types/config.d.ts +53 -0
  389. package/src/types/control.d.ts +56 -0
  390. package/src/types/docs.d.ts +6 -0
  391. package/src/types/doctrine.d.ts +22 -0
  392. package/src/types/llm.d.ts +28 -0
  393. package/src/types/mcp.d.ts +32 -0
  394. package/src/types/memory.d.ts +17 -0
  395. package/src/types/mission.d.ts +46 -0
  396. package/src/types/orchestrator.d.ts +33 -0
  397. package/src/types/routing.d.ts +25 -0
  398. package/src/types/rules.d.ts +30 -0
  399. package/src/types/snapshots.d.ts +25 -0
  400. package/src/types/teardown.d.ts +13 -0
  401. package/src/types/usage.d.ts +30 -0
@@ -0,0 +1,39 @@
1
+ // src/engine/orchestrate-route.js — selectPhase : provider à utiliser pour chaque
2
+ // phase d'orchestration (analyse = smart_model, patch = fast_model), selon le
3
+ // routage hybride. Primaire cloud → comportement historique INCHANGÉ. Primaire
4
+ // local (Ollama) + secondaire cloud → bascule conditionnelle (a)/(b)/(c).
5
+
6
+ import { readSecondaryProvider } from "../core/llm-config-secondary.js";
7
+ import { isLocalBase, decideRoute } from "./route-llm.js";
8
+ import { readLocalWindow, localCapacity, readHeavyTokens } from "./context-margin.js";
9
+
10
+ // Le modèle local gère-t-il l'orchestration multi-phase ? (défaut : oui)
11
+ // PLUGCTO_LOCAL_CAN_MULTI_AGENT=false force la bascule cloud (condition a).
12
+ /** @param {Record<string, string | undefined>} [env] @returns {boolean} */
13
+ export function localCanMultiAgent(env = process.env) {
14
+ return String(env.PLUGCTO_LOCAL_CAN_MULTI_AGENT || "").toLowerCase() !== "false";
15
+ }
16
+
17
+ // Sélectionne le provider + les modèles d'une phase. Rétrocompatible.
18
+ /** @param {{ primary: ProviderPrimary }} pcfg @param {"analysis" | "patch"} phase @param {number} promptTokens @returns {{ primary: ProviderPrimary, smartModel: string, fastModel: string, route: RouteDecision["route"], reason: string }} */
19
+ export function selectPhase(pcfg, phase, promptTokens) {
20
+ const primary = pcfg && pcfg.primary;
21
+ if (!primary) {
22
+ const err = /** @type {EnrichedError} */ (new Error("Provider principal manquant."));
23
+ err.code = "no_provider";
24
+ throw err;
25
+ }
26
+ const local = isLocalBase(primary.base) ? primary : null;
27
+ if (!local) return { primary, smartModel: primary.smartModel || primary.model, fastModel: primary.fastModel || primary.model, route: "primary", reason: `${phase}:primary_cloud` };
28
+ const { secondary } = readSecondaryProvider();
29
+ if (!secondary) return { primary, smartModel: primary.smartModel || primary.model, fastModel: primary.fastModel || primary.model, route: "local", reason: `${phase}:local_only` };
30
+ const d = decideRoute({
31
+ local, secondary, promptTokens,
32
+ capacityTokens: localCapacity(readLocalWindow()),
33
+ localMultiAgent: localCanMultiAgent(),
34
+ heavyTokens: readHeavyTokens(),
35
+ });
36
+ const chosen = d.route === "cloud" ? secondary : primary;
37
+ return { primary: chosen, smartModel: chosen.smartModel || chosen.model, fastModel: chosen.fastModel || chosen.model, route: d.route, reason: `${phase}:${d.reason}` };
38
+ }
39
+ // MODULE_COMPLETE
@@ -0,0 +1,37 @@
1
+ // src/engine/orchestrator-build.js — Assemblage du userText d'orchestration (Phase 1 analyse).
2
+ // Concatène contexte fichier + doctrine + lastDiff + correction + code dans le bloc DYNAMIQUE (user).
3
+ // Le system prompt byte-stable reste isolé (prompt caching Anthropic intact).
4
+
5
+ import { ANALYSIS_DOCTRINE, DOCTRINE_REMINDER, cachePrefixOf } from "./doctrine-prefix.js";
6
+
7
+ // Construit le userText à partir des args (contexte + doctrine + correction + code).
8
+ /** @param {OrchestratorArgs} args @returns {string} */
9
+ export function buildOrchestrationUserText(args) {
10
+ const filePath = args.file_path || "";
11
+ const language = args.language || "";
12
+ const domain = args.domain || "";
13
+ const code = args.code || "";
14
+ const doctrine = (args.doctrine || "").trim();
15
+ const lastDiff = (args.lastDiff || "").trim();
16
+ const correction = (args.correction || "").trim();
17
+ const ctx = [
18
+ filePath && `Fichier: ${filePath}`,
19
+ language && `Langage: ${language}`,
20
+ domain && `Domaine: ${domain}`,
21
+ ].filter(Boolean).join("\n");
22
+ const userParts = [];
23
+ if (ctx) userParts.push(ctx);
24
+ if (doctrine) userParts.push("Doctrine (règles absolues à vérifier strictement):\n" + doctrine);
25
+ if (lastDiff) userParts.push("Diff du commit précédent (contexte continu — ne pas reciter):\n" + lastDiff);
26
+ if (correction) userParts.push("Correction requise (Hooks déterministes ont rejeté le patch précédent):\n" + correction);
27
+ userParts.push("Code:\n" + code);
28
+ return userParts.join("\n\n");
29
+ }
30
+ // Même contenu, autre ORDRE : doctrine (+ mission) en préfixe de cache, le reste (fichier, diff, correction, code) après, rappel en dernière ligne.
31
+ /** @param {OrchestratorArgs} args @param {string} missionCtx @returns {{ prefix: string, rest: string }} */
32
+ export function buildOrchestrationSplit(args, missionCtx) {
33
+ const doctrine = (args.doctrine || "").trim();
34
+ const rest = buildOrchestrationUserText({ ...args, doctrine: "" });
35
+ return { prefix: cachePrefixOf(ANALYSIS_DOCTRINE, doctrine, missionCtx), rest: doctrine ? rest + "\n\n" + DOCTRINE_REMINDER : rest };
36
+ }
37
+ // MODULE_COMPLETE
@@ -0,0 +1,55 @@
1
+ // src/engine/orchestrator-phases.js — Sous-agents Phase 1 (analyse) + Phase 2 (patch).
2
+ // Héritage de doctrine : le patch reçoit la même doctrine que l'analyse. Erreurs enrichies typées.
3
+ import { callLLM } from "../core/llm-call.js";
4
+ import { analysisPrompt, patchPrompt } from "../core/llm-prompts.js";
5
+ import { withLanguage } from "../core/lang.js";
6
+ import { patchMaxTokens } from "../core/constants.js";
7
+ import { ANALYSIS_MAX_TOKENS } from "../core/constants.js";
8
+ import { PATCH_DOCTRINE, DOCTRINE_REMINDER, cachePrefixOf } from "./doctrine-prefix.js";
9
+
10
+ // Phase 1 : analyse (provider primaire, smart_model). Jette phase1_failed enrichie.
11
+ /** @param {ProviderPrimary} primary @param {string} smartModel @param {string} userText @param {string} missionCtx @returns {Promise<string>} */
12
+ export async function runAnalysisPhase(primary, smartModel, userText, missionCtx) {
13
+ try {
14
+ return await callLLM({
15
+ provider: primary.provider, key: primary.key,
16
+ model: smartModel, base: primary.base,
17
+ systemPrompt: withLanguage(analysisPrompt()), userText, cachePrefix: missionCtx,
18
+ maxTokens: ANALYSIS_MAX_TOKENS,
19
+ });
20
+ } catch (err) {
21
+ const e0 = /** @type {Error} */ (err);
22
+ const e = /** @type {EnrichedError} */ (new Error(`Échec Phase 1 (analyse, ${primary.provider}) : ${e0.message}`));
23
+ e.code = "phase1_failed";
24
+ e.provider = primary.provider;
25
+ throw e;
26
+ }
27
+ }
28
+
29
+ // Phase 2 : patch (clé unique globale, fast_model). Jette phase2_failed (rend le diagnostic).
30
+ /** @param {ProviderPrimary} primary @param {string} fastModel @param {string} analysis @param {string} code @param {string} doctrine @param {string} correction @param {string} missionCtx @param {string} [askNote] @returns {Promise<string>} */
31
+ export async function runPatchPhase(primary, fastModel, analysis, code, doctrine, correction, missionCtx, askNote = "") {
32
+ const patchParts = [`Diagnostic:\n${analysis}`, `Code:\n${code}`];
33
+ if (correction) patchParts.push("Correction requise:\n" + correction);
34
+ if (askNote) patchParts.push(askNote); // pause ASK_USER proposée au modèle (seulement si l'interface peut répondre)
35
+ if (doctrine.trim()) patchParts.push(DOCTRINE_REMINDER);
36
+ try {
37
+ const patch = await callLLM({
38
+ provider: primary.provider, key: primary.key,
39
+ model: fastModel, base: primary.base,
40
+ systemPrompt: patchPrompt(), cachePrefix: cachePrefixOf(PATCH_DOCTRINE, doctrine, missionCtx), // doctrine + mission en tête : préfixe de cache
41
+
42
+ maxTokens: patchMaxTokens(code.length),
43
+ userText: patchParts.join("\n\n"),
44
+ });
45
+ return patch.trim();
46
+ } catch (err) {
47
+ const e0 = /** @type {Error} */ (err);
48
+ const e = /** @type {EnrichedError} */ (new Error(`Échec Phase 2 (patch, ${primary.provider}) : ${e0.message}`));
49
+ e.code = "phase2_failed";
50
+ e.provider = primary.provider;
51
+ e.analysis = analysis;
52
+ throw e;
53
+ }
54
+ }
55
+ // MODULE_COMPLETE
@@ -0,0 +1,40 @@
1
+ // src/engine/orchestrator.js — orchestrateLLM : orchestration Multi-LLM (BYOK)
2
+ // avec ROUTAGE HYBRIDE par phase. Phase 1 analyse (smart_model) → Phase 2 patch
3
+ // (fast_model) → Phase 3 contrôle/doute (modèle SECONDAIRE, 1 passage borné, si
4
+ // configuré). Retourne { analysis, patch, models, route } + champs duo/control.
5
+ import { buildOrchestrationUserText, buildOrchestrationSplit } from "./orchestrator-build.js";
6
+ import { runPatchPhase } from "./orchestrator-phases.js";
7
+ import { runAnalysisRelayed } from "./relay-analysis.js";
8
+ import { selectPhase } from "./orchestrate-route.js";
9
+ import { estimateTokens } from "./context-margin.js";
10
+ import { runDuoControl } from "./control-duo.js";
11
+ import { patchOrAsk } from "../auto/patch-ask.js";
12
+
13
+ const NO_CONTROL = { duo: false, duoSameProvider: false, patch: "", control: { status: /** @type {const} */ ("skipped"), reason: "question_utilisateur", model: null } };
14
+
15
+ // Orchestre Phase 1 + Phase 2 avec routage hybride local/cloud par phase.
16
+ /** @param {OrchestratorArgs} args @param {{ primary: ProviderPrimary }} pcfg @returns {Promise<OrchestratorResult>} */
17
+ export async function orchestrateLLM(args, pcfg) {
18
+ if (!pcfg || !pcfg.primary) {
19
+ const err = /** @type {EnrichedError} */ (new Error("Provider principal manquant."));
20
+ err.code = "no_provider";
21
+ throw err;
22
+ }
23
+ const missionCtx = args.missionContext || "";
24
+ const doctrine = (args.doctrine || "").trim();
25
+ const correction = (args.correction || "").trim();
26
+ const userText = buildOrchestrationUserText(args);
27
+ const selA = selectPhase(pcfg, "analysis", estimateTokens(userText));
28
+ const analysis = await runAnalysisRelayed(selA.primary, selA.smartModel, userText, missionCtx, undefined, buildOrchestrationSplit(args, missionCtx));
29
+ const patchTokens = estimateTokens(analysis) + estimateTokens(args.code || "") + estimateTokens(doctrine);
30
+ const selP = selectPhase(pcfg, "patch", patchTokens);
31
+ const { patch, asking } = await patchOrAsk((note) => runPatchPhase(selP.primary, selP.fastModel, analysis, args.code || "", doctrine, correction, missionCtx, note), !!args.canAsk, doctrine); // une vraie question n'est pas un patch : ni contrôle du Modèle 2, ni application
32
+ const duo = asking ? NO_CONTROL : await runDuoControl({ ...selP.primary, model: selP.fastModel }, args, analysis, patch, missionCtx, doctrine);
33
+ return {
34
+ analysis, patch: duo.patch,
35
+ models: { analysis: selA.smartModel, patch: selP.fastModel },
36
+ route: { analysis: selA.route, patch: selP.route, reason: `${selA.reason} | ${selP.reason}` },
37
+ duo: duo.duo, duo_same_provider: duo.duoSameProvider, control: duo.control, ...(asking ? { asking } : {}),
38
+ };
39
+ }
40
+ // MODULE_COMPLETE
@@ -0,0 +1,43 @@
1
+ // src/engine/osv.js — Interrogation de la base publique OSV.dev (Google, gratuite, sans clé) : failles connues par dépendance. SEULS le nom, l'écosystème et la version des dépendances sont envoyés — jamais le code.
2
+ // Toute panne (réseau, quota, réponse inattendue) lève une erreur typée : l'appelant dit « non vérifié », jamais « aucune faille ».
3
+ import { asArr, asObj } from "./json-safe.js";
4
+
5
+ const API = "https://api.osv.dev/v1";
6
+ const BATCH = 500;
7
+ const MAX_DETAILS = 40;
8
+ const TIMEOUT_MS = 15_000;
9
+ export const SEVERITY = /** @type {Record<string, string>} */ ({ CRITICAL: "critique", HIGH: "élevée", MODERATE: "moyenne", MEDIUM: "moyenne", LOW: "faible" });
10
+
11
+ /** @typedef {{ ecosystem: string, name: string, version: string, pinned: boolean, priv?: boolean }} Dep */
12
+ /** @typedef {{ id: string, cve: string, severity: string, fixed: string }} VulnInfo */
13
+
14
+ /** @param {typeof fetch} f @param {string} url @param {RequestInit} [init] @returns {Promise<Record<string, unknown>>} */
15
+ async function call(f, url, init) {
16
+ const r = await f(url, { ...init, signal: AbortSignal.timeout(TIMEOUT_MS) });
17
+ if (!r.ok) throw new Error(`OSV HTTP ${r.status}`);
18
+ return asObj(await r.json());
19
+ }
20
+
21
+ /** @param {typeof fetch} f @param {string} id @returns {Promise<VulnInfo>} */
22
+ async function detail(f, id) {
23
+ const v = await call(f, `${API}/vulns/${encodeURIComponent(id)}`);
24
+ const sev = String(asObj(v.database_specific).severity ?? "").toUpperCase();
25
+ const fixed = asArr(v.affected).flatMap((a) => asArr(asObj(a).ranges)).flatMap((r) => asArr(asObj(r).events)).map((e) => asObj(e).fixed).find((x) => typeof x === "string" && /^\d[\w.+-]{0,19}$/.test(x)); // une version (jamais un code de commit)
26
+ return { id, cve: String(asArr(v.aliases).find((a) => /^CVE-/.test(String(a))) ?? ""), severity: SEVERITY[sev] ?? "inconnue", fixed: typeof fixed === "string" ? fixed : "" };
27
+ }
28
+
29
+ /** @param {Dep[]} deps @param {typeof fetch} [f] @returns {Promise<{ scanned: number, hits: Array<{ dep: Dep, ids: string[] }>, info: Map<string, VulnInfo> }>} */
30
+ export async function osvScan(deps, f = fetch) {
31
+ const targets = deps.filter((d) => d.pinned && d.version && !d.priv); // un paquet privé ou local n'est JAMAIS envoyé
32
+ /** @type {Array<{ dep: Dep, ids: string[] }>} */ const hits = [];
33
+ for (let i = 0; i < targets.length; i += BATCH) {
34
+ const chunk = targets.slice(i, i + BATCH);
35
+ const body = JSON.stringify({ queries: chunk.map((d) => ({ package: { name: d.name, ecosystem: d.ecosystem }, version: d.version })) });
36
+ const j = await call(f, `${API}/querybatch`, { method: "POST", headers: { "content-type": "application/json" }, body });
37
+ asArr(j.results).forEach((res, k) => { const ids = asArr(asObj(res).vulns).map((v) => String(asObj(v).id ?? "")).filter(Boolean); if (ids.length) hits.push({ dep: chunk[k], ids }); });
38
+ }
39
+ const ids = [...new Set(hits.flatMap((h) => h.ids))].slice(0, MAX_DETAILS);
40
+ const infos = await Promise.all(ids.map((id) => detail(f, id).catch(() => ({ id, cve: "", severity: "inconnue", fixed: "" }))));
41
+ return { scanned: targets.length, hits, info: new Map(infos.map((x) => [x.id, x])) };
42
+ }
43
+ // MODULE_COMPLETE
@@ -0,0 +1,36 @@
1
+ // src/engine/parse-json-loose.js — Extrait le JSON d'une réponse LLM (tolère fences + texte autour).
2
+ // Par défaut : premier OBJET (comportement historique). opts.arrays : un tableau racine est accepté. opts.recover : un JSON tronqué est
3
+ // rattrapé par équilibrage de pile (json-balance.js) et marqué TRUNCATED.
4
+ import { TRUNCATED, recoverTruncated } from "./json-balance.js";
5
+
6
+ export { TRUNCATED };
7
+
8
+ /** @param {string} body @param {number} start @returns {number} index de fermeture, -1 si le texte s'arrête avant */
9
+ function matchEnd(body, start) {
10
+ let depth = 0, inStr = false, esc = false;
11
+ for (let i = start; i < body.length; i++) {
12
+ const c = body[i];
13
+ if (inStr) {
14
+ if (esc) esc = false;
15
+ else if (c === "\\") esc = true;
16
+ else if (c === '"') inStr = false;
17
+ } else if (c === '"') inStr = true;
18
+ else if (c === "{" || c === "[") depth++;
19
+ else if (c === "}" || c === "]") { depth--; if (depth === 0) return i; }
20
+ }
21
+ return -1;
22
+ }
23
+
24
+ /** @param {string} text @param {{ arrays?: boolean, recover?: boolean }} [opts] @returns {unknown} */
25
+ export function parseJsonLoose(text, opts = {}) {
26
+ if (!text) return null;
27
+ const t = String(text).trim();
28
+ const fenced = t.match(/```(?:json)?\s*([\s\S]*?)```/i);
29
+ const body = fenced ? fenced[1] : t;
30
+ const start = body.search(opts.arrays ? /[{[]/ : /\{/);
31
+ if (start === -1) return null;
32
+ const end = matchEnd(body, start);
33
+ if (end === -1) return opts.recover ? recoverTruncated(body.slice(start)) : null;
34
+ try { return JSON.parse(body.slice(start, end + 1)); } catch { return null; }
35
+ }
36
+ // MODULE_COMPLETE
@@ -0,0 +1,28 @@
1
+ // src/engine/patch-file-sections.js — Découpage d'un patch Unified Diff en
2
+ // sections par fichier : chemin, lignes ajoutées (+) et supprimées (-),
3
+ // isNew (header `--- /dev/null` = fichier créé par ce patch). Les lignes de
4
+ // contexte sont ignorées ; les headers +++/--- ne comptent jamais comme code.
5
+
6
+ // Sections fichier du patch, dans l'ordre d'apparition des headers.
7
+ /** @param {string} patch @returns {Section[]} */
8
+ export function fileSections(patch) {
9
+ /** @type {Section[]} */ const out = [];
10
+ /** @type {Section | null} */ let cur = null;
11
+ let fromNull = false;
12
+ for (const l of String(patch).split("\n")) {
13
+ if (l.startsWith("--- ")) { fromNull = l.startsWith("--- /dev/null"); continue; }
14
+ if (l.startsWith("+++ b/")) {
15
+ cur = {
16
+ file: l.slice(6).trim().replace(/"/g, "").replace(/\\/g, "/"),
17
+ added: [], removed: [], isNew: fromNull,
18
+ };
19
+ out.push(cur);
20
+ continue;
21
+ }
22
+ if (!cur) continue;
23
+ if (l.startsWith("+")) cur.added.push(l.slice(1));
24
+ else if (l.startsWith("-")) cur.removed.push(l.slice(1));
25
+ }
26
+ return out;
27
+ }
28
+ // MODULE_COMPLETE
@@ -0,0 +1,52 @@
1
+ // src/engine/patch-scan-cheat.js — Floor guard anti-triche (NON NEGO) sur les diffs produits : un patch qui rend
2
+ // les tests verts SANS implémenter est rejeté. 5 gestes : (1) assertion affaiblie/supprimée, (2) catch vide,
3
+ // (3) test skippé ou commenté, (4) résultat codé en dur, (5) stub TODO/FIXME. Analyse PAR FICHIER : gestes 1 et 4
4
+ // comparent lignes supprimées vs ajoutées. Motifs et TEXTES servis par le serveur (lot Pro) — ici la mécanique.
5
+ import { fileSections } from "./patch-file-sections.js";
6
+ import { getRules } from "../rules/rules-registry.js";
7
+
8
+ /** @type {WeakMap<object, Record<string, RegExp>>} */
9
+ const compiled = new WeakMap();
10
+ /** Compile (une fois par lot de règles) les motifs reçus du serveur. @param {ScanRules["cheat"]} c @returns {Record<string, RegExp>} */
11
+ function patterns(c) {
12
+ let p = compiled.get(c);
13
+ if (!p) { p = { file: new RegExp(c.testFile, "i"), tauto: new RegExp(c.tauto), assert: new RegExp(c.assert), empty: new RegExp(c.emptyCatch), skip: new RegExp(c.skip), commented: new RegExp(c.commented), call: new RegExp(c.callAssign, "g"), stub: new RegExp(c.stub, "i") }; compiled.set(c, p); }
14
+ return p;
15
+ }
16
+
17
+ /** Les 5 gestes, dans l'ordre des règles du serveur. @param {ScanRules["cheat"]} c @returns {((s: Section) => boolean)[]} */
18
+ function gestures(c) {
19
+ const p = patterns(c);
20
+ const isTest = (/** @type {Section} */ s) => p.file.test(s.file);
21
+ return [
22
+ (s) => s.added.some((l) => p.tauto.test(l)) || (isTest(s) && s.removed.some((l) => p.assert.test(l)) && s.added.length > 0 && !s.added.some((l) => p.assert.test(l))),
23
+ (s) => p.empty.test(s.added.join("\n")),
24
+ (s) => isTest(s) && (s.added.some((l) => p.skip.test(l)) || s.added.filter((l) => p.commented.test(l)).length >= c.commentedMin),
25
+ (s) => {
26
+ const names = new Set();
27
+ for (const l of s.removed) for (const m of l.matchAll(p.call)) names.add(m[1]);
28
+ if (!names.size || isTest(s)) return false;
29
+ const lit = new RegExp(`(?:const|let|var)\\s+(?:${[...names].join("|")})\\s*=\\s*(?:["'\`]|-?\\d)`);
30
+ return s.added.some((l) => lit.test(l));
31
+ },
32
+ (s) => s.added.some((l) => p.stub.test(l)),
33
+ ];
34
+ }
35
+
36
+ // Scanne le patch PAR FICHIER : une violation max par règle (jamais un déluge). Lot Pro absent : aucune règle.
37
+ /** @param {string} patch @param {Violation[]} out */
38
+ export function collectCheatViolations(patch, out) {
39
+ const scan = getRules().scan;
40
+ if (!scan) return;
41
+ const checks = gestures(scan.cheat);
42
+ const seen = checks.map(() => false);
43
+ for (const s of fileSections(patch)) {
44
+ checks.forEach((check, i) => {
45
+ if (seen[i] || !check(s)) return;
46
+ seen[i] = true;
47
+ const r = scan.cheat.rules[i];
48
+ if (r) out.push({ kind: "constraint", rule: r.rule, alternative: r.alt, matched: "cheat" });
49
+ });
50
+ }
51
+ }
52
+ // MODULE_COMPLETE
@@ -0,0 +1,37 @@
1
+ // src/engine/patch-scan-prep.js — Préparation de l'entrée du scanner de patch
2
+ // (extraction des lignes ajoutées + détection de patch UI).
3
+
4
+ // Limite de taille de l'entrée soumise au scanner (~10 KB de lignes ajoutées).
5
+ // Garde-fou anti-ReDoS : borne le coût maximal d'une regex même si un pattern
6
+ // douteux passait la sanitization.
7
+ const MAX_SCAN_INPUT_CHARS = 10240;
8
+
9
+ // Extrait les lignes ajoutées du patch (préfixe `+`, hors `+++` headers) et
10
+ // retourne l'entrée de scan, ou null si le patch n'ajoute rien de significatif.
11
+ // `added` conserve le préfixe `+` (patterns regex métier) ; `addedClean` le
12
+ // retire (parser AST — sinon il reçoit `+const x = …` malformé → perte des
13
+ // déclarations en error-recovery).
14
+ /** @param {string} patch @returns {{ lines: string[], added: string, addedClean: string } | null} */
15
+ export function extractScanInput(patch) {
16
+ const lines = String(patch).split("\n");
17
+ const addedRaw = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++"));
18
+ const addedFull = addedRaw.join("\n");
19
+ if (!addedFull.trim()) return null;
20
+ return {
21
+ lines,
22
+ added: addedFull.slice(0, MAX_SCAN_INPUT_CHARS),
23
+ addedClean: addedRaw.map((l) => l.slice(1)).join("\n").slice(0, MAX_SCAN_INPUT_CHARS),
24
+ };
25
+ }
26
+
27
+ // Détection d'un patch UI : headers `+++ b/...ext` de présentation.
28
+ /** @param {string[]} lines @returns {boolean} */
29
+ export function isUiPatch(lines) {
30
+ const uiExt = /\.(css|scss|sass|less|jsx|tsx|vue|svelte|html|htm)(\s|$)/i;
31
+ return lines.some((l) => {
32
+ if (!l.startsWith("+++ b/")) return false;
33
+ const p = l.slice(7).trim();
34
+ return uiExt.test(p);
35
+ });
36
+ }
37
+ // MODULE_COMPLETE
@@ -0,0 +1,51 @@
1
+ // src/engine/patch-scan-rules.js — Violations métier du scanner de patch :
2
+ // anti-patterns (§3), invariants réglementaires (§2), Design System (§6).
3
+ import { sanitizePattern } from "./pattern-sanitize.js";
4
+ import { getRules } from "../rules/rules-registry.js";
5
+
6
+ // Teste des patterns regex (assainis via sanitizePattern, anti-ReDoS) contre le
7
+ // code ajouté. Retourne true au premier pattern qui matche.
8
+ /** @param {string[] | undefined} patterns @param {string} added @returns {boolean} */
9
+ export const testPatterns = (patterns, added) => (patterns || []).some((p) => {
10
+ const safe = sanitizePattern(p);
11
+ if (!safe) return false;
12
+ try { return new RegExp(safe, "i").test(added); } catch { return false; }
13
+ });
14
+
15
+ // Anti-patterns métier (§3 du scratchpad).
16
+ /** @param {MissionPlan} mission @param {string} added @param {Violation[]} out */
17
+ export function collectPitfallViolations(mission, added, out) {
18
+ for (const f of (mission.pitfalls || [])) {
19
+ if (testPatterns(f.patterns, added)) {
20
+ out.push({ kind: "pitfall", rule: f.never, alternative: f.always, matched: "pitfall" });
21
+ }
22
+ }
23
+ }
24
+
25
+ // Invariants réglementaires (§2 du scratchpad) — NON NEGO.
26
+ /** @param {MissionPlan} mission @param {string} added @param {Violation[]} out */
27
+ export function collectConstraintViolations(mission, added, out) {
28
+ for (const c of (mission.constraints || [])) {
29
+ if (testPatterns(c.patterns, added)) {
30
+ out.push({ kind: "constraint", rule: c.text, alternative: c.invariant, matched: "constraint" });
31
+ }
32
+ }
33
+ }
34
+
35
+ // Design System (§6) : invalide tout patch UI générique hors charte visuelle
36
+ // métier (animations distrayantes en famille A, focus retiré WCAG, etc.).
37
+ /** @param {MissionPlan} mission @param {string} added @param {Violation[]} out */
38
+ export function collectDesignViolations(mission, added, out) {
39
+ const ds = mission.designSystem || {};
40
+ for (const v of (ds.violations || [])) {
41
+ if (testPatterns(v.patterns, added)) {
42
+ out.push({
43
+ kind: "design",
44
+ rule: v.rule,
45
+ alternative: getRules().scan?.design.charterAlt ?? "",
46
+ matched: "design",
47
+ });
48
+ }
49
+ }
50
+ }
51
+ // MODULE_COMPLETE
@@ -0,0 +1,38 @@
1
+ // src/engine/patch-scan.js — Scanner de patch Unified Diff : garde-fous AST
2
+ // génériques (sécurité langage) + patterns métier du scratchpad (§2/§3/§6).
3
+ import { extractScanInput, isUiPatch } from "./patch-scan-prep.js";
4
+ import { collectPitfallViolations, collectConstraintViolations, collectDesignViolations } from "./patch-scan-rules.js";
5
+ import { collectFrontendShellViolations } from "./patch-ui-guard.js";
6
+ import { collectSecretTextViolations } from "./patch-secret-guard.js";
7
+ import { collectGraphViolations } from "./ast-rules-graph.js";
8
+ import { collectCheatViolations } from "./patch-scan-cheat.js";
9
+ import { detectAstLang } from "./ast-lang.js";
10
+ import { scanAstViolations } from "./ast-scan.js";
11
+
12
+ // Scanne un patch Unified Diff contre la mission. Ne considère que les lignes
13
+ // ajoutées (préfixe `+`, hors `+++` headers). Retourne [{ kind:"ast"|"pitfall"|
14
+ // "constraint"|"design", rule, alternative, matched }]. Fallback implicite vers
15
+ // regex si la langue n'est pas supportée par le parser AST (detectAstLang →
16
+ // null) ou si le runtime WASM n'est pas prêt. Inactif si la mission n'a pas
17
+ // chargé ses patterns (sidecar absent) — évite les faux positifs.
18
+ /** @param {string} patch @param {MissionPlan | null} mission @returns {Violation[]} */
19
+ export function scanPatchForViolations(patch, mission) {
20
+ if (!patch || !mission) return [];
21
+ if (mission.patternsLoaded === false) return [];
22
+ const input = extractScanInput(patch);
23
+ if (!input) return [];
24
+ /** @type {Violation[]} */ const violations = [];
25
+ const astLang = detectAstLang(input.lines);
26
+ if (astLang) {
27
+ for (const v of scanAstViolations(input.addedClean, astLang)) violations.push(v);
28
+ }
29
+ collectPitfallViolations(mission, input.added, violations);
30
+ collectConstraintViolations(mission, input.added, violations);
31
+ if (isUiPatch(input.lines)) collectDesignViolations(mission, input.added, violations);
32
+ collectFrontendShellViolations(patch, violations);
33
+ collectSecretTextViolations(patch, violations);
34
+ collectGraphViolations(patch, violations);
35
+ collectCheatViolations(patch, violations);
36
+ return violations;
37
+ }
38
+ // MODULE_COMPLETE
@@ -0,0 +1,50 @@
1
+ // src/engine/patch-secret-guard.js — RÈGLE ZERO_SECRET, modèle de sécurité
2
+ // RELATIF « Concepteur vs Public » : tout ce qui est exposé au PUBLIC final
3
+ // (UI, front-end web/webview, toasts, logs clients) subit le blocage STRICT
4
+ // des secrets de conception (chemins locaux absolus, formats de credentials).
5
+ // Les surfaces du CONCEPTEUR (backend/serveur, configuration métier, documents
6
+ // de conception .md, scratchpads) ne sont PAS bloquées : elles portent les
7
+ // secrets d'architecture et les directives nécessaires au développement. La
8
+ // détection de surface publique est partagée avec patch-ui-guard
9
+ // (isFrontendFile) : un seul modèle de périmètre, zéro divergence.
10
+ import { isFrontendFile } from "./patch-ui-guard.js";
11
+ import { testPatterns } from "./patch-scan-rules.js";
12
+
13
+ // Règles réservées aux surfaces PUBLIQUES (aucun groupe (?:…) quantifié —
14
+ // compatible sanitizePattern anti-ReDoS).
15
+ /** @type {{ rule: string, patterns: string[], alternative: string }[]} */
16
+ const SECRET_RULES = [
17
+ {
18
+ rule: "ZERO_SECRET (Public) — chemin local absolu de l'utilisateur exposé au Public (UI/front-end, toast, log client) : interdit.",
19
+ patterns: ["[A-Za-z]:[\\\\/]+Users", "/home/[A-Za-z0-9._-]+/", "/Users/"],
20
+ alternative: "Côté Public : chemin relatif, racine configurable ou placeholder masqué. Côté Concepteur : les chemins internes restent libres (jamais exposés).",
21
+ },
22
+ {
23
+ rule: "ZERO_SECRET (Public) — clé API/licence/jeton en dur exposé au Public (UI/front-end, toast, log client) : interdit.",
24
+ patterns: [
25
+ "sk-ant-[A-Za-z0-9-]{8,}", "sk-[A-Za-z0-9]{16,}", "AIza[A-Za-z0-9_-]{10,}",
26
+ "ghp_[A-Za-z0-9]{20,}", "AKIA[0-9A-Z]{16}", "xoxb-[A-Za-z0-9-]{10,}",
27
+ "PLUGCTO-[A-Za-z0-9-]{8,}", "Bearer\\s+[A-Za-z0-9._-]{16,}",
28
+ ],
29
+ alternative: "Côté Public : env/coffre et placeholders masqués (<VOTRE_CLÉ_API>). Côté Concepteur : les secrets d'architecture restent internes, jamais exposés au Public.",
30
+ },
31
+ ];
32
+
33
+ // Scanne le patch PAR FICHIER : seules les lignes ajoutées des fichiers de
34
+ // surface PUBLIQUE sont testées — les surfaces du Concepteur passent librement.
35
+ // Dédoublonné par règle : jamais un déluge.
36
+ /** @param {string} patch @param {Violation[]} out */
37
+ export function collectSecretTextViolations(patch, out) {
38
+ const seen = SECRET_RULES.map(() => false);
39
+ let isPublic = false;
40
+ for (const l of String(patch).split("\n")) {
41
+ if (l.startsWith("+++ b/")) { isPublic = isFrontendFile(l.slice(6)); continue; }
42
+ if (!isPublic || !l.startsWith("+") || l.startsWith("+++")) continue;
43
+ SECRET_RULES.forEach((r, i) => {
44
+ if (seen[i] || !testPatterns(r.patterns, l)) return;
45
+ seen[i] = true;
46
+ out.push({ kind: "constraint", rule: r.rule, alternative: r.alternative, matched: "constraint" });
47
+ });
48
+ }
49
+ }
50
+ // MODULE_COMPLETE
@@ -0,0 +1,49 @@
1
+ // src/engine/patch-ui-guard.js — RÈGLE ZERO_TRUST_UI : interdit formellement
2
+ // spawn/exec/child_process dans le code FRONT-END (Webview/UI). Détection
3
+ // PAR FICHIER : extensions de présentation, ou .js/.ts dans un répertoire
4
+ // front-end explicite (webview, renderer, client, public, browser…) — un
5
+ // fichier back-end n'est JAMAIS concerné. Pure, testable en Node.
6
+ import { getRules } from "../rules/rules-registry.js";
7
+ import { testPatterns } from "./patch-scan-rules.js";
8
+
9
+ // Extensions de présentation (miroir patch-scan-prep isUiPatch).
10
+ const UI_EXT = /\.(css|scss|sass|less|jsx|tsx|vue|svelte|html|htm)(\s|$)/i;
11
+ // Segments de chemin front-end pour .js/.ts (comparaison par SEGMENT exact —
12
+ // jamais par sous-chaîne : « webhook.js » n'est pas « web »).
13
+ const FRONTEND_DIR = /^(webview|web|www|front[-_]?end|renderer|client|browser|public|ui|views?|pages|panels?|widgets?|components?)$/i;
14
+
15
+ // Un fichier du patch est-il front-end ? (extension UI, ou .js/.ts sous un
16
+ // répertoire front-end explicite).
17
+ /** @param {string} headerPath @returns {boolean} */
18
+ export function isFrontendFile(headerPath) {
19
+ const p = String(headerPath).trim().replace(/"/g, "");
20
+ if (UI_EXT.test(p)) return true;
21
+ if (!/\.(js|ts|mjs|cjs)(\s|$)/i.test(p)) return false;
22
+ return p.split(/[\\/]/).some((seg) => FRONTEND_DIR.test(seg));
23
+ }
24
+
25
+ // Scanne le patch PAR FICHIER : toute ligne ajoutée d'un fichier front-end qui
26
+ // spawn/exécute un script terminal → violation design (une par règle, jamais
27
+ // un déluge), bloquée par mission-guard avec l'alternative postMessage. Règles et texte servis par le serveur.
28
+ /** @param {string} patch @param {Violation[]} out */
29
+ export function collectFrontendShellViolations(patch, out) {
30
+ const scan = getRules().scan;
31
+ if (!scan) return; // lot Pro absent (compte gratuit) : pas de règle « shell dans l'UI ».
32
+ const rules = scan.design.shellInUi;
33
+ let frontend = false;
34
+ const seen = rules.map(() => false);
35
+ for (const l of String(patch).split("\n")) {
36
+ if (l.startsWith("+++ b/")) { frontend = isFrontendFile(l.slice(6)); continue; }
37
+ if (!frontend || !l.startsWith("+") || l.startsWith("+++")) continue;
38
+ rules.forEach((r, i) => {
39
+ if (seen[i] || !testPatterns(r.patterns, l)) return;
40
+ seen[i] = true;
41
+ out.push({
42
+ kind: "design", rule: r.rule,
43
+ alternative: scan.design.shellAlt,
44
+ matched: "design",
45
+ });
46
+ });
47
+ }
48
+ }
49
+ // MODULE_COMPLETE
@@ -0,0 +1,23 @@
1
+ // src/engine/pattern-sanitize.js — Assainissement regex anti-ReDoS (patterns LLM/sidecar).
2
+ // Les quantificateurs imbriqués (ex: (.+)+, (.*)*) créent des chemins exponentiels :
3
+ // pattern valide mais gèle le serveur. On REJETTE ces patterns. Limite aussi la longueur.
4
+
5
+ // Assainit un pattern regex : retourne la chaîne saine, ou null si dangereuse/invalide.
6
+ /** @param {unknown} p @returns {string | null} */
7
+ export function sanitizePattern(p) {
8
+ if (typeof p !== "string") return null;
9
+ const s = p.trim();
10
+ if (!s) return null;
11
+ if (s.length > 200) return null;
12
+ if (/\([^()]*[+*?][^()]*\)[+*?{]/.test(s)) return null;
13
+ if (/\([^()]*[+*?][^()]*\)\{\d/.test(s)) return null;
14
+ return s;
15
+ }
16
+
17
+ // Mappe un tableau de patterns LLM vers des patterns assainis (ReDoS filtrés).
18
+ /** @param {unknown} patterns @returns {string[]} */
19
+ export function sanitizePatternsLLM(patterns) {
20
+ if (!Array.isArray(patterns)) return [];
21
+ return patterns.map(sanitizePattern).filter((p) => typeof p === "string");
22
+ }
23
+ // MODULE_COMPLETE
@@ -0,0 +1,37 @@
1
+ // src/engine/plan-draft.js — Persistance du draft de consentement de
2
+ // plugcto_plan_mission (le scratchpad mission.md n'est écrit qu'après Y/n).
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+
6
+ // Lit le draft persisté lors du consentement ; null si absent/corrompu ou si le
7
+ // besoin a changé (le draft ne vaut que pour le `need` qui l'a produit).
8
+ /** @param {string} draftPath @param {string} need @returns {Promise<MissionPlan | null>} */
9
+ export async function readPlanDraft(draftPath, need) {
10
+ try {
11
+ const draft = JSON.parse(await fs.readFile(draftPath, "utf8"));
12
+ return draft && draft.need === need ? /** @type {MissionPlan} */ (draft) : null;
13
+ } catch {
14
+ return null; // absent ou corrompu → réextraction (fail-safe)
15
+ }
16
+ }
17
+
18
+ // Persiste le draft (réutilisable à la confirmation) SANS écrire mission.md.
19
+ /** @param {MissionPlan} plan @param {string} draftPath @returns {Promise<void>} */
20
+ export async function savePlanDraft(plan, draftPath) {
21
+ try {
22
+ await fs.mkdir(path.dirname(draftPath), { recursive: true });
23
+ await fs.writeFile(draftPath, JSON.stringify(plan, null, 2), { encoding: "utf8", mode: 0o600 });
24
+ } catch {
25
+ // Best-effort : l'échec de persistance du draft n'invalide pas le
26
+ // diagnostic (la réextraction reste possible à la confirmation).
27
+ }
28
+ }
29
+
30
+ // Supprime le draft après confirmation Auto Mode (mission.md devient la vérité).
31
+ /** @param {string} draftPath @returns {Promise<void>} */
32
+ export async function removePlanDraft(draftPath) {
33
+ try { await fs.rm(draftPath, { force: true }); } catch {
34
+ // Best-effort : un draft résiduel sera écrasé à la prochaine planification.
35
+ }
36
+ }
37
+ // MODULE_COMPLETE