pipcode 0.0.0-stage → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +47 -0
- package/README.md +44 -2
- package/auto.js +40 -0
- package/bin/setup.js +31 -0
- package/bin/teardown.js +18 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/index.js +39 -0
- package/package.json +45 -4
- package/src/auth/auth-email.js +32 -0
- package/src/auth/trusted-key.js +13 -0
- package/src/auto/adjust.js +41 -0
- package/src/auto/amnesia.js +37 -0
- package/src/auto/anonymize-tasks.js +13 -0
- package/src/auto/apply-diff-backed.js +10 -0
- package/src/auto/apply-diff.js +60 -0
- package/src/auto/ask-guard.js +16 -0
- package/src/auto/ask-pause.js +36 -0
- package/src/auto/ask-protocol.js +6 -0
- package/src/auto/ask-user.js +29 -0
- package/src/auto/backup-touched.js +30 -0
- package/src/auto/budget.js +52 -0
- package/src/auto/cancel.js +20 -0
- package/src/auto/constants.js +30 -0
- package/src/auto/crypto-util.js +17 -0
- package/src/auto/decompose.js +47 -0
- package/src/auto/diff.js +19 -0
- package/src/auto/fs-list.js +34 -0
- package/src/auto/git-base.js +16 -0
- package/src/auto/git-init.js +39 -0
- package/src/auto/git-restore.js +34 -0
- package/src/auto/git-scope.js +10 -0
- package/src/auto/git.js +38 -0
- package/src/auto/mission-docs.js +29 -0
- package/src/auto/mission-line.js +22 -0
- package/src/auto/node-check.js +30 -0
- package/src/auto/observe.js +23 -0
- package/src/auto/orchestrator.js +30 -0
- package/src/auto/patch-ask.js +16 -0
- package/src/auto/paths.js +18 -0
- package/src/auto/plan.js +42 -0
- package/src/auto/pricing.js +49 -0
- package/src/auto/project-files.js +12 -0
- package/src/auto/prompts.js +24 -0
- package/src/auto/quote-format.js +33 -0
- package/src/auto/retry.js +40 -0
- package/src/auto/run-lock.js +40 -0
- package/src/auto/run-loop.js +38 -0
- package/src/auto/run.js +49 -0
- package/src/auto/snapshot-gate.js +51 -0
- package/src/auto/snapshot-paths.js +33 -0
- package/src/auto/snapshot-restore.js +59 -0
- package/src/auto/snapshot-take.js +30 -0
- package/src/auto/state.js +52 -0
- package/src/auto/step-commit.js +46 -0
- package/src/auto/step-green.js +38 -0
- package/src/auto/step-loop.js +56 -0
- package/src/auto/step-prep.js +60 -0
- package/src/auto/step-qa.js +53 -0
- package/src/auto/step.js +30 -0
- package/src/auto/stop-options.js +34 -0
- package/src/auto/stop-report.js +17 -0
- package/src/auto/tasks.js +44 -0
- package/src/cli/apply-loop.js +29 -0
- package/src/cli/apply-toml.js +26 -0
- package/src/cli/apply.js +45 -0
- package/src/cli/ask-prepare.js +37 -0
- package/src/cli/auth-branches.js +53 -0
- package/src/cli/auth-interactive.js +46 -0
- package/src/cli/auto-ask.js +33 -0
- package/src/cli/auto-audit.js +16 -0
- package/src/cli/auto-cli.js +46 -0
- package/src/cli/auto-consent.js +22 -0
- package/src/cli/auto-flags.js +39 -0
- package/src/cli/auto-gate-session.js +27 -0
- package/src/cli/auto-gate.js +34 -0
- package/src/cli/auto-loader.js +17 -0
- package/src/cli/auto-plan.js +36 -0
- package/src/cli/auto-preflight.js +35 -0
- package/src/cli/auto-quote.js +28 -0
- package/src/cli/auto-report.js +36 -0
- package/src/cli/auto-resume.js +28 -0
- package/src/cli/auto-rules.js +20 -0
- package/src/cli/auto-run.js +26 -0
- package/src/cli/auto-task.js +26 -0
- package/src/cli/banner.js +20 -0
- package/src/cli/centralize.js +23 -0
- package/src/cli/collect-step.js +25 -0
- package/src/cli/config-pairs.js +46 -0
- package/src/cli/config.js +51 -0
- package/src/cli/detect-os.js +11 -0
- package/src/cli/dispatch.js +36 -0
- package/src/cli/env-file.js +44 -0
- package/src/cli/env-load.js +28 -0
- package/src/cli/env-parse.js +39 -0
- package/src/cli/env-rebuild.js +33 -0
- package/src/cli/gitignore-env.js +46 -0
- package/src/cli/help.js +28 -0
- package/src/cli/ide-entries.js +42 -0
- package/src/cli/ide-paths.js +37 -0
- package/src/cli/ide-roots.js +25 -0
- package/src/cli/interactive.js +26 -0
- package/src/cli/jetbrains.js +40 -0
- package/src/cli/legacy-entry.js +17 -0
- package/src/cli/main.js +42 -0
- package/src/cli/memory-cli.js +15 -0
- package/src/cli/prompts.js +24 -0
- package/src/cli/server-entry.js +42 -0
- package/src/cli/setup-args.js +39 -0
- package/src/cli/target-manual.js +24 -0
- package/src/cli/target-select.js +51 -0
- package/src/cli/toml-merge.js +56 -0
- package/src/cli/trial-check.js +15 -0
- package/src/cli/welcome.js +7 -0
- package/src/core/abort-signal.js +38 -0
- package/src/core/auto-deps.js +29 -0
- package/src/core/cache-ttl.js +21 -0
- package/src/core/config-base.js +38 -0
- package/src/core/config-cache.js +50 -0
- package/src/core/config.js +58 -0
- package/src/core/constants.js +31 -0
- package/src/core/crypto.js +45 -0
- package/src/core/dlp.js +51 -0
- package/src/core/doctrine-all.js +31 -0
- package/src/core/doctrine-caches.js +10 -0
- package/src/core/doctrine-fetch.js +49 -0
- package/src/core/doctrine-one.js +18 -0
- package/src/core/doctrine.js +39 -0
- package/src/core/dual-stream-parser.js +55 -0
- package/src/core/exports.js +28 -0
- package/src/core/google-route.js +17 -0
- package/src/core/identity.js +23 -0
- package/src/core/lang.js +19 -0
- package/src/core/leak-guard.js +40 -0
- package/src/core/llm-builder-anthropic.js +26 -0
- package/src/core/llm-builder-gemini.js +22 -0
- package/src/core/llm-builder-openai.js +26 -0
- package/src/core/llm-call.js +53 -0
- package/src/core/llm-config-secondary.js +28 -0
- package/src/core/llm-config.js +32 -0
- package/src/core/llm-errors.js +25 -0
- package/src/core/llm-exchange.js +27 -0
- package/src/core/llm-extract.js +52 -0
- package/src/core/llm-label.js +33 -0
- package/src/core/llm-ollama-request.js +32 -0
- package/src/core/llm-ollama.js +42 -0
- package/src/core/llm-prompts.js +14 -0
- package/src/core/llm-providers.js +26 -0
- package/src/core/llm-request.js +17 -0
- package/src/core/llm-retry-429.js +23 -0
- package/src/core/llm-stream-events.js +29 -0
- package/src/core/llm-stream.js +42 -0
- package/src/core/llm-unbounded.js +15 -0
- package/src/core/llm-warmup.js +44 -0
- package/src/core/manifest.js +42 -0
- package/src/core/master-prompt.js +11 -0
- package/src/core/model-id.js +19 -0
- package/src/core/ollama-lines.js +48 -0
- package/src/core/ollama.js +43 -0
- package/src/core/pkg-manager.js +35 -0
- package/src/core/process-guards.js +47 -0
- package/src/core/progress-emit.js +25 -0
- package/src/core/purge-key.js +20 -0
- package/src/core/reasoning-cut.js +19 -0
- package/src/core/reasoning-models.js +8 -0
- package/src/core/session-net.js +60 -0
- package/src/core/session-resilient.js +44 -0
- package/src/core/session.js +49 -0
- package/src/core/sse-parser.js +25 -0
- package/src/core/stdout-guard.js +11 -0
- package/src/core/tier.js +48 -0
- package/src/core/trial-resolve.js +22 -0
- package/src/core/trial-session.js +60 -0
- package/src/core/trial-status.js +35 -0
- package/src/core/usage-cost.js +25 -0
- package/src/core/usage-extract.js +28 -0
- package/src/core/usage-meter.js +41 -0
- package/src/engine/analyze-need.js +24 -0
- package/src/engine/analyze-ux.js +42 -0
- package/src/engine/ask-history.js +43 -0
- package/src/engine/ask-llm.js +40 -0
- package/src/engine/ask-prompt.js +48 -0
- package/src/engine/ask-rules.js +13 -0
- package/src/engine/ast-call-rules-danger.js +24 -0
- package/src/engine/ast-call-rules-inject.js +29 -0
- package/src/engine/ast-context.js +28 -0
- package/src/engine/ast-import-helpers.js +46 -0
- package/src/engine/ast-lang.js +24 -0
- package/src/engine/ast-node-helpers.js +51 -0
- package/src/engine/ast-rules-calls.js +16 -0
- package/src/engine/ast-rules-graph.js +58 -0
- package/src/engine/ast-rules-js.js +30 -0
- package/src/engine/ast-rules-jsx-py.js +26 -0
- package/src/engine/ast-runtime.js +60 -0
- package/src/engine/ast-scan.js +48 -0
- package/src/engine/audit-citations.js +28 -0
- package/src/engine/audit-facts.js +47 -0
- package/src/engine/audit-report.js +40 -0
- package/src/engine/audit-task.js +37 -0
- package/src/engine/cap-cache.js +50 -0
- package/src/engine/cap-eval.js +49 -0
- package/src/engine/cap-http.js +32 -0
- package/src/engine/cap-meta-anthropic.js +15 -0
- package/src/engine/cap-meta-ollama.js +15 -0
- package/src/engine/cap-meta-openrouter.js +19 -0
- package/src/engine/cap-meta.js +19 -0
- package/src/engine/cap-probe.js +34 -0
- package/src/engine/cap-types.js +29 -0
- package/src/engine/chat-doctrine.js +31 -0
- package/src/engine/checkpoint-control.js +23 -0
- package/src/engine/checkpoint-detect.js +19 -0
- package/src/engine/checkpoint-rules.js +19 -0
- package/src/engine/code-decls.js +12 -0
- package/src/engine/code-graph.js +52 -0
- package/src/engine/coerce.js +8 -0
- package/src/engine/collect.js +21 -0
- package/src/engine/compute-impact.js +17 -0
- package/src/engine/context-files.js +42 -0
- package/src/engine/context-fit.js +28 -0
- package/src/engine/context-margin.js +37 -0
- package/src/engine/context-relay.js +60 -0
- package/src/engine/control-duo.js +38 -0
- package/src/engine/control-phase.js +32 -0
- package/src/engine/control-prompt.js +25 -0
- package/src/engine/control-verdict.js +36 -0
- package/src/engine/default-steps.js +7 -0
- package/src/engine/deps-parse-js.js +49 -0
- package/src/engine/deps-parse-more.js +30 -0
- package/src/engine/deps-parse-other.js +41 -0
- package/src/engine/deps-parse.js +31 -0
- package/src/engine/det-builders-1.js +38 -0
- package/src/engine/det-builders-2.js +50 -0
- package/src/engine/detect-domains.js +27 -0
- package/src/engine/diff-files.js +35 -0
- package/src/engine/doc-suite.js +39 -0
- package/src/engine/doctrine-prefix.js +13 -0
- package/src/engine/domain-regex.js +11 -0
- package/src/engine/exports.js +35 -0
- package/src/engine/extract-deterministic.js +33 -0
- package/src/engine/extract-llm.js +48 -0
- package/src/engine/facts-binary.js +12 -0
- package/src/engine/facts-embedded.js +18 -0
- package/src/engine/facts-functions.js +21 -0
- package/src/engine/facts-html.js +22 -0
- package/src/engine/facts-inventory.js +33 -0
- package/src/engine/facts-lang.js +34 -0
- package/src/engine/facts-measure.js +43 -0
- package/src/engine/facts-patterns.js +28 -0
- package/src/engine/facts-project.js +26 -0
- package/src/engine/facts-size.js +26 -0
- package/src/engine/facts-wordpress.js +36 -0
- package/src/engine/generate-architecture-guide.js +46 -0
- package/src/engine/generate-readme.js +38 -0
- package/src/engine/generate-runbook.js +47 -0
- package/src/engine/git-facts.js +16 -0
- package/src/engine/history-purge.js +16 -0
- package/src/engine/json-balance.js +37 -0
- package/src/engine/json-safe.js +13 -0
- package/src/engine/line-numbers.js +16 -0
- package/src/engine/md-build-mid.js +35 -0
- package/src/engine/md-build-top.js +58 -0
- package/src/engine/md-build.js +36 -0
- package/src/engine/md-parse-helpers.js +7 -0
- package/src/engine/md-parse-pillars.js +38 -0
- package/src/engine/md-parse-ux-design.js +31 -0
- package/src/engine/md-parse.js +33 -0
- package/src/engine/mission-context-prefix.js +30 -0
- package/src/engine/mission-guard.js +36 -0
- package/src/engine/mission-helpers.js +23 -0
- package/src/engine/mission-log.js +56 -0
- package/src/engine/mission-paths.js +7 -0
- package/src/engine/mission-store.js +51 -0
- package/src/engine/normalize-extraction.js +35 -0
- package/src/engine/normalize-impact-design.js +39 -0
- package/src/engine/normalize-pillars.js +36 -0
- package/src/engine/orchestrate-route.js +39 -0
- package/src/engine/orchestrator-build.js +37 -0
- package/src/engine/orchestrator-phases.js +55 -0
- package/src/engine/orchestrator.js +40 -0
- package/src/engine/osv.js +43 -0
- package/src/engine/parse-json-loose.js +36 -0
- package/src/engine/patch-file-sections.js +28 -0
- package/src/engine/patch-scan-cheat.js +52 -0
- package/src/engine/patch-scan-prep.js +37 -0
- package/src/engine/patch-scan-rules.js +51 -0
- package/src/engine/patch-scan.js +38 -0
- package/src/engine/patch-secret-guard.js +50 -0
- package/src/engine/patch-ui-guard.js +49 -0
- package/src/engine/pattern-sanitize.js +23 -0
- package/src/engine/plan-draft.js +37 -0
- package/src/engine/plan-mission.js +60 -0
- package/src/engine/plan-reply-legacy.js +34 -0
- package/src/engine/plan-reply-pillars.js +34 -0
- package/src/engine/plan-reply-ux.js +17 -0
- package/src/engine/plan-reply.js +28 -0
- package/src/engine/plan-summary.js +16 -0
- package/src/engine/preflight-text.js +27 -0
- package/src/engine/preflight.js +27 -0
- package/src/engine/project-brief.js +41 -0
- package/src/engine/project-context.js +49 -0
- package/src/engine/project-instructions.js +19 -0
- package/src/engine/py-imports.js +31 -0
- package/src/engine/relay-analysis.js +44 -0
- package/src/engine/relay-digest.js +42 -0
- package/src/engine/route-llm.js +25 -0
- package/src/engine/rule-text.js +12 -0
- package/src/engine/self-assessment.js +33 -0
- package/src/engine/steps-block.js +19 -0
- package/src/engine/ux-violations-leak.js +43 -0
- package/src/engine/vuln-facts.js +37 -0
- package/src/fs/atomic-text.js +45 -0
- package/src/fs/atomic-write.js +21 -0
- package/src/fs/backup-once.js +27 -0
- package/src/fs/config-path.js +39 -0
- package/src/fs/config-read.js +23 -0
- package/src/fs/config-write.js +60 -0
- package/src/fs/home-root.js +55 -0
- package/src/fs/load-config.js +36 -0
- package/src/fs/os-errors.js +26 -0
- package/src/fs/permissions.js +48 -0
- package/src/mcp/boot-warmup.js +9 -0
- package/src/mcp/call-tool.js +20 -0
- package/src/mcp/client-info.js +35 -0
- package/src/mcp/engine-rules.js +16 -0
- package/src/mcp/handle-dlp.js +37 -0
- package/src/mcp/handle-licence.js +46 -0
- package/src/mcp/handle-list.js +15 -0
- package/src/mcp/handle-memory.js +37 -0
- package/src/mcp/handle-skill-fetch.js +50 -0
- package/src/mcp/handle-skill.js +38 -0
- package/src/mcp/licence-text.js +29 -0
- package/src/mcp/prompt-licence.js +11 -0
- package/src/mcp/server-create.js +33 -0
- package/src/mcp/server-main.js +39 -0
- package/src/mcp/tool-dispatch.js +28 -0
- package/src/mcp/tool-dlp.js +20 -0
- package/src/mcp/tool-licence.js +17 -0
- package/src/mcp/tool-memory.js +32 -0
- package/src/mcp/tool-skill.js +24 -0
- package/src/mcp/tools-list.js +13 -0
- package/src/memory/exports.js +5 -0
- package/src/memory/memory-filter.js +32 -0
- package/src/memory/memory-gate.js +21 -0
- package/src/memory/memory-store.js +44 -0
- package/src/net/auth-browser.js +49 -0
- package/src/net/auth-endpoints.js +33 -0
- package/src/net/local-fetch.js +31 -0
- package/src/net/loopback.js +8 -0
- package/src/net/proxy-env.js +59 -0
- package/src/project-memory/batches.js +17 -0
- package/src/project-memory/cahiers.js +42 -0
- package/src/project-memory/decision-settle.js +15 -0
- package/src/project-memory/decisions.js +36 -0
- package/src/project-memory/index-build.js +35 -0
- package/src/project-memory/memory-build.js +16 -0
- package/src/project-memory/memory-tags.js +40 -0
- package/src/project-memory/pending.js +24 -0
- package/src/project-memory/qa-apply.js +39 -0
- package/src/project-memory/qa-tags.js +28 -0
- package/src/project-memory/recall.js +38 -0
- package/src/project-memory/store.js +37 -0
- package/src/project-memory/window.js +13 -0
- package/src/rules/rules-advisor.js +17 -0
- package/src/rules/rules-frugality.js +17 -0
- package/src/rules/rules-load.js +56 -0
- package/src/rules/rules-project.js +16 -0
- package/src/rules/rules-registry.js +32 -0
- package/src/rules/rules-validate-pricing.js +22 -0
- package/src/rules/rules-validate-scan.js +45 -0
- package/src/rules/rules-validate.js +53 -0
- package/src/teardown/consent.js +31 -0
- package/src/teardown/flags.js +39 -0
- package/src/teardown/main.js +41 -0
- package/src/teardown/plan.js +31 -0
- package/src/teardown/remove-entry.js +39 -0
- package/src/teardown/remove-file.js +37 -0
- package/src/teardown/remove-toml.js +33 -0
- package/src/teardown/report.js +34 -0
- package/src/types/ast.d.ts +40 -0
- package/src/types/auto-ask.d.ts +6 -0
- package/src/types/auto-orch.d.ts +59 -0
- package/src/types/auto.d.ts +45 -0
- package/src/types/capability.d.ts +12 -0
- package/src/types/cli-auto-gate.d.ts +45 -0
- package/src/types/cli-auto.d.ts +42 -0
- package/src/types/config.d.ts +53 -0
- package/src/types/control.d.ts +56 -0
- package/src/types/docs.d.ts +6 -0
- package/src/types/doctrine.d.ts +22 -0
- package/src/types/llm.d.ts +28 -0
- package/src/types/mcp.d.ts +32 -0
- package/src/types/memory.d.ts +17 -0
- package/src/types/mission.d.ts +46 -0
- package/src/types/orchestrator.d.ts +33 -0
- package/src/types/routing.d.ts +25 -0
- package/src/types/rules.d.ts +30 -0
- package/src/types/snapshots.d.ts +25 -0
- package/src/types/teardown.d.ts +13 -0
- package/src/types/usage.d.ts +30 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/engine/context-fit.js — Placement SANS PERTE dans un budget : on ajoute des éléments ENTIERS tant qu'ils tiennent ; ce qui
|
|
2
|
+
// ne tient pas est listé (le modèle sait ce qui lui manque). Un fichier plus grand que le budget restant est transmis par
|
|
3
|
+
// parties successives (début d'abord) : le reste est annoncé, jamais coupé en silence.
|
|
4
|
+
|
|
5
|
+
import { splitLossless } from "../project-memory/batches.js";
|
|
6
|
+
|
|
7
|
+
/** Lignes entières dans le budget. @param {string[]} lines @param {number} budget @returns {{ text: string, omitted: number }} */
|
|
8
|
+
export function fitLines(lines, budget) {
|
|
9
|
+
const kept = []; let used = 0;
|
|
10
|
+
for (const l of lines) { if (used + l.length + 1 > budget) break; used += l.length + 1; kept.push(l); }
|
|
11
|
+
return { text: kept.join("\n"), omitted: lines.length - kept.length };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Fichiers entiers (ou 1re partie) dans le budget ; `left` = fichiers/parties non chargés. @param {Array<{ path: string, text: string }>} files @param {number} budget @returns {{ blocks: string[], left: string[] }} */
|
|
15
|
+
export function fitFiles(files, budget) {
|
|
16
|
+
const blocks = []; const left = []; let used = 0;
|
|
17
|
+
for (const f of files) {
|
|
18
|
+
const room = budget - used - f.path.length - 20;
|
|
19
|
+
if (f.text.length <= room) { blocks.push(`### ${f.path}\n${f.text}`); used += f.text.length + f.path.length + 10; continue; }
|
|
20
|
+
const parts = room > 500 && used === 0 ? splitLossless(f.text, room) : []; // seul le 1er élément (le plus pertinent) peut être servi par parties
|
|
21
|
+
if (parts.length) {
|
|
22
|
+
blocks.push(`### ${f.path} (partie 1/${parts.length}, lignes 1-${parts[0].text.split("\n").length - 1})\n${parts[0].text}`);
|
|
23
|
+
used = budget; left.push(`${f.path} (parties 2-${parts.length}, à partir de la ligne ${parts[1]?.fromLine})`);
|
|
24
|
+
} else left.push(f.path);
|
|
25
|
+
}
|
|
26
|
+
return { blocks, left };
|
|
27
|
+
}
|
|
28
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/engine/context-margin.js — Marge de respiration contextuelle du modèle
|
|
2
|
+
// local : 20 % de la fenêtre restent réservés (anti-saturation/anti-hallucination).
|
|
3
|
+
// Estimation de tokens (~4 chars/token), fenêtre locale et seuil de sous-tâche
|
|
4
|
+
// lourde configurables par env. Pur, aucune I/O.
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_LOCAL_WINDOW = 32768; // fenêtre locale par défaut (tokens).
|
|
7
|
+
export const BREATHING_RATIO = 0.8; // 20 % de marge de respiration réservée.
|
|
8
|
+
export const DEFAULT_HEAVY_TOKENS = 6000; // seuil de sous-tâche lourde (déport cloud).
|
|
9
|
+
|
|
10
|
+
// Estimation prudente du nombre de tokens (~4 chars/token).
|
|
11
|
+
/** @param {unknown} text @returns {number} */
|
|
12
|
+
export function estimateTokens(text) {
|
|
13
|
+
const s = typeof text === "string" ? text : String(text ?? "");
|
|
14
|
+
return Math.ceil(s.length / 4);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Capacité réelle du local = fenêtre × 0.8 (marge de respiration déduite).
|
|
18
|
+
/** @param {number} windowTokens @returns {number} */
|
|
19
|
+
export function localCapacity(windowTokens) {
|
|
20
|
+
const w = Number(windowTokens) > 0 ? Math.floor(Number(windowTokens)) : DEFAULT_LOCAL_WINDOW;
|
|
21
|
+
return Math.floor(w * BREATHING_RATIO);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Fenêtre locale (env PLUGCTO_LOCAL_CONTEXT_WINDOW, défaut 32k).
|
|
25
|
+
/** @param {Record<string, string | undefined>} [env] @returns {number} */
|
|
26
|
+
export function readLocalWindow(env = process.env) {
|
|
27
|
+
const n = Number(env.PLUGCTO_LOCAL_CONTEXT_WINDOW);
|
|
28
|
+
return n > 0 ? Math.floor(n) : DEFAULT_LOCAL_WINDOW;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Seuil de sous-tâche lourde (env PLUGCTO_HEAVY_TOKENS).
|
|
32
|
+
/** @param {Record<string, string | undefined>} [env] @returns {number} */
|
|
33
|
+
export function readHeavyTokens(env = process.env) {
|
|
34
|
+
const n = Number(env.PLUGCTO_HEAVY_TOKENS);
|
|
35
|
+
return n > 0 ? Math.floor(n) : DEFAULT_HEAVY_TOKENS;
|
|
36
|
+
}
|
|
37
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/engine/context-relay.js — Relais de contexte « zone de confort » (PUR).
|
|
2
|
+
// Dès que le contexte approche la capacité confortable (fenêtre × 0.8), un nouvel
|
|
3
|
+
// agent LLM vierge prend le relais. MAX_RELAYS = terminaison garantie ; au-delà,
|
|
4
|
+
// erreur typée "relay_limit" honnête — jamais de troncature silencieuse.
|
|
5
|
+
import { localCapacity, readLocalWindow } from "./context-margin.js";
|
|
6
|
+
|
|
7
|
+
export const MAX_RELAYS = 8; // Plafond prudent : ~9 agents × zone de confort.
|
|
8
|
+
export const FRAGMENT_RATIO = 0.55; // Part de la capacité pour le fragment (reste = digest).
|
|
9
|
+
|
|
10
|
+
// Budget tokens d'un fragment (le digest de passation occupe le reste).
|
|
11
|
+
/** @param {number} capacityTokens @returns {number} */
|
|
12
|
+
export function fragmentBudget(capacityTokens) {
|
|
13
|
+
return Math.max(1, Math.floor((Number(capacityTokens) || 0) * FRAGMENT_RATIO));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Nombre de relais (agents SUPPLÉMENTAIRES) : 0 si tout tient en un seul agent.
|
|
17
|
+
/** @param {number} totalTokens @param {number} perAgentTokens @returns {number} */
|
|
18
|
+
export function countRelays(totalTokens, perAgentTokens) {
|
|
19
|
+
const cap = Math.max(1, Math.floor(Number(perAgentTokens) || 0));
|
|
20
|
+
const total = Math.max(0, Math.floor(Number(totalTokens) || 0));
|
|
21
|
+
return total === 0 ? 0 : Math.max(0, Math.ceil(total / cap) - 1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Erreur typée de plafond dépassé (analyse incomplète assumée, jamais feinte).
|
|
25
|
+
/** @param {number} relays @param {number} capacityTokens @param {number} totalTokens @returns {EnrichedError} */
|
|
26
|
+
export function relayLimitError(relays, capacityTokens, totalTokens) {
|
|
27
|
+
const err = /** @type {EnrichedError} */ (new Error(
|
|
28
|
+
`Limite de relais dépassée (${relays} > ${MAX_RELAYS}) : ${totalTokens} tokens accumulés pour une zone de confort de ${capacityTokens}. L'analyse est INCOMPLÈTE par design — refusez la tâche ou augmentez PLUGCTO_LOCAL_CONTEXT_WINDOW.`
|
|
29
|
+
));
|
|
30
|
+
err.code = "relay_limit";
|
|
31
|
+
return err;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Décision de relais : requis ? combien au total ?
|
|
35
|
+
* @param {number} totalTokens @param {number} [capacityTokens] @returns {{ relayRequired: boolean, relays: number, agents: number, capacity: number, fragmentTokens: number }} */
|
|
36
|
+
export function planRelay(totalTokens, capacityTokens = localCapacity(readLocalWindow())) {
|
|
37
|
+
const cap = Number(capacityTokens) > 0 ? Math.floor(Number(capacityTokens)) : localCapacity(readLocalWindow());
|
|
38
|
+
const frag = fragmentBudget(cap);
|
|
39
|
+
const relays = countRelays(totalTokens, frag);
|
|
40
|
+
if (relays > MAX_RELAYS) throw relayLimitError(relays, cap, Math.max(0, Math.floor(Number(totalTokens) || 0)));
|
|
41
|
+
return { relayRequired: relays > 0, relays, agents: relays + 1, capacity: cap, fragmentTokens: frag };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Découpe un texte en fragments ≤ fragmentTokens (frontières de ligne préservées).
|
|
45
|
+
/** @param {string} text @param {number} fragmentTokens @returns {string[]} */
|
|
46
|
+
export function splitRelayFragments(text, fragmentTokens) {
|
|
47
|
+
const s = typeof text === "string" ? text : "";
|
|
48
|
+
const budget = Math.max(1, Math.floor(Number(fragmentTokens) || 0)) * 4; // ~4 chars/token.
|
|
49
|
+
if (s.length <= budget) return [s];
|
|
50
|
+
const out = [];
|
|
51
|
+
let cur = "";
|
|
52
|
+
for (const line of s.split("\n")) {
|
|
53
|
+
if (cur && cur.length + line.length + 1 > budget) { out.push(cur); cur = ""; }
|
|
54
|
+
cur = cur ? `${cur}\n${line}` : line;
|
|
55
|
+
while (cur.length > budget) { out.push(cur.slice(0, budget)); cur = cur.slice(budget); }
|
|
56
|
+
}
|
|
57
|
+
if (cur) out.push(cur);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/engine/control-duo.js — Décision du duo (Phase 3) : si un provider
|
|
2
|
+
// SECONDAIRE est configuré, UN passage de contrôle borné relit le patch draft
|
|
3
|
+
// (incarnation doctrine §11 Doubt : le second modèle corrige les failles du
|
|
4
|
+
// premier). Sinon repli honnête : duo:false, comportement strictement identique
|
|
5
|
+
// au moteur historique — aucun faux « duo ». Secondaire identique au provider du
|
|
6
|
+
// patch = autorisé mais étiqueté (auto-doute, pas un duo distinct).
|
|
7
|
+
import { readSecondaryProvider } from "../core/llm-config-secondary.js";
|
|
8
|
+
import { runCheckpointControl } from "./checkpoint-control.js";
|
|
9
|
+
import { runSelfAssessment } from "./self-assessment.js";
|
|
10
|
+
|
|
11
|
+
// Applique (ou non) le contrôle duo : patch final + métadonnées honnêtes.
|
|
12
|
+
/** @param {ProviderPrimary} patchProvider provider ayant produit le draft
|
|
13
|
+
* @param {OrchestratorArgs} args
|
|
14
|
+
* @param {string} analysis @param {string} draftPatch
|
|
15
|
+
* @param {string} missionCtx @param {string} doctrine
|
|
16
|
+
* @param {Record<string, string | undefined>} [env]
|
|
17
|
+
* @param {(a: CallLLMArgs) => Promise<string>} [llmCall]
|
|
18
|
+
* @returns {Promise<DuoOutcome>} */
|
|
19
|
+
export async function runDuoControl(patchProvider, args, analysis, draftPatch, missionCtx, doctrine, env = process.env, llmCall) {
|
|
20
|
+
const { secondary } = readSecondaryProvider(env);
|
|
21
|
+
if (!secondary) {
|
|
22
|
+
return {
|
|
23
|
+
duo: false, duoSameProvider: false, patch: draftPatch,
|
|
24
|
+
control: { status: "not_configured", reason: "controle_non_configure", model: null },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const bilan = await runSelfAssessment({ writer: patchProvider, analysis, patch: draftPatch, missionContext: missionCtx, llmCall });
|
|
28
|
+
const outcome = await runCheckpointControl({
|
|
29
|
+
secondary, missionContext: missionCtx, doctrine, analysis, draftPatch,
|
|
30
|
+
code: args.code || "", selfAssessment: bilan.text, doubts: bilan.state === "doubts", llmCall,
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
duo: true, patch: outcome.patch,
|
|
34
|
+
duoSameProvider: secondary.provider === patchProvider.provider && secondary.base === patchProvider.base,
|
|
35
|
+
control: { status: outcome.status, reason: outcome.reason, model: outcome.model, checkpoints: outcome.checkpoints, selfAssessment: bilan.state },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/engine/control-phase.js — Phase 3 duo : UN seul passage de contrôle borné
|
|
2
|
+
// (jamais de boucle) où le modèle SECONDAIRE relit le patch draft via callLLM
|
|
3
|
+
// (clé du secondaire en header, jamais dans le prompt). Échec LLM → fail-safe :
|
|
4
|
+
// draft conservé + raison typée (aucun crash, aucun catch vide).
|
|
5
|
+
import { callLLM } from "../core/llm-call.js";
|
|
6
|
+
import { patchMaxTokens } from "../core/constants.js";
|
|
7
|
+
import { controlPrompt, buildControlUserText } from "./control-prompt.js";
|
|
8
|
+
import { parseControlVerdict } from "./control-verdict.js";
|
|
9
|
+
|
|
10
|
+
// Exécute le passage de contrôle. Retourne TOUJOURS un patch sûr : le draft est
|
|
11
|
+
// conservé en cas d'échec LLM ou de verdict rejeté ; seul un verdict validé le remplace.
|
|
12
|
+
/** @param {{ secondary: ProviderPrimary, missionContext?: string, doctrine?: string, analysis: string, draftPatch: string, code: string, selfAssessment?: string, llmCall?: (a: CallLLMArgs) => Promise<string> }} v
|
|
13
|
+
* @returns {Promise<ControlPhaseOutcome>} */
|
|
14
|
+
export async function runControlPhase({ secondary, missionContext, doctrine, analysis, draftPatch, code, selfAssessment, llmCall = callLLM }) {
|
|
15
|
+
const userText = buildControlUserText({ missionContext, doctrine, analysis, draftPatch, code, selfAssessment });
|
|
16
|
+
const model = secondary.smartModel || secondary.model;
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = await llmCall({
|
|
20
|
+
provider: secondary.provider, key: secondary.key, model, base: secondary.base,
|
|
21
|
+
systemPrompt: controlPrompt(), cachePrefix: (missionContext || "").trim(),
|
|
22
|
+
maxTokens: patchMaxTokens(code.length), userText,
|
|
23
|
+
});
|
|
24
|
+
} catch (err) {
|
|
25
|
+
const e0 = /** @type {Error} */ (err);
|
|
26
|
+
return { status: "failed", reason: `controle_echec_llm: ${e0.message}`.slice(0, 200), model, patch: draftPatch };
|
|
27
|
+
}
|
|
28
|
+
const verdict = parseControlVerdict(raw);
|
|
29
|
+
const patch = verdict.status === "corrected" && verdict.patch ? verdict.patch : draftPatch;
|
|
30
|
+
return { status: verdict.status, reason: verdict.reason, model, patch };
|
|
31
|
+
}
|
|
32
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/engine/control-prompt.js — Phase 3 duo : prompt de contrôle/doute (byte-stable
|
|
2
|
+
// pour prompt caching). Le modèle SECONDAIRE reçoit demande + diagnostic + patch
|
|
3
|
+
// draft + code, et doit produire un verdict STRICT (OK ou patch corrigé). Aucune
|
|
4
|
+
// clé n'y figure jamais : uniquement des textes (anti-fuite BYOK).
|
|
5
|
+
|
|
6
|
+
import { getRules } from "../rules/rules-registry.js";
|
|
7
|
+
|
|
8
|
+
/** Prompt de contrôle (texte servi par le serveur, lot Pro) — byte-stable tant que les règles sont en mémoire. @returns {string} */
|
|
9
|
+
export const controlPrompt = () => getRules().prompts.control;
|
|
10
|
+
|
|
11
|
+
// userText du contrôle : demande (mission) + doctrine + diagnostic + draft + code.
|
|
12
|
+
/** @param {{ missionContext?: string, doctrine?: string, analysis: string, draftPatch: string, code: string, selfAssessment?: string }} v @returns {string} */
|
|
13
|
+
export function buildControlUserText({ missionContext, doctrine, analysis, draftPatch, code, selfAssessment }) {
|
|
14
|
+
const parts = [];
|
|
15
|
+
const mission = (missionContext || "").trim();
|
|
16
|
+
const rules = (doctrine || "").trim();
|
|
17
|
+
if (mission) parts.push("Demande (mission):\n" + mission);
|
|
18
|
+
if (rules) parts.push("Doctrine (règles absolues):\n" + rules);
|
|
19
|
+
parts.push("Diagnostic (IA de cadrage):\n" + analysis);
|
|
20
|
+
if (selfAssessment) parts.push("Autobilan du Modèle 1 (déclaratif : une indication à vérifier, jamais une preuve):\n" + selfAssessment);
|
|
21
|
+
parts.push("Patch proposé (draft de l'IA de patch, à contrôler):\n" + draftPatch);
|
|
22
|
+
parts.push("Code:\n" + code);
|
|
23
|
+
return parts.join("\n\n");
|
|
24
|
+
}
|
|
25
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/engine/control-verdict.js — Validation STRICTE de la sortie du contrôle
|
|
2
|
+
// (frontière externe LLM, Zero Trust) : format « VERDICT: OK » / « VERDICT: PATCH »
|
|
3
|
+
// + Unified Diff. Toute sortie non conforme est REJETÉE : le draft est conservé,
|
|
4
|
+
// une sortie non validée n'est JAMAIS appliquée.
|
|
5
|
+
|
|
6
|
+
export const VERDICT_OK = "VERDICT: OK";
|
|
7
|
+
export const VERDICT_PATCH = "VERDICT: PATCH";
|
|
8
|
+
|
|
9
|
+
// Raisons typées remontées honnêtement dans les métadonnées (jamais de vague silence).
|
|
10
|
+
export const REJECT_NON_TEXT = "controle_sortie_non_texte";
|
|
11
|
+
export const REJECT_EMPTY = "controle_sortie_vide";
|
|
12
|
+
export const REJECT_NO_VERDICT = "controle_sans_verdict";
|
|
13
|
+
export const REJECT_NO_DIFF = "controle_patch_corrige_absent";
|
|
14
|
+
export const OK_REASON = "controle_ok";
|
|
15
|
+
export const CORRECTED_REASON = "controle_patch_corrige";
|
|
16
|
+
|
|
17
|
+
/** @param {string} reason @returns {ControlVerdict} */
|
|
18
|
+
function rejected(reason) {
|
|
19
|
+
return { status: "rejected", reason, patch: null };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Parse la sortie du contrôle. Strict : casse exacte, verdict unique, diff réel (@@).
|
|
23
|
+
/** @param {unknown} raw @returns {ControlVerdict} */
|
|
24
|
+
export function parseControlVerdict(raw) {
|
|
25
|
+
if (typeof raw !== "string") return rejected(REJECT_NON_TEXT);
|
|
26
|
+
const text = raw.trim();
|
|
27
|
+
if (!text) return rejected(REJECT_EMPTY);
|
|
28
|
+
const nl = text.indexOf("\n");
|
|
29
|
+
const first = (nl === -1 ? text : text.slice(0, nl)).trim();
|
|
30
|
+
const rest = nl === -1 ? "" : text.slice(nl + 1).trim();
|
|
31
|
+
if (first === VERDICT_OK) return { status: "ok", reason: OK_REASON, patch: null };
|
|
32
|
+
if (first !== VERDICT_PATCH) return rejected(REJECT_NO_VERDICT);
|
|
33
|
+
if (!rest || !rest.includes("@@")) return rejected(REJECT_NO_DIFF);
|
|
34
|
+
return { status: "corrected", reason: CORRECTED_REASON, patch: rest };
|
|
35
|
+
}
|
|
36
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// src/engine/default-steps.js — Étapes canoniques du plan mission (checklist 4) : TEXTE servi par le serveur
|
|
2
|
+
// (lot de base, accessible à tous les comptes) ; ici seulement l'accès au registre mémoire.
|
|
3
|
+
import { getRules } from "../rules/rules-registry.js";
|
|
4
|
+
|
|
5
|
+
/** @returns {string[]} */
|
|
6
|
+
export const defaultSteps = () => getRules().plan.steps;
|
|
7
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/engine/deps-parse-js.js — Dépendances npm : package-lock.json (versions EXACTES, transitives comprises) ou package.json (versions déclarées, approximatives). Texte → liste, jamais d'exécution ; JSON invalide = liste vide.
|
|
2
|
+
// Paquet PRIVÉ ou local (priv) : téléchargé hors du registre public, lien local, ou nom à portée (@x/…) sans verrou pour prouver sa source → jamais envoyé à OSV.
|
|
3
|
+
import { asObj, jsonObject } from "./json-safe.js";
|
|
4
|
+
/** @typedef {{ ecosystem: string, name: string, version: string, pinned: boolean, priv?: boolean }} Dep */
|
|
5
|
+
|
|
6
|
+
const PUBLIC_REGISTRY = /^https:\/\/registry\.(?:npmjs\.org|yarnpkg\.com)\//;
|
|
7
|
+
|
|
8
|
+
/** @param {string} name @param {unknown} raw entrée du verrou @returns {Dep | null} */
|
|
9
|
+
function lockEntry(name, raw) {
|
|
10
|
+
const v = asObj(raw);
|
|
11
|
+
return typeof v.version === "string" ? { ecosystem: "npm", name, version: v.version, pinned: true, priv: v.link === true || !PUBLIC_REGISTRY.test(String(v.resolved ?? "")) } : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @param {Record<string, unknown>} tree @param {Dep[]} out @returns {void} package-lock v1 : arbre « dependencies » imbriqué */
|
|
15
|
+
function walkV1(tree, out) {
|
|
16
|
+
for (const [name, raw] of Object.entries(tree)) {
|
|
17
|
+
const d = lockEntry(name, raw);
|
|
18
|
+
if (d) out.push(d);
|
|
19
|
+
walkV1(asObj(asObj(raw).dependencies), out);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @param {string} text @returns {Dep[]} */
|
|
24
|
+
export function parsePackageLock(text) {
|
|
25
|
+
const j = jsonObject(text);
|
|
26
|
+
/** @type {Dep[]} */ const out = [];
|
|
27
|
+
if (j.packages) {
|
|
28
|
+
for (const [k, raw] of Object.entries(asObj(j.packages))) {
|
|
29
|
+
const m = /(?:^|\/)node_modules\/((?:@[^/]+\/)?[^/]+)$/.exec(k);
|
|
30
|
+
const d = m ? lockEntry(m[1], raw) : null;
|
|
31
|
+
if (d) out.push(d);
|
|
32
|
+
}
|
|
33
|
+
} else walkV1(asObj(j.dependencies), out);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @param {string} text @returns {Dep[]} version = première version lisible de l'intervalle (« ^1.2.3 » → 1.2.3), « pinned » seulement si exacte */
|
|
38
|
+
export function parsePackageJson(text) {
|
|
39
|
+
const j = jsonObject(text);
|
|
40
|
+
/** @type {Dep[]} */ const out = [];
|
|
41
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
42
|
+
for (const [name, spec] of Object.entries(asObj(j[section]))) {
|
|
43
|
+
const v = /\d+\.\d+\.\d+(?:[-+][\w.]+)?/.exec(String(spec))?.[0] ?? "";
|
|
44
|
+
out.push({ ecosystem: "npm", name, version: v, pinned: v !== "" && String(spec).trim() === v, priv: name.startsWith("@") });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/engine/deps-parse-more.js — Dépendances PHP (composer.lock), Go (go.mod), Ruby (Gemfile.lock). Texte → liste, jamais d'exécution.
|
|
2
|
+
// Paquet PRIVÉ (priv) : téléchargé hors d'un hébergeur public, module du même propriétaire que le projet, section Git/chemin ou serveur de gemmes privé → jamais envoyé à OSV.
|
|
3
|
+
import { asArr, asObj, jsonObject } from "./json-safe.js";
|
|
4
|
+
/** @typedef {{ ecosystem: string, name: string, version: string, pinned: boolean, priv?: boolean }} Dep */
|
|
5
|
+
|
|
6
|
+
const PUBLIC_HOST = /^https:\/\/(?:api\.github\.com|github\.com|codeload\.github\.com|gitlab\.com|bitbucket\.org)\//;
|
|
7
|
+
|
|
8
|
+
/** @param {string} text @returns {Dep[]} */
|
|
9
|
+
export function parseComposerLock(text) {
|
|
10
|
+
const j = jsonObject(text);
|
|
11
|
+
return [...asArr(j.packages), ...asArr(j["packages-dev"])].map(asObj).filter((p) => typeof p.name === "string" && typeof p.version === "string")
|
|
12
|
+
.map((p) => ({ ecosystem: "Packagist", name: String(p.name), version: String(p.version).replace(/^v/, ""), pinned: true, priv: !PUBLIC_HOST.test(String(asObj(p.dist).url ?? "")) }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @param {string} text @returns {Dep[]} un module du même propriétaire que le projet (github.com/acme/…) ou du même domaine est considéré privé */
|
|
16
|
+
export function parseGoMod(text) {
|
|
17
|
+
const own = /^module\s+(\S+)/m.exec(text)?.[1] ?? "";
|
|
18
|
+
const parts = own.split("/");
|
|
19
|
+
const ownPrefix = parts[0] && /^(?:github\.com|gitlab\.com|bitbucket\.org)$/.test(parts[0]) ? parts.slice(0, 2).join("/") : parts[0];
|
|
20
|
+
return [...text.matchAll(/^\s*(?:require\s+)?([\w.~/-]+\.[\w.~/-]+)\s+v(\d[\w.+-]*)/gm)].map((m) => ({ ecosystem: "Go", name: m[1], version: m[2], pinned: true, priv: !!ownPrefix && (m[1] === ownPrefix || m[1].startsWith(`${ownPrefix}/`)) }));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @param {string} text @returns {Dep[]} seule la section GEM du serveur public rubygems.org est publique (GIT, PATH, autres serveurs = privés) */
|
|
24
|
+
export function parseGemfileLock(text) {
|
|
25
|
+
return text.split(/\n(?=[A-Z]+\n)/).flatMap((sec) => {
|
|
26
|
+
const pub = sec.startsWith("GEM") && /remote: https:\/\/rubygems\.org\/?\s*$/m.test(sec);
|
|
27
|
+
return [...sec.matchAll(/^ {4}([\w.-]+) \((\d[\w.]*)\)$/gm)].map((m) => ({ ecosystem: "RubyGems", name: m[1], version: m[2], pinned: true, priv: !pub }));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/engine/deps-parse-other.js — Dépendances Python (requirements.txt, Pipfile.lock, poetry.lock) et Rust (Cargo.lock). Texte → liste, jamais d'exécution.
|
|
2
|
+
// Paquet PRIVÉ (priv) : index privé déclaré, dépôt Git, chemin local, source autre que le registre public → jamais envoyé à OSV.
|
|
3
|
+
import { asObj, jsonObject } from "./json-safe.js";
|
|
4
|
+
/** @typedef {{ ecosystem: string, name: string, version: string, pinned: boolean, priv?: boolean }} Dep */
|
|
5
|
+
|
|
6
|
+
/** @param {string} n @returns {string} nom PyPI normalisé (PEP 503) */
|
|
7
|
+
const pypi = (n) => n.toLowerCase().replace(/[_.]+/g, "-");
|
|
8
|
+
const PRIVATE_INDEX = /^\s*(?:--index-url|--extra-index-url|-i)[\s=]+(?!https?:\/\/(?:[\w-]+\.)?pypi\.org)/m;
|
|
9
|
+
|
|
10
|
+
/** @param {string} text @returns {Dep[]} « nom==1.2.3 » est épinglée ; autre opérateur ou aucune version = non vérifiable ; index privé déclaré dans le fichier = tout le fichier privé */
|
|
11
|
+
export function parseRequirements(text) {
|
|
12
|
+
/** @type {Dep[]} */ const out = [];
|
|
13
|
+
const priv = PRIVATE_INDEX.test(text);
|
|
14
|
+
for (const raw of text.split("\n")) {
|
|
15
|
+
const line = raw.split("#")[0].trim();
|
|
16
|
+
const m = /^([A-Za-z0-9][A-Za-z0-9_.-]*)(?:\[[^\]]*\])?\s*(==|>=|<=|~=|!=|>|<)?\s*([\w.+!*-]*)/.exec(line);
|
|
17
|
+
if (!m) continue;
|
|
18
|
+
const pinned = m[2] === "==" && m[3] !== "" && !m[3].includes("*");
|
|
19
|
+
out.push({ ecosystem: "PyPI", name: pypi(m[1]), version: pinned ? m[3] : "", pinned, priv: priv || /@\s*(?:git\+|https?:|file:)/.test(line) });
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @param {string} text @returns {Dep[]} */
|
|
25
|
+
export function parsePipfileLock(text) {
|
|
26
|
+
const j = jsonObject(text);
|
|
27
|
+
return ["default", "develop"].flatMap((s) => Object.entries(asObj(j[s])).map(([name, v]) => ({ name, p: asObj(v) })))
|
|
28
|
+
.filter((x) => String(x.p.version ?? "").startsWith("==")).map((x) => ({ ecosystem: "PyPI", name: pypi(x.name), version: String(x.p.version).slice(2), pinned: true,
|
|
29
|
+
priv: (x.p.index !== undefined && x.p.index !== "pypi") || "git" in x.p || "path" in x.p || "file" in x.p || x.p.editable === true }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @param {string} text @param {string} ecosystem « PyPI » (poetry.lock) ou « crates.io » (Cargo.lock) @returns {Dep[]} blocs « [[package]] » ; poetry : un bloc [package.source] est privé ; Cargo : privé sans source de registre public */
|
|
33
|
+
export function parseTomlPackages(text, ecosystem) {
|
|
34
|
+
return text.split("[[package]]").slice(1).flatMap((block) => {
|
|
35
|
+
const m = /name = "([^"]+)"\s*\nversion = "([^"]+)"/.exec(block);
|
|
36
|
+
if (!m) return [];
|
|
37
|
+
const priv = ecosystem === "PyPI" ? /\[package\.source\]/.test(block) : !/source = "(?:registry\+https:\/\/github\.com\/rust-lang\/crates\.io-index|sparse\+https:\/\/index\.crates\.io)/.test(block);
|
|
38
|
+
return [{ ecosystem, name: ecosystem === "PyPI" ? pypi(m[1]) : m[1], version: m[2], pinned: true, priv }];
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/engine/deps-parse.js — Lit les fichiers de dépendances du projet (npm, PyPI, Packagist, Go, crates.io, RubyGems) et en tire la liste « écosystème / nom / version ». Lecture seule, bornée ; un package.json
|
|
2
|
+
// qui a son verrou voisin est ignoré (le verrou donne les versions exactes). Gestionnaires non pris en charge (Maven, Gradle, NuGet…) : signalés, jamais devinés.
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parsePackageLock, parsePackageJson } from "./deps-parse-js.js";
|
|
6
|
+
import { parseRequirements, parsePipfileLock, parseTomlPackages } from "./deps-parse-other.js";
|
|
7
|
+
import { parseComposerLock, parseGoMod, parseGemfileLock } from "./deps-parse-more.js";
|
|
8
|
+
|
|
9
|
+
/** @typedef {{ ecosystem: string, name: string, version: string, pinned: boolean, priv?: boolean }} Dep */
|
|
10
|
+
const MAX_DEPS = 3000;
|
|
11
|
+
const MAX_MANIFESTS = 12;
|
|
12
|
+
/** @type {Array<[RegExp, (text: string) => Dep[]]>} */
|
|
13
|
+
const PARSERS = [
|
|
14
|
+
[/(^|\/)package-lock\.json$/, parsePackageLock], [/(^|\/)package\.json$/, parsePackageJson], [/(^|\/)requirements[\w.-]*\.txt$/, parseRequirements],
|
|
15
|
+
[/(^|\/)Pipfile\.lock$/, parsePipfileLock], [/(^|\/)poetry\.lock$/, (t) => parseTomlPackages(t, "PyPI")], [/(^|\/)composer\.lock$/, parseComposerLock],
|
|
16
|
+
[/(^|\/)go\.mod$/, parseGoMod], [/(^|\/)Cargo\.lock$/, (t) => parseTomlPackages(t, "crates.io")], [/(^|\/)Gemfile\.lock$/, parseGemfileLock],
|
|
17
|
+
];
|
|
18
|
+
const UNSUPPORTED = /(^|\/)(pom\.xml|build\.gradle(\.kts)?|packages\.config|pubspec\.yaml|mix\.exs)$|\.csproj$/;
|
|
19
|
+
|
|
20
|
+
/** @param {string} root @param {string[]} files chemins relatifs « / » @returns {Promise<{ deps: Dep[], sources: string[], unsupported: string[] }>} */
|
|
21
|
+
export async function readDeps(root, files) {
|
|
22
|
+
const set = new Set(files);
|
|
23
|
+
const manifests = files.filter((f) => PARSERS.some(([re]) => re.test(f)) && !(f.endsWith("package.json") && set.has(f.replace(/package\.json$/, "package-lock.json")))).slice(0, MAX_MANIFESTS);
|
|
24
|
+
/** @type {Map<string, Dep>} */ const seen = new Map();
|
|
25
|
+
for (const f of manifests) {
|
|
26
|
+
const text = (await fs.readFile(path.join(root, f), "utf8").catch(() => "")).slice(0, 5_000_000);
|
|
27
|
+
for (const d of (PARSERS.find(([re]) => re.test(f))?.[1] ?? (() => []))(text)) seen.set(`${d.ecosystem}|${d.name}|${d.version}`, d);
|
|
28
|
+
}
|
|
29
|
+
return { deps: [...seen.values()].slice(0, MAX_DEPS), sources: manifests, unsupported: files.filter((f) => UNSUPPORTED.test(f)).slice(0, 5) };
|
|
30
|
+
}
|
|
31
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// src/engine/det-builders-1.js — Builders déterministes : personas + contraintes.
|
|
2
|
+
import { getRules } from "../rules/rules-registry.js";
|
|
3
|
+
|
|
4
|
+
// (a) Personas : un par rôle, mandate = première règle du domaine du rôle.
|
|
5
|
+
/** @param {{ key: string, label: string, roles: string[], regles: string[], contraintes: string[], failles: string[] }[]} domains @returns {{ role_name: string, scope: string, mandate: string }[]} */
|
|
6
|
+
export function buildPersonas(domains) {
|
|
7
|
+
const personas = [];
|
|
8
|
+
for (const d of domains) {
|
|
9
|
+
for (const role of d.roles) {
|
|
10
|
+
personas.push({
|
|
11
|
+
role_name: role,
|
|
12
|
+
scope: `${d.label} — périmètre projet`,
|
|
13
|
+
mandate: d.regles[0] || "Respecter la doctrine métier ; refuser tout shortcut non conforme.",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return personas;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// (b) Contraintes : textes (contraintes) + invariants (règles), enrichis des
|
|
21
|
+
// patterns d'extraction par domaine.
|
|
22
|
+
/** @param {{ key: string, label: string, regles: string[], contraintes: string[] }[]} domains @param {string[]} keys @returns {DomainConstraint[]} */
|
|
23
|
+
export function buildConstraints(domains, keys) {
|
|
24
|
+
/** @type {DomainConstraint[]} */
|
|
25
|
+
const constraints = [];
|
|
26
|
+
for (const d of domains) {
|
|
27
|
+
d.contraintes.forEach((c, i) => {
|
|
28
|
+
constraints.push({ text: c, invariant: d.regles[i] || d.regles[0] || "", patterns: [] });
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
for (const k of keys) {
|
|
32
|
+
for (const c of (getRules().patterns[k]?.constraints || [])) {
|
|
33
|
+
if (!constraints.some((x) => x.text === c.text)) constraints.push({ ...c });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return constraints;
|
|
37
|
+
}
|
|
38
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/engine/det-builders-2.js — Builders déterministes : failles + glossaire + impact.
|
|
2
|
+
import { getRules } from "../rules/rules-registry.js";
|
|
3
|
+
import { defaultSteps } from "./default-steps.js";
|
|
4
|
+
import { computeImpact } from "./compute-impact.js";
|
|
5
|
+
import { riskLevel } from "./mission-helpers.js";
|
|
6
|
+
|
|
7
|
+
// (c) Failles : never (failles) + always (règles), enrichies des patterns.
|
|
8
|
+
/** @param {{ key: string, regles: string[], failles: string[] }[]} domains @param {string[]} keys @returns {DomainPitfall[]} */
|
|
9
|
+
export function buildPitfalls(domains, keys) {
|
|
10
|
+
/** @type {DomainPitfall[]} */
|
|
11
|
+
const pitfalls = [];
|
|
12
|
+
for (const d of domains) {
|
|
13
|
+
d.failles.forEach((f, i) => {
|
|
14
|
+
pitfalls.push({ never: f, always: d.regles[i] || d.regles[0] || "", patterns: [] });
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
for (const k of keys) {
|
|
18
|
+
for (const p of (getRules().patterns[k]?.pitfalls || [])) {
|
|
19
|
+
if (!pitfalls.some((x) => x.never === p.never)) pitfalls.push({ ...p });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return pitfalls;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// (d) Glossaire : termes exacts par domaine détecté.
|
|
26
|
+
/** @param {string[]} keys @returns {GlossaryEntry[]} */
|
|
27
|
+
export function buildGlossary(keys) {
|
|
28
|
+
/** @type {GlossaryEntry[]} */
|
|
29
|
+
const glossary = [];
|
|
30
|
+
for (const k of keys) {
|
|
31
|
+
for (const g of (getRules().glossary[k] || [])) {
|
|
32
|
+
if (!glossary.some((x) => x.term === g.term)) glossary.push({ ...g });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return glossary;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// (e) Impact : fichiers, étapes, tokens, risque.
|
|
39
|
+
/** @param {string[]} keys @param {{ domains: { roles: string[], regles: string[], contraintes: string[], failles: string[] }[], roles: string[], regles: string[], contraintes: string[], failles: string[], fiches: string[] }} matrix @returns {{ files: string[], steps: string[], tokens: number, risk: string }} */
|
|
40
|
+
export function buildImpact(keys, matrix) {
|
|
41
|
+
const imp = computeImpact(matrix);
|
|
42
|
+
const files = keys.flatMap((k) => [`src/${k}/schema.sql`, `src/${k}/service.js`]).slice(0, imp.fichiers);
|
|
43
|
+
return {
|
|
44
|
+
files: files.length ? files : ["src/main.js"],
|
|
45
|
+
steps: [...defaultSteps()],
|
|
46
|
+
tokens: imp.tokens,
|
|
47
|
+
risk: riskLevel(keys),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/engine/detect-domains.js — Détection des domaines mobilisés + mapping slugs doctrine.
|
|
2
|
+
import { keywordRegex } from "./domain-regex.js";
|
|
3
|
+
import { getRules } from "../rules/rules-registry.js";
|
|
4
|
+
|
|
5
|
+
// Détecte les domaines mobilisés par un besoin (toujours au moins le domaine de repli).
|
|
6
|
+
/** @param {string} needText @returns {string[]} */
|
|
7
|
+
export function detectDomains(needText) {
|
|
8
|
+
const t = String(needText || "").toLowerCase();
|
|
9
|
+
const domains = getRules().domains;
|
|
10
|
+
const matched = Object.keys(domains).filter((k) => {
|
|
11
|
+
if (k === "logiciel_generique") return false; // repli géré séparément
|
|
12
|
+
return domains[k].keywords.some((kw) => keywordRegex(kw).test(t));
|
|
13
|
+
});
|
|
14
|
+
if (matched.length === 0) return ["logiciel_generique"];
|
|
15
|
+
return matched;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Mappe les domaines vers les slugs de fiches de doctrine (dédupliqués, présents au catalogue).
|
|
19
|
+
/** @param {string[]} domainKeys @returns {string[]} */
|
|
20
|
+
export function mapDomainsToSlugs(domainKeys) {
|
|
21
|
+
const slugs = new Set();
|
|
22
|
+
for (const k of domainKeys) {
|
|
23
|
+
for (const s of getRules().domains[k].fiches || []) slugs.add(s);
|
|
24
|
+
}
|
|
25
|
+
return [...slugs];
|
|
26
|
+
}
|
|
27
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/engine/diff-files.js — Patch (Unified Diff) découpé PAR FICHIER, recomposé, et fusion ciblée d'un correctif. Pur, sans I/O.
|
|
2
|
+
|
|
3
|
+
/** @param {string} line @returns {string} */
|
|
4
|
+
const cleanPath = (line) => line.slice(4).split("\t")[0].trim().replace(/^[ab]\//, "");
|
|
5
|
+
|
|
6
|
+
/** @param {string} patch @returns {DiffSection[]} */
|
|
7
|
+
export function splitDiff(patch) {
|
|
8
|
+
/** @type {{ lines: string[], minus: string, plus: string }[]} */
|
|
9
|
+
const raw = [];
|
|
10
|
+
/** @type {{ lines: string[], minus: string, plus: string } | null} */ let cur = null;
|
|
11
|
+
for (const line of String(patch).split("\n")) {
|
|
12
|
+
const fresh = line.startsWith("diff ") || (line.startsWith("--- ") && (!cur || cur.minus !== ""));
|
|
13
|
+
if (fresh) { cur = { lines: [], minus: "", plus: "" }; raw.push(cur); }
|
|
14
|
+
if (!cur) continue;
|
|
15
|
+
if (line.startsWith("--- ")) cur.minus = cleanPath(line);
|
|
16
|
+
else if (line.startsWith("+++ ")) cur.plus = cleanPath(line);
|
|
17
|
+
cur.lines.push(line);
|
|
18
|
+
}
|
|
19
|
+
return raw.filter((r) => r.minus || r.plus).map((r) => {
|
|
20
|
+
const added = r.lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1));
|
|
21
|
+
const removed = r.lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
|
|
22
|
+
return { path: r.plus && r.plus !== "/dev/null" ? r.plus : r.minus, text: r.lines.join("\n"), added, changed: added.length + removed };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @param {DiffSection[]} sections @returns {string} */
|
|
27
|
+
export const joinDiff = (sections) => sections.map((s) => s.text).join("\n");
|
|
28
|
+
|
|
29
|
+
/** Remplace, dans `base`, les fichiers de `paths` par leur version corrigée ; null si le correctif n'en contient aucun. */
|
|
30
|
+
/** @param {DiffSection[]} base @param {string} corrected @param {Set<string>} paths @returns {string | null} */
|
|
31
|
+
export function mergeDiff(base, corrected, paths) {
|
|
32
|
+
const fixes = new Map(splitDiff(corrected).filter((s) => paths.has(s.path)).map((s) => [s.path, s.text]));
|
|
33
|
+
return fixes.size === 0 ? null : base.map((s) => fixes.get(s.path) ?? s.text).join("\n");
|
|
34
|
+
}
|
|
35
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/engine/doc-suite.js — Socle partagé du trio documentaire client (anti-
|
|
2
|
+
// redondance : noms de fichiers, écriture atomique et en-tête de mission vivent
|
|
3
|
+
// ICI, une seule fois — les 3 générateurs ne dupliquent aucune logique commune).
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
|
|
8
|
+
// Source unique de vérité des noms — aucun générateur n'en durcode un autre.
|
|
9
|
+
/** @type {{ readme: string, architecture: string, runbook: string }} */
|
|
10
|
+
export const DOC_FILES = {
|
|
11
|
+
readme: "README.md",
|
|
12
|
+
architecture: "GUIDE_ARCHITECTURE_CODE.md",
|
|
13
|
+
runbook: "RUNBOOK.md",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Écrit (ou met à jour) UN document client : tmp entropie + rename (atomique).
|
|
17
|
+
/** @param {string} projectPath @param {string} name @param {string} content @returns {Promise<void>} */
|
|
18
|
+
export async function writeDoc(projectPath, name, content) {
|
|
19
|
+
const file = path.join(projectPath, name);
|
|
20
|
+
const tmp = `${file}.${crypto.randomBytes(4).toString("hex")}.tmp`;
|
|
21
|
+
await fs.writeFile(tmp, content, "utf8");
|
|
22
|
+
try { await fs.rename(tmp, file); }
|
|
23
|
+
catch { await fs.writeFile(file, content, "utf8"); await fs.rm(tmp, { force: true }); }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// En-tête commun des 3 documents (projet, mission, run, progression, date).
|
|
27
|
+
/** @param {string} title @param {AutoState} state @param {MissionPlan | null} mission @returns {string} */
|
|
28
|
+
export function docHeader(title, state, mission) {
|
|
29
|
+
const done = state.tasks.filter((t) => t.status === "done").length;
|
|
30
|
+
const name = (mission && mission.projectName) || path.basename(state.projectPath);
|
|
31
|
+
return [
|
|
32
|
+
`# ${title}`, "",
|
|
33
|
+
`- **Projet** : ${name}`,
|
|
34
|
+
`- **Mission** : ${(mission && mission.need) || "—"}`,
|
|
35
|
+
`- **Run Auto** : \`${state.runId}\` — ${done}/${state.tasks.length} tâches — statut \`${state.status}\``,
|
|
36
|
+
`- **Généré le** : ${new Date().toISOString()} par PipCode (mise à jour automatique)`, "",
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
// MODULE_COMPLETE
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/engine/doctrine-prefix.js — La doctrine (stable d'un appel à l'autre) est envoyée EN TÊTE de la requête, comme préfixe de cache, avant tout ce qui change (fichier, diagnostic, code) :
|
|
2
|
+
// le fournisseur la réutilise au lieu de la retraiter à chaque tâche. Même texte, même consigne, autre ordre. Phases d'analyse et de patch (communes au mode solo et à l'orchestration).
|
|
3
|
+
export const ANALYSIS_DOCTRINE = "Doctrine (règles absolues à vérifier strictement):\n";
|
|
4
|
+
export const PATCH_DOCTRINE = "Doctrine (règles absolues à respecter dans le patch):\n";
|
|
5
|
+
/** Rappel final : la doctrine est loin devant le code, on la rappelle en une ligne à la fin de la requête. */
|
|
6
|
+
export const DOCTRINE_REMINDER = "Rappel : applique strictement la doctrine fournie en tête de cette requête.";
|
|
7
|
+
|
|
8
|
+
/** @param {string} label @param {string} doctrine @param {string} [missionCtx] @returns {string} préfixe = doctrine (globale) puis mission (propre à la mission) */
|
|
9
|
+
export function cachePrefixOf(label, doctrine, missionCtx = "") {
|
|
10
|
+
const rules = (doctrine || "").trim();
|
|
11
|
+
return [rules ? label + rules : "", (missionCtx || "").trim()].filter(Boolean).join("\n\n");
|
|
12
|
+
}
|
|
13
|
+
// MODULE_COMPLETE
|