diff --git a/--others --exclude-standard b/--others --exclude-standard new file mode 100644 index 0000000..a449ff0 --- /dev/null +++ b/--others --exclude-standard @@ -0,0 +1,56 @@ +diff --git a/agents/acw.py b/agents/acw.py +index 9a3617c..109c9be 100644 +--- a/agents/acw.py ++++ b/agents/acw.py +@@ -3,6 +3,8 @@ from __future__ import annotations +  + from typing import Any, Dict, List, Optional, Tuple + from textwrap import indent ++from pathlib import Path ++ + import hashlib + import re +  +@@ -145,6 +147,42 @@ def _hash_payload(s: str) -> str: + return hashlib.sha256(s.encode("utf-8")).hexdigest()[:12] +  +  ++ ++def _sanitize_marker(marker: str) -> str: ++ """ ++ Normalise un marker fourni par l'ACWP/runner. ++ - vide -> "" ++ - supprime espaces superflus et nouvelles lignes ++ - s'assure que le marker commence par un commentaire Python '#' pour sécurité ++ - retourne la chaîne sur une seule ligne (sans \n final) ++ """ ++ if not marker: ++ return "" ++ s = str(marker).strip() ++ # Retirer nouvelles lignes internes ++ s = " ".join(s.splitlines()) ++ # Préfixer par '# ' si l'utilisateur n'a fourni qu'un identifiant nu ++ if not s.startswith("#"): ++ s = "# " + s ++ return s ++ ++ ++def _default_markers(plan_line_id: str, file_path: str) -> tuple[str, str]: ++ """ ++ Génère des marqueurs begin/end sûrs et uniques pour une plan_line. ++ Format lisible et stable, évite collisions courantes. ++ """ ++ # rendre le plan_line_id sûr (chars non-alphanum -> underscore) ++ safe_id = re.sub(r"[^\w]", "_", (plan_line_id or "pl").strip()) ++ # incorporer un fragment de nom de fichier (utile pour debug) ++ fname = Path(file_path).stem if file_path else "file" ++ fname_safe = re.sub(r"[^\w]", "_", fname)[:24] ++ begin = f"# === ARCHCODE BEGIN {safe_id} {fname_safe} ===" ++ end = f"# === ARCHCODE END {safe_id} {fname_safe} ===" ++ return begin, end ++ ++ ++ + def _render_meta_inline(meta: Dict[str, Any]) -> str: + """Rend le dict `meta` en ligne juste après #{begin_meta: ...} avec tri des clés (diff stable).""" + items = [] diff --git a/.archcode/execution_context.yaml b/.archcode/execution_context.yaml new file mode 100644 index 0000000..c7f0f14 --- /dev/null +++ b/.archcode/execution_context.yaml @@ -0,0 +1,16 @@ +bus_message_id: BM-0001 +spec_version: v0.0.1 +title: Test projet minimal +loop_iteration: 0 +user_stories: +- id: US-1 + title: Authentification + description: Login / token +input_sources: [] +output_targets: [] +functional_objectives: +- Authentifier utilisateur +non_functional_constraints: [] +modules: +- auth +plan_validated_id: PV-f4facffe diff --git a/.archcode/execution_plan.yaml b/.archcode/execution_plan.yaml new file mode 100644 index 0000000..7638c70 --- /dev/null +++ b/.archcode/execution_plan.yaml @@ -0,0 +1,56 @@ +execution_plan: + project_name: project + bus_message_id: BM-0001 + plan_validated_id: PV-f4facffe + spec_version_ref: v0.0.1 + loop_iteration: 0 + folder_root: archcode_app/ + generated_at: '2025-08-15T08:35:52' + total_lines: 3 + lines: + - plan_line_id: pl-0001-auth-c61e8a96 + module_name: auth + user_story_id: US-1 + responsibilities: &id001 + - Gérer l'authentification (login, token) + depends_on: [] + priority: haute + file_target: archcode_app/auth/__init__.py + file_kind: code + action: create_or_update + role_hint: null + meta: + bus_message_id: BM-0001 + plan_validated_id: PV-f4facffe + plan_line_ref: pl-0001-auth-c61e8a96 + loop_iteration: 0 + - plan_line_id: pl-0002-auth-4100de5c + module_name: auth + user_story_id: US-1 + responsibilities: *id001 + depends_on: [] + priority: haute + file_target: archcode_app/auth/handlers.py + file_kind: code + action: create_or_update + role_hint: handler + meta: + bus_message_id: BM-0001 + plan_validated_id: PV-f4facffe + plan_line_ref: pl-0002-auth-4100de5c + loop_iteration: 0 + - plan_line_id: pl-0003-auth-a94c0c0e + module_name: auth + user_story_id: US-1 + responsibilities: *id001 + depends_on: [] + priority: haute + file_target: archcode_app/auth/models.py + file_kind: code + action: create_or_update + role_hint: model + meta: + bus_message_id: BM-0001 + plan_validated_id: PV-f4facffe + plan_line_ref: pl-0003-auth-a94c0c0e + loop_iteration: 0 diff --git a/.archcode/patches/pl-0001-auth-c61e8a96.patch.txt b/.archcode/patches/pl-0001-auth-c61e8a96.patch.txt new file mode 100644 index 0000000..10ef953 --- /dev/null +++ b/.archcode/patches/pl-0001-auth-c61e8a96.patch.txt @@ -0,0 +1,14 @@ +#{begin_meta: { content_hash: 9d283c7d8641, file: archcode_app/auth/__init__.py, marker_begin: # === ARCHCODE BEGIN pl_0001_auth_c61e8a96 __init__ ===, marker_end: # === ARCHCODE END pl_0001_auth_c61e8a96 __init__ ===, markers_auto: true, module: archcode_app, plan_line_id: pl-0001-auth-c61e8a96, role: function, status_agent_file_checker: pending, status_agent_module_checker: pending, timestamp: 2025-08-15T09:20:55 }} +# === ARCHCODE BEGIN pl_0001_auth_c61e8a96 __init__ === +def init() -> None: + """mARCHCode/ACW + Rôle: function + Acceptance (rappel): + - Gérer l'authentification (login, token) + NOTE: Implémentation minimale générée automatiquement. + Compléter la logique lors des itérations suivantes. + """ + # TODO: implémenter la logique métier + raise NotImplementedError("À implémenter par itération suivante") +# === ARCHCODE END pl_0001_auth_c61e8a96 __init__ === +#{end_meta} diff --git a/.archcode/patches/pl-0002-auth-4100de5c.patch.txt b/.archcode/patches/pl-0002-auth-4100de5c.patch.txt new file mode 100644 index 0000000..97b7861 --- /dev/null +++ b/.archcode/patches/pl-0002-auth-4100de5c.patch.txt @@ -0,0 +1,14 @@ +#{begin_meta: { content_hash: 9084de6f7771, file: archcode_app/auth/handlers.py, marker_begin: # === ARCHCODE BEGIN pl_0002_auth_4100de5c handlers ===, marker_end: # === ARCHCODE END pl_0002_auth_4100de5c handlers ===, markers_auto: true, module: archcode_app, plan_line_id: pl-0002-auth-4100de5c, role: function, status_agent_file_checker: pending, status_agent_module_checker: pending, timestamp: 2025-08-15T09:20:55 }} +# === ARCHCODE BEGIN pl_0002_auth_4100de5c handlers === +def handlers() -> None: + """mARCHCode/ACW + Rôle: function + Acceptance (rappel): + - Gérer l'authentification (login, token) + NOTE: Implémentation minimale générée automatiquement. + Compléter la logique lors des itérations suivantes. + """ + # TODO: implémenter la logique métier + raise NotImplementedError("À implémenter par itération suivante") +# === ARCHCODE END pl_0002_auth_4100de5c handlers === +#{end_meta} diff --git a/.archcode/patches/pl-0003-auth-a94c0c0e.patch.txt b/.archcode/patches/pl-0003-auth-a94c0c0e.patch.txt new file mode 100644 index 0000000..17bd5f4 --- /dev/null +++ b/.archcode/patches/pl-0003-auth-a94c0c0e.patch.txt @@ -0,0 +1,14 @@ +#{begin_meta: { content_hash: 81f51aca1b11, file: archcode_app/auth/models.py, marker_begin: # === ARCHCODE BEGIN pl_0003_auth_a94c0c0e models ===, marker_end: # === ARCHCODE END pl_0003_auth_a94c0c0e models ===, markers_auto: true, module: archcode_app, plan_line_id: pl-0003-auth-a94c0c0e, role: function, status_agent_file_checker: pending, status_agent_module_checker: pending, timestamp: 2025-08-15T09:20:55 }} +# === ARCHCODE BEGIN pl_0003_auth_a94c0c0e models === +def models() -> None: + """mARCHCode/ACW + Rôle: function + Acceptance (rappel): + - Gérer l'authentification (login, token) + NOTE: Implémentation minimale générée automatiquement. + Compléter la logique lors des itérations suivantes. + """ + # TODO: implémenter la logique métier + raise NotImplementedError("À implémenter par itération suivante") +# === ARCHCODE END pl_0003_auth_a94c0c0e models === +#{end_meta} diff --git a/.archcode/plan_draft_aggregated.yaml b/.archcode/plan_draft_aggregated.yaml new file mode 100644 index 0000000..238cc57 --- /dev/null +++ b/.archcode/plan_draft_aggregated.yaml @@ -0,0 +1,36 @@ +plan_draft_aggregated: + project_name: project + bus_message_id: BM-0001 + spec_version_ref: v0.0.1 + loop_iteration: 0 + modules: + - auth + dependencies: [] + folder_structure: {} + items: + - status: ok + source_path: C:\Users\Utilisateur\Documents\mARCHCode\modules\auth\module_draft.yaml + ingested_at: '2025-08-15T08:31:23' + module_draft: + module_name: auth + user_story_id: US-1 + responsibilities: + - Gérer l'authentification (login, token) + files_expected: + - auth/__init__.py + - auth/handlers.py + - auth/models.py + depends_on: [] + inputs: [] + outputs: [] + validator_status: ok + meta: + priority: haute + warnings: [] + stats: + total_items: 1 + validated: 1 + pending: 0 + rejected: 0 + issued_at: '2025-08-15T08:31:23' + aggregated_at: '2025-08-15T08:31:23' diff --git a/.archcode/plan_validated.yaml b/.archcode/plan_validated.yaml new file mode 100644 index 0000000..4361d00 --- /dev/null +++ b/.archcode/plan_validated.yaml @@ -0,0 +1,26 @@ +plan_validated: + plan_validated_id: PV-f4facffe + bus_message_id: BM-0001 + spec_version_ref: v0.0.1 + loop_iteration: 0 + project_name: project + modules: + - module_name: auth + user_story_id: US-1 + responsibilities: + - Gérer l'authentification (login, token) + inputs: [] + outputs: [] + files_expected: + - auth/__init__.py + - auth/handlers.py + - auth/models.py + depends_on: [] + technical_constraints: [] + meta: + priority: haute + meta: + comment_agent_plan_validator: '' + created_at: '2025-08-15T08:33:36' + validated_at: '2025-08-15T08:33:36' + plan_diffs: [] diff --git a/.gitignore b/.gitignore index 91a415c..b0439d9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,12 @@ build/ *.tar.gz !.archcode/context_snapshots/ + +cat >> .gitignore <<'EOF' +# Bytecode python +__pycache__/ +*.py[cod] +*$py.class +EOF__pycache__/ +*.py[cod] +*$py.class diff --git a/Git_help.txt b/Git_help.txt new file mode 100644 index 0000000..8b29416 --- /dev/null +++ b/Git_help.txt @@ -0,0 +1,201 @@ + + + +créer environnement .venv + + +dir /b +(si .venv n'est pas listé, alors le créer) + +python -m venv .venv + + +Tester agent_module_compilator (commande à lancer depuis la racine du repo) +.venv\Scripts\python.exe -m agents.agent_module_compilator collect --reset-if-missing --update-ec + + +quand plan_draft_aggregated a été généré, faire + +.\.venv\Scripts\python.exe -m agents.agent_plan_validator validate ^ + --ec .archcode\execution_context.yaml ^ + --pga .archcode\plan_draft_aggregated.yaml ^ + --out .archcode\plan_validated.yaml ^ + --comment-out .archcode\comment_agent_plan_validator.yaml ^ + --update-ec + +quand le plan_validated a été généré, construire l’execution_plan.yaml (transformateur déterministe) + +.\.venv\Scripts\python.exe -m scripts.execution_plan_transformer build ^ + --pv .archcode\plan_validated.yaml ^ + --pd .archcode\project_draft.yaml ^ + --ec .archcode\execution_context.yaml ^ + --out .archcode\execution_plan.yaml + + + + + + + +Git + + +Vérifier ce qui a changé (rapide, sans risque) + +Ça te permet de voir exactement quelles modifications ont été faites avant d’agir. + +REM voir l'état synthétique +git status + +REM voir le diff d'un fichier modifié important +git diff agents\acw.py + +REM lister les fichiers non trackés (pour confirmer) +git ls-files --others --exclude-standard + +##################################################### + +Tu veux aussi committer les modifications de code + +Si tu as volontairement patché agents/acw.py ou runner/run_plan.py et que tu veux les inclure : + +git checkout -b archcode-self/local-run +git add agents\acw.py agents\acwp.py runner\run_plan.py agents\agent_module_compilator.py +git add .archcode\* (ou les fichiers .archcode listés avant) +git commit -m "feat: update ACW/runner + add generated patches from local run" + +##################################################### + +Tu veux annuler / retirer les modifications non désirées + +Si les fichiers listés comme modified sont accidentels (par ex. tu n’as pas voulu toucher agents/*.py) : + +Pour annuler les changements non commités (retour à HEAD) : + +git restore agents\acw.py +git restore agents\acwp.py +git restore agents\agent_module_compilator.py +git restore runner\run_plan.py + + +Pour stocker les modifications temporairement (stash) : + +git stash push -m "wip before applying patches" +############################################################################ +) Si tu veux revenir à un état propre puis refaire le run + +Si tu veux annuler tout et repartir : + +REM nettoyer modifications non commités +git restore . + +REM supprimer les fichiers non trackés créés par le run (danger : destructif) +REM liste d'abord pour vérifier +git clean -n -d +REM puis exécuter si OK +git clean -f -d +############################################################ +stager tout et committer + + +0/ commande unique (à faire dans mARCHCode) +changer éventuellement le nom de la branche archcode-self/local-run + +git checkout -b archcode-self/local-run2 && git add -A && git commit -m "chore(run): commit local run artifacts and fixes" && git push --set-upstream origin archcode-self/local-run + +et vérifier éventuellement avec + +git status +git log -1 --stat +git branch -vv + + + +############################################################ + + + + + + + + + + + + + + +1) (Recommandé) créer une branche de travail + +Cette étape évite de polluer master si tu veux expérimenter : + +git checkout -b archcode-self/local-run + +2) Stager tout et committer +git add -A +git commit -m "chore(run): commit all local changes and generated artifacts from local run" + + +Vérifie le commit effectué : + +git status +git log -1 --stat + + +3) (Optionnel) pousser la branche vers le remote + +Tentative simple (va demander tes identifiants si nécessaire) : + +git push --set-upstream origin archcode-self/local-run + + + + + +Si le push réussit : top. + +Si le push échoue avec 403 / permission denied (cas fréquent avec github-actions[bot] ou PAT manquant), tu as plusieurs options : + +Option A — utiliser Git Credential Manager (recommandé) +Configure GCM et relance le push ; il te demandera ton token une seule fois : + +git config --global credential.helper manager-core +git push --set-upstream origin archcode-self/local-run + + +Quand ça demande username/password, mets ton nom d'utilisateur GitHub et en mot de passe colle ton PAT (token). + +Option B — pousser en ligne de commande en incorporant un token (moins sûr : laisse une trace dans l’historique shell — évite si possible) +Remplace GH_PAT et USER/REPO par tes valeurs : + +git push https://x-access-token:GH_PAT@github.com/USER/REPO.git archcode-self/local-run:archcode-self/local-run + + +Après, restaure l'URL du remote pour ne pas garder le token en clair : + +git remote set-url origin https://github.com/USER/REPO.git + + +Option C — créer un fork / sandbox remote +Si tu préfères, tu peux simplement garder le commit local (pas de push) et nous travaillerons dessus ensuite. + +######################################################## +4) Si tu as commis par erreur et veux annuler le commit + +Pour annuler le dernier commit mais garder les modifications staged (soft): + +git reset --soft HEAD~1 + + +Pour annuler le commit et désindexer (remet tout en unstaged): + +git reset --mixed HEAD~1 + + +Pour annuler tout et revenir à l’état du dernier commit (destructif — perd les changements non commités) : + +git restore . # ou, si git ancien : git checkout -- . + + + diff --git a/agents/__pycache__/acw.cpython-311.pyc b/agents/__pycache__/acw.cpython-311.pyc deleted file mode 100644 index b1d987f..0000000 Binary files a/agents/__pycache__/acw.cpython-311.pyc and /dev/null differ diff --git a/agents/__pycache__/acwp.cpython-311.pyc b/agents/__pycache__/acwp.cpython-311.pyc deleted file mode 100644 index a238f44..0000000 Binary files a/agents/__pycache__/acwp.cpython-311.pyc and /dev/null differ diff --git a/agents/__pycache__/agent_file_checker.cpython-311.pyc b/agents/__pycache__/agent_file_checker.cpython-311.pyc deleted file mode 100644 index 98211ec..0000000 Binary files a/agents/__pycache__/agent_file_checker.cpython-311.pyc and /dev/null differ diff --git a/agents/__pycache__/agent_module_checker.cpython-311.pyc b/agents/__pycache__/agent_module_checker.cpython-311.pyc deleted file mode 100644 index e12a450..0000000 Binary files a/agents/__pycache__/agent_module_checker.cpython-311.pyc and /dev/null differ diff --git a/agents/acw.py b/agents/acw.py index 9a3617c..109c9be 100644 --- a/agents/acw.py +++ b/agents/acw.py @@ -3,6 +3,8 @@ from typing import Any, Dict, List, Optional, Tuple from textwrap import indent +from pathlib import Path + import hashlib import re @@ -145,6 +147,42 @@ def _hash_payload(s: str) -> str: return hashlib.sha256(s.encode("utf-8")).hexdigest()[:12] + +def _sanitize_marker(marker: str) -> str: + """ + Normalise un marker fourni par l'ACWP/runner. + - vide -> "" + - supprime espaces superflus et nouvelles lignes + - s'assure que le marker commence par un commentaire Python '#' pour sécurité + - retourne la chaîne sur une seule ligne (sans \n final) + """ + if not marker: + return "" + s = str(marker).strip() + # Retirer nouvelles lignes internes + s = " ".join(s.splitlines()) + # Préfixer par '# ' si l'utilisateur n'a fourni qu'un identifiant nu + if not s.startswith("#"): + s = "# " + s + return s + + +def _default_markers(plan_line_id: str, file_path: str) -> tuple[str, str]: + """ + Génère des marqueurs begin/end sûrs et uniques pour une plan_line. + Format lisible et stable, évite collisions courantes. + """ + # rendre le plan_line_id sûr (chars non-alphanum -> underscore) + safe_id = re.sub(r"[^\w]", "_", (plan_line_id or "pl").strip()) + # incorporer un fragment de nom de fichier (utile pour debug) + fname = Path(file_path).stem if file_path else "file" + fname_safe = re.sub(r"[^\w]", "_", fname)[:24] + begin = f"# === ARCHCODE BEGIN {safe_id} {fname_safe} ===" + end = f"# === ARCHCODE END {safe_id} {fname_safe} ===" + return begin, end + + + def _render_meta_inline(meta: Dict[str, Any]) -> str: """Rend le dict `meta` en ligne juste après #{begin_meta: ...} avec tri des clés (diff stable).""" items = [] diff --git a/agents/acwp.py b/agents/acwp.py index 8503e2a..2bf7e55 100644 --- a/agents/acwp.py +++ b/agents/acwp.py @@ -1,4 +1,4 @@ -# agents/agent_code_writer_planner.py +# agents/ACWP.py from __future__ import annotations from typing import Any, Dict, Iterable, List, Optional @@ -6,6 +6,8 @@ import uuid from core.types import PlanLine + + """ agent_code_writer_planner (ACWP) — mARCHCode / Phase 3 ====================================================== diff --git a/agents/agent_module_compilator.py b/agents/agent_module_compilator.py index 595d34c..08128e8 100644 --- a/agents/agent_module_compilator.py +++ b/agents/agent_module_compilator.py @@ -521,6 +521,38 @@ def cmd_show(out: Path) -> None: for m in mods: print(f" - {m:12s} : {statuses.get(m, '∅')}") +def collect_modules( + *, + ec_yaml: Path, + pd_yaml: Path, + out: Path, + roots: List[Path], + patterns: Optional[List[str]], + reset: bool, + reset_if_missing: bool, + allow_non_ok: bool, + accept_untagged: bool, + update_ec: bool, +) -> None: + """ + Wrapper for backward compatibility: forwards call to cmd_collect. + + Ce wrapper existe pour conserver l'API historique (anciennement certains + appels testaient collect_modules(...) directement). Il doit être défini + avant main() pour être disponible lorsque le module est exécuté en script. + """ + return cmd_collect( + ec_yaml=ec_yaml, + pd_yaml=pd_yaml, + out=out, + roots=roots, + patterns=patterns, + reset=reset, + reset_if_missing=reset_if_missing, + allow_non_ok=allow_non_ok, + accept_untagged=accept_untagged, + update_ec=update_ec, + ) # ----------------------------------------------------------------------------- # CLI diff --git a/agents/dir b/agents/dir new file mode 100644 index 0000000..e69de29 diff --git a/cli/__pycache__/main.cpython-311.pyc b/cli/__pycache__/main.cpython-311.pyc deleted file mode 100644 index 5b304af..0000000 Binary files a/cli/__pycache__/main.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/archiver.cpython-311.pyc b/core/__pycache__/archiver.cpython-311.pyc deleted file mode 100644 index 092242d..0000000 Binary files a/core/__pycache__/archiver.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/error_policy.cpython-311.pyc b/core/__pycache__/error_policy.cpython-311.pyc deleted file mode 100644 index 4665867..0000000 Binary files a/core/__pycache__/error_policy.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/fs_apply.cpython-311.pyc b/core/__pycache__/fs_apply.cpython-311.pyc deleted file mode 100644 index 4bc3d63..0000000 Binary files a/core/__pycache__/fs_apply.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/git_diffstats.cpython-311.pyc b/core/__pycache__/git_diffstats.cpython-311.pyc deleted file mode 100644 index 3c7e4ef..0000000 Binary files a/core/__pycache__/git_diffstats.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/types.cpython-311.pyc b/core/__pycache__/types.cpython-311.pyc deleted file mode 100644 index 11b0eff..0000000 Binary files a/core/__pycache__/types.cpython-311.pyc and /dev/null differ diff --git a/core/__pycache__/yaml_io.cpython-311.pyc b/core/__pycache__/yaml_io.cpython-311.pyc deleted file mode 100644 index da96bb5..0000000 Binary files a/core/__pycache__/yaml_io.cpython-311.pyc and /dev/null differ diff --git a/modules/auth/module_draft.yaml b/modules/auth/module_draft.yaml new file mode 100644 index 0000000..bb94246 --- /dev/null +++ b/modules/auth/module_draft.yaml @@ -0,0 +1,15 @@ +module_draft: + module_name: auth + user_story_id: US-1 + responsibilities: + - "Gérer l'authentification (login, token)" + files_expected: + - auth/__init__.py + - auth/handlers.py + - auth/models.py + depends_on: [] + inputs: [] + outputs: [] + validator_status: ok + meta: + priority: haute diff --git a/project_draft.yaml b/project_draft.yaml new file mode 100644 index 0000000..4f275f8 --- /dev/null +++ b/project_draft.yaml @@ -0,0 +1,18 @@ +project_draft: + project_name: test_project + global_objectives: + - "API minimal" + initial_modules: + - auth + - core + dependencies: + - auth → core + folder_structure: + root: marchcode_app/ + structure: + - name: core/ + description: "Core code" + - name: auth/ + description: "Auth module" + - name: tests/ + description: "Tests" diff --git a/runner/__pycache__/run_plan.cpython-311.pyc b/runner/__pycache__/run_plan.cpython-311.pyc deleted file mode 100644 index eaf2e4d..0000000 Binary files a/runner/__pycache__/run_plan.cpython-311.pyc and /dev/null differ diff --git a/runner/run_plan.py b/runner/run_plan.py index 276992b..ea88ffa 100644 --- a/runner/run_plan.py +++ b/runner/run_plan.py @@ -3,197 +3,491 @@ import argparse import os +import re +from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Optional - -from core.yaml_io import load_execution_plan -from core.fs_apply import apply_patchblock_to_file as apply_patch -from core.types import PlanLine -from agents.acwp import build_prompt -from agents.acw import run_acw -from agents.agent_file_checker import check_file -from agents.agent_module_checker import check_module -from core.git_diffstats import ensure_branch, stage_and_commit - -# Injection SHA best-effort : présent chez toi dans adapters/git_adapter -try: - from adapters.git_adapter import inject_commit_sha_into_meta # type: ignore -except Exception: +from typing import Any, Dict, List, Optional - def inject_commit_sha_into_meta(pb, sha: Optional[str]) -> None: - """ - Injecte le SHA de commit dans `pb.meta.commit_sha` si possible (fallback no-op). +import yaml +import sys +import importlib - Args: - pb: PatchBlock (ou objet duck-typed) dont l'attribut `meta` peut contenir `commit_sha`. - sha: SHA de commit à injecter (ou None pour ne rien faire). +# Forcer la racine du repo en tête du PYTHONPATH (évite le conflit avec un paquet "agents" tiers) +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) - Returns: - None. Effet de bord tolérant sur `pb.meta` si l'attribut existe. - """ - return - -from core.archiver import ( - archive_execution_plan, - archive_patch_before, - archive_patch_after, - archive_patch_post_commit, - append_console_log, - archive_run_info, -) """ =============================================================================== -mARCHCode — runner/run_plan.py (MVP Phase 3) +mARCHCode — runner/run_plan.py (Fusion 'bridge local' + 'runner complet') ------------------------------------------------------------------------------- Rôle -- Exécuter un execution_plan YAML : pour chaque PlanLine → ACWP → ACW → checkers - → apply (FS) → commit Git → archivage (.arch_runs/…). +- Lire un execution_plan (Phase 2) au format "lines" (transformer) **ou** + un plan typé "modules/plan_lines" (format historique). +- Construire des PlanLine minimales conformes aux attentes d'ACWP. +- Deux modes : + • --dry-run : ACWP → ACW → écrit les patchs dans .archcode/patches (pas d'effets FS) + • mode normal : ACWP → ACW → checkers → apply FS → commit Git → archives Entrées -- ep_path : chemin du fichier execution_plan.yaml -- repo_root : racine du dépôt cible (écriture FS + Git) - -Sorties & effets -- Fichiers écrits dans repo_root selon pb.meta.file -- Commits Git (si repo initialisé) + SHA injecté dans pb.meta (best-effort) -- Archives lisibles dans .arch_runs// - - plan.yaml, patch_before.yaml, patch_after.yaml, post_commit.yaml, console.log +- --ep : chemin du execution_plan.yaml +- --repo : racine du dépôt cible (pour apply/commit) +- --archive-dir: dossier d’archives (si absent → .arch_runs/) +- --dry-run : désactive checkers/apply/commit/archives, n’émet que les patchs +- --patch-dir : dossier de sortie des patchs (défaut .archcode/patches) Notes -- Pas de “init_run_dir” dans core.archiver : on crée le dossier nous-mêmes (os.makedirs). -- apply_patch : alias de core.fs_apply.apply_patchblock_to_file (signature locale). -- Robustesse : best-effort sur Git (le run continue même si Git est absent). +- Tente d’importer les utilitaires (archiver, git, fs_apply). S’il manque quelque chose, + le runner continue en best-effort (ou échoue proprement selon le mode). =============================================================================== """ +# -----------------------------------------------------------------------------# +# Utils import +# -----------------------------------------------------------------------------# + +def _import_first(candidates: List[str]): + """Importe le premier module disponible dans `candidates`.""" + last_err: Optional[BaseException] = None + for name in candidates: + try: + return importlib.import_module(name) + except BaseException as e: + last_err = e + continue + raise ModuleNotFoundError(f"Impossible d'importer l'un de {candidates}: {last_err}") + +# -----------------------------------------------------------------------------# +# Utils YAML / FS +# -----------------------------------------------------------------------------# + +def _read_yaml(path: Path) -> Dict[str, Any]: + """Charge un YAML en dict ({} si vide).""" + data = yaml.safe_load(path.read_text(encoding="utf-8")) + return data or {} + + +def _ensure_dir(p: Path) -> None: + """Crée le répertoire p si nécessaire (parents inclus).""" + p.mkdir(parents=True, exist_ok=True) + + +def _posix(s: str) -> str: + """Normalise un chemin en séparateurs '/' (utile pour les metas/patchs).""" + return s.replace("\\", "/") + + +# -----------------------------------------------------------------------------# +# Shims & heuristiques PlanLine +# -----------------------------------------------------------------------------# + +@dataclass +class SimplePlanLine: + """ + Duck-typed PlanLine minimale pour ACWP._validate_plan_line(). + Champs requis : + - plan_line_id, file (.py), op ∈ {'create','modify'}, role, target_symbol, signature, acceptance[] + """ + plan_line_id: str + file: str + op: str + role: str + target_symbol: str + signature: str + acceptance: List[str] + + # champs optionnels + path: Optional[str] = None + description: Optional[str] = None + depends_on: Optional[List[str]] = None + constraints: Optional[Dict[str, Any]] = None + allow_create: bool = True + markers: Optional[Dict[str, str]] = None + plan_line_ref: Optional[str] = None + intent_fingerprint: Optional[str] = None + + +def _safe_ident(s: str, default: str = "func") -> str: + """Convertit une chaîne en identifiant Python simple (a-z, 0-9, _)""" + base = re.sub(r"[^\w]", "_", (s or "").strip()) + base = re.sub(r"_+", "_", base).strip("_") + return base or default + + +def _role_from_hint(role_hint: Optional[str], file_kind: Optional[str]) -> str: + """Infère un rôle conservateur : 'dto' si demandé, sinon 'function'.""" + rh = (role_hint or "").lower().strip() + if rh == "dto": + return "dto" + return "function" + -def run_plan(ep_path: str, repo_root: str, *, archive_dir: Optional[str] = None) -> None: +def _derive_sig_and_symbol(role: str, filename: str) -> tuple[str, str]: + """ + Déduit un symbole et une signature exécutable minimale depuis le nom de fichier. + - DTO → def make__dto() -> dict: + - sinon → def () -> None: + """ + stem = Path(filename).stem + ident = _safe_ident(stem, "func") + if role == "dto": + symbol = f"make_{ident}_dto" + sig = f"def {symbol}() -> dict:" + return sig, symbol + symbol = ident + sig = f"def {symbol}() -> None:" + return sig, symbol + + +def _ensure_py_target(path: str) -> str: + """Force une cible .py (si dossier → __init__.py ; si sans suffixe → .py).""" + if path.endswith(".py"): + return _posix(path) + p = Path(path) + if path.endswith("/") or p.suffix == "": + return _posix(str(p / "__init__.py")) + return _posix(str(p.with_suffix(".py"))) + + +# -----------------------------------------------------------------------------# +# Charge/normalise les PlanLines depuis un execution_plan +# -----------------------------------------------------------------------------# + +def _from_ep_lines(ep_root: Dict[str, Any]) -> tuple[List[SimplePlanLine], Dict[str, Any]]: + """ + Lecture du format Phase 2 (scripts.execution_plan_transformer) : + { + execution_plan: { + bus_message_id, loop_iteration, lines: [ { plan_line_id, file_target, role_hint, ... }, ... ] + } + } + """ + ep = ep_root.get("execution_plan") or ep_root + bus_message_id = ep.get("bus_message_id") + loop_iteration = ep.get("loop_iteration") + raw_lines = ep.get("lines") or [] + plan_lines: List[SimplePlanLine] = [] + + for ln in raw_lines: + file_target = str(ln.get("file_target") or "").strip() + if not file_target: + continue + file_posix = _ensure_py_target(file_target) + role = _role_from_hint(ln.get("role_hint"), ln.get("file_kind")) + sig, symbol = _derive_sig_and_symbol(role, Path(file_posix).name) + + responsibilities = ln.get("responsibilities") or [] + acceptance = [str(x) for x in responsibilities if str(x).strip()] + if not acceptance: + acceptance = [f"fonction {symbol} existe", "fichier Python valide (imports ok)"] + + pl = SimplePlanLine( + plan_line_id=str(ln.get("plan_line_id") or ""), + file=file_posix, + op=("create" if str(ln.get("action") or "create").lower().startswith("create") else "modify"), + role=role, + target_symbol=symbol, + signature=sig, + acceptance=acceptance, + path=None, + description=None, + depends_on=list(ln.get("depends_on") or []), + constraints={}, + allow_create=True, + markers=None, + plan_line_ref=str(ln.get("plan_line_id") or None), + intent_fingerprint=None, + ) + if pl.plan_line_id and pl.file.endswith(".py"): + plan_lines.append(pl) + + return plan_lines, {"bus_message_id": bus_message_id, "loop_iteration": loop_iteration} + + +def _from_module_plan(ep_root: Dict[str, Any]) -> tuple[List[SimplePlanLine], Dict[str, Any]]: + """ + Lecture d’un format historique : + { + execution_plan_id, modules: [ + { module: "auth", plan_lines: [ {plan_line_id, file, role, signature, ...}, ... ] } + ] + } + On convertit vers SimplePlanLine en comblant les trous (signature/rôle si manquants). + """ + bus_message_id = ep_root.get("bus_message_id") + loop_iteration = ep_root.get("loop_iteration") + modules = ep_root.get("modules") or [] + plan_lines: List[SimplePlanLine] = [] + + for mod in modules: + for ln in mod.get("plan_lines") or []: + file_path = _ensure_py_target(str(ln.get("file") or "").strip()) + role = (ln.get("role") or "function").lower() + if role not in ("dto", "function"): + role = "function" + sig = str(ln.get("signature") or "") + symbol = str(ln.get("target_symbol") or "") + if not sig or not symbol: + sig, symbol = _derive_sig_and_symbol(role, Path(file_path).name) + + acc = ln.get("acceptance") or [f"fonction {symbol} existe"] + pl = SimplePlanLine( + plan_line_id=str(ln.get("plan_line_id") or ""), + file=file_path, + op=("modify" if ln.get("op") == "modify" else "create"), + role=role, + target_symbol=symbol, + signature=sig, + acceptance=[str(a) for a in acc], + path=ln.get("path"), + description=ln.get("description"), + depends_on=list(ln.get("depends_on") or []), + constraints=dict(ln.get("constraints") or {}), + allow_create=bool(ln.get("allow_create", True)), + markers=dict(ln.get("markers") or {}) or None, + plan_line_ref=ln.get("plan_line_ref"), + intent_fingerprint=ln.get("intent_fingerprint"), + ) + if pl.plan_line_id and pl.file.endswith(".py"): + plan_lines.append(pl) + + return plan_lines, {"bus_message_id": bus_message_id, "loop_iteration": loop_iteration} + + +def _load_plan_lines(ep_path: Path) -> tuple[List[SimplePlanLine], Dict[str, Any]]: + """ + Charge un execution_plan et renvoie (plan_lines, meta). + Supporte les deux formats (Phase 2 “lines” et format “modules/plan_lines”). """ - Exécute un plan d’exécution mARCHCode de bout en bout (MVP local). - - Pipeline: - 1) Archive les métadonnées de run et le plan source. - 2) Charge l'ExecutionPlan typé. - 3) (Best-effort) prépare la branche Git de travail. - 4) Pour chaque PlanLine: - - ACWP → prompt - - ACW → PatchBlock - - Checkers fichier & module - - (si OK) apply FS + commit (best-effort) + archivage post-commit + root = _read_yaml(ep_path) + # Heuristique : présence d'une clé 'execution_plan' avec 'lines' → format Phase 2 + ep = root.get("execution_plan") or root + if isinstance(ep.get("lines"), list): + return _from_ep_lines(root) + # Sinon : format historique (modules/plan_lines) + return _from_module_plan(ep) + + +# -----------------------------------------------------------------------------# +# Archiver / Git / FS (best-effort) +# -----------------------------------------------------------------------------# + +# archiver (best-effort) +try: + from core.archiver import ( # type: ignore + archive_execution_plan, + archive_patch_before, + archive_patch_after, + archive_patch_post_commit, + append_console_log, + archive_run_info, + ) +except Exception: + def archive_execution_plan(text: str, run_dir: str) -> None: ... + def archive_patch_before(pb, run_dir: str) -> None: ... + def archive_patch_after(pb, run_dir: str) -> None: ... + def archive_patch_post_commit(pb, run_dir: str) -> None: ... + def append_console_log(msg: str, run_dir: str) -> None: ... + def archive_run_info(run_dir: str, **kwargs) -> None: ... + +# git adapter (best-effort) +try: + from core.git_diffstats import ensure_branch, stage_and_commit # type: ignore +except Exception: + def ensure_branch(*, repo_root: str) -> None: ... + def stage_and_commit(paths: List[str], message: str, *, repo_root: str) -> Optional[str]: + return None + +try: + from adapters.git_adapter import inject_commit_sha_into_meta # type: ignore +except Exception: + def inject_commit_sha_into_meta(pb, sha: Optional[str]) -> None: ... + + +# fs apply (obligatoire si mode apply) +try: + from core.fs_apply import apply_patchblock_to_file as apply_patch # type: ignore +except Exception: + apply_patch = None # type: ignore + + +# -----------------------------------------------------------------------------# +# Pipeline d'exécution +# -----------------------------------------------------------------------------# + +def run_plan( + ep_path: str, + repo_root: str, + *, + archive_dir: Optional[str] = None, + dry_run: bool = False, + patch_dir: Optional[str] = None, +) -> None: + """ + Exécute un execution_plan YAML. + + Modes: + - dry_run=True → ACWP → ACW, écrit les patchs dans patch_dir, pas d'effets FS. + - dry_run=False → ACWP → ACW → checkers → apply FS → commit Git → archives. Args: - ep_path: Chemin vers le fichier `execution_plan.yaml`. - repo_root: Racine du dépôt cible (chemins d'écriture/commit relatifs). - archive_dir: Dossier des artefacts du run (créé si absent). Si None, - un dossier `.arch_runs/` sera créé. - - Returns: - None. Les effets se matérialisent sur le FS, Git (si dispo) et dans `archive_dir`. - - Raises: - FileNotFoundError: si `ep_path` est introuvable (via chargeur). - ValueError: si la structure du plan est invalide (via chargeur). - Toute autre exception est capturée localement lors des étapes best-effort - (ex. Git non initialisé) pour permettre la poursuite du run. + ep_path: Chemin du execution_plan.yaml. + repo_root: Racine du repo (pour apply/commit). + archive_dir: Dossier d’archive (créé si absent). Ignoré si dry_run. + dry_run: Active le mode “bridge local” sans side-effects. + patch_dir: Dossier de sortie des patchs (défaut: .archcode/patches). """ - # --- Prépare le répertoire d’archives du run --- - if not archive_dir: - ts = datetime.now().strftime("%Y%m%d-%H%M%S") - archive_dir = f".arch_runs/{ts}" - os.makedirs(archive_dir, exist_ok=True) - archive_run_info(archive_dir, started_at=datetime.now().isoformat(timespec="seconds")) - append_console_log(f"[arch] start run, archive_dir={archive_dir}", run_dir=archive_dir) - - # Archive le plan source (copie brute) - try: - ep_text = Path(ep_path).read_text(encoding="utf-8") - except Exception as e: - ep_text = f"# [warn] impossible de lire {ep_path}: {e}" - archive_execution_plan(ep_text, run_dir=archive_dir) + ep_p = Path(ep_path) + plan_lines, meta = _load_plan_lines(ep_p) + if not plan_lines: + raise ValueError("Aucune PlanLine valide n’a été trouvée dans le plan.") - # Charge le plan typé - ep = load_execution_plan(ep_path) - print(f"[ExecutionPlan] → {ep.execution_plan_id}") - append_console_log(f"[plan] id={ep.execution_plan_id}", run_dir=archive_dir) + # Imports agents (ACWP/ACW) avec alias -> fallback + ACWP = _import_first(["agents.acwp", "agents.agent_code_writer_planner"]) + ACW = _import_first(["agents.acw", "agents.agent_code_writer"]) - # Branche de travail (si Git est initialisé) - try: - # NOTE: impl réelle dans core.git_diffstats ; appel conservé (best-effort). - ensure_branch(repo_root=repo_root) # type: ignore[call-arg] - print("• Branche de travail prête (archcode-self/… ou équivalent)") - append_console_log("[git] ensure_branch ok", run_dir=archive_dir) - except Exception as e: - print("• Git indisponible — on continue sans commit (MVP)") - append_console_log(f"[git] ensure_branch skipped: {e}", run_dir=archive_dir) - - # Contexte d’exécution : écrire depuis repo_root + # Checkers si pas dry-run + if not dry_run: + mod_file_checker = _import_first(["agents.agent_file_checker"]) + mod_module_checker = _import_first(["agents.agent_module_checker"]) + check_file = getattr(mod_file_checker, "check_file") + check_module = getattr(mod_module_checker, "check_module") + + # Prépare archiver si mode apply + run_dir = None + if not dry_run: + if not archive_dir: + ts = datetime.now().strftime("%Y%m%d-%H%M%S") + archive_dir = f".arch_runs/{ts}" + run_dir = archive_dir + os.makedirs(run_dir, exist_ok=True) + archive_run_info(run_dir, started_at=datetime.now().isoformat(timespec="seconds")) + try: + ep_text = Path(ep_path).read_text(encoding="utf-8") + except Exception as e: + ep_text = f"# [warn] lecture échouée {ep_path}: {e}" + archive_execution_plan(ep_text, run_dir=run_dir) + + # Patch dir (toujours, pour inspection) + patch_dir_p = Path(patch_dir or ".archcode/patches") + _ensure_dir(patch_dir_p) + + # Branche Git (best-effort) si mode apply + if not dry_run: + try: + ensure_branch(repo_root=repo_root) # type: ignore[call-arg] + if run_dir: + append_console_log("[git] ensure_branch ok", run_dir=run_dir) + print("• Branche de travail prête (best-effort)") + except Exception as e: + if run_dir: + append_console_log(f"[git] ensure_branch skipped: {e}", run_dir=run_dir) + print("• Git indisponible — on continue sans commit") + + # Exécution prev_cwd = os.getcwd() os.chdir(repo_root) try: - for mod in ep.modules: - module_name = mod.get("module", "unknown") - plan_lines = mod.get("plan_lines", []) - print(f"→ Module: {module_name} ({len(plan_lines)} plan_lines)") - append_console_log(f"[module] {module_name} ({len(plan_lines)})", run_dir=archive_dir) - - for pl_data in plan_lines: - pl = PlanLine(**pl_data) - print(f" • PlanLine {pl.plan_line_id} ({pl.file})") - append_console_log(f"[plan_line] {pl.plan_line_id} file={pl.file}", run_dir=archive_dir) - - # ACWP → prompt - prompt = build_prompt(pl) - - # ACW : PatchBlock - pb = run_acw(pl, prompt) - archive_patch_before(pb, run_dir=archive_dir) - - # Checkers - pb = check_file(pb) - pb = check_module(pb) - archive_patch_after(pb, run_dir=archive_dir) - - if pb.global_status == "ok": - # Applique le patch sur FS (signature locale : pb → (path, count)) - try: - apply_patch(pb) - append_console_log("[apply] file written", run_dir=archive_dir) - except Exception as e: - print(f" → APPLY FAILED: {e}") - append_console_log(f"[apply] failed: {e}", run_dir=archive_dir) - break - - # Commit Git (best-effort) - message = f"feat(mARCH): {pl.plan_line_id} {pl.role} {pl.target_symbol} (status={pb.global_status})" - try: - sha = stage_and_commit([pb.meta.file], message, repo_root=repo_root) # type: ignore[arg-type] - inject_commit_sha_into_meta(pb, sha) - archive_patch_post_commit(pb, run_dir=archive_dir) - short = (sha or "")[:7] if sha else "∅" - print(f" → OK: fichier écrit & commit {short}") - append_console_log(f"[git] commit {sha}", run_dir=archive_dir) - except Exception as e: - print(f" → OK: fichier écrit (commit non effectué: {e})") - append_console_log(f"[git] commit skipped: {e}", run_dir=archive_dir) - else: - reason = pb.error_trace or "module checker" - print(f" → REJECTED: {reason}") - append_console_log(f"[reject] {pl.plan_line_id}: {reason}", run_dir=archive_dir) + # writer tasks depuis ACWP + tasks = ACWP.plan_to_writer_tasks( + plan_lines, + execution_context=None, + bus_message_id=meta.get("bus_message_id"), + user_story_id=None, + user_story=None, + loop_iteration=meta.get("loop_iteration"), + ) + + produced = 0 + for wt in tasks: + # ACW + pb = ACW.write_code(wt) + + # Toujours sauver le patch (y compris dry-run) + patch_path = patch_dir_p / f"{wt['plan_line_id']}.patch.txt" + patch_path.write_text(pb.code, encoding="utf-8") + produced += 1 + print(f"[patch] {patch_path}") + + if dry_run: + # Pas de checkers, pas d’apply + continue + + # Archive avant checks + if run_dir: + archive_patch_before(pb, run_dir=run_dir) + + # Checkers + pb = check_file(pb) + pb = check_module(pb) + + # Archive après checks + if run_dir: + archive_patch_after(pb, run_dir=run_dir) + + if pb.global_status == "ok": + # Apply (FS) + if apply_patch is None: + raise RuntimeError("apply_patch indisponible (core.fs_apply manquant).") + try: + apply_patch(pb) # type: ignore[misc] + except Exception as e: + print(f" → APPLY FAILED: {e}") + if run_dir: + append_console_log(f"[apply] failed: {e}", run_dir=run_dir) break + + # Commit (best-effort) + msg = f"feat(mARCH): {wt['plan_line_id']} {wt.get('role')} {wt.get('target_symbol')}" + try: + sha = stage_and_commit([pb.meta.file], msg, repo_root=repo_root) # type: ignore[arg-type] + inject_commit_sha_into_meta(pb, sha) + if run_dir: + archive_patch_post_commit(pb, run_dir=run_dir) + short = (sha or "")[:7] if sha else "∅" + print(f" → OK: fichier écrit & commit {short}") + except Exception as e: + print(f" → OK: fichier écrit (commit non effectué: {e})") + if run_dir: + append_console_log(f"[git] commit skipped: {e}", run_dir=run_dir) + else: + reason = getattr(pb, "error_trace", None) or "module checker" + print(f" → REJECTED: {reason}") + if run_dir: + append_console_log(f"[reject] {wt['plan_line_id']}: {reason}", run_dir=run_dir) + break + + if dry_run: + print(f"[DONE] dry-run : {produced} patch(s) écrit(s) dans {patch_dir_p}") + else: + print(f"[DONE] run complet : {produced} patch(s) traités") finally: os.chdir(prev_cwd) -if __name__ == "__main__": - ap = argparse.ArgumentParser(description="Exécute un execution_plan YAML (Phase 3 mARCHCode)") +# -----------------------------------------------------------------------------# +# CLI +# -----------------------------------------------------------------------------# + +def _build_parser() -> argparse.ArgumentParser: + """Construit le parseur CLI (compatible ancien runner + options dry-run).""" + ap = argparse.ArgumentParser(description="Exécute un execution_plan (ACWP → ACW → checkers → apply)") ap.add_argument("--ep", required=True, help="Chemin vers execution_plan.yaml") ap.add_argument("--repo", default=".", help="Racine du repo (où écrire/committer)") - ap.add_argument( - "--archive-dir", - default=None, - help="Dossier d’archive des artefacts (défaut: .arch_runs/)" - ) - args = ap.parse_args() - run_plan(args.ep, args.repo, archive_dir=args.archive_dir) + ap.add_argument("--archive-dir", default=None, help="Dossier d’archives (ignoré en --dry-run)") + ap.add_argument("--dry-run", action="store_true", help="N’émettre que les patchs (pas de checkers/apply/git)") + ap.add_argument("--patch-dir", default=".archcode/patches", help="Dossier de sortie des patchs") + return ap + + +def main(argv: Optional[List[str]] = None) -> None: + """Point d’entrée CLI.""" + parser = _build_parser() + args = parser.parse_args(argv) + run_plan(args.ep, args.repo, archive_dir=args.archive_dir, dry_run=bool(args.dry_run), patch_dir=args.patch_dir) + + +if __name__ == "__main__": + main()