diff --git a/03_agents/erpnext_backend/AGENT.md b/03_agents/erpnext_backend/AGENT.md index 04d78e2..0d6f900 100644 --- a/03_agents/erpnext_backend/AGENT.md +++ b/03_agents/erpnext_backend/AGENT.md @@ -37,7 +37,7 @@ le VPS reçoit — jamais l'inverse. vente, le barème commissions, le DocType Dossier Vente et le plan e-CF **résolvent** leurs rôles depuis ce fichier, jamais un nom Frappe en dur. Total backend **RBAC 60 tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests**, tous gated dans le CI (matrice -de régression du repo : **626 tests · 24 suites** · verdict `PASS`, source +de régression du repo : **627 tests · 24 suites** · verdict `PASS`, source `qa/regression/out/regression_run.json` — jamais compté à la main · #6). ## Hand-off reçus (à appliquer sur le VPS, dans l'ordre) diff --git a/03_agents/qa/AGENT.md b/03_agents/qa/AGENT.md index ea87c62..fb043b4 100644 --- a/03_agents/qa/AGENT.md +++ b/03_agents/qa/AGENT.md @@ -22,7 +22,7 @@ périmètre — un auditeur ne s'audite jamais. | Module | Sprint | Rôle | Entrée CLI | Job CI | Tests | |---|---|---|---|---|---| | [`audit_5d/`](../../05_deliverables_mvp/qa/audit_5d/README.md) | 5 (roadmap L57) | Audit **5D** de conformité : 17 contrôles / 5 dimensions, lit les `out/` amont, coche UAF + normes ISA/IFRS | `audit_5d_gen.py build\|validate` | `qa-audit-5d-tests` | 37 | -| [`audit_4big/`](../../05_deliverables_mvp/qa/audit_4big/README.md) | 7 (roadmap L69) | **Gate qualité** : note 4Big de 100 % des livrables, verdict `FAIL` si un module < 95/100 ou couverture incomplète | `audit_4big_gen.py build\|validate` | `qa-audit-4big-tests` | 35 | +| [`audit_4big/`](../../05_deliverables_mvp/qa/audit_4big/README.md) | 7 (roadmap L69) | **Gate qualité** : note 4Big de 100 % des livrables, verdict `FAIL` si un module < 95/100 ou couverture incomplète | `audit_4big_gen.py build\|validate` | `qa-audit-4big-tests` | 36 | | [`regression/`](../../05_deliverables_mvp/qa/regression/README.md) | 8 (roadmap L74) | **Matrice de régression** : agrège l'exécution de **toutes** les suites gated en un verdict unique + le compte agrégé faisant autorité | `regression_gen.py build\|validate\|run` | `qa-regression-tests` | 26 | | [`acceptance/`](../../05_deliverables_mvp/qa/acceptance/README.md) | 8 (roadmap L75 / L80-87) | **Recette / traçabilité** : mappe chaque promesse roadmap (8 livrables + 7 métriques) vers sa preuve, bijectif vs CI | `acceptance_gen.py build\|validate` | `qa-acceptance-tests` | 37 | @@ -32,7 +32,7 @@ périmètre — un auditeur ne s'audite jamais. **conformité** normative des hand-off. ## Verdict agrégé courant (source `qa/regression/out/regression_run.json`) -**24 suites gated · 626 tests · 609 passés · 0 échec · 0 erreur → verdict `PASS`.** +**24 suites gated · 627 tests · 610 passés · 0 échec · 0 erreur → verdict `PASS`.** Ce compte est **découvert automatiquement depuis le CI** (`q4lib.registry.parse_ci`, réutilisé sans duplication) — jamais compté à la main. Les **17 tests restants sont ignorés** (`skipped`) : ce sont les *oracles optionnels* diff --git a/05_activity_log/2026-08-11.md b/05_activity_log/2026-08-11.md index 35bfee7..b615e3e 100644 --- a/05_activity_log/2026-08-11.md +++ b/05_activity_log/2026-08-11.md @@ -217,3 +217,25 @@ Un lecteur (agent ou humain) du README — le doc le plus consulté — pouvait **Sûreté des gates (vérifiée AVANT édition).** `check_readme_claims` **exclut** `daily_reports/` de son scan (`:8239` `not f.startswith(f"{D}/daily_reports/")`) → chiffres non parsés. `check_docs` link-checke les `daily_reports/` mais la nouvelle section n'ajoute **aucun lien relatif** (uniquement des hashes de commit et chemins en code-spans, non link-checkés). `guard_constraints` : aucun URL/usage interdit (DCF/IRR/DSCR/LTV/LTC ≠ termes proscrits). **Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact reconstruit** (rapport actualisé depuis les artefacts, pas l'inverse), **0 gate ajouté** (#5). Aucune commande VPS (#8), aucune API externe. Fichiers : `05_deliverables_mvp/daily_reports/2026-08-11.md` (actualisation) + ce journal. + +--- + +## Session `20260811_082825` — HARDENING correctness · le critère CLI de l'audit 4Big loupait les gardes `__main__` simple-quotées/Yoda (détection figée sur une graphie · même classe que le fix ASCII de `ac595d0`) — teeth ajoutée + +**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation de l'audit = gate d'entrée · D-07 formules DCF/IRR/VAN/DSCR/LTV/LTC absentes · D-08 Master Data Model · directive `:90-91` « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Les bannières V18 des 4 modules-origine, le liage index et le fix `audit_4big` (session `072823`) sont clos. Les 2 nouvelles directives 08-10 (`OTO_3D_STUDIO`, `COMPTE_CLIENT_COURRIELS`) restent hors périmètre repo. + +**Vérif d'abord : le fix ASCII (`ac595d0`) a-t-il un rayon de blast CODE complet ?** La session `072823` avait vérifié le rayon **données** (« un seul nom de test non-ASCII dans le dépôt → publiciste seul »), mais **pas** le rayon **code** (d'autres regex partagent-elles le même défaut char-class-ASCII-sur-contenu-accentué ?). Sweep mené : (a) toutes les regex Python génératrices/`*lib/` (hors `/out/`, `/tests/`) → **2 compteurs de test** seulement, **tous deux `\w`** (`criteria.py:18` + `reglib/discovery.py:23`), et `check_readme_claims` **réutilise** `discovery.count_tests` (mono-source, `:663`) ; (b) `registry.py:30` `_JOB_RE` `[A-Za-z0-9_-]` = **domaine ASCII** (noms de jobs YAML, jamais accentués) — non-défaut ; (c) toutes les char-class ASCII des **gates shell** (`ci/*.sh`) → **toutes sur tokens ASCII-par-domaine** (slugs de rôles/portails kebab-case · lettres e-NCF · abréviations critères en CAPS · IDs d'invariants) — **aucune** ne scanne de la prose accentée. **Classe ASCII-vs-Unicode CLOSE au niveau code** (extension du check données de `072823`). Figures de régression en prose également **à jour** (aucun `624`/`625`/`607`/`608` stale hors snapshots datés). Le candidat `crit_cli` quote-style (littéral `"__main__"` figé) : **all 50 gardes du dépôt sont double-quotées** → aucun mis-scorage courant. + +**Défaut trouvé (classe #6 · robustesse de détection de l'outil d'audit lui-même · MÊME classe que le fix ASCII).** `q4lib/criteria.py:78` détectait l'entrypoint CLI par un **littéral figé** : `'__name__ == "__main__"' in src` — assume **une seule graphie**. Or `if __name__ == '__main__':` (**simple quote**), `if "__main__" == __name__:` (**« Yoda »**, opérandes inversés) et `if __name__=='__main__':` (**sans espaces**) sont tous du **Python valide** = un entrypoint réel. Le littéral les **loupe silencieusement** → le critère CLI échouerait sur un module sain (**−20 pts**, chute possible < 95), en violation du cœur anti-invention #6 de l'audit (« une note est recomputée à partir de faits vérifiables ») : le critère **sous-spécifie** ce qu'est un entrypoint valide. Exactement la même racine que le fix ASCII (`ac595d0`) : *une détection assume une forme syntaxique là où le langage en admet plusieurs.* + +**Pourquoi c'est légitime et pas de la sur-défense (#5).** Distinction du « ne pas coder défensivement des cas non-occurrents » : ici ce n'est **pas un gate** ajouté pour un cas d'entrée improbable d'un module produit — c'est la **correction de la logique de détection d'un critère existant** de l'**outil d'enforcement #6** lui-même, dont le contrat est d'évaluer correctement **n'importe quel Python valide**. La robustesse aux deux styles de guillemets est **dans le contrat** de l'outil, pas de l'ornement. Précédent direct : le fix ASCII a été accepté sur des bases identiques (regex ne matchant pas des identifiants Python valides). + +**Fix (chirurgical · regex de forme, pas de graphie · teeth).** `criteria.py` : nouveau `_MAIN_GUARD_RE = re.compile(r"__name__\s*==\s*['\"]__main__['\"]|['\"]__main__['\"]\s*==\s*__name__")` (deux styles de guillemets · ordre inversé · espacement libre) ; `crit_cli` passe de `'__name__ == "__main__"' in src` à `_MAIN_GUARD_RE.search(src)`. **+ teeth** : `test_cli_detects_single_quoted_and_yoda_guards` (3 fixtures : simple-quote · Yoda · sans-espaces) — **prouvé mordant** : sur l'ancien littéral les 3 rendent `False` (garde loupée) ; sur la regex les 3 rendent `True`. Aucun risque de faux positif nouveau (le motif exige la séquence complète `__name__ == "__main__"`, comme le littéral). + +**Sortie inchangée (robustesse pure · #6 auto-vérifié).** Les 50 gardes du dépôt étant double-quotées et **toujours matchées** par la nouvelle regex, `crit_cli` rend un résultat **identique pour les 24 modules** → **`quality_report.json` byte-identique** (correctement NON régénéré : `audit_4big` **s'auto-exclut** de son propre scoring — les modules qa scorés sont `qa-audit-5d`/`qa-acceptance`/`qa-regression`, pas `audit_4big` ; même patron d'auto-exclusion que la régression). Le fix ne change **aucun score** — c'est une garantie de robustesse, pas une correction de note. + +**Cascade d'artefacts (uniquement la matrice de régression · mémoire `artifact-reproducibility-gate`).** L'ajout du test augmente le décompte de la suite `audit_4big` : `regression_gen.py build` (plan) **puis** `run` (matrice) → `regression_run.json` **626 → 627 exécutés · 609 → 610 passés · 17 skippés · 0 échec** (le nouveau test). Surfaces de prose gatées re-dérivées (flaggées RED par `check_readme_claims`, corrigées) : fiche `03_agents/qa/AGENT.md` (colonne `audit_4big` **35 → 36** ← `count_tests` ; agrégat **626/609 → 627/610**) + `03_agents/erpnext_backend/AGENT.md:40` (claim live **626 → 627**). Le `daily_reports/2026-08-11` (snapshot signé) et les logs sont des instantanés datés → laissés (`two-logging-channels`). + +**Pourquoi pas un nouveau gate (#5).** La divergence de compte est déjà **structurellement gatée** (`check_readme_claims` a immédiatement rougi la fiche QA + le compte `audit_4big`) et la teeth verrouille le comportement de `crit_cli`. Occurrence latente (0 garde simple-quotée aujourd'hui) mais correction de contrat de l'outil, pas un cas d'entrée produit → pas de gate additionnel. + +**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** rétabli après régénération. **0 code moteur V18** (bloqué #6), **0 commande VPS** (#8), **0 API externe**. Fichiers : `qa/audit_4big/q4lib/criteria.py` (regex) + `qa/audit_4big/tests/test_audit_4big.py` (teeth) + `qa/regression/out/{regression_plan,regression_run,MANIFEST}.json` + `03_agents/qa/AGENT.md` + `03_agents/erpnext_backend/AGENT.md` + ce journal. `quality_report.json` **non touché** (auto-exclusion prouvée). diff --git a/05_deliverables_mvp/qa/audit_4big/q4lib/criteria.py b/05_deliverables_mvp/qa/audit_4big/q4lib/criteria.py index 1eb1b0e..1cd04a4 100644 --- a/05_deliverables_mvp/qa/audit_4big/q4lib/criteria.py +++ b/05_deliverables_mvp/qa/audit_4big/q4lib/criteria.py @@ -17,6 +17,15 @@ import re _TEST_DEF_RE = re.compile(r"^\s*def (test_\w+)\s*\(", re.MULTILINE) _ADD_PARSER_RE = re.compile(r"add_parser\(|add_subparsers\(") +# Garde `if __name__ == "__main__":` — les DEUX styles de guillemets sont du +# Python valide (PEP 8 n'impose rien), l'ordre des opérandes peut être inversé +# (« Yoda »), et l'espacement autour de `==` est libre. Un littéral figé +# `'__name__ == "__main__"'` louperait un entrypoint valide simple-quoté → +# sous-scorage silencieux du critère CLI. Détecter la forme, pas une graphie. +_MAIN_GUARD_RE = re.compile( + r"""__name__\s*==\s*['"]__main__['"]""" + r"""|['"]__main__['"]\s*==\s*__name__""" +) def _direct_files(mod_dir: str, suffix: str) -> list[str]: @@ -75,7 +84,7 @@ def crit_cli(mod_dir: str) -> dict: for py in _direct_files(mod_dir, ".py"): with open(py, encoding="utf-8") as fh: src = fh.read() - if '__name__ == "__main__"' in src and _ADD_PARSER_RE.search(src): + if _MAIN_GUARD_RE.search(src) and _ADD_PARSER_RE.search(src): return {"passed": True, "evidence": f"{os.path.basename(py)} (argparse + __main__)"} return {"passed": False, diff --git a/05_deliverables_mvp/qa/audit_4big/tests/test_audit_4big.py b/05_deliverables_mvp/qa/audit_4big/tests/test_audit_4big.py index cba1e83..9a29a76 100644 --- a/05_deliverables_mvp/qa/audit_4big/tests/test_audit_4big.py +++ b/05_deliverables_mvp/qa/audit_4big/tests/test_audit_4big.py @@ -137,6 +137,27 @@ class CriteriaTest(unittest.TestCase): mod = _make_module(d, cli=False) self.assertFalse(criteria.crit_cli(mod)["passed"]) + def test_cli_detects_single_quoted_and_yoda_guards(self): + # Robustesse : `if __name__ == '__main__':` (simple quote) et la forme + # « Yoda » `if "__main__" == __name__:` sont du Python VALIDE — un + # entrypoint réel. Le littéral figé `'__name__ == "__main__"'` les + # loupait → sous-scorage silencieux du critère CLI (−20 pts sur un + # module sain), en violation du cœur anti-invention #6 de l'audit. + for guard in ("if __name__ == '__main__':\n pass\n", + 'if "__main__" == __name__:\n pass\n', + "if __name__=='__main__':\n pass\n"): + with tempfile.TemporaryDirectory() as d: + mod = os.path.join(d, "mod") + os.makedirs(mod) + with open(os.path.join(mod, "mod_gen.py"), "w", + encoding="utf-8") as fh: + fh.write('import argparse\n' + 'ap = argparse.ArgumentParser()\n' + 'sub = ap.add_subparsers()\n' + 'sub.add_parser("build")\n' + guard) + self.assertTrue(criteria.crit_cli(mod)["passed"], + f"garde non détectée : {guard!r}") + def test_handoff_fails_without_manifest(self): with tempfile.TemporaryDirectory() as d: mod = _make_module(d, handoff=False) diff --git a/05_deliverables_mvp/qa/regression/out/MANIFEST.json b/05_deliverables_mvp/qa/regression/out/MANIFEST.json index 786df5a..5cd095d 100644 --- a/05_deliverables_mvp/qa/regression/out/MANIFEST.json +++ b/05_deliverables_mvp/qa/regression/out/MANIFEST.json @@ -5,7 +5,7 @@ "version": "1.0", "verdict": "PASS", "suites": 24, - "test_methods": 626, + "test_methods": 627, "coverage_ok": true, "artifacts": [ "regression_plan.json" diff --git a/05_deliverables_mvp/qa/regression/out/regression_plan.json b/05_deliverables_mvp/qa/regression/out/regression_plan.json index ea6becc..221032f 100644 --- a/05_deliverables_mvp/qa/regression/out/regression_plan.json +++ b/05_deliverables_mvp/qa/regression/out/regression_plan.json @@ -211,7 +211,7 @@ "in_gate": true, "has_tests_dir": true, "test_files": 1, - "test_methods": 35 + "test_methods": 36 }, { "id": "qa.audit_5d", @@ -295,7 +295,7 @@ "totals": { "suites": 24, "test_files": 24, - "test_methods": 626, + "test_methods": 627, "min_methods": 10, "under_threshold": 0 }, diff --git a/05_deliverables_mvp/qa/regression/out/regression_run.json b/05_deliverables_mvp/qa/regression/out/regression_run.json index dbfc864..fa9ecdd 100644 --- a/05_deliverables_mvp/qa/regression/out/regression_run.json +++ b/05_deliverables_mvp/qa/regression/out/regression_run.json @@ -198,9 +198,9 @@ { "id": "qa.audit_4big", "path": "qa/audit_4big", - "expected_methods": 35, - "ran": 35, - "passed": 35, + "expected_methods": 36, + "ran": 36, + "passed": 36, "failures": 0, "errors": 0, "skipped": 0, @@ -296,8 +296,8 @@ "suites": 24, "green": 24, "red": 0, - "ran": 626, - "passed": 609, + "ran": 627, + "passed": 610, "failures": 0, "errors": 0, "skipped": 17