From cac65386abc564b66bbc55aff985bdd4e684b39e Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Thu, 30 Jul 2026 10:39:24 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=207=20=C2=B7=20G=C3=A9n?= =?UTF-8?q?=C3=A9rateur=20Sc=C3=A9narios=20d=C3=A9mo=20(run-sheet=20P07=20?= =?UTF-8?q?banquier=20/=20P05=20client)=20(CRM+Faisabilit=C3=A9=20=C2=B7?= =?UTF-8?q?=20roadmap=20L68)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Orchestrateur méta-niveau : compose les hand-offs out/ des 10 modules déjà livrés en un run-sheet de démo jouable, via pointeurs RFC 6901 résolus à la construction (zéro chiffre en dur · #6). Réutilise le validateur Publiciste et la preuve de couverture CI de l'auditeur 4Big (#5) — une démo ne s'appuie que sur des modules gated. Enregistre demo/scenarios dans l'audit 4Big (17→18, verdict PASS 18/18 à 100). 32 tests (9 injections négatives) · régression 474 tests verts · guards CI verts · builds déterministes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/ci.yml | 13 +- 05_activity_log/2026-07-30.md | 52 +++ .../daily_reports/2026-07-30-session21.md | 82 +++++ 05_deliverables_mvp/demo/scenarios/.gitignore | 2 + 05_deliverables_mvp/demo/scenarios/README.md | 87 +++++ .../demo/scenarios/demo_scenario_gen.py | 250 +++++++++++++ .../demo/scenarios/out/MANIFEST.json | 69 ++++ .../demo/scenarios/out/run_sheet.json | 342 ++++++++++++++++++ .../demo/scenarios/scenario.schema.json | 73 ++++ .../demo/scenarios/scenario_spec.json | 151 ++++++++ .../demo/scenarios/scenlib/__init__.py | 9 + .../demo/scenarios/scenlib/builder.py | 123 +++++++ .../demo/scenarios/scenlib/deps.py | 44 +++ .../demo/scenarios/scenlib/evidence.py | 112 ++++++ .../scenarios/tests/test_demo_scenario.py | 230 ++++++++++++ .../qa/audit_4big/out/MANIFEST.json | 2 +- .../qa/audit_4big/out/quality_report.json | 52 ++- .../qa/audit_4big/quality_spec.json | 5 +- .../qa/audit_4big/tests/test_audit_4big.py | 4 +- 19 files changed, 1691 insertions(+), 11 deletions(-) create mode 100644 05_deliverables_mvp/daily_reports/2026-07-30-session21.md create mode 100644 05_deliverables_mvp/demo/scenarios/.gitignore create mode 100644 05_deliverables_mvp/demo/scenarios/README.md create mode 100644 05_deliverables_mvp/demo/scenarios/demo_scenario_gen.py create mode 100644 05_deliverables_mvp/demo/scenarios/out/MANIFEST.json create mode 100644 05_deliverables_mvp/demo/scenarios/out/run_sheet.json create mode 100644 05_deliverables_mvp/demo/scenarios/scenario.schema.json create mode 100644 05_deliverables_mvp/demo/scenarios/scenario_spec.json create mode 100644 05_deliverables_mvp/demo/scenarios/scenlib/__init__.py create mode 100644 05_deliverables_mvp/demo/scenarios/scenlib/builder.py create mode 100644 05_deliverables_mvp/demo/scenarios/scenlib/deps.py create mode 100644 05_deliverables_mvp/demo/scenarios/scenlib/evidence.py create mode 100644 05_deliverables_mvp/demo/scenarios/tests/test_demo_scenario.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ca6ec41..dcf153e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -362,10 +362,21 @@ jobs: - name: Tests générateur Audit 4Big run: python3 -m unittest discover -s tests -v + demo-scenario-tests: + name: Démo · Scénarios (run-sheet P07 banquier / P05 client) + runs-on: ubuntu-latest + defaults: + run: + working-directory: 05_deliverables_mvp/demo/scenarios + steps: + - uses: actions/checkout@v4 + - name: Tests générateur Scénarios démo + run: python3 -m unittest discover -s tests -v + gate: name: Gate qualité (agrégat) runs-on: ubuntu-latest - needs: [constraints-guard, validate-json, check-docs, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests, bancable-tests, crm-workflow-vente-tests, crm-dossier-vente-tests, crm-commissions-tests, fiscal-ecf-tests, frontend-portails-tests, legal-confotur-tests, qa-audit-5d-tests, seo-tests, chat-otoia-tests, qa-audit-4big-tests] + needs: [constraints-guard, validate-json, check-docs, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests, bancable-tests, crm-workflow-vente-tests, crm-dossier-vente-tests, crm-commissions-tests, fiscal-ecf-tests, frontend-portails-tests, legal-confotur-tests, qa-audit-5d-tests, seo-tests, chat-otoia-tests, qa-audit-4big-tests, demo-scenario-tests] steps: - name: Résultat run: echo "✅ Gate qualité 4Big franchi — tous les checks verts." diff --git a/05_activity_log/2026-07-30.md b/05_activity_log/2026-07-30.md index 90d393e..466ca5a 100644 --- a/05_activity_log/2026-07-30.md +++ b/05_activity_log/2026-07-30.md @@ -1,5 +1,57 @@ # Activity Log · 2026-07-30 · Claude Code DTP +## Session `20260730_102730` (session 21) + +**Tâche** : **Sprint 7 · CRM + Faisabilité** — Générateur des **Scénarios démo +(run-sheet de pitch)** (roadmap **l.68** « CRM + Faisabilité : Scénarios démo +(**P07 pitch banquier · P05 client ready**) »). Dernier volet Sprint 7 réalisable +en repo — le polish `otov7.com` (Frontend) et le branchement au `data_room` réel +dépendent du site live / archives serveur (hors périmètre worker · #8). + +**Décision d'architecture** : **orchestrateur de méta-niveau**, pas du contenu +marketing écrit à la main. Un scénario = suite de **beats** ; chaque beat cite une +**preuve** = un **pointeur RFC 6901** vers un hand-off `out/` d'un module déjà +livré, **résolu à la construction**. Zéro chiffre dans le spec ; une valeur +introuvable devient un placeholder `{{module:file#pointer}}`, jamais un `0` +fabriqué (#6). Zéro duplication (#5) : réutilise le validateur Publiciste **et** la +preuve de couverture CI de l'auditeur 4Big (`q4lib/registry.py`) — une démo ne +s'appuie QUE sur des modules **gated**. + +**Fichiers créés** — `05_deliverables_mvp/demo/scenarios/` : +- `scenario_spec.json` (2 scénarios · 12 beats · 23 pointeurs · 0 chiffre en dur) +- `scenlib/{__init__,deps,evidence,builder}.py` (résolveur RFC 6901 + assemblage + déterministe · anti-invention) +- `demo_scenario_gen.py` (CLI `build`/`validate` · **7 familles d'invariants**) +- `scenario.schema.json` (contrat de sortie draft-07) +- `out/{run_sheet,MANIFEST}.json` (hand-off) · `tests/test_demo_scenario.py` + (**32 tests** dont 9 injections négatives) · `README.md` · `.gitignore` + +**Fichiers modifiés** : +- `.gitea/workflows/ci.yml` : job `demo-scenario-tests` + ajout au `gate`. +- `qa/audit_4big/quality_spec.json` + `tests/` + `out/` : enregistrement du module + `demo/scenarios` (couverture bijective **17 → 18 modules** · verdict **PASS + 18/18 à 100**). L'auditeur DOIT tracer chaque nouveau livrable, sinon sa preuve + de couverture rougit. + +**Anti-invention (cœur · #6)** : `_check_anti_invention` **re-résout +indépendamment** chaque pointeur depuis le disque et compare au run-sheet ; un +`null` amont est traité comme non résolu (jamais promu en chiffre de pitch). + +**Résultat** : run-sheet `pret=true` — **2 scénarios · 12 beats · 23/23 citations +résolues · 10 modules cités tous gated**. + +**Vérifs** : 32/32 tests module ; 34/34 audit_4big ; régression **474 tests verts** +(442 → +32) · 0 module en échec ; guards CI locaux verts ; `ci.yml` YAML valide ; +builds déterministes. + +**Hors périmètre worker (VPS · #8)** : rendu jouable (deck / prompteur / page démo +`otov7.com`) + branchement `data_room` réel → agent Frontend / DevOps. + +**Détail complet** : voir +[`05_deliverables_mvp/daily_reports/2026-07-30-session21.md`](../05_deliverables_mvp/daily_reports/2026-07-30-session21.md). + +**Auto-score 4Big** : 96/100. + ## Session `20260730_095728` (session 20) **Tâche** : **Sprint 7 · QA** — Générateur de l'**Audit 4Big qualité** (roadmap diff --git a/05_deliverables_mvp/daily_reports/2026-07-30-session21.md b/05_deliverables_mvp/daily_reports/2026-07-30-session21.md new file mode 100644 index 0000000..935a60c --- /dev/null +++ b/05_deliverables_mvp/daily_reports/2026-07-30-session21.md @@ -0,0 +1,82 @@ +# Rapport de session · 2026-07-30 · session 21 + +## Tâche + +**Sprint 7 · CRM + Faisabilité** — Générateur des **Scénarios démo (run-sheet de +pitch)** (roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` **l.68** : +« CRM + Faisabilité : Scénarios démo (**P07 pitch banquier · P05 client ready**) »). + +C'est le **dernier volet Sprint 7 réalisable en repo**. Les deux autres volets +(polish `otov7.com` côté Frontend, branchement au `data_room` réel des projets) +dépendent du site live / des archives serveur — hors périmètre worker (#8). + +## Décision d'architecture + +**Orchestrateur de méta-niveau**, pas un contenu marketing écrit à la main. Un +scénario démo est une suite de **beats** (scènes) ; chaque beat cite une **preuve** += un **pointeur RFC 6901** vers un hand-off `out/` d'un module **déjà livré**. Le +générateur **résout** la valeur à la construction. Conséquences : + +- **Zéro invention (#6)** : aucune valeur chiffrée n'est écrite dans le spec ; une + valeur introuvable devient un placeholder `{{module:file#pointer}}` — jamais un + `0` fabriqué, ni un `null` amont promu en chiffre de pitch. +- **Zéro duplication (#5)** : réutilise le validateur maison du Publiciste **et** + la preuve de couverture CI de l'auditeur 4Big (`q4lib/registry.py`) pour + garantir qu'une démo ne s'appuie QUE sur des modules **gated** par le CI. + +## Fichiers créés — `05_deliverables_mvp/demo/scenarios/` + +- `scenario_spec.json` — 2 scénarios · 12 beats · 23 pointeurs de preuve + (**0 chiffre en dur** · libellés projets P05/P07 verbatim de CLAUDE.md §Projets) +- `scenlib/{__init__,deps,evidence,builder}.py` + - `deps` réutilise `validator.validate` (Publiciste) + `registry.ci_module_paths` + (audit 4Big) · `evidence` charge les `out/` amont + résout les pointeurs + RFC 6901 · `builder` assemble run-sheet + manifeste de façon déterministe +- `demo_scenario_gen.py` — CLI `build`/`validate` + **7 familles d'invariants** +- `scenario.schema.json` — contrat de sortie draft-07 +- `out/{run_sheet,MANIFEST}.json` — hand-off +- `tests/test_demo_scenario.py` — **32 tests** (dont 9 injections négatives) +- `README.md` · `.gitignore` + +## Fichiers modifiés + +- `.gitea/workflows/ci.yml` : job `demo-scenario-tests` + ajout au `gate`. +- `qa/audit_4big/quality_spec.json` : enregistrement du module `demo/scenarios` + (archétype `generator`) + prose « 17 → 18 modules ». +- `qa/audit_4big/tests/test_audit_4big.py` : `totals.modules` **17 → 18** + (2 assertions) — l'auditeur doit tracer chaque nouveau livrable, sinon sa + **preuve de couverture bijective rougit** (garde-fou anti-omission). +- `qa/audit_4big/out/*` régénéré : **verdict PASS · 18/18 modules ≥ 95 (min 100)**. + +## Anti-invention (cœur · #6) + +Une valeur du run-sheet **ne peut pas être posée** : `_check_anti_invention` +**re-résout indépendamment** chaque pointeur depuis le disque et compare +(valeur + statut résolu) au run-sheet. Recoupe aussi spec ↔ run-sheet (mêmes +pointeurs, même ordre). Un `null` amont (`condition: null` d'une transition) est +traité comme **non résolu** → placeholder, jamais promu en chiffre. + +## Résultat + +Run-sheet `pret=true` : **2 scénarios · 12 beats · 23/23 citations résolues · +10 modules cités tous gated**. Valeur = **contrat déterministe** qu'un rendu VPS +(deck / prompteur / page démo) consommera sans risque d'invention. + +## Vérifications + +- 32/32 tests du module ; 34/34 tests audit_4big (après passage à 18 modules). +- Régression complète : **474 tests verts** (442 → +32), **0 module en échec**. +- Guards CI locaux verts : `guard_constraints` · `validate_json` · `check_docs`. +- `ci.yml` : YAML valide · job + `needs` du gate présents. +- Builds déterministes (double build identique). + +## Hors périmètre worker (VPS · #8) + +Rendu final (deck / prompteur / page démo `otov7.com`) + branchement au +`data_room` réel → agent Frontend / DevOps. Le run-sheet est le **contrat**. + +## Auto-score 4Big + +**96/100** — orchestrateur cross-module, anti-invention prouvée par re-résolution, +couverture CI réutilisée, injections négatives. −4 : le rendu jouable (deck) reste +côté VPS, non démontrable en repo. diff --git a/05_deliverables_mvp/demo/scenarios/.gitignore b/05_deliverables_mvp/demo/scenarios/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/05_deliverables_mvp/demo/scenarios/README.md b/05_deliverables_mvp/demo/scenarios/README.md new file mode 100644 index 0000000..6801647 --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/README.md @@ -0,0 +1,87 @@ +# Scénarios démo · run-sheet de pitch — Sprint 7 (CRM + Faisabilité) + +> Roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` · Sprint 7 l.68 — +> « **CRM + Faisabilité : Scénarios démo (P07 pitch banquier · P05 client ready)** ». + +Générateur **de méta-niveau** qui **compose** les livrables déjà produits en un +**run-sheet de démo** prêt à jouer, sans écrire une seule donnée métier lui-même. +Deux scénarios sont assemblés : + +| Scénario | Projet | Audience | Angle | +|---|---|---|---| +| `S-P07-BANQUIER` | **P07 Aqua Terra Las Terrenas** | banquier | bancabilité, conformité fiscale, CONFOTUR, gouvernance, gate qualité | +| `S-P05-CLIENT` | **P05 Las Colinas Najayo Arriba** | client | portail, OTOIA embarqué, pipeline lisible, dossier transparent, SEO | + +## Le principe : zéro invention (CLAUDE.md #6) + +Un scénario est une suite de **beats** (scènes). Chaque beat cite une **preuve** +(`evidence`) : un **pointeur RFC 6901** vers un hand-off `out/` d'un module déjà +livré. Le générateur **résout** la valeur à la construction — elle n'est **jamais +saisie à la main**. + +``` +scenario_spec.json out/ des modules livrés (10 modules cités) + (pointeurs, 0 chiffre) ───────────────────────────────────────────┐ + │ │ + ▼ demo_scenario_gen build ▼ + out/run_sheet.json ◄──── résolution RFC 6901 + placeholders ──── crm/workflow_vente + out/MANIFEST.json fiscal/ecf_dgii + legal/confotur + rbac/apply_plan + qa/audit_4big … +``` + +Une valeur introuvable devient un **placeholder** `{{module:file#pointer}}` — +jamais un `0` fabriqué, jamais un `null` amont promu en chiffre de pitch. + +## Garanties (le CLI REFUSE d'écrire si l'une casse) + +1. **Anti-invention** — chaque citation est **re-résolue indépendamment** depuis + le disque et comparée au run-sheet (`_check_anti_invention`). +2. **Couverture CI** — tout module cité doit être **gated** par + `.gitea/workflows/ci.yml`. On **réutilise** la preuve de couverture de + l'auditeur 4Big (`qa/audit_4big/q4lib/registry.py`) — une démo ne repose que + sur du validé. Zéro duplication (#5). +3. **Orchestration réelle** — chaque scénario tisse **≥ 2 modules distincts** et + **≥ 3 beats**, et ne **s'auto-cite jamais** (séparation des pouvoirs). +4. **Prêt à jouer** — l'artefact livré est `pret=true` : 0 citation non résolue, + 0 module non gated. Sinon, rien n'est écrit. +5. **Contrat de sortie** — `MANIFEST.json` est validé contre `scenario.schema.json` + par le **validateur maison** du Publiciste (draft-07, sans pip · #2). + +## Utilisation + +```bash +# Assemble le run-sheet (défaut: ./out) +python3 demo_scenario_gen.py build [-o OUT_DIR] + +# Recalcule le manifeste + les invariants, sans écrire +python3 demo_scenario_gen.py validate + +# Tests (32 méthodes, dont injections négatives) +python3 -m unittest discover -s tests -v +``` + +## Fichiers + +| Fichier | Rôle | +|---|---| +| `scenario_spec.json` | Les 2 scénarios · beats · pointeurs de preuve (**0 chiffre en dur**) | +| `scenlib/deps.py` | Réutilise validateur Publiciste + couverture CI 4Big | +| `scenlib/evidence.py` | Chargement `out/` amont + résolveur RFC 6901 + placeholders | +| `scenlib/builder.py` | Assemblage déterministe run-sheet + manifeste | +| `demo_scenario_gen.py` | CLI `build`/`validate` + invariants | +| `scenario.schema.json` | Contrat de sortie draft-07 du manifeste | +| `out/run_sheet.json` | **Hand-off principal** — run-sheet jouable | +| `out/MANIFEST.json` | Diagnostic + preuve de couverture + drapeau `pret` | + +## Hors périmètre worker (VPS · #8) + +Le rendu final (deck / prompteur / page démo `otov7.com`) et le branchement au +`data_room` réel des projets restent **côté serveur** — ce worker n'écrit jamais +sur le VPS. Le run-sheet est le **contrat** que ce rendu consommera. + +## Provenance des libellés projets + +`P05 Las Colinas Najayo Arriba` et `P07 Aqua Terra Las Terrenas` proviennent +**verbatim** de `CLAUDE.md · §Projets` — aucun projet inventé. diff --git a/05_deliverables_mvp/demo/scenarios/demo_scenario_gen.py b/05_deliverables_mvp/demo/scenarios/demo_scenario_gen.py new file mode 100644 index 0000000..76a0c0f --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/demo_scenario_gen.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Générateur de scénarios démo (run-sheet de pitch) · Sprint 7 · CRM + Faisabilité. + +Livrable **Sprint 7** (roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md` l.68 : +« CRM + Faisabilité : Scénarios démo (P07 pitch banquier · P05 client ready) »). + +C'est un **orchestrateur de méta-niveau** : il ne crée aucune donnée métier, il +COMPOSE les hand-offs `out/` déjà produits par les modules livrés en un run-sheet +de démo prêt à jouer (P07 banquier · P05 client). Toute valeur chiffrée du +run-sheet est RÉSOLUE depuis un artefact amont via un pointeur RFC 6901 — jamais +saisie à la main (anti-invention CLAUDE.md #6). Une valeur introuvable devient un +placeholder `{{…}}`, jamais un chiffre fabriqué. + +Chaîne de valeur : + + out/ des modules livrés (workflow, e-CF, CONFOTUR, RBAC, 4Big, portails, …) + │ pointeurs RFC 6901 (scenario_spec.json) + ▼ + demo_scenario_gen ──► out/run_sheet.json + out/MANIFEST.json + │ + ▼ (VPS · hors périmètre worker · #8) + rendu deck / prompteur ─► démo publique otov7.com + +Garanties (le CLI REFUSE d'écrire si l'une casse) : + · chaque module cité est GATED par le CI (`.gitea/workflows/ci.yml`) — la démo + ne s'appuie que sur du validé (réutilise la preuve de l'auditeur 4Big) ; + · chaque valeur est re-résolue indépendamment et comparée (anti-invention) ; + · l'artefact livré est `pret=true` (0 citation non résolue · 0 module non gated). + +Sous-commandes : + build [-o OUT_DIR] → OUT_DIR/{run_sheet,MANIFEST}.json + validate → manifeste + invariants (sans écrire) +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, _HERE) + +from scenlib import builder, deps, evidence # noqa: E402 + + +def _eprint(*args) -> None: + print(*args, file=sys.stderr) + + +def _load_spec() -> dict: + with open(os.path.join(_HERE, "scenario_spec.json"), encoding="utf-8") as fh: + return json.load(fh) + + +def _load_schema() -> dict: + with open(os.path.join(_HERE, "scenario.schema.json"), encoding="utf-8") as fh: + return json.load(fh) + + +# --------------------------------------------------------------------------- # +# Invariants — honnêteté #6 : le CLI n'écrit RIEN si l'un casse. +# --------------------------------------------------------------------------- # +def check_invariants(spec: dict, run_sheet: dict, manifest: dict) -> list[str]: + motifs: list[str] = [] + + # (a) manifeste conforme au schéma de sortie (validateur maison Publiciste). + motifs += [f"manifest: {e}" for e in deps.validate(manifest, _load_schema())] + + # (b) identité des scénarios : ids uniques, projet/audience déclarés au spec. + ids = [sc["id"] for sc in run_sheet["scenarios"]] + if len(ids) != len(set(ids)): + motifs.append(f"ids de scénario non uniques : {ids}") + for sc in run_sheet["scenarios"]: + if sc["projet"] not in spec["projets"]: + motifs.append(f"{sc['id']} : projet {sc['projet']} absent du spec.projets") + if sc["audience"] not in spec["audiences"]: + motifs.append(f"{sc['id']} : audience {sc['audience']} hors spec.audiences") + if sc["projet_libelle"] != spec["projets"].get(sc["projet"]): + motifs.append(f"{sc['id']} : libellé projet incohérent avec le spec") + + # (c) structure de chaque scénario : beats ordonnés 1..n, ≥3 beats, + # ≥2 modules distincts (un orchestrateur DOIT tisser plusieurs livrables), + # et jamais s'auto-citer (pas de preuve circulaire · SoD). + for sc in run_sheet["scenarios"]: + ordres = [b["ordre"] for b in sc["beats"]] + if ordres != list(range(1, len(sc["beats"]) + 1)): + motifs.append(f"{sc['id']} : ordres de beats non contigus {ordres}") + if len(sc["beats"]) < 3: + motifs.append(f"{sc['id']} : {len(sc['beats'])} beat(s) (min 3)") + mods = {c["module"] for b in sc["beats"] for c in b["evidence"]} + if len(mods) < 2: + motifs.append(f"{sc['id']} : {len(mods)} module cité (min 2 · orchestration)") + if builder.SELF_MODULE in mods: + motifs.append(f"{sc['id']} : auto-citation de {builder.SELF_MODULE} (SoD)") + + # (d) ANTI-INVENTION : re-résolution indépendante de CHAQUE citation. + motifs += _check_anti_invention(spec, run_sheet) + + # (e) placeholder ⟺ non résolu (aucune fuite de None ni de faux chiffre). + for sc in run_sheet["scenarios"]: + for b in sc["beats"]: + for c in b["evidence"]: + ph = evidence.is_placeholder(c["valeur"]) + if c["resolu"] and (ph or c["valeur"] is None): + motifs.append(f"{c['module']}#{c['pointer']} : resolu mais valeur placeholder/None") + if not c["resolu"] and not ph: + motifs.append(f"{c['module']}#{c['pointer']} : non résolu sans placeholder") + + # (f) comptes recalculés == manifeste (aucun compteur « posé »). + motifs += _check_counts(run_sheet, manifest) + + # (g) couverture CI : tout module cité doit être gated + drapeau pret. + if not manifest["couverture_ci"]["ok"] or manifest["couverture_ci"]["modules_cites_non_gated"]: + motifs.append("module cité NON gated par le CI : " + f"{manifest['couverture_ci']['modules_cites_non_gated']}") + if not manifest["pret"]: + motifs.append("run-sheet non prêt (citations non résolues ou modules non gated) — " + "un livrable démo doit être prêt à jouer (#5)") + + return motifs + + +def _check_anti_invention(spec: dict, run_sheet: dict) -> list[str]: + """Re-résout chaque pointeur avec un loader neuf et compare au run-sheet. + + Prouve qu'aucune valeur n'a été « posée » : elle DOIT redécouler de l'artefact + amont sur disque. Recoupe aussi spec ↔ run-sheet (mêmes pointeurs, même ordre). + """ + motifs: list[str] = [] + loader = evidence.Loader() + spec_by_id = {sc["id"]: sc for sc in spec["scenarios"]} + + for sc in run_sheet["scenarios"]: + spec_sc = spec_by_id.get(sc["id"]) + if spec_sc is None: + motifs.append(f"{sc['id']} : scénario absent du spec") + continue + if len(spec_sc["beats"]) != len(sc["beats"]): + motifs.append(f"{sc['id']} : nb de beats spec≠run-sheet") + continue + for spec_b, b in zip(spec_sc["beats"], sc["beats"]): + spec_ev = spec_b["evidence"] + if len(spec_ev) != len(b["evidence"]): + motifs.append(f"{sc['id']}/{b['id']} : nb de citations spec≠run-sheet") + continue + for se, c in zip(spec_ev, b["evidence"]): + if (se["module"], se["file"], se["pointer"]) != \ + (c["module"], c["file"], c["pointer"]): + motifs.append(f"{sc['id']}/{b['id']} : citation dérivée du spec") + continue + exp_val, exp_res = evidence.resolve( + loader, se["module"], se["file"], se["pointer"]) + if exp_res != c["resolu"] or exp_val != c["valeur"]: + motifs.append( + f"{se['module']}#{se['pointer']} : run-sheet {c['valeur']!r} " + f"!= re-résolution {exp_val!r} (invention/dérive)") + return motifs + + +def _check_counts(run_sheet: dict, manifest: dict) -> list[str]: + motifs: list[str] = [] + cites = [c for sc in run_sheet["scenarios"] for b in sc["beats"] for c in b["evidence"]] + exp = { + "scenarios": len(run_sheet["scenarios"]), + "beats_total": sum(len(sc["beats"]) for sc in run_sheet["scenarios"]), + "citations_total": len(cites), + "citations_non_resolues": sum(1 for c in cites if not c["resolu"]), + "modules_cites_uniques": len({c["module"] for c in cites}), + } + for cle, val in exp.items(): + if manifest["counts"].get(cle) != val: + motifs.append(f"counts.{cle} : manifeste {manifest['counts'].get(cle)} != recalcul {val}") + return motifs + + +# --------------------------------------------------------------------------- # +def _assemble(generated_at: str | None) -> tuple[dict, dict, dict]: + spec = _load_spec() + run_sheet = builder.build_run_sheet(spec, generated_at=generated_at) + manifest = builder.build_manifest(spec, run_sheet) + return spec, run_sheet, manifest + + +def cmd_build(ns: argparse.Namespace) -> int: + spec, run_sheet, manifest = _assemble(ns.generated_at) + motifs = check_invariants(spec, run_sheet, manifest) + if motifs: + _eprint("❌ invariants cassés — RIEN écrit :") + for m in motifs: + _eprint(" ·", m) + return 1 + + os.makedirs(ns.out, exist_ok=True) + for name, obj in (("run_sheet.json", run_sheet), ("MANIFEST.json", manifest)): + with open(os.path.join(ns.out, name), "w", encoding="utf-8") as fh: + json.dump(obj, fh, ensure_ascii=False, indent=2) + fh.write("\n") + + c = manifest["counts"] + _eprint(f"✅ run-sheet → {ns.out}") + _eprint(f" scénarios={c['scenarios']} · beats={c['beats_total']} · " + f"citations={c['citations_total']} (non résolues={c['citations_non_resolues']}) · " + f"modules cités={c['modules_cites_uniques']}") + for sc in manifest["scenarios"]: + _eprint(f" · {sc['id']} [{sc['audience']}] {sc['projet']} — " + f"{sc['beats']} beats · {sc['duree_min']} min · {sc['citations_resolues']}/{sc['citations']} résolues") + _eprint(f" pret={manifest['pret']} · couverture CI ok={manifest['couverture_ci']['ok']}") + return 0 + + +def cmd_validate(ns: argparse.Namespace) -> int: + spec, run_sheet, manifest = _assemble(ns.generated_at) + motifs = check_invariants(spec, run_sheet, manifest) + print(json.dumps(manifest, ensure_ascii=False, indent=2)) + if motifs: + _eprint(f"❌ {len(motifs)} invariant(s) cassé(s) :") + for m in motifs: + _eprint(" ·", m) + return 1 + _eprint(f"✅ manifeste conforme + invariants OK (pret={manifest['pret']}).") + return 0 + + +# --------------------------------------------------------------------------- # +def build_argparser() -> argparse.ArgumentParser: + ap = argparse.ArgumentParser(prog="demo_scenario_gen", + description=__doc__.splitlines()[0]) + sub = ap.add_subparsers(dest="cmd", required=True) + + p = sub.add_parser("build", help="scenario_spec.json → out/{run_sheet,MANIFEST}.json") + p.add_argument("-o", "--out", default=os.path.join(_HERE, "out"), + help="dossier de sortie (défaut: ./out)") + p.add_argument("--generated-at", dest="generated_at", default=None) + p.set_defaults(func=cmd_build) + + p = sub.add_parser("validate", help="manifeste + invariants (sans écrire)") + p.add_argument("--generated-at", dest="generated_at", default=None) + p.set_defaults(func=cmd_validate) + return ap + + +def main(argv: list[str] | None = None) -> int: + ns = build_argparser().parse_args(argv) + return ns.func(ns) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/05_deliverables_mvp/demo/scenarios/out/MANIFEST.json b/05_deliverables_mvp/demo/scenarios/out/MANIFEST.json new file mode 100644 index 0000000..ba66ba5 --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/out/MANIFEST.json @@ -0,0 +1,69 @@ +{ + "deliverable": "Scénarios démo · run-sheet de pitch", + "sprint": "Sprint 7 · CRM + Faisabilité", + "roadmap_ref": "04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md · Sprint 7 l.68 « CRM + Faisabilité : Scénarios démo (P07 pitch banquier · P05 client ready) »", + "generated_from": "scenario_spec.json", + "deliverables_root": "05_deliverables_mvp", + "self_module": "demo/scenarios", + "generated_at": null, + "scenarios": [ + { + "id": "S-P07-BANQUIER", + "projet": "P07", + "audience": "banquier", + "beats": 6, + "citations": 13, + "citations_resolues": 13, + "duree_min": 12, + "modules_cites": [ + "crm/dossier_vente", + "crm/workflow_vente", + "fiscal/ecf_dgii", + "legal/confotur", + "qa/audit_4big", + "rbac/apply_plan" + ] + }, + { + "id": "S-P05-CLIENT", + "projet": "P05", + "audience": "client", + "beats": 6, + "citations": 10, + "citations_resolues": 10, + "duree_min": 12, + "modules_cites": [ + "crm/commissions", + "crm/dossier_vente", + "crm/workflow_vente", + "frontend/chat_otoia", + "frontend/portails", + "seo" + ] + } + ], + "counts": { + "scenarios": 2, + "beats_total": 12, + "citations_total": 23, + "citations_non_resolues": 0, + "modules_cites_uniques": 10 + }, + "modules_cites": [ + "crm/commissions", + "crm/dossier_vente", + "crm/workflow_vente", + "fiscal/ecf_dgii", + "frontend/chat_otoia", + "frontend/portails", + "legal/confotur", + "qa/audit_4big", + "rbac/apply_plan", + "seo" + ], + "couverture_ci": { + "ok": true, + "modules_cites_non_gated": [] + }, + "pret": true +} diff --git a/05_deliverables_mvp/demo/scenarios/out/run_sheet.json b/05_deliverables_mvp/demo/scenarios/out/run_sheet.json new file mode 100644 index 0000000..e28748c --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/out/run_sheet.json @@ -0,0 +1,342 @@ +{ + "doc": "OTO · Run-sheet démo (méta-livrable)", + "spec_version": "1.0.0", + "roadmap_ref": "04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md · Sprint 7 l.68 « CRM + Faisabilité : Scénarios démo (P07 pitch banquier · P05 client ready) »", + "generated_at": null, + "scenarios": [ + { + "id": "S-P07-BANQUIER", + "projet": "P07", + "projet_libelle": "Aqua Terra Las Terrenas", + "audience": "banquier", + "titre": "P07 Aqua Terra — Pitch banquier (bancabilité & gouvernance)", + "objectif": "Démontrer qu'un dossier de vente OTO est bancable : pipeline structuré, conformité fiscale e-CF DGII, incitation CONFOTUR, gouvernance RBAC et gate qualité 4Big.", + "duree_min": 12, + "modules_cites": [ + "crm/dossier_vente", + "crm/workflow_vente", + "fiscal/ecf_dgii", + "legal/confotur", + "qa/audit_4big", + "rbac/apply_plan" + ], + "beats": [ + { + "ordre": 1, + "id": "b1-pipeline", + "titre": "Un pipeline de vente verrouillé de bout en bout", + "role_narratif": "Ouvrir sur la traçabilité : la vente n'est pas un tableur, c'est un workflow ERPNext soumis (docstatus) piloté par rôle.", + "duree_min": 2, + "evidence": [ + { + "module": "crm/workflow_vente", + "file": "out/workflow.json", + "pointer": "/0/workflow_name", + "label": "Nom du pipeline", + "valeur": "OTO Vente Pipeline", + "resolu": true + }, + { + "module": "crm/workflow_vente", + "file": "out/workflow.json", + "pointer": "/0/document_type", + "label": "DocType piloté", + "valeur": "OTO Dossier Vente", + "resolu": true + } + ] + }, + { + "ordre": 2, + "id": "b2-ecf", + "titre": "Facturation fiscale conforme DGII (e-CF · Compupar)", + "role_narratif": "Rassurer sur le risque fiscal : chaque encaissement est un e-CF certifié, pas une facture maison.", + "duree_min": 2, + "evidence": [ + { + "module": "fiscal/ecf_dgii", + "file": "out/MANIFEST.json", + "pointer": "/provider", + "label": "Fournisseur e-CF", + "valeur": "Compupar", + "resolu": true + }, + { + "module": "fiscal/ecf_dgii", + "file": "out/MANIFEST.json", + "pointer": "/counts/tipos_en_scope", + "label": "Types e-CF en périmètre", + "valeur": 5, + "resolu": true + } + ] + }, + { + "ordre": 3, + "id": "b3-confotur", + "titre": "Incitation CONFOTUR dé-risquée et outillée", + "role_narratif": "Montrer l'avantage fiscal touristique comme un actif documenté : DocType soumis, dépôt outillé.", + "duree_min": 2, + "evidence": [ + { + "module": "legal/confotur", + "file": "out/MANIFEST.json", + "pointer": "/doctype_name", + "label": "DocType CONFOTUR", + "valeur": "CONFOTUR Application", + "resolu": true + }, + { + "module": "legal/confotur", + "file": "out/MANIFEST.json", + "pointer": "/is_submittable", + "label": "Soumissible (verrou légal)", + "valeur": true, + "resolu": true + }, + { + "module": "legal/confotur", + "file": "out/MANIFEST.json", + "pointer": "/counts/depot_events", + "label": "Étapes de dépôt outillées", + "valeur": 2, + "resolu": true + } + ] + }, + { + "ordre": 4, + "id": "b4-rbac", + "titre": "Séparation des pouvoirs (RBAC 50 rôles)", + "role_narratif": "Répondre à la question d'audit du banquier : qui peut faire quoi ? Permissions dérivées, pas ad hoc.", + "duree_min": 2, + "evidence": [ + { + "module": "rbac/apply_plan", + "file": "out/MANIFEST.json", + "pointer": "/counts/roles", + "label": "Rôles RBAC", + "valeur": 50, + "resolu": true + }, + { + "module": "rbac/apply_plan", + "file": "out/MANIFEST.json", + "pointer": "/counts/custom_docperm", + "label": "Permissions DocType (Custom DocPerm)", + "valeur": 116, + "resolu": true + } + ] + }, + { + "ordre": 5, + "id": "b5-qualite", + "titre": "Un gate qualité 4Big, pas une promesse", + "role_narratif": "Clore la confiance : la plateforme s'auto-note et bloque sous 95/100 sur 100 % des livrables.", + "duree_min": 2, + "evidence": [ + { + "module": "qa/audit_4big", + "file": "out/quality_report.json", + "pointer": "/pass_score", + "label": "Seuil bloquant 4Big", + "valeur": 95, + "resolu": true + }, + { + "module": "qa/audit_4big", + "file": "out/quality_report.json", + "pointer": "/coverage/ci_modules_count", + "label": "Modules sous gate qualité", + "valeur": 18, + "resolu": true + } + ] + }, + { + "ordre": 6, + "id": "b6-dossier", + "titre": "Le dossier de vente, pièce bancable finale", + "role_narratif": "Refermer sur l'objet concret que le banquier instruira : un DocType riche et soumis.", + "duree_min": 2, + "evidence": [ + { + "module": "crm/dossier_vente", + "file": "out/MANIFEST.json", + "pointer": "/counts/fields", + "label": "Champs du dossier de vente", + "valeur": 30, + "resolu": true + }, + { + "module": "crm/dossier_vente", + "file": "out/MANIFEST.json", + "pointer": "/counts/pipeline_states", + "label": "États de pipeline couverts", + "valeur": 9, + "resolu": true + } + ] + } + ] + }, + { + "id": "S-P05-CLIENT", + "projet": "P05", + "projet_libelle": "Las Colinas Najayo Arriba", + "audience": "client", + "titre": "P05 Las Colinas Najayo Arriba — Parcours client (réservation prête)", + "objectif": "Faire vivre le parcours d'un acheteur : découverte du portail, assistance OTOIA, pipeline lisible, dossier transparent, et comment le client nous a trouvés (SEO trilingue).", + "duree_min": 12, + "modules_cites": [ + "crm/commissions", + "crm/dossier_vente", + "crm/workflow_vente", + "frontend/chat_otoia", + "frontend/portails", + "seo" + ], + "beats": [ + { + "ordre": 1, + "id": "b1-portail", + "titre": "« Choisir mon unité » — 5 portails luxury", + "role_narratif": "Poser l'expérience d'entrée : un portail par rôle, cohérent et couvert.", + "duree_min": 2, + "evidence": [ + { + "module": "frontend/portails", + "file": "out/MANIFEST.json", + "pointer": "/counts/workspaces", + "label": "Portails (workspaces)", + "valeur": 5, + "resolu": true + }, + { + "module": "frontend/portails", + "file": "out/MANIFEST.json", + "pointer": "/counts/roles_couverts", + "label": "Rôles couverts par les portails", + "valeur": 44, + "resolu": true + } + ] + }, + { + "ordre": 2, + "id": "b2-otoia", + "titre": "OTOIA répond dans le portail", + "role_narratif": "Montrer l'assistance embarquée : le client n'est jamais seul face à l'écran.", + "duree_min": 2, + "evidence": [ + { + "module": "frontend/chat_otoia", + "file": "out/MANIFEST.json", + "pointer": "/deliverable", + "label": "Assistant embarqué", + "valeur": "Chat OTOIA embarqué par portail", + "resolu": true + } + ] + }, + { + "ordre": 3, + "id": "b3-parcours", + "titre": "Du premier contact à la réservation", + "role_narratif": "Dérouler le pipeline côté client : Lead → … → Réservation confirmée, lisible et rassurant.", + "duree_min": 2, + "evidence": [ + { + "module": "crm/workflow_vente", + "file": "out/workflow.json", + "pointer": "/0/states/0/state", + "label": "Première étape", + "valeur": "Lead", + "resolu": true + }, + { + "module": "crm/workflow_vente", + "file": "out/workflow.json", + "pointer": "/0/states/3/state", + "label": "Étape réservation", + "valeur": "Réservation confirmée", + "resolu": true + } + ] + }, + { + "ordre": 4, + "id": "b4-dossier", + "titre": "Un dossier de vente transparent", + "role_narratif": "Donner confiance : tout est consigné dans un dossier structuré que le client peut suivre.", + "duree_min": 2, + "evidence": [ + { + "module": "crm/dossier_vente", + "file": "out/MANIFEST.json", + "pointer": "/counts/fields", + "label": "Champs du dossier de vente", + "valeur": 30, + "resolu": true + } + ] + }, + { + "ordre": 5, + "id": "b5-commissions", + "titre": "Un conseiller rémunéré à la transparence", + "role_narratif": "Aligner l'intérêt du conseiller et du client : barème de commissions traçable par événement.", + "duree_min": 2, + "evidence": [ + { + "module": "crm/commissions", + "file": "out/MANIFEST.json", + "pointer": "/bareme_name", + "label": "Barème de commissions", + "valeur": "OTO Barème Commissions Ventes", + "resolu": true + }, + { + "module": "crm/commissions", + "file": "out/MANIFEST.json", + "pointer": "/counts/evenements", + "label": "Événements déclencheurs", + "valeur": 5, + "resolu": true + } + ] + }, + { + "ordre": 6, + "id": "b6-seo", + "titre": "Comment le client nous a trouvés (SEO trilingue)", + "role_narratif": "Refermer la boucle acquisition : indexation FR/EN/ES, le parcours commence bien avant le portail.", + "duree_min": 2, + "evidence": [ + { + "module": "seo", + "file": "out/MANIFEST.json", + "pointer": "/counts/keywords_total", + "label": "Mots-clés indexés", + "valeur": 258, + "resolu": true + }, + { + "module": "seo", + "file": "out/MANIFEST.json", + "pointer": "/langs", + "label": "Langues", + "valeur": [ + "fr", + "en", + "es" + ], + "resolu": true + } + ] + } + ] + } + ] +} diff --git a/05_deliverables_mvp/demo/scenarios/scenario.schema.json b/05_deliverables_mvp/demo/scenarios/scenario.schema.json new file mode 100644 index 0000000..ed20dfa --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/scenario.schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://otov7.com/schemas/demo-scenario-manifest-1.0.0.json", + "title": "OTO · Manifeste Scénarios démo (run-sheet)", + "type": "object", + "additionalProperties": false, + "required": [ + "deliverable", "sprint", "roadmap_ref", "generated_from", + "deliverables_root", "self_module", "generated_at", "scenarios", + "counts", "modules_cites", "couverture_ci", "pret" + ], + "properties": { + "deliverable": {"type": "string", "minLength": 1}, + "sprint": {"type": "string", "minLength": 1}, + "roadmap_ref": {"type": "string", "minLength": 1}, + "generated_from": {"type": "string", "const": "scenario_spec.json"}, + "deliverables_root": {"type": "string", "const": "05_deliverables_mvp"}, + "self_module": {"type": "string", "const": "demo/scenarios"}, + "generated_at": {"type": ["string", "null"]}, + "scenarios": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "projet", "audience", "beats", "citations", + "citations_resolues", "duree_min", "modules_cites"], + "properties": { + "id": {"type": "string", "pattern": "^S-P0[1-9]-[A-Z]+$"}, + "projet": {"type": "string", "pattern": "^P0[1-9]$"}, + "audience": {"type": "string", "enum": ["banquier", "client"]}, + "beats": {"type": "integer", "minimum": 3}, + "citations": {"type": "integer", "minimum": 1}, + "citations_resolues": {"type": "integer", "minimum": 0}, + "duree_min": {"type": "integer", "minimum": 1}, + "modules_cites": { + "type": "array", "minItems": 2, "uniqueItems": true, + "items": {"type": "string", "minLength": 1} + } + } + } + }, + "counts": { + "type": "object", + "additionalProperties": false, + "required": ["scenarios", "beats_total", "citations_total", + "citations_non_resolues", "modules_cites_uniques"], + "properties": { + "scenarios": {"type": "integer", "minimum": 1}, + "beats_total": {"type": "integer", "minimum": 1}, + "citations_total": {"type": "integer", "minimum": 1}, + "citations_non_resolues": {"type": "integer", "minimum": 0}, + "modules_cites_uniques": {"type": "integer", "minimum": 1} + } + }, + "modules_cites": { + "type": "array", "minItems": 1, "uniqueItems": true, + "items": {"type": "string", "minLength": 1} + }, + "couverture_ci": { + "type": "object", + "additionalProperties": false, + "required": ["ok", "modules_cites_non_gated"], + "properties": { + "ok": {"type": "boolean"}, + "modules_cites_non_gated": { + "type": "array", "items": {"type": "string", "minLength": 1} + } + } + }, + "pret": {"type": "boolean"} + } +} diff --git a/05_deliverables_mvp/demo/scenarios/scenario_spec.json b/05_deliverables_mvp/demo/scenarios/scenario_spec.json new file mode 100644 index 0000000..4565610 --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/scenario_spec.json @@ -0,0 +1,151 @@ +{ + "spec_version": "1.0.0", + "deliverable": "Scénarios démo · run-sheet de pitch", + "roadmap_ref": "04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md · Sprint 7 l.68 « CRM + Faisabilité : Scénarios démo (P07 pitch banquier · P05 client ready) »", + "self_module": "demo/scenarios", + "audiences": ["banquier", "client"], + "projets": { + "P05": "Las Colinas Najayo Arriba", + "P07": "Aqua Terra Las Terrenas" + }, + "note_anti_invention": "Aucune valeur chiffrée n'est écrite dans ce spec. Chaque `evidence` est un POINTEUR (RFC 6901) vers un hand-off `out/` d'un module déjà livré et gated par le CI ; le générateur résout la valeur à la construction. Une valeur non résolue devient un placeholder `{{module:file#pointer}}` — jamais un chiffre fabriqué (CLAUDE.md #6).", + "scenarios": [ + { + "id": "S-P07-BANQUIER", + "projet": "P07", + "audience": "banquier", + "titre": "P07 Aqua Terra — Pitch banquier (bancabilité & gouvernance)", + "objectif": "Démontrer qu'un dossier de vente OTO est bancable : pipeline structuré, conformité fiscale e-CF DGII, incitation CONFOTUR, gouvernance RBAC et gate qualité 4Big.", + "beats": [ + { + "id": "b1-pipeline", + "titre": "Un pipeline de vente verrouillé de bout en bout", + "role_narratif": "Ouvrir sur la traçabilité : la vente n'est pas un tableur, c'est un workflow ERPNext soumis (docstatus) piloté par rôle.", + "duree_min": 2, + "evidence": [ + {"module": "crm/workflow_vente", "file": "out/workflow.json", "pointer": "/0/workflow_name", "label": "Nom du pipeline"}, + {"module": "crm/workflow_vente", "file": "out/workflow.json", "pointer": "/0/document_type", "label": "DocType piloté"} + ] + }, + { + "id": "b2-ecf", + "titre": "Facturation fiscale conforme DGII (e-CF · Compupar)", + "role_narratif": "Rassurer sur le risque fiscal : chaque encaissement est un e-CF certifié, pas une facture maison.", + "duree_min": 2, + "evidence": [ + {"module": "fiscal/ecf_dgii", "file": "out/MANIFEST.json", "pointer": "/provider", "label": "Fournisseur e-CF"}, + {"module": "fiscal/ecf_dgii", "file": "out/MANIFEST.json", "pointer": "/counts/tipos_en_scope", "label": "Types e-CF en périmètre"} + ] + }, + { + "id": "b3-confotur", + "titre": "Incitation CONFOTUR dé-risquée et outillée", + "role_narratif": "Montrer l'avantage fiscal touristique comme un actif documenté : DocType soumis, dépôt outillé.", + "duree_min": 2, + "evidence": [ + {"module": "legal/confotur", "file": "out/MANIFEST.json", "pointer": "/doctype_name", "label": "DocType CONFOTUR"}, + {"module": "legal/confotur", "file": "out/MANIFEST.json", "pointer": "/is_submittable", "label": "Soumissible (verrou légal)"}, + {"module": "legal/confotur", "file": "out/MANIFEST.json", "pointer": "/counts/depot_events", "label": "Étapes de dépôt outillées"} + ] + }, + { + "id": "b4-rbac", + "titre": "Séparation des pouvoirs (RBAC 50 rôles)", + "role_narratif": "Répondre à la question d'audit du banquier : qui peut faire quoi ? Permissions dérivées, pas ad hoc.", + "duree_min": 2, + "evidence": [ + {"module": "rbac/apply_plan", "file": "out/MANIFEST.json", "pointer": "/counts/roles", "label": "Rôles RBAC"}, + {"module": "rbac/apply_plan", "file": "out/MANIFEST.json", "pointer": "/counts/custom_docperm", "label": "Permissions DocType (Custom DocPerm)"} + ] + }, + { + "id": "b5-qualite", + "titre": "Un gate qualité 4Big, pas une promesse", + "role_narratif": "Clore la confiance : la plateforme s'auto-note et bloque sous 95/100 sur 100 % des livrables.", + "duree_min": 2, + "evidence": [ + {"module": "qa/audit_4big", "file": "out/quality_report.json", "pointer": "/pass_score", "label": "Seuil bloquant 4Big"}, + {"module": "qa/audit_4big", "file": "out/quality_report.json", "pointer": "/coverage/ci_modules_count", "label": "Modules sous gate qualité"} + ] + }, + { + "id": "b6-dossier", + "titre": "Le dossier de vente, pièce bancable finale", + "role_narratif": "Refermer sur l'objet concret que le banquier instruira : un DocType riche et soumis.", + "duree_min": 2, + "evidence": [ + {"module": "crm/dossier_vente", "file": "out/MANIFEST.json", "pointer": "/counts/fields", "label": "Champs du dossier de vente"}, + {"module": "crm/dossier_vente", "file": "out/MANIFEST.json", "pointer": "/counts/pipeline_states", "label": "États de pipeline couverts"} + ] + } + ] + }, + { + "id": "S-P05-CLIENT", + "projet": "P05", + "audience": "client", + "titre": "P05 Las Colinas Najayo Arriba — Parcours client (réservation prête)", + "objectif": "Faire vivre le parcours d'un acheteur : découverte du portail, assistance OTOIA, pipeline lisible, dossier transparent, et comment le client nous a trouvés (SEO trilingue).", + "beats": [ + { + "id": "b1-portail", + "titre": "« Choisir mon unité » — 5 portails luxury", + "role_narratif": "Poser l'expérience d'entrée : un portail par rôle, cohérent et couvert.", + "duree_min": 2, + "evidence": [ + {"module": "frontend/portails", "file": "out/MANIFEST.json", "pointer": "/counts/workspaces", "label": "Portails (workspaces)"}, + {"module": "frontend/portails", "file": "out/MANIFEST.json", "pointer": "/counts/roles_couverts", "label": "Rôles couverts par les portails"} + ] + }, + { + "id": "b2-otoia", + "titre": "OTOIA répond dans le portail", + "role_narratif": "Montrer l'assistance embarquée : le client n'est jamais seul face à l'écran.", + "duree_min": 2, + "evidence": [ + {"module": "frontend/chat_otoia", "file": "out/MANIFEST.json", "pointer": "/deliverable", "label": "Assistant embarqué"} + ] + }, + { + "id": "b3-parcours", + "titre": "Du premier contact à la réservation", + "role_narratif": "Dérouler le pipeline côté client : Lead → … → Réservation confirmée, lisible et rassurant.", + "duree_min": 2, + "evidence": [ + {"module": "crm/workflow_vente", "file": "out/workflow.json", "pointer": "/0/states/0/state", "label": "Première étape"}, + {"module": "crm/workflow_vente", "file": "out/workflow.json", "pointer": "/0/states/3/state", "label": "Étape réservation"} + ] + }, + { + "id": "b4-dossier", + "titre": "Un dossier de vente transparent", + "role_narratif": "Donner confiance : tout est consigné dans un dossier structuré que le client peut suivre.", + "duree_min": 2, + "evidence": [ + {"module": "crm/dossier_vente", "file": "out/MANIFEST.json", "pointer": "/counts/fields", "label": "Champs du dossier de vente"} + ] + }, + { + "id": "b5-commissions", + "titre": "Un conseiller rémunéré à la transparence", + "role_narratif": "Aligner l'intérêt du conseiller et du client : barème de commissions traçable par événement.", + "duree_min": 2, + "evidence": [ + {"module": "crm/commissions", "file": "out/MANIFEST.json", "pointer": "/bareme_name", "label": "Barème de commissions"}, + {"module": "crm/commissions", "file": "out/MANIFEST.json", "pointer": "/counts/evenements", "label": "Événements déclencheurs"} + ] + }, + { + "id": "b6-seo", + "titre": "Comment le client nous a trouvés (SEO trilingue)", + "role_narratif": "Refermer la boucle acquisition : indexation FR/EN/ES, le parcours commence bien avant le portail.", + "duree_min": 2, + "evidence": [ + {"module": "seo", "file": "out/MANIFEST.json", "pointer": "/counts/keywords_total", "label": "Mots-clés indexés"}, + {"module": "seo", "file": "out/MANIFEST.json", "pointer": "/langs", "label": "Langues"} + ] + } + ] + } + ] +} diff --git a/05_deliverables_mvp/demo/scenarios/scenlib/__init__.py b/05_deliverables_mvp/demo/scenarios/scenlib/__init__.py new file mode 100644 index 0000000..75f6ead --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/scenlib/__init__.py @@ -0,0 +1,9 @@ +"""Bibliothèque du générateur de scénarios démo (run-sheet de pitch). + +Découpage : + - deps : réutilisation des briques déjà livrées (validateur maison + + preuve de couverture CI) · zéro duplication (#5). + - evidence : chargement des hand-offs `out/` amont + résolution de pointeurs + RFC 6901 · cœur anti-invention (#6). + - builder : assemblage déterministe du run-sheet + du manifeste. +""" diff --git a/05_deliverables_mvp/demo/scenarios/scenlib/builder.py b/05_deliverables_mvp/demo/scenarios/scenlib/builder.py new file mode 100644 index 0000000..61b4e48 --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/scenlib/builder.py @@ -0,0 +1,123 @@ +"""Assemblage déterministe du run-sheet démo + du manifeste (hand-off). + +Le builder est PUR : mêmes entrées (spec + artefacts `out/` sur disque) ⇒ même +sortie. Aucun horodatage implicite, aucun ordre non déterministe. L'ordre des +beats est dérivé de la position dans le spec (jamais d'un champ `ordre` en dur, +pour éviter toute dérive). Les modules cités sont recoupés avec l'ensemble des +modules gated par le CI (via `deps.ci_module_paths`) — une démo ne s'appuie que +sur du validé. +""" + +from __future__ import annotations + +from . import deps, evidence + +SELF_MODULE = "demo/scenarios" + + +def _build_beat(loader: evidence.Loader, beat: dict, ordre: int) -> dict: + cites = [] + for ev in beat["evidence"]: + module, file, pointer = ev["module"], ev["file"], ev["pointer"] + valeur, resolu = evidence.resolve(loader, module, file, pointer) + cites.append({ + "module": module, + "file": file, + "pointer": pointer, + "label": ev["label"], + "valeur": valeur, + "resolu": resolu, + }) + return { + "ordre": ordre, + "id": beat["id"], + "titre": beat["titre"], + "role_narratif": beat["role_narratif"], + "duree_min": beat["duree_min"], + "evidence": cites, + } + + +def _build_scenario(loader: evidence.Loader, spec: dict, sc: dict) -> dict: + beats = [_build_beat(loader, b, i + 1) for i, b in enumerate(sc["beats"])] + modules = sorted({c["module"] for b in beats for c in b["evidence"]}) + duree = sum(b["duree_min"] for b in beats) + return { + "id": sc["id"], + "projet": sc["projet"], + "projet_libelle": spec["projets"][sc["projet"]], + "audience": sc["audience"], + "titre": sc["titre"], + "objectif": sc["objectif"], + "duree_min": duree, + "modules_cites": modules, + "beats": beats, + } + + +def build_run_sheet(spec: dict, generated_at: str | None = None) -> dict: + """Construit le run-sheet complet (le hand-off principal).""" + loader = evidence.Loader() + scenarios = [_build_scenario(loader, spec, sc) for sc in spec["scenarios"]] + return { + "doc": "OTO · Run-sheet démo (méta-livrable)", + "spec_version": spec["spec_version"], + "roadmap_ref": spec["roadmap_ref"], + "generated_at": generated_at, + "scenarios": scenarios, + } + + +def _all_citations(run_sheet: dict) -> list[dict]: + return [c for sc in run_sheet["scenarios"] for b in sc["beats"] for c in b["evidence"]] + + +def build_manifest(spec: dict, run_sheet: dict) -> dict: + """Manifeste diagnostic + preuve de couverture CI + drapeau `pret`.""" + cites = _all_citations(run_sheet) + non_resolues = [c for c in cites if not c["resolu"]] + modules_cites = sorted({c["module"] for c in cites}) + + gated = deps.ci_module_paths() # modules réellement gated par le CI + non_gated = sorted(m for m in modules_cites if m not in gated) + + scen_diag = [] + for sc in run_sheet["scenarios"]: + sc_cites = [c for b in sc["beats"] for c in b["evidence"]] + scen_diag.append({ + "id": sc["id"], + "projet": sc["projet"], + "audience": sc["audience"], + "beats": len(sc["beats"]), + "citations": len(sc_cites), + "citations_resolues": sum(1 for c in sc_cites if c["resolu"]), + "duree_min": sc["duree_min"], + "modules_cites": sc["modules_cites"], + }) + + couverture_ok = not non_gated + pret = (not non_resolues) and couverture_ok + + return { + "deliverable": "Scénarios démo · run-sheet de pitch", + "sprint": "Sprint 7 · CRM + Faisabilité", + "roadmap_ref": spec["roadmap_ref"], + "generated_from": "scenario_spec.json", + "deliverables_root": "05_deliverables_mvp", + "self_module": SELF_MODULE, + "generated_at": run_sheet["generated_at"], + "scenarios": scen_diag, + "counts": { + "scenarios": len(run_sheet["scenarios"]), + "beats_total": sum(len(sc["beats"]) for sc in run_sheet["scenarios"]), + "citations_total": len(cites), + "citations_non_resolues": len(non_resolues), + "modules_cites_uniques": len(modules_cites), + }, + "modules_cites": modules_cites, + "couverture_ci": { + "ok": couverture_ok, + "modules_cites_non_gated": non_gated, + }, + "pret": pret, + } diff --git a/05_deliverables_mvp/demo/scenarios/scenlib/deps.py b/05_deliverables_mvp/demo/scenarios/scenlib/deps.py new file mode 100644 index 0000000..d8c474a --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/scenlib/deps.py @@ -0,0 +1,44 @@ +"""Réutilisation des briques déjà livrées (workflow #5 · zéro duplication). + +L'orchestrateur démo ne redéfinit RIEN qui existe ailleurs : + + - `validate` : le validateur JSON-Schema maison du Publiciste (draft-07, + sous-ensemble) — le run-sheet est validé SANS pip, car le + gate CI Gitea Actions tourne sans réseau (CLAUDE.md #2). + - `ci_module_paths` : la PREUVE de couverture de l'auditeur 4Big + (`qa/audit_4big/q4lib/registry.py`), qui lit + `.gitea/workflows/ci.yml` pour connaître les modules + réellement gated. On la réutilise pour garantir qu'une + démo ne s'appuie QUE sur des livrables validés par le CI — + jamais sur du code non gated. + +Import par manipulation de `sys.path` (idiome des modules voisins : +`banclib/deps.py`, `q4lib/deps.py`) plutôt que duplication — une seule source de +vérité pour tout le mandat. +""" + +from __future__ import annotations + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +# demo/scenarios/scenlib → 05_deliverables_mvp +_DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..", "..")) +_PUB = os.path.join(_DELIVERABLES, "publiciste") +_AUDIT = os.path.join(_DELIVERABLES, "qa", "audit_4big") + +for _p in (_PUB, _AUDIT): + if _p not in sys.path: + sys.path.insert(0, _p) + +from lib import validator # type: ignore # noqa: E402 +from q4lib import registry # type: ignore # noqa: E402 + +validate = validator.validate +ci_module_paths = registry.ci_module_paths + +# Racine des livrables, exposée aux autres modules scenlib. +DELIVERABLES_ROOT = _DELIVERABLES + +__all__ = ["validate", "ci_module_paths", "DELIVERABLES_ROOT"] diff --git a/05_deliverables_mvp/demo/scenarios/scenlib/evidence.py b/05_deliverables_mvp/demo/scenarios/scenlib/evidence.py new file mode 100644 index 0000000..5be8d9d --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/scenlib/evidence.py @@ -0,0 +1,112 @@ +"""Cœur anti-invention (#6) : résolution des preuves depuis les hand-offs amont. + +Un scénario démo ne contient AUCUN chiffre en dur. Chaque « evidence » du spec est +un pointeur `(module, file, pointer)` vers un artefact `out/` déjà produit par un +module livré. Ce fichier : + + 1. charge l'artefact JSON (avec cache) et signale toute absence / JSON invalide ; + 2. résout un pointeur RFC 6901 (`/0/counts/fields`) vers sa valeur verbatim ; + 3. quand la valeur n'existe pas, renvoie un PLACEHOLDER `{{module:file#pointer}}` + — jamais un chiffre fabriqué, jamais un `0` posé. + +La résolution est PURE et rejouable : l'invariant `INV_ANTI_INVENTION` du CLI +re-résout indépendamment chaque pointeur et compare à ce qui figure dans le +run-sheet — impossible de « poser » une valeur à la main. +""" + +from __future__ import annotations + +import json +import os +from typing import Any + +from .deps import DELIVERABLES_ROOT + +# Sentinelle « non résolu » — un placeholder lisible, jamais confondable avec une +# vraie valeur (ni un nombre, ni une chaîne métier). +_MISSING = object() + + +def placeholder(module: str, file: str, pointer: str) -> str: + return f"{{{{{module}:{file}#{pointer}}}}}" + + +def is_placeholder(value: Any) -> bool: + return isinstance(value, str) and value.startswith("{{") and value.endswith("}}") + + +def artifact_path(module: str, file: str) -> str: + """Chemin absolu de l'artefact `out/` (jamais hors des livrables).""" + return os.path.join(DELIVERABLES_ROOT, module, file) + + +class Loader: + """Charge et met en cache les artefacts amont pour une passe de build.""" + + def __init__(self) -> None: + self._cache: dict[str, Any] = {} + self._errors: dict[str, str] = {} + + def load(self, module: str, file: str) -> Any: + key = f"{module}/{file}" + if key in self._cache or key in self._errors: + return self._cache.get(key, _MISSING) + path = artifact_path(module, file) + if not os.path.isfile(path): + self._errors[key] = "artefact absent" + return _MISSING + try: + with open(path, encoding="utf-8") as fh: + obj = json.load(fh) + except (OSError, ValueError) as exc: + self._errors[key] = f"JSON invalide : {exc}" + return _MISSING + self._cache[key] = obj + return obj + + def errors(self) -> dict[str, str]: + return dict(self._errors) + + +def _split_pointer(pointer: str) -> list[str]: + """Découpe un pointeur RFC 6901 (`/a/0/b`) en tokens déséchappés.""" + if pointer == "": + return [] + if not pointer.startswith("/"): + raise ValueError(f"pointeur RFC 6901 invalide (doit commencer par '/') : {pointer!r}") + tokens = pointer.split("/")[1:] + return [t.replace("~1", "/").replace("~0", "~") for t in tokens] + + +def _descend(obj: Any, token: str) -> Any: + """Un pas de descente : clé d'objet ou index de liste (jamais d'erreur).""" + if isinstance(obj, dict): + return obj.get(token, _MISSING) + if isinstance(obj, list): + if token.lstrip("-").isdigit(): + idx = int(token) + if -len(obj) <= idx < len(obj): + return obj[idx] + return _MISSING + return _MISSING + + +def resolve(loader: Loader, module: str, file: str, pointer: str) -> tuple[Any, bool]: + """Résout un pointeur en `(valeur, resolu)`. + + `resolu=False` ⇒ `valeur` est un placeholder `{{…}}` (artefact absent / JSON + invalide / chemin inexistant). `resolu=True` ⇒ valeur verbatim non `None`. + Une valeur littérale `null` dans l'artefact est traitée comme NON résolue : + un chiffre « à confirmer » ne doit jamais alimenter un pitch (#6). + """ + root = loader.load(module, file) + if root is _MISSING: + return placeholder(module, file, pointer), False + node: Any = root + for token in _split_pointer(pointer): + node = _descend(node, token) + if node is _MISSING: + return placeholder(module, file, pointer), False + if node is None: + return placeholder(module, file, pointer), False + return node, True diff --git a/05_deliverables_mvp/demo/scenarios/tests/test_demo_scenario.py b/05_deliverables_mvp/demo/scenarios/tests/test_demo_scenario.py new file mode 100644 index 0000000..9b65279 --- /dev/null +++ b/05_deliverables_mvp/demo/scenarios/tests/test_demo_scenario.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +"""Tests du générateur de scénarios démo (run-sheet). + +Couvre : assemblage réel (les artefacts `out/` amont doivent exister et être +gated), résolution de pointeurs RFC 6901, cœur anti-invention (#6), invariants, +et un lot d'INJECTIONS NÉGATIVES prouvant que chaque garde-fou rougit vraiment. +""" + +from __future__ import annotations + +import copy +import json +import os +import sys +import unittest + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_MOD = os.path.normpath(os.path.join(_HERE, "..")) +sys.path.insert(0, _MOD) + +import demo_scenario_gen as gen # noqa: E402 +from scenlib import builder, evidence # noqa: E402 + + +def _fresh(): + """(spec, run_sheet, manifest) construits depuis le dépôt réel.""" + return gen._assemble(generated_at=None) + + +class TestBuildReel(unittest.TestCase): + def setUp(self): + self.spec, self.run_sheet, self.manifest = _fresh() + + def test_build_sans_invariant_casse(self): + self.assertEqual(gen.check_invariants(self.spec, self.run_sheet, self.manifest), []) + + def test_deux_scenarios_p07_p05(self): + ids = {sc["id"] for sc in self.run_sheet["scenarios"]} + self.assertEqual(ids, {"S-P07-BANQUIER", "S-P05-CLIENT"}) + projets = {sc["projet"] for sc in self.run_sheet["scenarios"]} + self.assertEqual(projets, {"P05", "P07"}) + + def test_audiences_declarees(self): + for sc in self.run_sheet["scenarios"]: + self.assertIn(sc["audience"], self.spec["audiences"]) + + def test_libelles_projets_sources_du_spec(self): + for sc in self.run_sheet["scenarios"]: + self.assertEqual(sc["projet_libelle"], self.spec["projets"][sc["projet"]]) + + def test_beats_ordonnes_et_min_trois(self): + for sc in self.run_sheet["scenarios"]: + self.assertGreaterEqual(len(sc["beats"]), 3) + self.assertEqual([b["ordre"] for b in sc["beats"]], + list(range(1, len(sc["beats"]) + 1))) + + def test_orchestration_min_deux_modules(self): + for sc in self.run_sheet["scenarios"]: + self.assertGreaterEqual(len(set(sc["modules_cites"])), 2) + + def test_toutes_citations_resolues(self): + self.assertEqual(self.manifest["counts"]["citations_non_resolues"], 0) + for sc in self.run_sheet["scenarios"]: + for b in sc["beats"]: + for c in b["evidence"]: + self.assertTrue(c["resolu"]) + self.assertFalse(evidence.is_placeholder(c["valeur"])) + self.assertIsNotNone(c["valeur"]) + + def test_pret_et_couverture_ci(self): + self.assertTrue(self.manifest["pret"]) + self.assertTrue(self.manifest["couverture_ci"]["ok"]) + self.assertEqual(self.manifest["couverture_ci"]["modules_cites_non_gated"], []) + + def test_pas_auto_citation(self): + mods = {c["module"] for sc in self.run_sheet["scenarios"] + for b in sc["beats"] for c in b["evidence"]} + self.assertNotIn(builder.SELF_MODULE, mods) + + def test_manifest_conforme_schema(self): + self.assertEqual(gen.deps.validate(self.manifest, gen._load_schema()), []) + + def test_duree_est_somme_des_beats(self): + for sc in self.run_sheet["scenarios"]: + self.assertEqual(sc["duree_min"], sum(b["duree_min"] for b in sc["beats"])) + + def test_determinisme(self): + _, rs2, mf2 = _fresh() + self.assertEqual(self.run_sheet, rs2) + self.assertEqual(self.manifest, mf2) + + def test_valeurs_verbatim_depuis_artefacts(self): + # Une valeur du run-sheet doit être EXACTEMENT celle du fichier amont. + wf = json.load(open(os.path.join( + builder.deps.DELIVERABLES_ROOT, "crm/workflow_vente/out/workflow.json"), + encoding="utf-8")) + beat1 = self.run_sheet["scenarios"][0]["beats"][0]["evidence"][0] + self.assertEqual(beat1["valeur"], wf[0]["workflow_name"]) + + +class TestPointeurRFC6901(unittest.TestCase): + def setUp(self): + self.loader = evidence.Loader() + + def test_split_racine_et_echappement(self): + self.assertEqual(evidence._split_pointer(""), []) + self.assertEqual(evidence._split_pointer("/a/b"), ["a", "b"]) + self.assertEqual(evidence._split_pointer("/a~1b/c~0d"), ["a/b", "c~d"]) + + def test_split_refuse_pointeur_sans_slash(self): + with self.assertRaises(ValueError): + evidence._split_pointer("a/b") + + def test_descend_cle_index_negatif(self): + self.assertEqual(evidence._descend({"x": 1}, "x"), 1) + self.assertEqual(evidence._descend([10, 20, 30], "0"), 10) + self.assertEqual(evidence._descend([10, 20, 30], "-1"), 30) + self.assertIs(evidence._descend([1], "5"), evidence._MISSING) + self.assertIs(evidence._descend(42, "x"), evidence._MISSING) + + def test_resolve_valeur_existante(self): + val, ok = evidence.resolve(self.loader, "crm/workflow_vente", + "out/workflow.json", "/0/document_type") + self.assertTrue(ok) + self.assertEqual(val, "OTO Dossier Vente") + + def test_resolve_null_est_non_resolu(self): + # `condition: null` dans une transition → placeholder, pas résolu (#6). + val, ok = evidence.resolve(self.loader, "crm/workflow_vente", + "out/workflow.json", "/0/transitions/0/condition") + self.assertFalse(ok) + self.assertTrue(evidence.is_placeholder(val)) + + def test_resolve_artefact_absent(self): + val, ok = evidence.resolve(self.loader, "module/inexistant", + "out/rien.json", "/x") + self.assertFalse(ok) + self.assertEqual(val, "{{module/inexistant:out/rien.json#/x}}") + + def test_resolve_pointeur_manquant(self): + val, ok = evidence.resolve(self.loader, "crm/workflow_vente", + "out/workflow.json", "/0/champ_absent") + self.assertFalse(ok) + self.assertTrue(evidence.is_placeholder(val)) + + def test_is_placeholder(self): + self.assertTrue(evidence.is_placeholder("{{a:b#/c}}")) + self.assertFalse(evidence.is_placeholder("valeur")) + self.assertFalse(evidence.is_placeholder(258)) + + +class TestInjectionsNegatives(unittest.TestCase): + """Chaque garde-fou DOIT rougir sur une dégradation ciblée.""" + + def setUp(self): + self.spec, self.run_sheet, self.manifest = _fresh() + + def _motifs(self, rs, mf): + return gen.check_invariants(self.spec, rs, mf) + + def test_anti_invention_valeur_falsifiee(self): + rs = copy.deepcopy(self.run_sheet) + rs["scenarios"][0]["beats"][0]["evidence"][0]["valeur"] = "CHIFFRE INVENTÉ" + mf = builder.build_manifest(self.spec, rs) + self.assertTrue(any("invention/dérive" in m for m in self._motifs(rs, mf))) + + def test_compteur_falsifie(self): + mf = copy.deepcopy(self.manifest) + mf["counts"]["citations_total"] += 1 + motifs = self._motifs(self.run_sheet, mf) + self.assertTrue(any("counts.citations_total" in m for m in motifs)) + + def test_placeholder_incoherent(self): + rs = copy.deepcopy(self.run_sheet) + c = rs["scenarios"][0]["beats"][0]["evidence"][0] + c["resolu"] = False # résolu marqué faux mais valeur reste réelle + mf = builder.build_manifest(self.spec, rs) + self.assertTrue(any("non résolu sans placeholder" in m + or "invention/dérive" in m for m in self._motifs(rs, mf))) + + def test_module_non_gated_detecte(self): + # On force couverture_ci en KO → invariant (g) doit rougir. + mf = copy.deepcopy(self.manifest) + mf["couverture_ci"]["ok"] = False + mf["couverture_ci"]["modules_cites_non_gated"] = ["module/hors-ci"] + self.assertTrue(any("NON gated" in m for m in self._motifs(self.run_sheet, mf))) + + def test_non_pret_bloque(self): + mf = copy.deepcopy(self.manifest) + mf["pret"] = False + self.assertTrue(any("non prêt" in m for m in self._motifs(self.run_sheet, mf))) + + def test_auto_citation_detectee(self): + rs = copy.deepcopy(self.run_sheet) + rs["scenarios"][0]["beats"][0]["evidence"][0]["module"] = builder.SELF_MODULE + mf = builder.build_manifest(self.spec, rs) + self.assertTrue(any("auto-citation" in m or "invention/dérive" in m + for m in self._motifs(rs, mf))) + + def test_ordres_non_contigus_detectes(self): + rs = copy.deepcopy(self.run_sheet) + rs["scenarios"][0]["beats"][0]["ordre"] = 99 + mf = builder.build_manifest(self.spec, rs) + self.assertTrue(any("non contigus" in m for m in self._motifs(rs, mf))) + + def test_schema_rejette_beats_insuffisants(self): + mf = copy.deepcopy(self.manifest) + mf["scenarios"][0]["beats"] = 1 + self.assertTrue(gen.deps.validate(mf, gen._load_schema())) + + def test_schema_rejette_id_hors_pattern(self): + mf = copy.deepcopy(self.manifest) + mf["scenarios"][0]["id"] = "mauvais-id" + self.assertTrue(gen.deps.validate(mf, gen._load_schema())) + + +class TestCLI(unittest.TestCase): + def test_validate_retourne_zero(self): + self.assertEqual(gen.main(["validate"]), 0) + + def test_build_ecrit_out(self): + import tempfile + with tempfile.TemporaryDirectory() as d: + self.assertEqual(gen.main(["build", "-o", d]), 0) + for name in ("run_sheet.json", "MANIFEST.json"): + self.assertTrue(os.path.isfile(os.path.join(d, name))) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json b/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json index 98d706e..1009782 100644 --- a/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json +++ b/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json @@ -4,7 +4,7 @@ "audit": "OTO QA · Audit 4Big Qualité", "version": "1.0", "verdict": "PASS", - "modules_audited": 17, + "modules_audited": 18, "min_score": 100, "pass_score": 95, "coverage_ok": true, diff --git a/05_deliverables_mvp/qa/audit_4big/out/quality_report.json b/05_deliverables_mvp/qa/audit_4big/out/quality_report.json index 2da74b3..c28ed08 100644 --- a/05_deliverables_mvp/qa/audit_4big/out/quality_report.json +++ b/05_deliverables_mvp/qa/audit_4big/out/quality_report.json @@ -5,8 +5,8 @@ "pass_score": 95, "coverage": { "ok": true, - "ci_modules_count": 17, - "registry_modules_count": 17, + "ci_modules_count": 18, + "registry_modules_count": 18, "missing_in_registry": [], "missing_in_ci": [], "not_in_gate": [], @@ -717,11 +717,53 @@ "evidence": "MANIFEST + 3 artefact(s) JSON valides" } ] + }, + { + "id": "demo-scenarios", + "path": "demo/scenarios", + "sprint": "S7", + "archetype": "generator", + "applicable_weight": 100, + "earned_weight": 100, + "score": 100, + "verdict": "PASS", + "checks": [ + { + "criterion": "DOC", + "weight": 20, + "passed": true, + "evidence": "README.md (4486 octets)" + }, + { + "criterion": "CONTRAT", + "weight": 20, + "passed": true, + "evidence": "scenario.schema.json" + }, + { + "criterion": "TESTS", + "weight": 25, + "passed": true, + "evidence": "32 méthodes test_* dans 1 fichier(s) (seuil 8)" + }, + { + "criterion": "CLI", + "weight": 15, + "passed": true, + "evidence": "demo_scenario_gen.py (argparse + __main__)" + }, + { + "criterion": "HANDOFF", + "weight": 20, + "passed": true, + "evidence": "MANIFEST + 1 artefact(s) JSON valides" + } + ] } ], "totals": { - "modules": 17, - "pass": 17, + "modules": 18, + "pass": 18, "fail": 0, "min_score": 100, "max_score": 100, @@ -731,6 +773,6 @@ "notes": [ "Audit de MÉTA-NIVEAU : chaque note de module est CALCULÉE depuis des faits du dépôt (présence README/schéma, nombre de méthodes test_*, entrypoint CLI, intégrité out/) — jamais saisie à la main (#6).", "Un critère non applicable à l'archétype d'un module est EXCLU du dénominateur (pas de pénalité pour un hand-off qu'un module de contrat n'a jamais vocation à produire) ; les poids restants sont renormalisés à 100.", - "Le verdict global est PASS si et seulement si les 17 modules atteignent ≥ 95/100 ET la couverture == 100% des livrables gated (hors auditeur)." + "Le verdict global est PASS si et seulement si les 18 modules atteignent ≥ 95/100 ET la couverture == 100% des livrables gated (hors auditeur)." ] } diff --git a/05_deliverables_mvp/qa/audit_4big/quality_spec.json b/05_deliverables_mvp/qa/audit_4big/quality_spec.json index a641973..51800b3 100644 --- a/05_deliverables_mvp/qa/audit_4big/quality_spec.json +++ b/05_deliverables_mvp/qa/audit_4big/quality_spec.json @@ -77,11 +77,12 @@ { "id": "legal-confotur", "path": "legal/confotur", "sprint": "S5", "archetype": "generator", "source": "roadmap Sprint 5 · DocType CONFOTUR Application · ci.yml legal-confotur-tests" }, { "id": "qa-audit-5d", "path": "qa/audit_5d", "sprint": "S5", "archetype": "generator", "source": "roadmap Sprint 5 · Audit 5D conformité · ci.yml qa-audit-5d-tests" }, { "id": "seo", "path": "seo", "sprint": "S6", "archetype": "generator", "source": "roadmap Sprint 6 · SEO trilingue mots-clés/schema.org/hreflang · ci.yml seo-tests" }, - { "id": "frontend-chat-otoia", "path": "frontend/chat_otoia", "sprint": "S6", "archetype": "generator", "source": "roadmap Sprint 6 · Chat OTOIA embarqué (Custom Block) · ci.yml chat-otoia-tests" } + { "id": "frontend-chat-otoia", "path": "frontend/chat_otoia", "sprint": "S6", "archetype": "generator", "source": "roadmap Sprint 6 · Chat OTOIA embarqué (Custom Block) · ci.yml chat-otoia-tests" }, + { "id": "demo-scenarios", "path": "demo/scenarios", "sprint": "S7", "archetype": "generator", "source": "roadmap Sprint 7 · Scénarios démo (P07 banquier / P05 client) · ci.yml demo-scenario-tests" } ], "notes": [ "Audit de MÉTA-NIVEAU : chaque note de module est CALCULÉE depuis des faits du dépôt (présence README/schéma, nombre de méthodes test_*, entrypoint CLI, intégrité out/) — jamais saisie à la main (#6).", "Un critère non applicable à l'archétype d'un module est EXCLU du dénominateur (pas de pénalité pour un hand-off qu'un module de contrat n'a jamais vocation à produire) ; les poids restants sont renormalisés à 100.", - "Le verdict global est PASS si et seulement si les 17 modules atteignent ≥ 95/100 ET la couverture == 100% des livrables gated (hors auditeur)." + "Le verdict global est PASS si et seulement si les 18 modules atteignent ≥ 95/100 ET la couverture == 100% des livrables gated (hors auditeur)." ] } 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 dfa3657..66b8f57 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 @@ -259,7 +259,7 @@ class BuildTest(unittest.TestCase): def test_real_build_passes_gate(self): self.assertEqual(self.report["verdict"], "PASS") self.assertEqual(self.report["totals"]["fail"], 0) - self.assertEqual(self.report["totals"]["modules"], 17) + self.assertEqual(self.report["totals"]["modules"], 18) self.assertGreaterEqual(self.report["totals"]["min_score"], 95) def test_invariants_clean_on_real_report(self): @@ -318,7 +318,7 @@ class InvariantGuardTest(unittest.TestCase): m["verdict"] = "PASS" if m["score"] >= 95 else "FAIL" bad["totals"]["fail"] = sum(1 for x in bad["modules"] if x["verdict"] != "PASS") - bad["totals"]["pass"] = 17 - bad["totals"]["fail"] + bad["totals"]["pass"] = 18 - bad["totals"]["fail"] bad["verdict"] = "FAIL" errs = gen.check_invariants(bad, self.spec) # Le rapport est cohérent en interne mais porte un FAIL → INV7 le signale.