[DTP-Worker] Sprint 8 · buffer L75 · Traçabilité non vérifiée : les n° de ligne roadmap_line de la matrice d'acceptation pouvaient pointer à côté en silence (roadmap éditée) → INV11 ancrage roadmap (dérive 8+7 du fichier · #6) + parse_roadmap_anchors + 6 tests ; régénéré 558→564
INV10 ne garantissait que « roadmap_line est un entier positif ». Ajout de parse_roadmap_anchors (deps) qui DÉRIVE la structure réelle de la roadmap, et d'INV11 qui exige que chaque roadmap_line pointe RÉELLEMENT son bullet (DELIVERABLE du sprint SX · k-ième bullet métrique) et que le « 8 + 7 » soit dérivé du fichier, pas figé. Morsure prouvée sur le spec réel (S1=999, M3=200). Régénéré consommateurs : regression 558→564 (run/plan/MANIFEST), quality_report (acceptance 31→37 méthodes, 100/100 inchangé), fiches QA + Backend, README acceptance (10→11 invariants). 7 gates verts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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**
|
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
|
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
|
tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests**, tous gated dans le CI (matrice
|
||||||
de régression du repo : **558 tests · 22 suites** · verdict `PASS`, source
|
de régression du repo : **564 tests · 22 suites** · verdict `PASS`, source
|
||||||
`qa/regression/out/regression_run.json` — jamais compté à la main · #6).
|
`qa/regression/out/regression_run.json` — jamais compté à la main · #6).
|
||||||
|
|
||||||
## Hand-off reçus (à appliquer sur le VPS, dans l'ordre)
|
## Hand-off reçus (à appliquer sur le VPS, dans l'ordre)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ périmètre — un auditeur ne s'audite jamais.
|
|||||||
**conformité** normative des hand-off.
|
**conformité** normative des hand-off.
|
||||||
|
|
||||||
## Verdict agrégé courant (source `qa/regression/out/regression_run.json`)
|
## Verdict agrégé courant (source `qa/regression/out/regression_run.json`)
|
||||||
**22 suites gated · 558 tests · 558 passés · 0 échec · 0 erreur → verdict `PASS`.**
|
**22 suites gated · 564 tests · 564 passés · 0 échec · 0 erreur → verdict `PASS`.**
|
||||||
Ce compte est **découvert automatiquement depuis le CI** (`q4lib.registry.parse_ci`,
|
Ce compte est **découvert automatiquement depuis le CI** (`q4lib.registry.parse_ci`,
|
||||||
réutilisé sans duplication) — jamais compté à la main.
|
réutilisé sans duplication) — jamais compté à la main.
|
||||||
|
|
||||||
|
|||||||
@@ -667,3 +667,69 @@ l'artefact.
|
|||||||
**Hors périmètre worker (VPS · #8)** : néant (docs + gate bash/stdlib en-repo).
|
**Hors périmètre worker (VPS · #8)** : néant (docs + gate bash/stdlib en-repo).
|
||||||
|
|
||||||
**Auto-score 4Big** : 96/100.
|
**Auto-score 4Big** : 96/100.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session `20260731_060012` · Buffer S8 · Traçabilité NON vérifiée : les n° de ligne `roadmap_line` de la matrice d'acceptation pouvaient pointer à côté en silence
|
||||||
|
|
||||||
|
**Tâche** : **Sprint 8 · buffer L75 (« Regression tests exhaustifs »)** — roadmap
|
||||||
|
fonctionnellement close (22/22 modules gated · 558 tests · 7 gates verts au démarrage).
|
||||||
|
Recherche d'un **défaut réel** (traçabilité `CLAUDE.md` #6) plutôt qu'un cosmétique.
|
||||||
|
|
||||||
|
### Défaut trouvé — `qa/acceptance` : `roadmap_line` déclaré, jamais ancré
|
||||||
|
|
||||||
|
- **Symptôme** : chaque promesse de `acceptance_spec.json` (8 livrables S1..S8 + 7
|
||||||
|
métriques M1..M7) porte un `roadmap_line` (33, 39, … 87) censé pointer son bullet
|
||||||
|
dans `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md`. Le docstring promet des « CITATIONS
|
||||||
|
verbatim de la roadmap » (#6), mais le **seul** invariant (INV10) ne vérifiait que
|
||||||
|
« `roadmap_line` est un entier positif ».
|
||||||
|
- **Contradiction** : si la roadmap est éditée (une ligne insérée/retirée, un sprint
|
||||||
|
ré-ordonné), **tous** les `roadmap_line` se décalent en silence — ils pointeraient un
|
||||||
|
bullet qui n'existe plus, tout en restant « vert ». Même classe de « vert trompeur »
|
||||||
|
que les compteurs périmés (534→551→558) ou l'`orphan_tests_dirs`, appliquée cette
|
||||||
|
fois à une **référence de ligne** (traçabilité déclarée mais non prouvée).
|
||||||
|
- **Vérif (#1 analyser)** : recomputé la structure réelle → 8 bullets `**DELIVERABLE**`
|
||||||
|
(S1@33 … S8@76) + 7 bullets métrique `- ✅` (81..87). Les `roadmap_line` du spec sont
|
||||||
|
**exacts aujourd'hui** — la faille est l'absence de garde, pas une valeur fausse.
|
||||||
|
|
||||||
|
### Fix (2 volets · vérifier + garder)
|
||||||
|
|
||||||
|
1. **`acclib/deps.py`** — nouveau `parse_roadmap_anchors(roadmap_ref)` qui **DÉRIVE**
|
||||||
|
la structure de la roadmap (jamais une liste de n° saisie · #6) : `{SX → ligne du
|
||||||
|
DELIVERABLE}` (SX **lu** dans le titre `## SPRINT n · SX (…`) + lignes ordonnées des
|
||||||
|
bullets métrique. `OSError` honnête si la roadmap est absente (référence fantôme = red).
|
||||||
|
2. **`acceptance_gen.py` · INV11 (ancrage roadmap)** — exige que (a) le nombre de bullets
|
||||||
|
DELIVERABLE == nb de livrables du spec et nb de bullets métrique == nb de métriques
|
||||||
|
(le **« 8 + 7 » devient dérivé du fichier**, plus un compte figé), (b) chaque livrable
|
||||||
|
SX pointe le DELIVERABLE de SX, (c) chaque métrique (dans l'ordre) pointe le k-ième
|
||||||
|
bullet `- ✅`. Docstring #6 mis à jour (« n° de ligne VÉRIFIÉ, pas déclaré »).
|
||||||
|
3. **Tests** — fixture roadmap **synthétique** (temp file, mêmes motifs en miniature :
|
||||||
|
S2@L3, S3@L5, M1@L7) pour contrôler l'écart injecté ; classe `TestRoadmapAnchor`
|
||||||
|
(+6 méthodes) : parse réel (8+7), DELIVERABLE désancré, métrique décalée, mauvais
|
||||||
|
propriétaire de sprint, compte en trop, roadmap absente. **37 méthodes** (31→37).
|
||||||
|
|
||||||
|
### Preuves
|
||||||
|
|
||||||
|
- **Passe** : build/validate réel **vert** ; `parse_roadmap_anchors` réel = {S1:33 …
|
||||||
|
S8:76} + métriques [81..87] ; les 15 `roadmap_line` du spec **matchent**.
|
||||||
|
- **Morsure (spec RÉEL)** : `S1.roadmap_line=999` ⇒ `roadmap: S1: … ne pointe pas le
|
||||||
|
DELIVERABLE de S1 (ligne 33)` ; `M3=200` ⇒ `… ≠ 3e bullet métrique (ligne 83)`.
|
||||||
|
|
||||||
|
**Régénération des consommateurs (mémoire projet)** : +6 méthodes test dans
|
||||||
|
`qa/acceptance` → matrice **558→564** (22 suites inchangées). Régénérés :
|
||||||
|
`qa/regression/out/{regression_run,regression_plan,MANIFEST}.json` (564/564 · PASS),
|
||||||
|
`qa/audit_4big/out/quality_report.json` (evidence acceptance « 31→37 méthodes », score
|
||||||
|
**100/100 inchangé**), fiches **QA** et **Backend** (`558→564`, gate `check_readme_claims`
|
||||||
|
vert), README acceptance (« 10→11 invariants »). L'artefact `acceptance_matrix.json` est
|
||||||
|
**byte-identique** (INV11 ne change que la validation, pas la sortie).
|
||||||
|
|
||||||
|
**Vérifs** : **7 gates** locaux verts (`guard_constraints` · `validate_json` ·
|
||||||
|
`check_docs` · `check_artifacts` · `check_regression` · `check_ci_integrity` ·
|
||||||
|
`check_readme_claims`) ; matrice **564/22 · PASS** ; 22/22 modules **100/100**.
|
||||||
|
|
||||||
|
**Anti-invention (#6)** : rien inventé — INV11 **dérive** la vérité de la roadmap et
|
||||||
|
compare ; le « 8 + 7 » cesse d'être un compte à la main.
|
||||||
|
|
||||||
|
**Hors périmètre worker (VPS · #8)** : néant (stdlib pur en-repo).
|
||||||
|
|
||||||
|
**Auto-score 4Big** : 96/100.
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ qu'aucune promesse n'est silencieusement oubliée.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 acceptance_gen.py build # écrit out/acceptance_matrix.json + out/MANIFEST.json
|
python3 acceptance_gen.py build # écrit out/acceptance_matrix.json + out/MANIFEST.json
|
||||||
python3 acceptance_gen.py validate # (re)génère en mémoire + valide (schéma + 10 invariants)
|
python3 acceptance_gen.py validate # (re)génère en mémoire + valide (schéma + 11 invariants)
|
||||||
python3 -m unittest discover -s tests -p 'test_*.py' -v
|
python3 -m unittest discover -s tests -p 'test_*.py' -v
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ python3 -m unittest discover -s tests -p 'test_*.py' -v
|
|||||||
|
|
||||||
Sortie **déterministe** (ordre du spec, listes triées, aucun horodatage).
|
Sortie **déterministe** (ordre du spec, listes triées, aucun horodatage).
|
||||||
|
|
||||||
## Invariants (10 familles)
|
## Invariants (11 familles)
|
||||||
|
|
||||||
1. ids uniques ; livrables S1..S8 + métriques M1..M7 ordonnés.
|
1. ids uniques ; livrables S1..S8 + métriques M1..M7 ordonnés.
|
||||||
2. toute preuve module est **gated** (drapeau recalculé depuis le CI).
|
2. toute preuve module est **gated** (drapeau recalculé depuis le CI).
|
||||||
@@ -68,6 +68,10 @@ Sortie **déterministe** (ordre du spec, listes triées, aucun horodatage).
|
|||||||
8. `mvp_metric` ⇒ sprint nul ; `sprint_deliverable` ⇒ sprint renseigné.
|
8. `mvp_metric` ⇒ sprint nul ; `sprint_deliverable` ⇒ sprint renseigné.
|
||||||
9. **SoD** : la matrice ne se cite jamais elle-même comme preuve.
|
9. **SoD** : la matrice ne se cite jamais elle-même comme preuve.
|
||||||
10. `roadmap_line` entier positif.
|
10. `roadmap_line` entier positif.
|
||||||
|
11. **ancrage roadmap** : chaque `roadmap_line` pointe RÉELLEMENT son bullet dans
|
||||||
|
`04_roadmap/…` (n° DELIVERABLE du sprint SX · k-ième bullet métrique), et le
|
||||||
|
compte « 8 + 7 » est **dérivé du fichier roadmap**, pas figé — une promesse
|
||||||
|
ajoutée/retirée ou des lignes décalées rougissent au lieu de dériver en silence.
|
||||||
|
|
||||||
## Hors périmètre worker (#8)
|
## Hors périmètre worker (#8)
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ module tracé non gated → génération refusée.
|
|||||||
|
|
||||||
Anti-invention (#6) : les nombres des énoncés (<1h, 95/100, 7 dashboards, 2
|
Anti-invention (#6) : les nombres des énoncés (<1h, 95/100, 7 dashboards, 2
|
||||||
stores) sont des CITATIONS verbatim de la roadmap ; la matrice ne PRÉTEND aucune
|
stores) sont des CITATIONS verbatim de la roadmap ; la matrice ne PRÉTEND aucune
|
||||||
mesure de performance ; tout hors-périmètre porte une source.
|
mesure de performance ; tout hors-périmètre porte une source. Le n° de ligne
|
||||||
|
(`roadmap_line`) de chaque promesse est VÉRIFIÉ (INV11) contre la structure réelle
|
||||||
|
de la roadmap — il ne peut pas pointer à côté en silence si la roadmap est éditée.
|
||||||
|
|
||||||
Produit :
|
Produit :
|
||||||
- acceptance_matrix.json → la matrice (une ligne par promesse : preuves
|
- acceptance_matrix.json → la matrice (une ligne par promesse : preuves
|
||||||
@@ -218,6 +220,51 @@ def validate_bundle(bundle: dict, spec: dict, ci: dict, audit: dict) -> list[str
|
|||||||
if any(not isinstance(x, int) or x < 1 for x in lines):
|
if any(not isinstance(x, int) or x < 1 for x in lines):
|
||||||
errors.append(f"roadmap_line: valeur non positive {lines}.")
|
errors.append(f"roadmap_line: valeur non positive {lines}.")
|
||||||
|
|
||||||
|
# INV11 · ANCRAGE ROADMAP (traçabilité VÉRIFIÉE, pas déclarée · #6). INV10 ne
|
||||||
|
# garantit que « roadmap_line est un entier » : un n° de ligne qui pointe à
|
||||||
|
# côté (roadmap éditée, lignes décalées) reste « vert » alors qu'il cite un
|
||||||
|
# bullet qui n'existe plus. On DÉRIVE la structure réelle de la roadmap et on
|
||||||
|
# exige que chaque roadmap_line pointe RÉELLEMENT le bullet qu'il prétend :
|
||||||
|
# · nombre de bullets DELIVERABLE == nombre de livrables de sprint du spec,
|
||||||
|
# et nombre de bullets métrique == nombre de métriques (le « 8 + 7 » n'est
|
||||||
|
# plus un compte figé mais dérivé du fichier roadmap) ;
|
||||||
|
# · chaque livrable SX pointe le bullet **DELIVERABLE** du sprint SX ;
|
||||||
|
# · chaque métrique (dans l'ordre) pointe le k-ième bullet « - ✅ ».
|
||||||
|
try:
|
||||||
|
anchors = deps.parse_roadmap_anchors(spec["roadmap_ref"])
|
||||||
|
except OSError as e:
|
||||||
|
errors.append(f"roadmap: {spec.get('roadmap_ref')!r} illisible ({e}).")
|
||||||
|
else:
|
||||||
|
dmap = anchors["deliverables"]
|
||||||
|
specd = spec.get("sprint_deliverables", [])
|
||||||
|
specm = spec.get("mvp_metrics", [])
|
||||||
|
if len(dmap) != len(specd):
|
||||||
|
errors.append(
|
||||||
|
f"roadmap: {len(dmap)} bullet(s) DELIVERABLE dans la roadmap ≠ "
|
||||||
|
f"{len(specd)} livrable(s) de sprint du spec (promesse ajoutée/retirée non tracée).")
|
||||||
|
if len(anchors["metrics"]) != len(specm):
|
||||||
|
errors.append(
|
||||||
|
f"roadmap: {len(anchors['metrics'])} bullet(s) métrique dans la roadmap ≠ "
|
||||||
|
f"{len(specm)} métrique(s) du spec (promesse ajoutée/retirée non tracée).")
|
||||||
|
for d in specd:
|
||||||
|
want = dmap.get(d["sprint"])
|
||||||
|
if want is None:
|
||||||
|
errors.append(
|
||||||
|
f"roadmap: {d['id']}: aucun bullet DELIVERABLE pour le sprint {d['sprint']}.")
|
||||||
|
elif d["roadmap_line"] != want:
|
||||||
|
errors.append(
|
||||||
|
f"roadmap: {d['id']}: roadmap_line={d['roadmap_line']} ne pointe pas "
|
||||||
|
f"le DELIVERABLE de {d['sprint']} (ligne {want}).")
|
||||||
|
for k, d in enumerate(specm):
|
||||||
|
want = anchors["metrics"][k] if k < len(anchors["metrics"]) else None
|
||||||
|
if want is None:
|
||||||
|
errors.append(
|
||||||
|
f"roadmap: {d['id']}: aucun {k + 1}e bullet métrique dans la roadmap.")
|
||||||
|
elif d["roadmap_line"] != want:
|
||||||
|
errors.append(
|
||||||
|
f"roadmap: {d['id']}: roadmap_line={d['roadmap_line']} ≠ {k + 1}e bullet "
|
||||||
|
f"métrique (ligne {want}).")
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
# qa/acceptance/acclib → 05_deliverables_mvp
|
# qa/acceptance/acclib → 05_deliverables_mvp
|
||||||
_DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..", ".."))
|
_DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..", ".."))
|
||||||
|
# 05_deliverables_mvp → racine du dépôt (où vit 04_roadmap/).
|
||||||
|
_REPO_ROOT = os.path.normpath(os.path.join(_DELIVERABLES, ".."))
|
||||||
_PUB = os.path.join(_DELIVERABLES, "publiciste")
|
_PUB = os.path.join(_DELIVERABLES, "publiciste")
|
||||||
_AUDIT = os.path.join(_DELIVERABLES, "qa", "audit_4big")
|
_AUDIT = os.path.join(_DELIVERABLES, "qa", "audit_4big")
|
||||||
|
|
||||||
@@ -55,6 +58,54 @@ def audit_sprints() -> dict:
|
|||||||
return {m["path"]: m["sprint"] for m in spec["modules"]}
|
return {m["path"]: m["sprint"] for m in spec["modules"]}
|
||||||
|
|
||||||
|
|
||||||
|
# Ancres de traçabilité de la roadmap (structure, jamais des n° saisis à la main).
|
||||||
|
_SPRINT_HEAD = re.compile(r"##\s+SPRINT\s+\d+.*?·\s*(S\d+)\s*\(")
|
||||||
|
_METRIC_HEAD = re.compile(r"##\s+M[ée]triques\s+succ")
|
||||||
|
_DELIVERABLE = re.compile(r"\*\*DELIVERABLE")
|
||||||
|
_METRIC_BULLET = re.compile(r"^-\s*✅")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_roadmap_anchors(roadmap_ref: str) -> dict:
|
||||||
|
"""DÉRIVE les ancres de traçabilité depuis la roadmap réelle (#6 · jamais une
|
||||||
|
liste de n° de ligne saisie à la main).
|
||||||
|
|
||||||
|
Renvoie :
|
||||||
|
- ``deliverables`` : {sprint SX -> n° de ligne du bullet ``**DELIVERABLE**``
|
||||||
|
appartenant au sprint SX} (le SX est LU dans le titre ``## SPRINT n … · SX (…``) ;
|
||||||
|
- ``metrics`` : [n° de ligne] des bullets ``- ✅`` sous la section
|
||||||
|
``## Métriques succès MVP``, en ordre de fichier ;
|
||||||
|
- ``path`` : le chemin résolu (pour messages).
|
||||||
|
|
||||||
|
``roadmap_ref`` absolu est utilisé tel quel ; relatif est résolu depuis la
|
||||||
|
racine du dépôt. Lève ``OSError`` si la roadmap est absente/illisible — une
|
||||||
|
référence de traçabilité vers un fichier fantôme est un mensonge (red honnête)."""
|
||||||
|
path = roadmap_ref if os.path.isabs(roadmap_ref) \
|
||||||
|
else os.path.join(_REPO_ROOT, roadmap_ref)
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
lines = fh.read().splitlines()
|
||||||
|
|
||||||
|
deliverables: dict[str, int] = {}
|
||||||
|
metrics: list[int] = []
|
||||||
|
current_sprint: str | None = None
|
||||||
|
in_metrics = False
|
||||||
|
for i, ln in enumerate(lines, 1):
|
||||||
|
mh = _SPRINT_HEAD.match(ln)
|
||||||
|
if mh:
|
||||||
|
current_sprint, in_metrics = mh.group(1), False
|
||||||
|
continue
|
||||||
|
if _METRIC_HEAD.match(ln):
|
||||||
|
current_sprint, in_metrics = None, True
|
||||||
|
continue
|
||||||
|
if ln.startswith("## "): # toute autre section clôt le contexte
|
||||||
|
current_sprint, in_metrics = None, False
|
||||||
|
continue
|
||||||
|
if current_sprint and _DELIVERABLE.search(ln):
|
||||||
|
deliverables[current_sprint] = i
|
||||||
|
if in_metrics and _METRIC_BULLET.match(ln):
|
||||||
|
metrics.append(i)
|
||||||
|
return {"deliverables": deliverables, "metrics": metrics, "path": path}
|
||||||
|
|
||||||
|
|
||||||
def artifact_exists(rel_path: str) -> bool:
|
def artifact_exists(rel_path: str) -> bool:
|
||||||
"""Vrai ssi l'artefact cité (chemin relatif à 05_deliverables_mvp) existe.
|
"""Vrai ssi l'artefact cité (chemin relatif à 05_deliverables_mvp) existe.
|
||||||
|
|
||||||
@@ -67,6 +118,7 @@ __all__ = [
|
|||||||
"validate",
|
"validate",
|
||||||
"parse_ci",
|
"parse_ci",
|
||||||
"audit_sprints",
|
"audit_sprints",
|
||||||
|
"parse_roadmap_anchors",
|
||||||
"artifact_exists",
|
"artifact_exists",
|
||||||
"DELIVERABLES_ROOT",
|
"DELIVERABLES_ROOT",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
"""Tests de la Matrice d'acceptation / traçabilité MVP (stdlib pur · zéro pip).
|
"""Tests de la Matrice d'acceptation / traçabilité MVP (stdlib pur · zéro pip).
|
||||||
|
|
||||||
Couvre : le build réel (couverture bijective vs CI Gitea + registre 4Big), la
|
Couvre : le build réel (couverture bijective vs CI Gitea + registre 4Big), la
|
||||||
conformité au schéma, les 10 familles d'invariants, le déterminisme, et une
|
conformité au schéma, les 11 familles d'invariants, le déterminisme, et une
|
||||||
batterie d'injections négatives (chaque invariant DOIT rougir quand on le viole).
|
batterie d'injections négatives (chaque invariant DOIT rougir quand on le viole).
|
||||||
|
|
||||||
Les négatifs s'appuient sur une fixture SYNTHÉTIQUE minimale (CI + registre +
|
Les négatifs s'appuient sur une fixture SYNTHÉTIQUE minimale (CI + registre +
|
||||||
spec factices) pour contrôler exactement l'écart injecté, sans dépendre de la
|
spec + roadmap factices) pour contrôler exactement l'écart injecté, sans dépendre
|
||||||
forme du dépôt réel.
|
de la forme du dépôt réel.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import atexit
|
||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -40,6 +42,29 @@ def _mini_ci(paths, self_module=_SELF) -> dict:
|
|||||||
return {"job_to_path": j2p, "gate_needs": list(j2p)}
|
return {"job_to_path": j2p, "gate_needs": list(j2p)}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Roadmap SYNTHÉTIQUE : la même structure que la vraie (## SPRINT n · SX · bullet
|
||||||
|
# **DELIVERABLE**, puis section Métriques avec bullets « - ✅ »), en miniature.
|
||||||
|
# INV11 lit ce fichier ; on contrôle donc exactement les n° de ligne attendus :
|
||||||
|
# L3 → DELIVERABLE S2 · L5 → DELIVERABLE S3 · L7 → 1er bullet métrique (M1).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
_MINI_ROADMAP = (
|
||||||
|
"# Roadmap fixture\n" # L1
|
||||||
|
"## SPRINT 2 · Fixture · S2 (obj S1)\n" # L2
|
||||||
|
"- **DELIVERABLE** : livrable S2\n" # L3
|
||||||
|
"## SPRINT 3 · Fixture · S3 (obj S2)\n" # L4
|
||||||
|
"- **DELIVERABLE** : livrable S3\n" # L5
|
||||||
|
"## Métriques succès MVP (fixture)\n" # L6
|
||||||
|
"- ✅ métrique M1\n" # L7
|
||||||
|
)
|
||||||
|
_ROADMAP_S2, _ROADMAP_S3, _ROADMAP_M1 = 3, 5, 7
|
||||||
|
|
||||||
|
_fd, _ROADMAP_PATH = tempfile.mkstemp(suffix="_roadmap.md", prefix="acc_fixture_")
|
||||||
|
with os.fdopen(_fd, "w", encoding="utf-8") as _fh:
|
||||||
|
_fh.write(_MINI_ROADMAP)
|
||||||
|
atexit.register(lambda: os.path.exists(_ROADMAP_PATH) and os.remove(_ROADMAP_PATH))
|
||||||
|
|
||||||
|
|
||||||
def _mini_audit() -> dict:
|
def _mini_audit() -> dict:
|
||||||
return {"a/one": "S2", "b/two": "S3"}
|
return {"a/one": "S2", "b/two": "S3"}
|
||||||
|
|
||||||
@@ -48,23 +73,23 @@ def _mini_spec() -> dict:
|
|||||||
return {
|
return {
|
||||||
"version": "test",
|
"version": "test",
|
||||||
"reference_cadre": "fixture",
|
"reference_cadre": "fixture",
|
||||||
"roadmap_ref": "ROADMAP.md",
|
"roadmap_ref": _ROADMAP_PATH,
|
||||||
"self_module": _SELF,
|
"self_module": _SELF,
|
||||||
"cible_portage": "recette",
|
"cible_portage": "recette",
|
||||||
"extra_module_sprint": {},
|
"extra_module_sprint": {},
|
||||||
"sprint_deliverables": [
|
"sprint_deliverables": [
|
||||||
{"id": "S2", "kind": "sprint_deliverable", "sprint": "S2",
|
{"id": "S2", "kind": "sprint_deliverable", "sprint": "S2",
|
||||||
"roadmap_line": 39, "statement": "livrable S2", "status": "in_repo",
|
"roadmap_line": _ROADMAP_S2, "statement": "livrable S2", "status": "in_repo",
|
||||||
"evidence_modules": ["a/one"], "evidence_artifacts": [],
|
"evidence_modules": ["a/one"], "evidence_artifacts": [],
|
||||||
"out_of_scope": []},
|
"out_of_scope": []},
|
||||||
{"id": "S3", "kind": "sprint_deliverable", "sprint": "S3",
|
{"id": "S3", "kind": "sprint_deliverable", "sprint": "S3",
|
||||||
"roadmap_line": 46, "statement": "livrable S3", "status": "in_repo",
|
"roadmap_line": _ROADMAP_S3, "statement": "livrable S3", "status": "in_repo",
|
||||||
"evidence_modules": ["b/two"], "evidence_artifacts": [],
|
"evidence_modules": ["b/two"], "evidence_artifacts": [],
|
||||||
"out_of_scope": []},
|
"out_of_scope": []},
|
||||||
],
|
],
|
||||||
"mvp_metrics": [
|
"mvp_metrics": [
|
||||||
{"id": "M1", "kind": "mvp_metric", "sprint": None,
|
{"id": "M1", "kind": "mvp_metric", "sprint": None,
|
||||||
"roadmap_line": 81, "statement": "métrique M1", "status": "in_repo",
|
"roadmap_line": _ROADMAP_M1, "statement": "métrique M1", "status": "in_repo",
|
||||||
"evidence_modules": ["a/one"], "evidence_artifacts": [],
|
"evidence_modules": ["a/one"], "evidence_artifacts": [],
|
||||||
"out_of_scope": []},
|
"out_of_scope": []},
|
||||||
],
|
],
|
||||||
@@ -279,6 +304,63 @@ class TestNegativeInjections(unittest.TestCase):
|
|||||||
self.assertFalse(b["verdict"])
|
self.assertFalse(b["verdict"])
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
class TestRoadmapAnchor(unittest.TestCase):
|
||||||
|
"""INV11 · les n° de ligne `roadmap_line` sont ANCRÉS à la structure réelle
|
||||||
|
de la roadmap (traçabilité vérifiée, pas déclarée)."""
|
||||||
|
|
||||||
|
def test_parse_real_roadmap_structure(self):
|
||||||
|
# Le parseur dérive 8 livrables (S1..S8) + 7 métriques de la vraie roadmap.
|
||||||
|
spec = _spec()
|
||||||
|
a = deps.parse_roadmap_anchors(spec["roadmap_ref"])
|
||||||
|
self.assertEqual(sorted(a["deliverables"]),
|
||||||
|
[f"S{i}" for i in range(1, 9)])
|
||||||
|
self.assertEqual(len(a["metrics"]), 7)
|
||||||
|
# Chaque roadmap_line du spec réel pointe bien son bullet.
|
||||||
|
for d in spec["sprint_deliverables"]:
|
||||||
|
self.assertEqual(d["roadmap_line"], a["deliverables"][d["sprint"]],
|
||||||
|
f"{d['id']} désancré")
|
||||||
|
for k, d in enumerate(spec["mvp_metrics"]):
|
||||||
|
self.assertEqual(d["roadmap_line"], a["metrics"][k], f"{d['id']} désancré")
|
||||||
|
|
||||||
|
def test_deliverable_line_offby_fails(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["sprint_deliverables"][0]["roadmap_line"] = 99 # ne pointe plus S2
|
||||||
|
self.assertTrue(any("ne pointe pas" in e for e in _mini_errors(spec)),
|
||||||
|
_mini_errors(spec))
|
||||||
|
|
||||||
|
def test_metric_line_offby_fails(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["mvp_metrics"][0]["roadmap_line"] = 1 # pas le bullet métrique
|
||||||
|
self.assertTrue(any("bullet métrique" in e for e in _mini_errors(spec)),
|
||||||
|
_mini_errors(spec))
|
||||||
|
|
||||||
|
def test_wrong_sprint_owner_fails(self):
|
||||||
|
# Le livrable prétend S3 mais garde la ligne du DELIVERABLE de S2.
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["sprint_deliverables"][0]["sprint"] = "S3"
|
||||||
|
spec["sprint_deliverables"][0]["roadmap_line"] = _ROADMAP_S2
|
||||||
|
self.assertTrue(any("ne pointe pas" in e for e in _mini_errors(spec)),
|
||||||
|
_mini_errors(spec))
|
||||||
|
|
||||||
|
def test_extra_deliverable_count_mismatch_fails(self):
|
||||||
|
# Un livrable de plus que ce que contient la roadmap fixture.
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["sprint_deliverables"].append({
|
||||||
|
"id": "S4", "kind": "sprint_deliverable", "sprint": "S4",
|
||||||
|
"roadmap_line": 3, "statement": "x", "status": "in_repo",
|
||||||
|
"evidence_modules": ["a/one"], "evidence_artifacts": [], "out_of_scope": []})
|
||||||
|
errs = _mini_errors(spec)
|
||||||
|
self.assertTrue(any("bullet(s) DELIVERABLE" in e for e in errs)
|
||||||
|
or any("aucun bullet DELIVERABLE" in e for e in errs), errs)
|
||||||
|
|
||||||
|
def test_missing_roadmap_file_fails(self):
|
||||||
|
spec = _mini_spec()
|
||||||
|
spec["roadmap_ref"] = "/nonexistent/path/ROADMAP.md"
|
||||||
|
self.assertTrue(any("illisible" in e for e in _mini_errors(spec)),
|
||||||
|
_mini_errors(spec))
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
class TestCLI(unittest.TestCase):
|
class TestCLI(unittest.TestCase):
|
||||||
"""Le CLI build/validate produit les artefacts et sort proprement."""
|
"""Le CLI build/validate produit les artefacts et sort proprement."""
|
||||||
|
|||||||
@@ -858,7 +858,7 @@
|
|||||||
"criterion": "DOC",
|
"criterion": "DOC",
|
||||||
"weight": 20,
|
"weight": 20,
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"evidence": "README.md (3714 octets)"
|
"evidence": "README.md (4061 octets)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"criterion": "CONTRAT",
|
"criterion": "CONTRAT",
|
||||||
@@ -870,7 +870,7 @@
|
|||||||
"criterion": "TESTS",
|
"criterion": "TESTS",
|
||||||
"weight": 25,
|
"weight": 25,
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"evidence": "31 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
"evidence": "37 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"criterion": "CLI",
|
"criterion": "CLI",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"verdict": "PASS",
|
"verdict": "PASS",
|
||||||
"suites": 22,
|
"suites": 22,
|
||||||
"test_methods": 558,
|
"test_methods": 564,
|
||||||
"coverage_ok": true,
|
"coverage_ok": true,
|
||||||
"artifacts": [
|
"artifacts": [
|
||||||
"regression_plan.json"
|
"regression_plan.json"
|
||||||
|
|||||||
@@ -178,7 +178,7 @@
|
|||||||
"in_gate": true,
|
"in_gate": true,
|
||||||
"has_tests_dir": true,
|
"has_tests_dir": true,
|
||||||
"test_files": 1,
|
"test_files": 1,
|
||||||
"test_methods": 31
|
"test_methods": 37
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "qa.audit_4big",
|
"id": "qa.audit_4big",
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
"totals": {
|
"totals": {
|
||||||
"suites": 22,
|
"suites": 22,
|
||||||
"test_files": 22,
|
"test_files": 22,
|
||||||
"test_methods": 558,
|
"test_methods": 564,
|
||||||
"min_methods": 10,
|
"min_methods": 10,
|
||||||
"under_threshold": 0
|
"under_threshold": 0
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -162,9 +162,9 @@
|
|||||||
{
|
{
|
||||||
"id": "qa.acceptance",
|
"id": "qa.acceptance",
|
||||||
"path": "qa/acceptance",
|
"path": "qa/acceptance",
|
||||||
"expected_methods": 31,
|
"expected_methods": 37,
|
||||||
"ran": 31,
|
"ran": 37,
|
||||||
"passed": 31,
|
"passed": 37,
|
||||||
"failures": 0,
|
"failures": 0,
|
||||||
"errors": 0,
|
"errors": 0,
|
||||||
"skipped": 0,
|
"skipped": 0,
|
||||||
@@ -272,8 +272,8 @@
|
|||||||
"suites": 22,
|
"suites": 22,
|
||||||
"green": 22,
|
"green": 22,
|
||||||
"red": 0,
|
"red": 0,
|
||||||
"ran": 558,
|
"ran": 564,
|
||||||
"passed": 558,
|
"passed": 564,
|
||||||
"failures": 0,
|
"failures": 0,
|
||||||
"errors": 0,
|
"errors": 0,
|
||||||
"skipped": 0
|
"skipped": 0
|
||||||
|
|||||||
Reference in New Issue
Block a user