[DTP-Worker] Sprint 7/8 · Livrable démo : prompteur Markdown (out/run_sheet.md)
Le seul livrable démo (demo/scenarios) ne produisait qu'un run-sheet JSON machine — aucun support lisible par un présentateur, alors que le Sprint 7 vise un scénario « prêt à jouer ». Rendu Markdown in-repo depuis le JSON = même pattern que faisabilite (rend des .md), zéro écriture VPS. - scenlib/render.py : render_markdown() PUR/déterministe, ne lit que le run-sheet (déjà anti-inventé), aucun chiffre nouveau (#6). - build émet out/run_sheet.md (prompteur : par beat, table « À dire | Chiffre | Source (preuve) » traçant le pointeur RFC 6901 amont). - Compat vérifiée : HANDOFF 4Big ne json.load que les .json (ignore .md) ; check_artifacts diffe tout fichier build → md commité + reproductible. - Consommateurs régénérés : régression 551→558 (22 suites PASS) ; quality_report.json (README 5011→5575 o · 32→39 test_*) PASS 22/22 ; 03_agents/qa/AGENT.md 551→558 ; README démo auto-score 96→97. - 7 gates verts · 39 tests démo · 34 tests audit · arbre propre. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 · 551 tests · 551 passés · 0 échec · 0 erreur → verdict `PASS`.**
|
**22 suites gated · 558 tests · 558 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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,64 @@
|
|||||||
# Activity Log · 2026-07-31 · Claude Code DTP
|
# Activity Log · 2026-07-31 · Claude Code DTP
|
||||||
|
|
||||||
|
## Session `20260731_040004` · Sprint 7/8 · Livrable démo : ajout du **prompteur Markdown** (`out/run_sheet.md`) — le support texte lisible manquait au « scénario démo prêt à jouer »
|
||||||
|
|
||||||
|
**Tâche** : **Sprint 7 l.68** (« CRM + Faisabilité : Scénarios démo P07 banquier ·
|
||||||
|
P05 client ready ») × **Sprint 8 buffer**. Après audit : les 7 gates CI étaient
|
||||||
|
verts, 15 promesses `in_repo`, faisabilité 4 volets complète. Le seul livrable démo
|
||||||
|
(`demo/scenarios`) ne produisait qu'un **run-sheet JSON machine** — aucun support
|
||||||
|
**lisible par un présentateur**. Son propre README plafonnait l'auto-score à 96/100
|
||||||
|
en qualifiant le « prompteur » de **hors périmètre (VPS)**. Or rendre un Markdown
|
||||||
|
in-repo depuis le JSON est exactement le pattern déjà utilisé par `faisabilite`
|
||||||
|
(rend des `.md`) — **aucune écriture VPS**. Gap réel, borné, à forte valeur.
|
||||||
|
|
||||||
|
### Livraison
|
||||||
|
|
||||||
|
- **`scenlib/render.py`** (nouveau) — `render_markdown(run_sheet) -> str` **pur et
|
||||||
|
déterministe**. Ne lit **que** le run-sheet (déjà résolu, déjà anti-inventé) :
|
||||||
|
aucun chiffre nouveau, aucun recalcul (anti-invention #6). `_fmt` : bool→oui/non,
|
||||||
|
liste→jointe, placeholder survivant rendu entre back-ticks (jamais promu en nombre).
|
||||||
|
- **`demo_scenario_gen.py build`** émet désormais `out/run_sheet.md` en plus des deux
|
||||||
|
JSON. Le rendu est un prompteur par scénario : en-tête (audience/projet/durée/
|
||||||
|
objectif), puis chaque beat en table « À dire | Chiffre | Source (preuve) » —
|
||||||
|
chaque ligne trace son pointeur RFC 6901 amont.
|
||||||
|
- **`out/run_sheet.md`** (nouveau, commité) — 2 scénarios · 12 beats · 24 min cumulés.
|
||||||
|
|
||||||
|
### Compatibilité architecture (vérifiée AVANT d'écrire)
|
||||||
|
|
||||||
|
- **HANDOFF 4Big** (`q4lib/criteria.py:91`) ne `json.load` que les fichiers `.json` —
|
||||||
|
un `.md` dans `out/` est **ignoré**, aucune casse du critère.
|
||||||
|
- **`ci/check_artifacts.sh`** diffe **tout** fichier produit par `build` → il exige
|
||||||
|
désormais `run_sheet.md` **commité + reproductible byte-for-byte** (render
|
||||||
|
déterministe ⇒ OK). Preuve de morsure implicite : fichier non suivi ⇒ gate exit 1
|
||||||
|
(constaté puis corrigé par `git add`).
|
||||||
|
|
||||||
|
### Consommateurs régénérés (dérive maîtrisée)
|
||||||
|
|
||||||
|
- +7 méthodes test (`TestRenderMarkdown`) → **régression 551→558 tests** (22 suites,
|
||||||
|
PASS) : `regression run` + `build` régénérés (`regression_run/plan/MANIFEST.json`).
|
||||||
|
- `demo/scenarios` : README `5011→5575` octets + `32→39` test_* → **`quality_report.json`
|
||||||
|
régénéré** (evidence DOC+TESTS) · verdict **PASS · 22/22 · min 100** inchangé.
|
||||||
|
- `03_agents/qa/AGENT.md` : verdict agrégé `551→558` (gate `check_readme_claims` vert).
|
||||||
|
- README démo : Fichiers (+`render.py`, +`run_sheet.md`), chaîne de valeur, « hors
|
||||||
|
périmètre » (seul l'**habillage visuel** reste VPS), auto-score **96→97**.
|
||||||
|
|
||||||
|
### Vérifs
|
||||||
|
|
||||||
|
| Contrôle | Résultat |
|
||||||
|
|---|---|
|
||||||
|
| 7 gates CI (`guard`/`json`/`docs`/`artifacts`/`regression`/`readme_claims`/`ci_integrity`) | ✅ exit 0 |
|
||||||
|
| Tests démo | ✅ 39/39 |
|
||||||
|
| Tests audit_4big | ✅ 34/34 |
|
||||||
|
| Régression exhaustive | ✅ 22/22 suites · 558 tests · PASS |
|
||||||
|
| `run_sheet.md` reproductible (build frais == commité) | ✅ (gate artifacts) |
|
||||||
|
| Arbre de travail | ✅ 0 fichier untracked |
|
||||||
|
|
||||||
|
**Hors périmètre worker (VPS · #8)** : habillage visuel deck / page démo `otov7.com` +
|
||||||
|
branchement `data_room` réel — inchangé. Le prompteur texte est le rendu de référence
|
||||||
|
in-repo que ce visuel habillera.
|
||||||
|
|
||||||
|
**Auto-score 4Big** : 97/100.
|
||||||
|
|
||||||
## Session `20260731_033001` · Buffer S8 · « Vert trompeur » : la fiche QA affichait un « verdict agrégé courant » PÉRIMÉ (21/534) en se disant « jamais compté à la main » — hors du périmètre du gate README
|
## Session `20260731_033001` · Buffer S8 · « Vert trompeur » : la fiche QA affichait un « verdict agrégé courant » PÉRIMÉ (21/534) en se disant « jamais compté à la main » — hors du périmètre du gate README
|
||||||
|
|
||||||
**Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA). Roadmap fonctionnellement
|
**Tâche** : **Sprint 8 · buffer L75** (DevOps CI/CD · QA). Roadmap fonctionnellement
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ scenario_spec.json out/ des modules livrés (10 modules cités)
|
|||||||
│ │
|
│ │
|
||||||
▼ demo_scenario_gen build ▼
|
▼ demo_scenario_gen build ▼
|
||||||
out/run_sheet.json ◄──── résolution RFC 6901 + placeholders ──── crm/workflow_vente
|
out/run_sheet.json ◄──── résolution RFC 6901 + placeholders ──── crm/workflow_vente
|
||||||
out/MANIFEST.json fiscal/ecf_dgii
|
out/run_sheet.md (prompteur, rendu pur du run-sheet) fiscal/ecf_dgii
|
||||||
legal/confotur
|
out/MANIFEST.json legal/confotur
|
||||||
rbac/apply_plan
|
rbac/apply_plan
|
||||||
qa/audit_4big …
|
qa/audit_4big …
|
||||||
```
|
```
|
||||||
@@ -52,13 +52,13 @@ jamais un `0` fabriqué, jamais un `null` amont promu en chiffre de pitch.
|
|||||||
## Utilisation
|
## Utilisation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Assemble le run-sheet (défaut: ./out)
|
# Assemble le run-sheet + le prompteur Markdown (défaut: ./out)
|
||||||
python3 demo_scenario_gen.py build [-o OUT_DIR]
|
python3 demo_scenario_gen.py build [-o OUT_DIR]
|
||||||
|
|
||||||
# Recalcule le manifeste + les invariants, sans écrire
|
# Recalcule le manifeste + les invariants, sans écrire
|
||||||
python3 demo_scenario_gen.py validate
|
python3 demo_scenario_gen.py validate
|
||||||
|
|
||||||
# Tests (32 méthodes, dont injections négatives)
|
# Tests (39 méthodes, dont injections négatives)
|
||||||
python3 -m unittest discover -s tests -v
|
python3 -m unittest discover -s tests -v
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -70,16 +70,21 @@ python3 -m unittest discover -s tests -v
|
|||||||
| `scenlib/deps.py` | Réutilise validateur Publiciste + couverture CI 4Big |
|
| `scenlib/deps.py` | Réutilise validateur Publiciste + couverture CI 4Big |
|
||||||
| `scenlib/evidence.py` | Chargement `out/` amont + résolveur RFC 6901 + placeholders |
|
| `scenlib/evidence.py` | Chargement `out/` amont + résolveur RFC 6901 + placeholders |
|
||||||
| `scenlib/builder.py` | Assemblage déterministe run-sheet + manifeste |
|
| `scenlib/builder.py` | Assemblage déterministe run-sheet + manifeste |
|
||||||
|
| `scenlib/render.py` | Rendu **prompteur Markdown** (pur, dérivé du run-sheet) |
|
||||||
| `demo_scenario_gen.py` | CLI `build`/`validate` + invariants |
|
| `demo_scenario_gen.py` | CLI `build`/`validate` + invariants |
|
||||||
| `scenario.schema.json` | Contrat de sortie draft-07 du manifeste |
|
| `scenario.schema.json` | Contrat de sortie draft-07 du manifeste |
|
||||||
| `out/run_sheet.json` | **Hand-off principal** — run-sheet jouable |
|
| `out/run_sheet.json` | **Hand-off principal** — run-sheet jouable (machine) |
|
||||||
|
| `out/run_sheet.md` | **Prompteur** lisible par un présentateur (dérivé de `run_sheet.json`) |
|
||||||
| `out/MANIFEST.json` | Diagnostic + preuve de couverture + drapeau `pret` |
|
| `out/MANIFEST.json` | Diagnostic + preuve de couverture + drapeau `pret` |
|
||||||
|
|
||||||
## Hors périmètre worker (VPS · #8)
|
## Hors périmètre worker (VPS · #8)
|
||||||
|
|
||||||
Le rendu final (deck / prompteur / page démo `otov7.com`) et le branchement au
|
Le **support texte** est désormais produit in-repo : `out/run_sheet.md` est un
|
||||||
`data_room` réel des projets restent **côté serveur** — ce worker n'écrit jamais
|
prompteur lisible, régénéré déterministement depuis le run-sheet (aucune donnée
|
||||||
sur le VPS. Le run-sheet est le **contrat** que ce rendu consommera.
|
nouvelle · #6). Ne restent **côté serveur** que l'**habillage visuel** (deck /
|
||||||
|
page démo `otov7.com`) et le branchement au `data_room` réel des projets — ce
|
||||||
|
worker n'écrit jamais sur le VPS. Le run-sheet JSON reste le **contrat** que ce
|
||||||
|
rendu visuel consommera ; le Markdown en est le rendu texte de référence.
|
||||||
|
|
||||||
## Provenance des libellés projets
|
## Provenance des libellés projets
|
||||||
|
|
||||||
@@ -88,9 +93,10 @@ sur le VPS. Le run-sheet est le **contrat** que ce rendu consommera.
|
|||||||
|
|
||||||
## Auto-score 4Big
|
## Auto-score 4Big
|
||||||
|
|
||||||
**96/100** — orchestration réelle prouvée (chaque beat cite une preuve RFC 6901
|
**97/100** — orchestration réelle prouvée (chaque beat cite une preuve RFC 6901
|
||||||
re-résolue depuis le disque, jamais saisie à la main), couverture CI réutilisée de
|
re-résolue depuis le disque, jamais saisie à la main), couverture CI réutilisée de
|
||||||
l'auditeur 4Big (zéro duplication · #5), garde `pret=true` qui refuse d'écrire sur
|
l'auditeur 4Big (zéro duplication · #5), garde `pret=true` qui refuse d'écrire sur
|
||||||
citation non résolue, 32 tests (dont injections négatives) verts. Retenue sur le
|
citation non résolue, **prompteur `run_sheet.md`** désormais livré (support texte
|
||||||
seuil : le rendu final (deck / page démo `otov7.com`) est produit côté VPS (#8) —
|
in-repo, dérivé purement du run-sheet · reproductible sous `ci/check_artifacts.sh`),
|
||||||
l'in-repo est le **run-sheet** contractuel, pas le support de présentation.
|
39 tests (dont injections négatives + rendu Markdown) verts. Retenue résiduelle :
|
||||||
|
l'**habillage visuel** (deck / page démo `otov7.com`) reste produit côté VPS (#8).
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import sys
|
|||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, _HERE)
|
sys.path.insert(0, _HERE)
|
||||||
|
|
||||||
from scenlib import builder, deps, evidence # noqa: E402
|
from scenlib import builder, deps, evidence, render # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def _eprint(*args) -> None:
|
def _eprint(*args) -> None:
|
||||||
@@ -197,9 +197,13 @@ def cmd_build(ns: argparse.Namespace) -> int:
|
|||||||
with open(os.path.join(ns.out, name), "w", encoding="utf-8") as fh:
|
with open(os.path.join(ns.out, name), "w", encoding="utf-8") as fh:
|
||||||
json.dump(obj, fh, ensure_ascii=False, indent=2)
|
json.dump(obj, fh, ensure_ascii=False, indent=2)
|
||||||
fh.write("\n")
|
fh.write("\n")
|
||||||
|
# Prompteur Markdown lisible (dérivé PUREMENT du run-sheet · #6). Reproductible
|
||||||
|
# à l'identique (gate ci/check_artifacts.sh) car render est déterministe.
|
||||||
|
with open(os.path.join(ns.out, "run_sheet.md"), "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(render.render_markdown(run_sheet))
|
||||||
|
|
||||||
c = manifest["counts"]
|
c = manifest["counts"]
|
||||||
_eprint(f"✅ run-sheet → {ns.out}")
|
_eprint(f"✅ run-sheet → {ns.out} (+ prompteur run_sheet.md)")
|
||||||
_eprint(f" scénarios={c['scenarios']} · beats={c['beats_total']} · "
|
_eprint(f" scénarios={c['scenarios']} · beats={c['beats_total']} · "
|
||||||
f"citations={c['citations_total']} (non résolues={c['citations_non_resolues']}) · "
|
f"citations={c['citations_total']} (non résolues={c['citations_non_resolues']}) · "
|
||||||
f"modules cités={c['modules_cites_uniques']}")
|
f"modules cités={c['modules_cites_uniques']}")
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# OTO · Run-sheet démo (méta-livrable)
|
||||||
|
|
||||||
|
> Prompteur généré **automatiquement** depuis `out/run_sheet.json` (`demo_scenario_gen build`). Chaque chiffre est une **preuve re-résolue** depuis un hand-off `out/` d'un module gaté — jamais saisi à la main (anti-invention · CLAUDE.md #6). **Ne pas éditer à la main** : régénérer.
|
||||||
|
|
||||||
|
- **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) »
|
||||||
|
- **Version du spec** : 1.0.0
|
||||||
|
- **2 scénario(s)** · durée cumulée 24 min
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P07 Aqua Terra — Pitch banquier (bancabilité & gouvernance)
|
||||||
|
|
||||||
|
- **Scénario** : `S-P07-BANQUIER` · **audience** : banquier · **projet** : P07 — Aqua Terra Las Terrenas
|
||||||
|
- **Durée** : 12 min · 6 temps forts
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
### 1. Un pipeline de vente verrouillé de bout en bout · 2 min
|
||||||
|
|
||||||
|
> Ouvrir sur la traçabilité : la vente n'est pas un tableur, c'est un workflow ERPNext soumis (docstatus) piloté par rôle.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Nom du pipeline | OTO Vente Pipeline | `crm/workflow_vente` · `out/workflow.json#/0/workflow_name` |
|
||||||
|
| DocType piloté | OTO Dossier Vente | `crm/workflow_vente` · `out/workflow.json#/0/document_type` |
|
||||||
|
|
||||||
|
### 2. Facturation fiscale conforme DGII (e-CF · Compupar) · 2 min
|
||||||
|
|
||||||
|
> Rassurer sur le risque fiscal : chaque encaissement est un e-CF certifié, pas une facture maison.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Fournisseur e-CF | Compupar | `fiscal/ecf_dgii` · `out/MANIFEST.json#/provider` |
|
||||||
|
| Types e-CF en périmètre | 5 | `fiscal/ecf_dgii` · `out/MANIFEST.json#/counts/tipos_en_scope` |
|
||||||
|
|
||||||
|
### 3. Incitation CONFOTUR dé-risquée et outillée · 2 min
|
||||||
|
|
||||||
|
> Montrer l'avantage fiscal touristique comme un actif documenté : DocType soumis, dépôt outillé.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| DocType CONFOTUR | CONFOTUR Application | `legal/confotur` · `out/MANIFEST.json#/doctype_name` |
|
||||||
|
| Soumissible (verrou légal) | oui | `legal/confotur` · `out/MANIFEST.json#/is_submittable` |
|
||||||
|
| Étapes de dépôt outillées | 2 | `legal/confotur` · `out/MANIFEST.json#/counts/depot_events` |
|
||||||
|
|
||||||
|
### 4. Séparation des pouvoirs (RBAC 50 rôles) · 2 min
|
||||||
|
|
||||||
|
> Répondre à la question d'audit du banquier : qui peut faire quoi ? Permissions dérivées, pas ad hoc.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Rôles RBAC | 50 | `rbac/apply_plan` · `out/MANIFEST.json#/counts/roles` |
|
||||||
|
| Permissions DocType (Custom DocPerm) | 116 | `rbac/apply_plan` · `out/MANIFEST.json#/counts/custom_docperm` |
|
||||||
|
|
||||||
|
### 5. Un gate qualité 4Big, pas une promesse · 2 min
|
||||||
|
|
||||||
|
> Clore la confiance : la plateforme s'auto-note et bloque sous 95/100 sur 100 % des livrables.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Seuil bloquant 4Big | 95 | `qa/audit_4big` · `out/quality_report.json#/pass_score` |
|
||||||
|
| Modules sous gate qualité | 22 | `qa/audit_4big` · `out/quality_report.json#/coverage/ci_modules_count` |
|
||||||
|
|
||||||
|
### 6. Le dossier de vente, pièce bancable finale · 2 min
|
||||||
|
|
||||||
|
> Refermer sur l'objet concret que le banquier instruira : un DocType riche et soumis.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Champs du dossier de vente | 30 | `crm/dossier_vente` · `out/MANIFEST.json#/counts/fields` |
|
||||||
|
| États de pipeline couverts | 9 | `crm/dossier_vente` · `out/MANIFEST.json#/counts/pipeline_states` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P05 Las Colinas Najayo Arriba — Parcours client (réservation prête)
|
||||||
|
|
||||||
|
- **Scénario** : `S-P05-CLIENT` · **audience** : client · **projet** : P05 — Las Colinas Najayo Arriba
|
||||||
|
- **Durée** : 12 min · 6 temps forts
|
||||||
|
- **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).
|
||||||
|
|
||||||
|
### 1. « Choisir mon unité » — 5 portails luxury · 2 min
|
||||||
|
|
||||||
|
> Poser l'expérience d'entrée : un portail par rôle, cohérent et couvert.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Portails (workspaces) | 5 | `frontend/portails` · `out/MANIFEST.json#/counts/workspaces` |
|
||||||
|
| Rôles couverts par les portails | 44 | `frontend/portails` · `out/MANIFEST.json#/counts/roles_couverts` |
|
||||||
|
|
||||||
|
### 2. OTOIA répond dans le portail · 2 min
|
||||||
|
|
||||||
|
> Montrer l'assistance embarquée : le client n'est jamais seul face à l'écran.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Assistant embarqué | Chat OTOIA embarqué par portail | `frontend/chat_otoia` · `out/MANIFEST.json#/deliverable` |
|
||||||
|
|
||||||
|
### 3. Du premier contact à la réservation · 2 min
|
||||||
|
|
||||||
|
> Dérouler le pipeline côté client : Lead → … → Réservation confirmée, lisible et rassurant.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Première étape | Lead | `crm/workflow_vente` · `out/workflow.json#/0/states/0/state` |
|
||||||
|
| Étape réservation | Réservation confirmée | `crm/workflow_vente` · `out/workflow.json#/0/states/3/state` |
|
||||||
|
|
||||||
|
### 4. Un dossier de vente transparent · 2 min
|
||||||
|
|
||||||
|
> Donner confiance : tout est consigné dans un dossier structuré que le client peut suivre.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Champs du dossier de vente | 30 | `crm/dossier_vente` · `out/MANIFEST.json#/counts/fields` |
|
||||||
|
|
||||||
|
### 5. Un conseiller rémunéré à la transparence · 2 min
|
||||||
|
|
||||||
|
> Aligner l'intérêt du conseiller et du client : barème de commissions traçable par événement.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Barème de commissions | OTO Barème Commissions Ventes | `crm/commissions` · `out/MANIFEST.json#/bareme_name` |
|
||||||
|
| Événements déclencheurs | 5 | `crm/commissions` · `out/MANIFEST.json#/counts/evenements` |
|
||||||
|
|
||||||
|
### 6. Comment le client nous a trouvés (SEO trilingue) · 2 min
|
||||||
|
|
||||||
|
> Refermer la boucle acquisition : indexation FR/EN/ES, le parcours commence bien avant le portail.
|
||||||
|
|
||||||
|
| À dire | Chiffre | Source (preuve) |
|
||||||
|
|---|---|---|
|
||||||
|
| Mots-clés indexés | 258 | `seo` · `out/MANIFEST.json#/counts/keywords_total` |
|
||||||
|
| Langues | fr, en, es | `seo` · `out/MANIFEST.json#/langs` |
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""Rendu Markdown du run-sheet démo — le **prompteur** lisible par un présentateur.
|
||||||
|
|
||||||
|
Transforme PUREMENT le `run_sheet.json` (déjà résolu, déjà anti-inventé) en un
|
||||||
|
script de pitch lisible à voix haute. Contraintes :
|
||||||
|
|
||||||
|
- **Zéro donnée nouvelle (CLAUDE.md #6)** : ce module ne lit QUE le run-sheet ;
|
||||||
|
chaque chiffre du Markdown provient d'une citation `evidence.valeur` déjà
|
||||||
|
résolue depuis un artefact amont. Aucune valeur n'est saisie, calculée ni
|
||||||
|
reformatée en un autre nombre.
|
||||||
|
- **Déterministe** : mêmes entrées ⇒ mêmes octets (aucun horodatage implicite ;
|
||||||
|
`generated_at` est celui, explicite, du run-sheet). C'est ce qui permet au
|
||||||
|
gate `ci/check_artifacts.sh` de prouver la reproductibilité de `out/run_sheet.md`.
|
||||||
|
|
||||||
|
Le rendu final riche (deck, page démo `otov7.com`) reste côté VPS (#8) ; ce
|
||||||
|
prompteur Markdown est le support **texte** in-repo que ce rendu habillera.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from . import evidence
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt(valeur) -> str:
|
||||||
|
"""Formate une valeur de citation pour l'oral, sans jamais inventer.
|
||||||
|
|
||||||
|
Un placeholder non résolu (ne devrait pas survivre aux invariants, mais on
|
||||||
|
reste défensif) est rendu verbatim entre back-ticks — jamais promu en chiffre.
|
||||||
|
"""
|
||||||
|
if isinstance(valeur, bool):
|
||||||
|
return "oui" if valeur else "non"
|
||||||
|
if isinstance(valeur, list):
|
||||||
|
return ", ".join(_fmt(v) for v in valeur)
|
||||||
|
if valeur is None:
|
||||||
|
return "`—`"
|
||||||
|
if evidence.is_placeholder(valeur):
|
||||||
|
return f"`{valeur}`"
|
||||||
|
return str(valeur)
|
||||||
|
|
||||||
|
|
||||||
|
def _scenario_md(sc: dict) -> list[str]:
|
||||||
|
lignes: list[str] = []
|
||||||
|
lignes.append(f"## {sc['titre']}")
|
||||||
|
lignes.append("")
|
||||||
|
lignes.append(
|
||||||
|
f"- **Scénario** : `{sc['id']}` · **audience** : {sc['audience']} · "
|
||||||
|
f"**projet** : {sc['projet']} — {sc['projet_libelle']}"
|
||||||
|
)
|
||||||
|
lignes.append(f"- **Durée** : {sc['duree_min']} min · {len(sc['beats'])} temps forts")
|
||||||
|
lignes.append(f"- **Objectif** : {sc['objectif']}")
|
||||||
|
lignes.append("")
|
||||||
|
for b in sc["beats"]:
|
||||||
|
lignes.append(f"### {b['ordre']}. {b['titre']} · {b['duree_min']} min")
|
||||||
|
lignes.append("")
|
||||||
|
lignes.append(f"> {b['role_narratif']}")
|
||||||
|
lignes.append("")
|
||||||
|
lignes.append("| À dire | Chiffre | Source (preuve) |")
|
||||||
|
lignes.append("|---|---|---|")
|
||||||
|
for c in b["evidence"]:
|
||||||
|
src = f"`{c['module']}` · `{c['file']}#{c['pointer']}`"
|
||||||
|
lignes.append(f"| {c['label']} | {_fmt(c['valeur'])} | {src} |")
|
||||||
|
lignes.append("")
|
||||||
|
return lignes
|
||||||
|
|
||||||
|
|
||||||
|
def render_markdown(run_sheet: dict) -> str:
|
||||||
|
"""Run-sheet dict → prompteur Markdown (chaîne terminée par un saut de ligne)."""
|
||||||
|
lignes: list[str] = []
|
||||||
|
lignes.append(f"# {run_sheet['doc']}")
|
||||||
|
lignes.append("")
|
||||||
|
lignes.append(
|
||||||
|
"> Prompteur généré **automatiquement** depuis `out/run_sheet.json` "
|
||||||
|
"(`demo_scenario_gen build`). Chaque chiffre est une **preuve re-résolue** "
|
||||||
|
"depuis un hand-off `out/` d'un module gaté — jamais saisi à la main "
|
||||||
|
"(anti-invention · CLAUDE.md #6). **Ne pas éditer à la main** : régénérer."
|
||||||
|
)
|
||||||
|
lignes.append("")
|
||||||
|
lignes.append(f"- **Roadmap** : {run_sheet['roadmap_ref']}")
|
||||||
|
lignes.append(f"- **Version du spec** : {run_sheet['spec_version']}")
|
||||||
|
total = sum(sc["duree_min"] for sc in run_sheet["scenarios"])
|
||||||
|
lignes.append(
|
||||||
|
f"- **{len(run_sheet['scenarios'])} scénario(s)** · durée cumulée {total} min"
|
||||||
|
)
|
||||||
|
if run_sheet.get("generated_at"):
|
||||||
|
lignes.append(f"- **Généré le** : {run_sheet['generated_at']}")
|
||||||
|
lignes.append("")
|
||||||
|
lignes.append("---")
|
||||||
|
lignes.append("")
|
||||||
|
for i, sc in enumerate(run_sheet["scenarios"]):
|
||||||
|
lignes += _scenario_md(sc)
|
||||||
|
if i < len(run_sheet["scenarios"]) - 1:
|
||||||
|
lignes.append("---")
|
||||||
|
lignes.append("")
|
||||||
|
return "\n".join(lignes).rstrip("\n") + "\n"
|
||||||
@@ -19,7 +19,7 @@ _MOD = os.path.normpath(os.path.join(_HERE, ".."))
|
|||||||
sys.path.insert(0, _MOD)
|
sys.path.insert(0, _MOD)
|
||||||
|
|
||||||
import demo_scenario_gen as gen # noqa: E402
|
import demo_scenario_gen as gen # noqa: E402
|
||||||
from scenlib import builder, evidence # noqa: E402
|
from scenlib import builder, evidence, render # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
def _fresh():
|
def _fresh():
|
||||||
@@ -214,6 +214,51 @@ class TestInjectionsNegatives(unittest.TestCase):
|
|||||||
self.assertTrue(gen.deps.validate(mf, gen._load_schema()))
|
self.assertTrue(gen.deps.validate(mf, gen._load_schema()))
|
||||||
|
|
||||||
|
|
||||||
|
class TestRenderMarkdown(unittest.TestCase):
|
||||||
|
"""Le prompteur Markdown : déterministe et sans donnée nouvelle (#6)."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
_, self.run_sheet, _ = _fresh()
|
||||||
|
self.md = render.render_markdown(self.run_sheet)
|
||||||
|
|
||||||
|
def test_deterministe(self):
|
||||||
|
self.assertEqual(self.md, render.render_markdown(self.run_sheet))
|
||||||
|
|
||||||
|
def test_termine_par_un_saut_de_ligne(self):
|
||||||
|
self.assertTrue(self.md.endswith("\n"))
|
||||||
|
self.assertFalse(self.md.endswith("\n\n"))
|
||||||
|
|
||||||
|
def test_titres_scenarios_presents(self):
|
||||||
|
for sc in self.run_sheet["scenarios"]:
|
||||||
|
self.assertIn(sc["titre"], self.md)
|
||||||
|
self.assertIn(sc["id"], self.md)
|
||||||
|
|
||||||
|
def test_chaque_beat_rendu(self):
|
||||||
|
for sc in self.run_sheet["scenarios"]:
|
||||||
|
for b in sc["beats"]:
|
||||||
|
self.assertIn(b["titre"], self.md)
|
||||||
|
self.assertIn(b["role_narratif"], self.md)
|
||||||
|
|
||||||
|
def test_valeurs_proviennent_du_run_sheet(self):
|
||||||
|
# Anti-invention : chaque valeur affichée est celle, formatée, d'une
|
||||||
|
# citation — jamais un chiffre nouveau. On vérifie la présence verbatim.
|
||||||
|
for sc in self.run_sheet["scenarios"]:
|
||||||
|
for b in sc["beats"]:
|
||||||
|
for c in b["evidence"]:
|
||||||
|
self.assertIn(render._fmt(c["valeur"]), self.md)
|
||||||
|
|
||||||
|
def test_fmt_bool_liste_none(self):
|
||||||
|
self.assertEqual(render._fmt(True), "oui")
|
||||||
|
self.assertEqual(render._fmt(False), "non")
|
||||||
|
self.assertEqual(render._fmt(["fr", "en"]), "fr, en")
|
||||||
|
self.assertEqual(render._fmt(50), "50")
|
||||||
|
self.assertEqual(render._fmt(None), "`—`")
|
||||||
|
|
||||||
|
def test_placeholder_non_promu_en_chiffre(self):
|
||||||
|
# Un placeholder survivant est rendu entre back-ticks, jamais nu.
|
||||||
|
self.assertEqual(render._fmt("{{m:f#/p}}"), "`{{m:f#/p}}`")
|
||||||
|
|
||||||
|
|
||||||
class TestCLI(unittest.TestCase):
|
class TestCLI(unittest.TestCase):
|
||||||
def test_validate_retourne_zero(self):
|
def test_validate_retourne_zero(self):
|
||||||
self.assertEqual(gen.main(["validate"]), 0)
|
self.assertEqual(gen.main(["validate"]), 0)
|
||||||
@@ -222,7 +267,7 @@ class TestCLI(unittest.TestCase):
|
|||||||
import tempfile
|
import tempfile
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
self.assertEqual(gen.main(["build", "-o", d]), 0)
|
self.assertEqual(gen.main(["build", "-o", d]), 0)
|
||||||
for name in ("run_sheet.json", "MANIFEST.json"):
|
for name in ("run_sheet.json", "MANIFEST.json", "run_sheet.md"):
|
||||||
self.assertTrue(os.path.isfile(os.path.join(d, name)))
|
self.assertTrue(os.path.isfile(os.path.join(d, name)))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -774,7 +774,7 @@
|
|||||||
"criterion": "DOC",
|
"criterion": "DOC",
|
||||||
"weight": 20,
|
"weight": 20,
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"evidence": "README.md (5011 octets)"
|
"evidence": "README.md (5575 octets)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"criterion": "CONTRAT",
|
"criterion": "CONTRAT",
|
||||||
@@ -786,7 +786,7 @@
|
|||||||
"criterion": "TESTS",
|
"criterion": "TESTS",
|
||||||
"weight": 25,
|
"weight": 25,
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"evidence": "32 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
"evidence": "39 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": 551,
|
"test_methods": 558,
|
||||||
"coverage_ok": true,
|
"coverage_ok": true,
|
||||||
"artifacts": [
|
"artifacts": [
|
||||||
"regression_plan.json"
|
"regression_plan.json"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
"in_gate": true,
|
"in_gate": true,
|
||||||
"has_tests_dir": true,
|
"has_tests_dir": true,
|
||||||
"test_files": 1,
|
"test_files": 1,
|
||||||
"test_methods": 32
|
"test_methods": 39
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "devops.deploy_runbook",
|
"id": "devops.deploy_runbook",
|
||||||
@@ -273,7 +273,7 @@
|
|||||||
"totals": {
|
"totals": {
|
||||||
"suites": 22,
|
"suites": 22,
|
||||||
"test_files": 22,
|
"test_files": 22,
|
||||||
"test_methods": 551,
|
"test_methods": 558,
|
||||||
"min_methods": 10,
|
"min_methods": 10,
|
||||||
"under_threshold": 0
|
"under_threshold": 0
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,9 +42,9 @@
|
|||||||
{
|
{
|
||||||
"id": "demo.scenarios",
|
"id": "demo.scenarios",
|
||||||
"path": "demo/scenarios",
|
"path": "demo/scenarios",
|
||||||
"expected_methods": 32,
|
"expected_methods": 39,
|
||||||
"ran": 32,
|
"ran": 39,
|
||||||
"passed": 32,
|
"passed": 39,
|
||||||
"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": 551,
|
"ran": 558,
|
||||||
"passed": 551,
|
"passed": 558,
|
||||||
"failures": 0,
|
"failures": 0,
|
||||||
"errors": 0,
|
"errors": 0,
|
||||||
"skipped": 0
|
"skipped": 0
|
||||||
|
|||||||
Reference in New Issue
Block a user