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,60 @@
1
+ // src/engine/plan-mission.js — Logique métier de l'outil plugcto_plan_mission.
2
+ // args = { need, auto_mode, missionPath?, pcfg?, plan? } → { isError, text,
3
+ // autoMode, wrote, plan }. `plan` court-circuite l'extraction (tests) ; `pcfg`
4
+ // active l'extraction LLM (sinon déterministe). Sans auto_mode : extraction +
5
+ // DRAFT persisté (réutilisable à la confirmation) + question Y/n — mission.md
6
+ // NON écrit. Avec auto_mode=true : réextraction/réutilisation du draft puis écriture mission.md.
7
+ import { MISSION_PATH } from "./mission-paths.js";
8
+ import { extractMission } from "./extract-llm.js";
9
+ import { writeMission } from "./mission-store.js";
10
+ import { formatConsoleSummary } from "./plan-summary.js";
11
+ import { formatPlanReply } from "./plan-reply.js";
12
+ import { readPlanDraft, savePlanDraft, removePlanDraft } from "./plan-draft.js";
13
+
14
+ /** @typedef {{ need?: string, auto_mode?: boolean, missionPath?: string, pcfg?: { primary: ProviderPrimary } | null, plan?: MissionPlan }} PlanMissionArgs */
15
+
16
+ /** @param {PlanMissionArgs} args */
17
+ export async function planMission(args) {
18
+ const need = (args && typeof args.need === "string" ? args.need : "").trim();
19
+ if (!need) {
20
+ return {
21
+ isError: true,
22
+ text: "Paramètre `need` manquant ou vide pour plugcto_plan_mission. Décrivez le besoin métier à planifier.",
23
+ };
24
+ }
25
+ const autoMode = !!(args && args.auto_mode);
26
+ const missionPath = (args && args.missionPath) || MISSION_PATH;
27
+ const draftPath = missionPath + ".draft.json";
28
+
29
+ let plan;
30
+ if (args && args.plan) {
31
+ plan = args.plan; // injection directe (tests)
32
+ } else if (autoMode) {
33
+ // Réutilise le draft de la phase de consentement si le besoin correspond
34
+ // (évite un 2e appel LLM après confirmation).
35
+ plan = (await readPlanDraft(draftPath, need)) || (await extractMission(need, args && args.pcfg));
36
+ } else {
37
+ plan = await extractMission(need, args && args.pcfg);
38
+ }
39
+
40
+ plan.need = need;
41
+ plan.autoMode = autoMode;
42
+ plan.iterations = plan.iterations || 0;
43
+ plan.checkedSteps = plan.checkedSteps || (plan.impact.steps || []).map(() => false);
44
+
45
+ // Résumé diagnostic sur stderr (stdout = canal protocole MCP, interdit).
46
+ console.error(formatConsoleSummary(plan));
47
+
48
+ if (!autoMode) {
49
+ // Consentement : draft persisté SANS écrire mission.md + question Y/n.
50
+ await savePlanDraft(plan, draftPath);
51
+ return { isError: false, autoMode: false, wrote: false, plan, text: formatPlanReply(plan, false) };
52
+ }
53
+
54
+ // Auto Mode confirmé : écrit le scratchpad + sidecar, supprime le draft.
55
+ plan.validatedAt = new Date().toISOString();
56
+ await writeMission(plan, missionPath);
57
+ await removePlanDraft(draftPath);
58
+ return { isError: false, autoMode: true, wrote: true, plan, text: formatPlanReply(plan, true) };
59
+ }
60
+ // MODULE_COMPLETE
@@ -0,0 +1,34 @@
1
+ // src/engine/plan-reply-legacy.js — Cas "code spaghetti/legacy" (exigence produit) :
2
+ // si l'audit Phase 0 révèle des failles, la réponse de plan propose EXPLICITEMENT
3
+ // deux issues — refactoring contrôlé OU clone propre sécurisé — et impose le
4
+ // Stop & Wait : aucun code avant le choix de l'utilisateur (§11 core_rules).
5
+
6
+ // Seuil heuristique : à partir de 4 failles cumulées + risque Élevé, le clone
7
+ // propre devient l'option recommandée (le refactoring coûte plus cher que la
8
+ // réécriture). Sinon, priorité au refactoring incrémental (moindre risque).
9
+ const CLONE_THRESHOLD = 4;
10
+
11
+ // Pousse la section décision legacy (rien si aucune faille détectée).
12
+ /** @param {MissionPlan} plan @param {string[]} L @returns {void} */
13
+ export function pushLegacyDecision(plan, L) {
14
+ const pitfalls = plan.pitfalls || [];
15
+ if (pitfalls.length === 0) return; // projet sain : pas de bloc décision.
16
+ const nConstraints = (plan.constraints || []).length;
17
+ const highRisk = !!plan.impact && plan.impact.risk === "Élevé";
18
+ const cloneFirst = highRisk && pitfalls.length >= CLONE_THRESHOLD;
19
+ L.push("");
20
+ L.push(`⚠️ **Code legacy détecté** — ${pitfalls.length} faille(s) et ${nConstraints} contrainte(s) d'audit.`);
21
+ if (cloneFirst) {
22
+ L.push("Recommandation : **clone propre sécurisé** (réécriture) — le refactoring coûterait");
23
+ L.push("plus cher que la réécriture vu la densité de failles et le risque Élevé.");
24
+ } else {
25
+ L.push("Recommandation : **refactoring contrôlé** (incremental, tests de non-régression");
26
+ L.push("à chaque étape) — la base existante reste récupérable à coût maîtrisé.");
27
+ }
28
+ L.push("");
29
+ L.push("> **Deux issues possibles — votre choix est requis (Stop & Wait, §11) :**");
30
+ L.push("> 1️⃣ Refactoring contrôlé du code existant.");
31
+ L.push("> 2️⃣ Clone propre sécurisé (nouvelle base, specs reprises de l'audit).");
32
+ L.push("> Aucune ligne de code ne sera écrite avant votre décision explicite.");
33
+ }
34
+ // MODULE_COMPLETE
@@ -0,0 +1,34 @@
1
+ // src/engine/plan-reply-pillars.js — Réply plan_mission, sections (a)-(e) :
2
+ // header projet + les 5 piliers métier (personas, contraintes, failles,
3
+ // glossaire, estimation). Retourne nSteps (réutilisé par la question Y/n).
4
+ /** @param {MissionPlan} plan @param {string[]} L @returns {number} */
5
+ export function pushPillarSections(plan, L) {
6
+ const { personas, constraints, pitfalls, glossary, impact, domainKeys, fiches } = plan;
7
+ const nSteps = (impact.steps || []).length;
8
+ L.push("## 🎯 PipCode — Plan de mission (Phase 0 — 5 piliers)");
9
+ L.push("");
10
+ L.push(`**Projet** : ${plan.projectName}`);
11
+ L.push(`**Besoin** : ${plan.need}`);
12
+ L.push(`**Domaines** : ${(domainKeys || []).join(", ") || "—"}`);
13
+ L.push("");
14
+ L.push("### (a) Personas métiers & garde-fous");
15
+ for (const p of personas) L.push(`- **${p.role_name}** — ${p.mandate}`);
16
+ L.push("");
17
+ L.push("### (b) Contraintes légales & invariants (NON NEGO)");
18
+ for (const c of constraints) L.push(`- ${c.text}${c.invariant ? ` — Invariant : ${c.invariant}` : ""}`);
19
+ L.push("");
20
+ L.push("### (c) Failles métiers à bloquer");
21
+ for (const f of pitfalls) L.push(`- ❌ NE JAMAIS : ${f.never} → ✅ TOUJOURS : ${f.always}`);
22
+ L.push("");
23
+ L.push("### (d) Glossaire & lexique métier");
24
+ for (const g of glossary) L.push(`- \`${g.term}\` : ${g.definition}`);
25
+ L.push("");
26
+ L.push("### (e) Estimation & impact");
27
+ L.push(`- Fichiers : ${(impact.files || []).join(", ") || "—"}`);
28
+ L.push(`- Étapes (${nSteps}) : ${(impact.steps || []).join(" · ")}`);
29
+ L.push(`- Tokens : ~${impact.tokens} | Risque métier : ${impact.risk}`);
30
+ L.push(`- Fiches de doctrine : ${(fiches || []).join(", ") || "—"}`);
31
+ L.push("");
32
+ return nSteps;
33
+ }
34
+ // MODULE_COMPLETE
@@ -0,0 +1,17 @@
1
+ // src/engine/plan-reply-ux.js — Réply plan_mission, sections (f)-(g) :
2
+ // directives UX (§5) et charte visuelle / Design System contextuel (§6).
3
+ /** @param {MissionPlan} plan @param {string[]} L */
4
+ export function pushUxSections(plan, L) {
5
+ const ux = plan.uxDirectives || [];
6
+ const ds = plan.designSystem || {};
7
+ L.push("### (f) Directives UX & ergonomie métier");
8
+ for (const r of ux) L.push(`- ${r}`);
9
+ L.push(`- Accessibilité : ${ds.wcag || "WCAG 2.1 AA"}`);
10
+ L.push("");
11
+ L.push("### (g) Charte visuelle & Design System contextuel");
12
+ L.push(`- Famille ${ds.family || ""} : ${ds.style || ""} | densité ${ds.density || ""}`);
13
+ L.push(`- Palette : ${ds.palette || "—"} | Micro-interactions : ${ds.microInteractions || "—"}`);
14
+ for (const v of (ds.violations || [])) L.push(`- ❌ Hors charte : ${v.rule}`);
15
+ L.push("");
16
+ }
17
+ // MODULE_COMPLETE
@@ -0,0 +1,28 @@
1
+ // src/engine/plan-reply.js — Formate la réponse de plan_mission pour l'IDE
2
+ // (5 piliers + UX/Design + question de consentement Auto Mode).
3
+ import { pushPillarSections } from "./plan-reply-pillars.js";
4
+ import { pushUxSections } from "./plan-reply-ux.js";
5
+ import { pushLegacyDecision } from "./plan-reply-legacy.js";
6
+
7
+ /** @param {MissionPlan} plan @param {boolean} autoMode @returns {string} */
8
+ export function formatPlanReply(plan, autoMode) {
9
+ const L = /** @type {string[]} */ ([]);
10
+ const nSteps = pushPillarSections(plan, L);
11
+ pushUxSections(plan, L);
12
+ pushLegacyDecision(plan, L);
13
+ if (autoMode) {
14
+ L.push("✅ **Auto Mode ACTIVÉ** — scratchpad `.pipcode/mission.md` écrit.");
15
+ L.push("Relisez ce contexte à chaque itération : ne produisez aucun patch qui viole les");
16
+ L.push("anti-patterns (§3), invariants réglo (§2) ou le Design System (§6),");
17
+ L.push("et cochez la checklist (§4) au fil de l'exécution.");
18
+ } else {
19
+ L.push(`⏸️ **Consentement Auto Mode requis** — ${nSteps} étapes prévues sans déviation.`);
20
+ L.push("");
21
+ L.push("> **Activer le Auto Mode pour exécuter les N étapes sans déviation ? [Y/n]**");
22
+ L.push(">");
23
+ L.push("> Répondez `Y` en rappelant `plugcto_plan_mission` avec `auto_mode: true`.");
24
+ L.push("> Le scratchpad `.pipcode/mission.md` ne sera écrit qu'après confirmation.");
25
+ }
26
+ return L.join("\n");
27
+ }
28
+ // MODULE_COMPLETE
@@ -0,0 +1,16 @@
1
+ // src/engine/plan-summary.js — Résumé diagnostic concis de plan_mission (stderr ;
2
+ // stdout = canal protocole MCP, interdit au bruit applicatif).
3
+ /** @param {MissionPlan} plan @returns {string} */
4
+ export function formatConsoleSummary(plan) {
5
+ const nSteps = (plan.impact.steps || []).length;
6
+ return [
7
+ "[plugcto_plan_mission] Diagnostic Phase 0",
8
+ ` Projet : ${plan.projectName}`,
9
+ ` Domaines : ${(plan.domainKeys || []).join(", ") || "—"}`,
10
+ ` Personas : ${(plan.personas || []).length}`,
11
+ ` Contraintes: ${(plan.constraints || []).length} | Failles: ${(plan.pitfalls || []).length} | Glossaire: ${(plan.glossary || []).length}`,
12
+ ` Impact : ~${plan.impact.tokens} tokens | ${nSteps} étapes | risque ${plan.impact.risk}`,
13
+ ` UX/Design : ${(plan.uxDirectives || []).length} directives | charte ${plan.designSystem ? plan.designSystem.family : "?"} (${plan.designSystem ? plan.designSystem.style : "—"})`,
14
+ ].join("\n");
15
+ }
16
+ // MODULE_COMPLETE
@@ -0,0 +1,27 @@
1
+ // src/engine/preflight-text.js — Messages du pré-flight de capacités. Le message
2
+ // d'arrêt suit EXACTEMENT la formulation validée ; le détail dit, pour chaque
3
+ // modèle, ce qui est confirmé, refusé ou non confirmé, et d'où vient l'info.
4
+
5
+ import { CAP_LABEL } from "./cap-types.js";
6
+
7
+ /** @param {boolean | null} v @returns {string} */
8
+ const state = (v) => (v === true ? "oui" : v === false ? "non" : "non confirmé");
9
+
10
+ /** @param {string} label @param {CapModel | null | undefined} model @param {CapReport | null | undefined} report @returns {string} */
11
+ function line(label, model, report) {
12
+ if (!model || !report) return `${label} : non configuré`;
13
+ return `${label} (${model.model}) : ${report.detail.map((d) => `${CAP_LABEL[d.cap]} ${state(d.value)} — ${d.source}`).join(", ")}`;
14
+ }
15
+
16
+ /** Arrêt AVANT l'appel principal : 0 token consommé sur la tâche. */
17
+ /** @param {CapModel} m1 @param {CapReport} r1 @param {CapModel | null | undefined} m2 @param {CapReport | null | undefined} r2 @returns {string} */
18
+ export function stopMessage(m1, r1, m2, r2) {
19
+ return `Aucun des deux modèles configurés ne supporte cette tâche [${line("Modèle 1", m1, r1)} ; ${line("Modèle 2", m2, r2)}]. Veuillez sélectionner ou ajouter un modèle adapté dans Mes API.`;
20
+ }
21
+
22
+ /** Le Modèle 1 est incapable mais le Modèle 2 confirme : la tâche lui est confiée. */
23
+ /** @param {CapModel} m1 @param {CapModel} m2 @returns {string} */
24
+ export function routeNotice(m1, m2) {
25
+ return `Le Modèle 1 (${m1.model}) ne supporte pas cette tâche : elle est confiée au Modèle 2 (${m2.model}).`;
26
+ }
27
+ // MODULE_COMPLETE
@@ -0,0 +1,27 @@
1
+ // src/engine/preflight.js — Cascade de contrôle AVANT tout appel principal :
2
+ // 1. le Modèle 1 évalue s'il maîtrise la capacité requise ; 2. sinon on vérifie
3
+ // le Modèle 2 (MÊME évaluation, symétrique) ; 3. si aucun des deux : arrêt de la
4
+ // chaîne avec message explicite — la requête principale n'est JAMAIS envoyée
5
+ // (0 token sur la tâche ; seule la micro-question de niveau 2, mise en cache,
6
+ // a pu être posée). Aucune capacité requise → aucun coût, aucune requête.
7
+
8
+ import { evaluateModel } from "./cap-eval.js";
9
+ import { stopMessage, routeNotice } from "./preflight-text.js";
10
+
11
+ /** @typedef {{ ok: true, runner: "primary" | "secondary", notice: string, reports: CapReport[] } | { ok: false, message: string, reports: CapReport[] }} PreflightResult */
12
+
13
+ /**
14
+ * @param {{ required: Capability[], primary: CapModel, secondary?: CapModel | null }} args
15
+ * @param {{ evaluate?: typeof evaluateModel } & import("./cap-eval.js").EvalDeps} [deps]
16
+ * @returns {Promise<PreflightResult>}
17
+ */
18
+ export async function runPreflight({ required, primary, secondary }, deps = {}) {
19
+ if (required.length === 0) return { ok: true, runner: "primary", notice: "", reports: [] };
20
+ const evaluate = deps.evaluate || evaluateModel;
21
+ const r1 = await evaluate(primary, required, deps);
22
+ if (r1.ok) return { ok: true, runner: "primary", notice: "", reports: [r1] };
23
+ const r2 = secondary ? await evaluate(secondary, required, deps) : null;
24
+ if (secondary && r2 && r2.ok) return { ok: true, runner: "secondary", notice: routeNotice(primary, secondary), reports: [r1, r2] };
25
+ return { ok: false, message: stopMessage(primary, r1, secondary, r2), reports: r2 ? [r1, r2] : [r1] };
26
+ }
27
+ // MODULE_COMPLETE
@@ -0,0 +1,41 @@
1
+ // src/engine/project-brief.js — Cahier de départ transmis à la décomposition du Mode Auto : CLAUDE.md du client (prioritaire) puis
2
+ // DÉCISIONS de la mémoire du projet (choix de l'utilisateur, puis choix proposés « à confirmer »). Lecture seule. Chaque texte est
3
+ // repris tel quel ; ce qui ne tient pas dans le budget est annoncé.
4
+
5
+ import { CAHIERS } from "../project-memory/cahiers.js";
6
+ import { parseDecisions } from "../project-memory/decisions.js";
7
+ import { readMemoryFile } from "../project-memory/store.js";
8
+ import { projectInstructions } from "./project-instructions.js";
9
+ import { fitLines } from "./context-fit.js";
10
+ import fs from "node:fs/promises";
11
+ import path from "node:path";
12
+
13
+ const MAX_REPORT = 20_000;
14
+
15
+ /** Rapport d'audit du projet (.pipcode/AUDIT_REPORT.md) : base du plan ; absent = "". @param {string} root @returns {Promise<string>} */
16
+ async function auditReport(root) {
17
+ const md = (await fs.readFile(path.join(root, ".pipcode", "AUDIT_REPORT.md"), "utf8").catch(() => "")).trim();
18
+ return md ? `--- Rapport d'audit du projet (AUDIT_REPORT.md) : base du plan ---\n${md.slice(0, MAX_REPORT)}${md.length > MAX_REPORT ? "\n[… rapport tronqué]" : ""}` : "";
19
+ }
20
+
21
+ /** @param {{ source: string, status: string, confidence?: number }} d @returns {string} */
22
+ const labelOf = (d) => d.status === "refuse" ? "REFUSÉ (ne pas reproposer)" : d.status === "confirme" || d.source === "utilisateur" ? "VALIDÉ" : `À CONFIRMER${d.confidence !== undefined ? ` (≈ ${d.confidence} % estimé)` : ""}`;
23
+
24
+ /** @param {string} root @returns {Promise<string>} */
25
+ async function decisionsBlock(root) {
26
+ const lines = [];
27
+ for (const c of CAHIERS) {
28
+ for (const d of parseDecisions((await readMemoryFile(root, `${c}.md`)) ?? "")) {
29
+ lines.push(`[${c}] ${labelOf(d)} — ${d.question} → ${d.answer.replace(/\n+/g, " ")}`);
30
+ }
31
+ }
32
+ if (lines.length === 0) return "";
33
+ const { text, omitted } = fitLines(lines, 12_000);
34
+ return `--- Décisions du projet (mémoire) : les VALIDÉES s'imposent, les REFUSÉES sont exclues ---\n${text}${omitted ? `\n[… ${omitted} décisions non chargées : budget plein]` : ""}`;
35
+ }
36
+
37
+ /** Cahier complet ("" si rien). @param {string} root @returns {Promise<string>} */
38
+ export async function projectBrief(root) {
39
+ return [await projectInstructions(root, 20_000), await decisionsBlock(root), await auditReport(root)].filter(Boolean).join("\n\n");
40
+ }
41
+ // MODULE_COMPLETE
@@ -0,0 +1,49 @@
1
+ // src/engine/project-context.js — Contexte projet du chat : arborescence + relations entre fichiers (graphe) + mémoire des cahiers
2
+ // pertinente + contenu des fichiers (ouverts, voisins, reste) dans la fenêtre du modèle. AUCUNE perte silencieuse : éléments
3
+ // entiers, ce qui ne tient pas est listé. Chaque information apparaît UNE fois ; les règles de doctrine restent dans le prompt système.
4
+
5
+ import { listProjectFiles } from "../auto/project-files.js";
6
+ import { buildGraph } from "./code-graph.js";
7
+ import { NOISE_FILE, orderFiles, safeRead } from "./context-files.js";
8
+ import { fitFiles, fitLines } from "./context-fit.js";
9
+ import { contextChars } from "../project-memory/window.js";
10
+ import { recallSections } from "../project-memory/recall.js";
11
+ import { numberLines } from "./line-numbers.js";
12
+
13
+ const MAX_NAMES = 12;
14
+
15
+ /** @param {Map<string, { exports: string[], imports: Array<{ to: string, names: string[] }> }>} graph @returns {string[]} */
16
+ export function relationLines(graph) {
17
+ const lines = [];
18
+ for (const [f, n] of graph) {
19
+ if (n.exports.length) lines.push(`${f} déclare : ${n.exports.slice(0, MAX_NAMES).join(", ")}`);
20
+ for (const i of n.imports) lines.push(`${f} → ${i.to}${i.names.length ? ` {${i.names.slice(0, MAX_NAMES).join(", ")}}` : ""}`);
21
+ }
22
+ return lines;
23
+ }
24
+ /** @param {Parameters<typeof relationLines>[0]} graph @returns {string} */
25
+ export const formatRelations = (graph) => relationLines(graph).join("\n");
26
+
27
+ /** @param {string} title @param {string[]} lines @param {number} budget @returns {string} */
28
+ function section(title, lines, budget) {
29
+ const { text, omitted } = fitLines(lines, budget);
30
+ return `--- ${title} ---\n${text}${omitted ? `\n[… ${omitted} lignes non chargées : fenêtre pleine]` : ""}`;
31
+ }
32
+
33
+ /** Bloc de contexte ("" si projet vide). @param {string} projectPath @param {string[]} [openFiles] @param {string} [question] @param {number} [windowChars] budget du contexte (défaut : fenêtre du modèle) @param {{ numbered?: boolean }} [opts] numbered : chaque ligne de code porte son numéro (audit : références exactes) @returns {Promise<string>} */
34
+ export async function buildProjectContext(projectPath, openFiles = [], question = "", windowChars = contextChars(), opts = {}) {
35
+ const tree = (await listProjectFiles(projectPath)).filter((f) => !NOISE_FILE.test(f));
36
+ if (tree.length === 0) return "";
37
+ const W = windowChars; // modèle local (Ollama) : budget réduit, évaluation du prompt rapide
38
+ const graph = await buildGraph(projectPath, tree);
39
+ const parts = [section(`Projet : arborescence (${tree.length} fichiers source)`, tree, W * 0.1), section("Projet : relations entre fichiers (imports, symboles déclarés)", relationLines(graph), W * 0.15)];
40
+ const files = [];
41
+ for (const f of orderFiles(tree, graph, openFiles.slice(0, 20))) { const text = await safeRead(projectPath, f); if (text !== null) files.push({ path: f, text: opts.numbered ? numberLines(text) : text }); }
42
+ const { blocks, left } = fitFiles(files, W * 0.5);
43
+ if (blocks.length) parts.push(`--- Projet : contenu des fichiers (ouverts d'abord) ---\n${blocks.join("\n\n")}`);
44
+ if (left.length) parts.push(`--- Fichiers NON chargés (hors fenêtre) : demandez-les au besoin ---\n${left.join("\n")}`);
45
+ const mem = await recallSections(projectPath, question, openFiles, W * 0.25); // le plus VARIABLE (dépend de la question) : EN DERNIER, le début reste identique d'un message à l'autre (cache)
46
+ if (mem.text) parts.push(`--- Mémoire du projet (cahiers) ---\n${mem.text}${mem.omitted.length ? `\n[Sections non chargées : ${mem.omitted.join(", ")}]` : ""}`);
47
+ return parts.join("\n\n");
48
+ }
49
+ // MODULE_COMPLETE
@@ -0,0 +1,19 @@
1
+ // src/engine/project-instructions.js — CLAUDE.md du projet de l'utilisateur : lu (JAMAIS modifié, supprimé ni déplacé), transmis
2
+ // comme instructions PRIORITAIRES du projet (placées dans le prompt système). Les règles PipCode le complètent en garde-fous
3
+ // silencieux : aucun message de conflit n'est jamais affiché. Lecture seule, sans créer de fichier ; trouvé quelle que soit la casse (CLAUDE.md, claude.md…).
4
+
5
+ import fs from "node:fs/promises";
6
+ import { safeRead } from "./context-files.js";
7
+ import { fitLines } from "./context-fit.js";
8
+
9
+ const TITLE = "--- Instructions du projet (CLAUDE.md) : PRIORITAIRES ---";
10
+
11
+ /** Bloc prêt à injecter ("" si absent ou vide). @param {string} root @param {number} budget caractères disponibles @returns {Promise<string>} */
12
+ export async function projectInstructions(root, budget) {
13
+ const name = await fs.readdir(root).then((names) => names.find((n) => n.toLowerCase() === "claude.md"), () => undefined);
14
+ const text = name ? await safeRead(root, name) : null;
15
+ if (!text || !text.trim()) return "";
16
+ const { text: kept, omitted } = fitLines(text.replace(/\r\n/g, "\n").split("\n"), budget);
17
+ return `${TITLE}\n${kept}${omitted ? `\n[… ${omitted} lignes de CLAUDE.md non chargées : fenêtre pleine]` : ""}`;
18
+ }
19
+ // MODULE_COMPLETE
@@ -0,0 +1,31 @@
1
+ // src/engine/py-imports.js — Imports Python du projet (relatifs ET absolus) résolus vers un fichier CONNU : « import orchestrator », « from app.db import x ».
2
+ // Motifs seulement, jamais d'exécution ; une bibliothèque tierce (fastapi, os…) n'est pas un fichier connu donc ignorée.
3
+ import path from "node:path";
4
+
5
+ const REL = /^from\s+(\.+)([\w.]+)\s+import\s+(.+)$/gm;
6
+ const FROM = /^from\s+([A-Za-z_][\w.]*)\s+import\s+(.+)$/gm;
7
+ const PLAIN = /^import\s+([A-Za-z_][\w.]*(?:\s+as\s+\w+)?(?:\s*,\s*[A-Za-z_][\w.]*(?:\s+as\s+\w+)?)*)\s*$/gm;
8
+
9
+ /** @param {string} list @returns {string[]} */
10
+ const namesIn = (list) => list.replace(/[()]/g, "").split("#")[0].split(",").map((s) => s.trim().split(/\s+as\s+/)[0]).filter(Boolean);
11
+
12
+ /** @param {string} file @param {string} dotted @param {Set<string>} known @returns {string | undefined} module du projet : à la racine ou à côté du fichier */
13
+ function locate(file, dotted, known) {
14
+ const stem = dotted.replace(/\./g, "/");
15
+ const bases = ["", path.posix.dirname(file)];
16
+ return bases.flatMap((b) => [stem + ".py", stem + "/__init__.py"].map((c) => path.posix.normalize(path.posix.join(b, c)))).find((c) => known.has(c) && c !== file);
17
+ }
18
+
19
+ /** @param {string} file @param {string} text @param {Set<string>} known @returns {Array<{ to: string, names: string[] }>} */
20
+ export function pythonImports(file, text, known) {
21
+ const out = [];
22
+ for (const m of text.matchAll(REL)) {
23
+ const dir = path.posix.join(path.posix.dirname(file), ...Array(m[1].length - 1).fill(".."));
24
+ const to = path.posix.normalize(path.posix.join(dir, m[2].replace(/\./g, "/"))) + ".py";
25
+ if (known.has(to)) out.push({ to, names: namesIn(m[3]) });
26
+ }
27
+ for (const m of text.matchAll(FROM)) { const to = locate(file, m[1], known); if (to) out.push({ to, names: namesIn(m[2]) }); }
28
+ for (const m of text.matchAll(PLAIN)) for (const mod of m[1].split(",")) { const to = locate(file, mod.trim().split(/\s+as\s+/)[0], known); if (to) out.push({ to, names: ["*"] }); }
29
+ return out;
30
+ }
31
+ // MODULE_COMPLETE
@@ -0,0 +1,44 @@
1
+ // src/engine/relay-analysis.js — Boucle de relais de contexte (Phase 1 analyse) :
2
+ // chaque agent LLM analyse UN fragment dans sa zone de confort ; l'appel SUIVANT
3
+ // est ensemencé par le digest (constats acquis + reste + mission), même
4
+ // provider/modèle, cachePrefix (missionCtx) conservé. Digestion complète
5
+ // garantie par MAX_RELAYS (erreur relay_limit honnête sinon).
6
+ import { callLLM } from "../core/llm-call.js";
7
+ import { analysisPrompt } from "../core/llm-prompts.js";
8
+ import { withLanguage } from "../core/lang.js";
9
+ import { ANALYSIS_MAX_TOKENS } from "../core/constants.js";
10
+ import { runAnalysisPhase } from "./orchestrator-phases.js";
11
+ import { estimateTokens, localCapacity, readLocalWindow } from "./context-margin.js";
12
+ import { MAX_RELAYS, planRelay, relayLimitError, splitRelayFragments } from "./context-relay.js";
13
+ import { buildRelayDigest, buildRelayUserText } from "./relay-digest.js";
14
+
15
+ // Phase 1 avec relais de contexte : single-shot si la zone de confort suffit.
16
+ /** @param {ProviderPrimary} primary @param {string} smartModel @param {string} userText @param {string} missionCtx @param {(args: CallLLMArgs) => Promise<string>} [llmCall] @param {{ prefix: string, rest: string } | null} [split] même requête, doctrine en tête (appel unique seulement : le découpage en relais garde le texte complet) @returns {Promise<string>} */
17
+ export async function runAnalysisRelayed(primary, smartModel, userText, missionCtx, llmCall = callLLM, split = null) {
18
+ const capacity = localCapacity(readLocalWindow());
19
+ const plan = planRelay(estimateTokens(userText), capacity);
20
+ if (!plan.relayRequired) return split ? runAnalysisPhase(primary, smartModel, split.rest, split.prefix) : runAnalysisPhase(primary, smartModel, userText, missionCtx);
21
+ const fragments = splitRelayFragments(userText, plan.fragmentTokens);
22
+ if (fragments.length - 1 > MAX_RELAYS) throw relayLimitError(fragments.length - 1, capacity, estimateTokens(userText));
23
+ /** @type {string[]} */ const findings = [];
24
+ try {
25
+ for (let i = 0; i < fragments.length; i++) {
26
+ const { digest } = buildRelayDigest(
27
+ { findings: [...findings], remaining: `${fragments.length - i - 1} fragment(s) encore à analyser après celui-ci`, mission: missionCtx },
28
+ capacity
29
+ );
30
+ findings.push(await llmCall({
31
+ provider: primary.provider, key: primary.key, model: smartModel, base: primary.base,
32
+ systemPrompt: withLanguage(analysisPrompt()), cachePrefix: missionCtx,
33
+ maxTokens: ANALYSIS_MAX_TOKENS, userText: buildRelayUserText(digest, fragments[i]),
34
+ }));
35
+ }
36
+ return findings.join("\n\n");
37
+ } catch (err) {
38
+ const e0 = /** @type {EnrichedError} */ (err);
39
+ const e = /** @type {EnrichedError} */ (new Error(`Échec Phase 1 relais (analyse, ${primary.provider}) : ${e0.message}`));
40
+ e.code = "phase1_failed"; e.provider = primary.provider;
41
+ throw e;
42
+ }
43
+ }
44
+ // MODULE_COMPLETE
@@ -0,0 +1,42 @@
1
+ // src/engine/relay-digest.js — Digest de passation entre 2 agents (PUR) :
2
+ // (constats acquis, compactés) + (travail restant) + (mission/consignes).
3
+ // Le digest DOIT tenir sous le seuil de confort (vérifié via estimateTokens) ;
4
+ // débordement → troncature des constats les plus ANCIENS, signalée DANS le digest.
5
+ import { estimateTokens } from "./context-margin.js";
6
+
7
+ const DIGEST_RATIO = 0.4; // ≤ 40 % de la capacité : laisse la place au fragment suivant.
8
+
9
+ // Construit le digest de passation ; tronque les constats anciens si trop gros.
10
+ /** @param {{ findings: string[], remaining: string, mission: string }} pass
11
+ * @param {number} capacityTokens @returns {{ digest: string, truncated: number }} */
12
+ export function buildRelayDigest({ findings, remaining, mission }, capacityTokens) {
13
+ const cap = Math.max(1, Math.floor(Number(capacityTokens) || 0));
14
+ const maxTokens = Math.floor(cap * DIGEST_RATIO);
15
+ const list = (Array.isArray(findings) ? findings : []).map((f) => String(f ?? "")).filter(Boolean);
16
+ let dropped = 0;
17
+ const head = () => `${dropped ? `[Relais : ${dropped} constat(s) antérieur(s) tronqué(s) pour tenir dans la zone de confort]\n` : ""}MISSION (consignes inchangées) :\n${mission || "—"}`;
18
+ const tail = `TRAVAIL RESTANT :\n${remaining || "—"}`;
19
+ const render = () => [
20
+ head(),
21
+ `CONSTATS ACQUIS (agents précédents) :\n${list.length ? list.join("\n---\n") : "(aucun pour l'instant)"}`,
22
+ tail,
23
+ ].join("\n\n");
24
+ let digest = render();
25
+ while (estimateTokens(digest) > maxTokens && list.length > 1) { list.shift(); dropped += 1; digest = render(); }
26
+ if (estimateTokens(digest) > maxTokens && list.length === 1) {
27
+ const original = list[0];
28
+ list[0] = ""; // Digest à en-têtes seuls = budget exact restant pour le constat.
29
+ const headers = estimateTokens(render());
30
+ const room = Math.max(0, (maxTokens - headers) * 4 - 20);
31
+ list[0] = `[constat tronqué] ${original.slice(0, room)}`;
32
+ dropped += 1; digest = render();
33
+ }
34
+ return { digest, truncated: dropped };
35
+ }
36
+
37
+ // UserText du nouvel agent : digest de passation + fragment à analyser.
38
+ /** @param {string} digest @param {string} fragment @returns {string} */
39
+ export function buildRelayUserText(digest, fragment) {
40
+ return `${digest || ""}\n\n=== RELAI DE CONTEXTE — tu prends la suite des agents précédents, poursuis sans répéter ===\n\n${fragment}`;
41
+ }
42
+ // MODULE_COMPLETE
@@ -0,0 +1,25 @@
1
+ // src/engine/route-llm.js — Routage hybride PUR : l'agent local (Ollama /
2
+ // openai-compatible localhost) reste SOUVERAIN et prioritaire (gratuit, privé).
3
+ // Bascule vers le cloud BYOK strictement conditionnelle : (a) le local ne gère
4
+ // pas le multi-agent, (b) le contexte dépasse la marge de respiration locale,
5
+ // (c) sous-tâche lourde nécessitant un traitement déporté (accélérer, décharger).
6
+
7
+ // Base locale ? (localhost, IP privées). Sinon : cloud.
8
+ /** @param {string | null | undefined} base @returns {boolean} */
9
+ export function isLocalBase(base) {
10
+ const s = String(base || "").toLowerCase();
11
+ return /localhost|127\.0\.0\.1|\[::1\]|192\.168\.|(^|\.)10\.|172\.(1[6-9]|2\d|3[01])\./i.test(s);
12
+ }
13
+
14
+ // Décision de routage. Retourne { route, reason } — "none" si aucun provider.
15
+ // Ordre strict des conditions utilisateur : (a) → (b) → (c) → local souverain.
16
+ /** @param {RouteArgs} a @returns {RouteDecision} */
17
+ export function decideRoute({ local, secondary, promptTokens, capacityTokens, localMultiAgent, heavyTokens }) {
18
+ if (!local) return { route: secondary ? "cloud" : "none", reason: secondary ? "no_local" : "no_provider" };
19
+ if (!secondary) return { route: "local", reason: "local_only" };
20
+ if (localMultiAgent === false) return { route: "cloud", reason: "local_cant_multi_agent" }; // (a)
21
+ if (Number(promptTokens) > Number(capacityTokens)) return { route: "cloud", reason: "context_overflow" }; // (b)
22
+ if (Number(promptTokens) >= Number(heavyTokens)) return { route: "cloud", reason: "heavy_subtask" }; // (c)
23
+ return { route: "local", reason: "local_sovereign" };
24
+ }
25
+ // MODULE_COMPLETE
@@ -0,0 +1,12 @@
1
+ // src/engine/rule-text.js — Rendu des textes de règles SERVIS par le serveur : gabarit à variables {nom} et
2
+ // choix par langage. Aucun texte de règle ici : seulement la mécanique de substitution.
3
+
4
+ /** @param {string} template @param {Record<string, string>} vars @returns {string} */
5
+ export const fmt = (template, vars) => template.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? "");
6
+
7
+ /** @param {RuleMsg} m @param {boolean} isPy @returns {string} */
8
+ export const pick = (m, isPy) => (typeof m === "string" ? m : isPy ? m.py : m.js);
9
+
10
+ /** Violation « ast » à partir d'un message serveur. @param {ScanMsg} m @param {boolean} isPy @param {Record<string, string>} vars @param {string} matched @returns {Violation} */
11
+ export const astViolation = (m, isPy, vars, matched) => ({ kind: "ast", rule: fmt(pick(m.rule, isPy), vars), alternative: fmt(pick(m.alt, isPy), vars), matched: matched.slice(0, 60) });
12
+ // MODULE_COMPLETE
@@ -0,0 +1,33 @@
1
+ // src/engine/self-assessment.js — Autobilan du Modèle 1 (rédacteur) sur SON travail : ce qu'il n'a pas pu traiter (contexte trop court,
2
+ // précision insuffisante, parties incertaines). Un seul petit appel, uniquement quand un superviseur existe. Déclaratif : le Modèle 2 le
3
+ // reçoit comme une indication à VÉRIFIER, jamais comme une preuve. Échec → "unavailable" (le contrôle continue sans).
4
+ import { callLLM } from "../core/llm-call.js";
5
+
6
+ const SYSTEM = [
7
+ "Tu viens de rédiger le patch ci-dessous. Fais ton autobilan HONNÊTE sur ton propre travail, en 8 lignes maximum, sans recopier le patch.",
8
+ "Première ligne EXACTEMENT « DOUTES: AUCUN » ou « DOUTES: OUI ».",
9
+ "Si OUI, indique ce qui est incertain ou n'a pas pu être traité correctement : fichiers concernés, contexte trop long ou tronqué, précision insuffisante, tests non écrits, hypothèses faites.",
10
+ ].join("\n");
11
+ export const BILAN_MAX_CHARS = 1500;
12
+ const BILAN_MAX_TOKENS = 400;
13
+
14
+ /** @param {string} raw @returns {SelfAssessment} */
15
+ export function parseSelfAssessment(raw) {
16
+ const text = String(raw ?? "").trim().slice(0, BILAN_MAX_CHARS);
17
+ if (!text) return { state: "unavailable", text: "" };
18
+ return { state: /^DOUTES:\s*AUCUN\b/i.test(text) ? "none" : "doubts", text }; // format inattendu = doute (le plus prudent)
19
+ }
20
+
21
+ /** @param {{ writer: ProviderPrimary, analysis: string, patch: string, missionContext?: string, llmCall?: (a: CallLLMArgs) => Promise<string> }} v @returns {Promise<SelfAssessment>} */
22
+ export async function runSelfAssessment({ writer, analysis, patch, missionContext, llmCall = callLLM }) {
23
+ try {
24
+ const raw = await llmCall({
25
+ provider: writer.provider, key: writer.key, model: writer.model, base: writer.base, systemPrompt: SYSTEM,
26
+ cachePrefix: (missionContext || "").trim(), maxTokens: BILAN_MAX_TOKENS, userText: `Diagnostic:\n${analysis}\n\nPatch rédigé:\n${patch}`,
27
+ });
28
+ return parseSelfAssessment(raw);
29
+ } catch {
30
+ return { state: "unavailable", text: "" }; // autobilan impossible : le contrôle par points clés continue seul
31
+ }
32
+ }
33
+ // MODULE_COMPLETE
@@ -0,0 +1,19 @@
1
+ // src/engine/steps-block.js — Raisonnement du modèle (<think>…</think>, ou <thinking>) séparé de la réponse et présenté en bloc REPLIABLE
2
+ // (:::steps … ::: — rendu fermé par défaut par le chat). Les balises de dialogue ne sont jamais cherchées dans le raisonnement.
3
+ // Un raisonnement coupé (balise non fermée) est conservé comme tel. Pur.
4
+
5
+ const THINK = /<(think|thinking)>([\s\S]*?)(<\/\1>|$)/gi;
6
+
7
+ /** Réponse brute → { thinking, answer } (raisonnements réunis, réponse sans eux). @param {string} raw @returns {{ thinking: string, answer: string }} */
8
+ export function splitThinking(raw) {
9
+ /** @type {string[]} */ const parts = [];
10
+ const answer = String(raw).replace(THINK, (_m, _tag, inner) => { if (String(inner).trim()) parts.push(String(inner).trim()); return ""; });
11
+ return { thinking: parts.join("\n\n"), answer: answer.trim() };
12
+ }
13
+
14
+ /** Bloc repliable ("" si rien) ; une ligne « ::: » du raisonnement est neutralisée (elle fermerait le bloc). @param {string} thinking @returns {string} */
15
+ export function stepsBlock(thinking) {
16
+ const body = thinking.trim().replace(/^:::$/gm, " :::");
17
+ return body ? `:::steps\n${body}\n:::\n\n` : "";
18
+ }
19
+ // MODULE_COMPLETE
@@ -0,0 +1,43 @@
1
+ // src/engine/ux-violations-leak.js — RÈGLE "ZERO_DATA_LEAK" (sécurité absolue) :
2
+ // aucune donnée sensible ne doit jamais s'afficher dans l'UI (interface, toast,
3
+ // log front) : chemins locaux absolus, adresses IP de l'utilisateur, clés
4
+ // API/licence en clair → patch UI bloqué. Fusion non négociable sur TOUS les
5
+ // domaines via ensureDataLeakGuard (deterministe, LLM, re-parse markdown).
6
+
7
+ /** @type {DesignViolation[]} */
8
+ export const DATA_LEAK_VIOLATIONS = [
9
+ {
10
+ rule: "ZERO_DATA_LEAK — chemin local absolu exposé dans l'UI (interface/toast) : interdit.",
11
+ patterns: ["[A-Za-z]:[\\\\/]+Users", "/home/[A-Za-z0-9._-]+/", "/Users/"],
12
+ },
13
+ {
14
+ rule: "ZERO_DATA_LEAK — adresse IP (privée ou publique) exposée dans l'UI : interdite.",
15
+ patterns: ["\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"],
16
+ },
17
+ {
18
+ rule: "ZERO_DATA_LEAK — clé API ou licence en clair exposée dans l'UI : interdite.",
19
+ patterns: [
20
+ "sk-ant-[A-Za-z0-9-]{8,}", "sk-[A-Za-z0-9]{16,}", "AIza[A-Za-z0-9_-]{10,}",
21
+ "ghp_[A-Za-z0-9]{20,}", "PLUGCTO-[A-Za-z0-9-]{8,}", "Bearer\\s+[A-Za-z0-9._-]{16,}",
22
+ ],
23
+ },
24
+ ];
25
+
26
+ // Fusionne les violations ZERO_DATA_LEAK (non négociables, tous domaines) dans
27
+ // une liste existante, dédoublonnée par règle. Backfille aussi les patterns
28
+ // d'une règle relue sans ses regex (ex. re-parse markdown). Ne mute jamais
29
+ // l'entrée : chaque élément est recopié.
30
+ /** @param {DesignViolation[]} violations @returns {DesignViolation[]} */
31
+ export function ensureDataLeakGuard(violations) {
32
+ const out = violations.map((v) => ({ rule: v.rule, patterns: [...(v.patterns || [])] }));
33
+ for (const d of DATA_LEAK_VIOLATIONS) {
34
+ const ex = out.find((v) => v.rule === d.rule);
35
+ if (ex) {
36
+ if (!ex.patterns.length) ex.patterns = [...d.patterns];
37
+ } else {
38
+ out.push({ rule: d.rule, patterns: [...d.patterns] });
39
+ }
40
+ }
41
+ return out;
42
+ }
43
+ // MODULE_COMPLETE