Compare commits
32 Commits
39def91880
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 15eebb41ca | |||
| c1e5fd9a6e | |||
| bd9f2b173a | |||
| e8e3dff1a3 | |||
| 1aaef23fe4 | |||
| 0ef7c1a079 | |||
| 0f7a1d8f6f | |||
| 1cf0e18fba | |||
| c73ed5b626 | |||
| 1732cc3de1 | |||
| 4a13557529 | |||
| e15129bc6a | |||
| fb2d3e16bf | |||
| 4eb615b174 | |||
| ea8458d63b | |||
| 3849375b66 | |||
| 1992ee63be | |||
| 626c39d958 | |||
| 4aece53921 | |||
| 85cd625dac | |||
| cb8069b83c | |||
| 123761e938 | |||
| e7cc3c4a3c | |||
| 5af86e3518 | |||
| aeb30b8311 | |||
| 3a40aee42d | |||
| 37c68aca28 | |||
| 1513e69a79 | |||
| a17d86a767 | |||
| 2cad5b2b06 | |||
| ee88d3698b | |||
| 031b5e762e |
@@ -23,13 +23,13 @@ AVANT le Workflow** qui le cible.
|
|||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| [`workflow_vente/`](../../05_deliverables_mvp/crm/workflow_vente/README.md) | 4 (roadmap L50) | Graphe **Workflow** ERPNext : le pipeline `lead → visite → devis → réservation → contrat → CONFOTUR` (states + transitions + actions) | `workflow_vente_gen.py build\|validate` | `crm-workflow-vente-tests` | 25 |
|
| [`workflow_vente/`](../../05_deliverables_mvp/crm/workflow_vente/README.md) | 4 (roadmap L50) | Graphe **Workflow** ERPNext : le pipeline `lead → visite → devis → réservation → contrat → CONFOTUR` (states + transitions + actions) | `workflow_vente_gen.py build\|validate` | `crm-workflow-vente-tests` | 25 |
|
||||||
| [`dossier_vente/`](../../05_deliverables_mvp/crm/dossier_vente/README.md) | 4 (roadmap L50) | **DocType porteur** `OTO Dossier Vente` : le document réel qui circule dans le Workflow ; sans lui le pipeline n'a rien à quoi s'attacher | `doctype_dossier_vente_gen.py build\|validate` | `crm-dossier-vente-tests` | 31 |
|
| [`dossier_vente/`](../../05_deliverables_mvp/crm/dossier_vente/README.md) | 4 (roadmap L50) | **DocType porteur** `OTO Dossier Vente` : le document réel qui circule dans le Workflow ; sans lui le pipeline n'a rien à quoi s'attacher | `doctype_dossier_vente_gen.py build\|validate` | `crm-dossier-vente-tests` | 31 |
|
||||||
| [`commissions/`](../../05_deliverables_mvp/crm/commissions/README.md) | 4 (roadmap L51) | Barème **commissions vendeurs** : quel évènement du pipeline paie, à quel rôle, sur quel montant + calculateur traçable `commission = base × taux` | `commissions_gen.py build\|validate` | `crm-commissions-tests` | 25 |
|
| [`commissions/`](../../05_deliverables_mvp/crm/commissions/README.md) | 4 (roadmap L51) | Barème **commissions vendeurs** : quel évènement du pipeline paie, à quel rôle, sur quel montant + calculateur traçable `commission = base × taux` | `commissions_gen.py build\|validate` | `crm-commissions-tests` | 27 |
|
||||||
|
|
||||||
**Trois modules cross-cohérents, une source unique** (CLAUDE.md #5 · éliminer les
|
**Trois modules cross-cohérents, une source unique** (CLAUDE.md #5 · éliminer les
|
||||||
doublons) : le nom du DocType, son champ d'état, ses valeurs de statut et son
|
doublons) : le nom du DocType, son champ d'état, ses valeurs de statut et son
|
||||||
caractère *submittable* sont **dérivés** de `workflow_vente_spec.json` (anti-dérive) ;
|
caractère *submittable* sont **dérivés** de `workflow_vente_spec.json` (anti-dérive) ;
|
||||||
les rôles sont **résolus** depuis `rbac/rbac_50_roles.json` — jamais un nom Frappe
|
les rôles sont **résolus** depuis `rbac/rbac_50_roles.json` — jamais un nom Frappe
|
||||||
en dur. Total CRM : **81 tests** (25 + 31 + 25), tous gated dans le CI — c'est le
|
en dur. Total CRM : **83 tests** (25 + 31 + 27), tous gated dans le CI — c'est le
|
||||||
total du **trio pipeline** ; le module financement bancaire ci-dessous porte ses
|
total du **trio pipeline** ; le module financement bancaire ci-dessous porte ses
|
||||||
**35 tests** à part (source distincte).
|
**35 tests** à part (source distincte).
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ de Michel, d'où son suivi séparé (il ne partage pas la source unique du trio)
|
|||||||
|
|
||||||
| Module | Sprint | Rôle | Entrée CLI | Job CI | Tests |
|
| Module | Sprint | Rôle | Entrée CLI | Job CI | Tests |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| [`financement_bancaire/`](../../05_deliverables_mvp/crm/financement_bancaire/README.md) | 4 (roadmap L52) | Parcours **hypothécaire RD** : cœur métier du **gate check 4 conditions** — aucun document n'est transmis à la banque tant que apport initial (20 % résident · 30 % étranger · Ley 189-11) + documents exigés + autorisations signées + validation référente ne sont pas réunis ; fonctions **pures** sans I/O | `financement_bancaire_gen.py build\|validate` | `crm-financement-bancaire-tests` | 35 |
|
| [`financement_bancaire/`](../../05_deliverables_mvp/crm/financement_bancaire/README.md) | 4 (roadmap L52) | Parcours **hypothécaire RD** : cœur métier du **gate check 4 conditions** — aucun document n'est transmis à la banque tant que apport initial (20 % résident · 30 % étranger · Ley 189-11) + documents exigés + autorisations signées + validation référente ne sont pas réunis ; fonctions **pures** sans I/O | `financement_bancaire_gen.py build\|validate` | `crm-financement-bancaire-tests` | 37 |
|
||||||
|
|
||||||
Matérialise [`DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md`](../../DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md)
|
Matérialise [`DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md`](../../DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md)
|
||||||
(**Phase 1 · P0 · MVP** : contrat de données + cœur du gate + 7 sections + bannière
|
(**Phase 1 · P0 · MVP** : contrat de données + cœur du gate + 7 sections + bannière
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ le VPS reçoit — jamais l'inverse.
|
|||||||
vente, le barème commissions, le DocType Dossier Vente et le plan e-CF **résolvent**
|
vente, le barème commissions, le DocType Dossier Vente et le plan e-CF **résolvent**
|
||||||
leurs rôles depuis ce fichier, jamais un nom Frappe en dur. Total backend **RBAC 60
|
leurs rôles depuis ce fichier, jamais un nom Frappe en dur. Total backend **RBAC 60
|
||||||
tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests**, tous gated dans le CI (matrice
|
tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests**, tous gated dans le CI (matrice
|
||||||
de régression du repo : **629 tests · 24 suites** · verdict `PASS`, source
|
de régression du repo : **635 tests · 24 suites** · verdict `PASS`, source
|
||||||
`qa/regression/out/regression_run.json` — jamais compté à la main · #6).
|
`qa/regression/out/regression_run.json` — jamais compté à la main · #6).
|
||||||
|
|
||||||
## Hand-off reçus (à appliquer sur le VPS, dans l'ordre)
|
## Hand-off reçus (à appliquer sur le VPS, dans l'ordre)
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ du générateur, gaté en CI. C'est le **seul module net-neuf** du mandat, sur l
|
|||||||
|
|
||||||
| Module | Sprint | Rôle | Entrée CLI | Job CI | Tests |
|
| Module | Sprint | Rôle | Entrée CLI | Job CI | Tests |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| [`publiciste/`](../../05_deliverables_mvp/publiciste/README.md) | 2 (README `scaffold (Sprint 2)` · GAP §3.13) | **Pipeline site public** : `data_room/PXX/` → `projets_master.json` → `index.html` luxury (`lib/parser.py` extraction · `lib/validator.py` schéma · `lib/generator.py` + `lib/branding.py` rendu dark+doré `#0a0a12`/`#f0b429` · `templates/site_public.html.tmpl`) | `publiciste.py parse\|validate\|generate\|run` | `publiciste-tests` | 24 |
|
| [`publiciste/`](../../05_deliverables_mvp/publiciste/README.md) | 2 (README `scaffold (Sprint 2)` · GAP §3.13) | **Pipeline site public** : `data_room/PXX/` → `projets_master.json` → `index.html` luxury (`lib/parser.py` extraction · `lib/validator.py` schéma · `lib/generator.py` + `lib/branding.py` rendu dark+doré `#0a0a12`/`#f0b429` · `templates/site_public.html.tmpl`) | `publiciste.py parse\|validate\|generate\|run` | `publiciste-tests` | 26 |
|
||||||
|
|
||||||
Le contrat d'échange `projets_master.json` n'est **pas** défini ici mais par le Faisabilité Agent
|
Le contrat d'échange `projets_master.json` n'est **pas** défini ici mais par le Faisabilité Agent
|
||||||
([`../../05_deliverables_mvp/faisabilite/projets_master.schema.json`](../../05_deliverables_mvp/faisabilite/projets_master.schema.json),
|
([`../../05_deliverables_mvp/faisabilite/projets_master.schema.json`](../../05_deliverables_mvp/faisabilite/projets_master.schema.json),
|
||||||
|
|||||||
@@ -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`)
|
||||||
**24 suites gated · 629 tests · 612 passés · 0 échec · 0 erreur → verdict `PASS`.**
|
**24 suites gated · 635 tests · 618 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.
|
||||||
Les **17 tests restants sont ignorés** (`skipped`) : ce sont les *oracles optionnels*
|
Les **17 tests restants sont ignorés** (`skipped`) : ce sont les *oracles optionnels*
|
||||||
|
|||||||
@@ -0,0 +1,540 @@
|
|||||||
|
# Activity Log · 2026-08-12 · Claude Code DTP Worker
|
||||||
|
|
||||||
|
## Session `20260812_153203` — MÉTA-MAINTENANCE · réduction de l'**index de mémoire agent** au-dessus de sa limite de taille (cause-racine du churn de session), plutôt qu'un énième sweep « CLEAN » ou une re-currency du rapport (le motif que les ~10 sessions précédentes du jour ont elles-mêmes signalé)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Matrice stakeholder re-dérivée **ce jour** depuis les artefacts commités (aucun chiffre figé · #6) : **635 / 618 / 17**. Séquence moteur V18 **intégralement bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). `daily_reports/2026-08-12.md` vérifié **dé-épinglé du HEAD** (session `150201`) → aucune re-currency due. Registre des décisions : citations résolvent (session `123144`).
|
||||||
|
|
||||||
|
**Diagnostic — la vraie cause n'est pas un défaut de code, c'est un défaut d'outillage.** Le journal des ~10 dernières sessions du jour est saturé de deux motifs : (a) des *sweeps de vérification* « CLEAN, teeth-prouvé, NOT a gate » sur des axes de plus en plus marginaux, et (b) des *re-currency* du rapport stakeholder qui, en épinglant un HEAD, se re-périmaient à chaque commit (boucle enfin brisée en `150201`). Ces deux motifs sont la **conséquence** d'une saturation : la mémoire agent avait déjà consigné, pour la plupart de ces axes, « déjà balayé · by-design · ne pas re-flag » — mais l'**index de mémoire ne se chargeait que partiellement**. Le harnais l'a signalé explicitement ce jour : *« MEMORY.md is 29KB (limit: 24.4KB) — index entries are too long. Only part of it was loaded. »* Un index tronqué ⟹ des insights « NOT a gate / by-design / don't re-flag » invisibles ⟹ re-litige du même terrain ⟹ churn. **Corriger l'outil, pas surveiller le symptôme** (même principe structurel que le dé-épinglage du HEAD en `150201`).
|
||||||
|
|
||||||
|
**Action — compaction de l'index de mémoire (`memory/MEMORY.md`, hors dépôt, maintenance d'outillage).** L'index avait accumulé le **corps entier** de chaque mémoire dans sa ligne (jusqu'à **1040 caractères**/ligne ; guide = « une ligne sous ~200 »). Le détail vit déjà dans les fichiers-sujets `.md` (un par entrée) ; l'index ne doit porter qu'un **hook** de pertinence. Chaque ligne réécrite en hook concis, en **préservant le distinguo anti-churn** de chaque note (« NOT a gate », « by-design », « don't re-flag », le piège-clé). Résultat vérifié :
|
||||||
|
- Taille **30 117 → 17 264 octets** (sous la limite 24.4 Ko, avec marge → l'index se charge désormais **en entier**).
|
||||||
|
- **88 entrées d'index ↔ 88 fichiers-sujets** = **bijection prouvée** (script : chaque `.md` du dossier est référencé, chaque lien de l'index pointe un fichier existant). Une entrée initialement perdue à la réécriture (`link-label-target-mismatch`) **détectée par ce contrôle de bijection et restaurée** avant clôture — aucune mémoire perdue.
|
||||||
|
- Ligne la plus longue **1040 → 252** caractères.
|
||||||
|
|
||||||
|
**Pourquoi PAS un sweep / une currency / un fix / un gate.** (a) Aucun défaut de code : les sweeps du jour ont épuisé les axes réels ; en ré-ouvrir un serait le churn même que ce diagnostic identifie. (b) Aucune currency due : rapport dé-épinglé, registre à jour. (c) Aucun fix : V18 bloqué D-06 (#6), 0 défaut résiduel. (d) Aucun gate (#5) : l'index de mémoire est **hors dépôt** (outillage agent, non gaté par nature) ; le corriger est de la maintenance, pas une invariant de produit.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucun fichier de dépôt de production/gate touché). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers de **dépôt** : ce journal seul. Maintenance **hors dépôt** (mémoire agent) : `memory/MEMORY.md` compacté 30→17 Ko, bijection 88/88 restaurée.
|
||||||
|
|
||||||
|
## Session `20260812_150201` — STRUCTUREL · fin de la boucle de re-currency du rapport stakeholder — **dé-épinglage total du HEAD** dans le document (la session précédente n'avait ôté le hash figé que du *sous-titre* de la table roulante, en le **laissant** dans l'en-tête du document `HEAD attesté: 4a13557`) → la cause du churn est supprimée, pas surveillée
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 **intégralement bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||||
|
|
||||||
|
**Locus = la cause récurrente du churn de session, pas un énième sweep.** Le journal des ~8 dernières sessions du jour montre une **boucle** : chaque commit worker déplace le HEAD ⟹ l'en-tête du `daily_report` qui **atteste** « HEAD attesté `<hash>` » périme ⟹ la session suivante « réconcilie la currency » ⟹ nouveau commit ⟹ HEAD re-périmé, *ad infinitum*. La session `143156` a **diagnostiqué** ce mécanisme et converti la §« Jalons de vérification » en **table roulante** — mais n'a ôté le hash figé que du **sous-titre de section**, en **laissant** l'épinglage `**HEAD attesté** : \`4a13557\`` dans l'**en-tête du document** (lignes 9-13) + « portée au HEAD courant `4a13557` » dans la ligne `Session` + « portant l'arbre au HEAD courant `4a13557` » dans la trajectoire §État courant. Le fix était donc **partiel** : la cause survivait dans 4 emplacements, et *mon propre commit d'aujourd'hui* aurait re-périmé l'en-tête → churn perpétué.
|
||||||
|
|
||||||
|
**Correction structurelle (4 emplacements de HEAD figé neutralisés · document éditorial hors gate).**
|
||||||
|
- **En-tête `Session`/`HEAD attesté`** (l.3-13) — remplacé le bloc `**HEAD attesté** : \`4a13557\`` + la trace de hashes par un **« Point d'attestation »** explicite : *le rapport n'épingle volontairement AUCUN hash de HEAD* (un hash périme à chaque commit = cause structurelle de la boucle) ; le HEAD courant **se lit** (`git log -1`), il ne **se fige** pas ; ce qui **fait foi** est la **matrice** (`635/618/17 · PASS`, dérivée d'artefacts commités, stable jusqu'au prochain fix de correctness). Les hashes des §Jalons sont désormais qualifiés de **jalons historiques immuables**, pas d'attestation de HEAD courant.
|
||||||
|
- **§Contexte** (l.25-29) — « ont porté l'arbre au **HEAD courant** `ee88d36` » → passé historique « à l'**émission initiale**… avaient porté l'arbre à `ee88d36` » (framing origine, plus une prétention de HEAD courant).
|
||||||
|
- **Sous-titre §Jalons de vérification** — « → HEAD courant `4a13557` » → « depuis le 5ᵉ fix `1992ee6` · au fil de l'eau » (dé-épinglé).
|
||||||
|
- **Trajectoire §État courant** — « portant l'arbre au HEAD courant `4a13557` » → « trace dans la table roulante ci-dessus, non ré-épinglée à un hash ».
|
||||||
|
|
||||||
|
**Résultat.** Le rapport **ne peut plus périmer** au prochain commit worker : il n'affirme plus aucun HEAD mutable. La boucle de re-currency (≥ 6 sessions consommées) est **cassée à la racine**. Les hashes restants (Jalons tables + `35de616`/`ee88d36` de l'émission initiale) sont des **faits historiques** stables. Aucun chiffre inventé — matrice re-dérivée de l'artefact commité (#6).
|
||||||
|
|
||||||
|
**Pourquoi PAS un gate (#5).** Le rapport est un **document éditorial explicitement hors gate** (`ci/check_docs.sh:50` `continue` sur `daily_reports/*` · `ci/check_readme_claims.sh:8239` exclut le préfixe). Gater de la prose stakeholder serait fragile et de faible valeur ; le **durcissement structurel** (aucun hash mutable à surveiller) **supprime la cause** au lieu de poser une sentinelle sur elle — sœur exacte de la doctrine `verify-non-defects` / « supprime la cause » invoquée (à moitié) par la session précédente.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — 0 dépendance de gate sur le rapport, confirmé par grep sur `ci/`+`.gitea/`). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit** (matrice stable, byte-repro implicite), **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (4 dé-épinglages de HEAD) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_143156` — CURRENCY · rapport stakeholder du jour porté au HEAD courant `4a13557` (le canal s'arrêtait à `fb2d3e1` tout en attestant « HEAD courant ») + §« Jalons de vérification » convertie en **table roulante** pour briser la boucle de re-figeage du HEAD à chaque commit
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection couverture 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord → vrai locus = currency du canal stakeholder (classe `e15129b`).** Plutôt qu'un énième sweep « CLEAN sans fix » (saturation des axes déjà couverts, reconnue par les sessions précédentes), j'ai audité la **currency** du rapport `daily_reports/2026-08-12.md`. Il **atteste** en en-tête « **HEAD attesté** `fb2d3e1` » + « **au HEAD courant** », or **deux commits worker** ont suivi le même jour sans être remontés : `e15129b` (la currency `133154` elle-même) et `4a13557` (sweep FRAIS mode d'arrondi). `git log fb2d3e1..HEAD` = ces 2 commits ; `grep -c` de chacun dans le rapport = **0** avant édition (dérive confirmée). Note : `e15129b` a **créé** la §« Jalons » mais **sans s'y inscrire** (auto-omission) — d'où la boucle de churn observée : chaque commit de currency re-fige un HEAD qui périme au commit suivant.
|
||||||
|
|
||||||
|
**Correction (document éditorial · 4 blocs cohérents) :**
|
||||||
|
- **En-tête** — `Session` → `20260812_143156` ; `HEAD attesté` **`fb2d3e1` → `4a13557`** ; distinction claire 5ᵉ fix (`1992ee6`, matrice `634→635`) vs commits de vérif/currency byte-identiques suivants (matrice **stable 635/618/17**).
|
||||||
|
- **§« Jalons de vérification depuis la 4ᵉ vague »** — convertie en **table roulante** : bandeau explicite « tout commit post-5ᵉ-fix est **non opposable par construction** ; la matrice ne bougera qu'au prochain fix de correctness ; cette section liste ces jalons au fil de l'eau **sans re-figer le HEAD ailleurs** » → **brise structurellement la boucle de churn** (plus de hash épinglé dans l'intro). En-tête de section `→ HEAD courant fb2d3e1` → `4a13557`.
|
||||||
|
- **2 lignes de table ajoutées** — `e15129b` (currency, non opposable) + `4a13557` (sweep FRAIS arrondi lecture-seule, journal only, aucun mis-flag, non opposable). Compte « quatre » → « **six** » jalons.
|
||||||
|
- **§« État courant »** trajectoire — « 4 commits de vérification (`…/fb2d3e1`) » → « **six** commits de vérification / currency (`…/fb2d3e1`/`e15129b`/`4a13557`) portant l'arbre au HEAD courant `4a13557` ».
|
||||||
|
|
||||||
|
**Zéro invention (#6).** Tous les chiffres re-dérivés de l'artefact commité : `regression_run.json.totals` = **635/618/17 · PASS**. La nature de chaque jalon reprend verbatim son message de commit (`git show --stat` : `e15129b` = activity_log + daily_report ; `4a13557` = activity_log **seul**, sweep sans note in-situ). Aucune décision/verdict modifié ; D-01→D-08 inchangés.
|
||||||
|
|
||||||
|
**Pourquoi PAS un gate (#5).** Dérive de currency d'un document éditorial (le rapport = *surface, ne tranche pas*), comme `daily_reports` par nature ; un gate line-number/hash sur de la prose stakeholder serait fragile (chaque commit worker le rougirait) et de faible valeur. Le durcissement **structurel** appliqué (table roulante, plus de HEAD re-figé dans l'intro) vaut mieux qu'un gate : il **supprime la cause** du churn au lieu de la surveiller.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — le rapport est un document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit** (matrice stable, byte-repro implicite), **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (4 blocs de currency + table roulante) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_140156` — VÉRIF · nouvel axe FRAIS « mode d'arrondi (banquier / round-half-to-even) » balayé repo-wide → CLEAN teeth-prouvé, 0 défaut · replayable · sans fix · sans gate
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu de l'artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (24 suites vertes · PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · all PASS**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` **à jour au HEAD `e15129b`** (grep-count 5 hashes 4ᵉ-vague = 10 hits) → rien à ré-émettre.
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (angle jamais balayé en ~45 sweeps : le MODE D'ARRONDI).** Aucun des sweeps antérieurs (arithmétique dérivée, date/time, parsing money/percent, déterminisme hash/locale/TZ/ordre) n'avait attaqué la question « Python `round()` est round-half-to-**even** (banquier) — un site s'appuie-t-il implicitement sur round-half-**up** ? ». Un `.5` exact y renverrait une valeur qui diffère de l'attente naïve (`round(2.5)==2`, pas 3), byte-repro-aveugle si l'échantillon figé ne tombe pas sur la bande. Recensement des `round()` de production (hors `test`, hors `_round1`) = **8 sites** sur 4 modules : `finlib/gate.py:54,64,72,96` · `genlib/scorer.py:57,81` · `lib/parser.py:101` · `audit_4big/scoring.py:8` (docstring).
|
||||||
|
|
||||||
|
**Classement des 8 sites (tous NON-DÉFAUT, teeth-prouvé) :**
|
||||||
|
- **`scorer.py:57` `round(20*filled/total)`** (axe Complétude) — SEUL site `round()`-sur-ratio dont l'immunité au verdict est **non-locale**. Teeth : `total=16` champs 🔴 → la bande `.5` **EST atteignable** (filled∈{2,6,10,14} → 2.5/7.5/12.5/17.5, banquier diffère de half-up de ±1 pt). **Mais verdict-immune** : `_statut` n'émet « complete » que si `not champs_manquants` ⟹ `filled==total` ⟹ `pts1 == round(20) == 20` **exact** ; l'arrondi banquier ne s'engage donc **jamais** sur le chemin décisionnel. Reproduit end-to-end : brief complet → completude=20/statut=complete ; masterplan vidé → completude=11, 7 champs manquants, statut=en_developpement (**jamais** complete). Le seul effet de l'arrondi = ±1 pt sur le **score partiel affiché** d'un brief **incomplet** — cosmétique, aucun `spec` ne définit half-up, découplé du verdict, épinglé byte-repro. **Non-défaut.**
|
||||||
|
- **`scorer.py:81`** (axe Anti-gap prix) — même famille, immunité par **inclusion** : une cellule manquante ⟹ `champs2 ⊆ champs_manquants` non vide ⟹ verdict ≠ complete, indépendamment de l'arrondi de `pts2` (même si `pts2` pouvait remonter à 20 avec `filled/total ≥ 0.975`, la clause `not champs_manquants` bloque). **Non-défaut.**
|
||||||
|
- **`gate.py:64,72,96`** (montants USD, `round(...,2)`) — arrondi **au cent** = montant métier canonique (un USD ne se paie pas au sous-cent). L'arrondi banquier au sous-cent est immatériel : `ok = paid+1e-9 >= required` compare des valeurs déjà en cents. **Non-défaut.**
|
||||||
|
- **`gate.py:54` `_round1`** (percents user-facing) — la classe « affichage arrondi ⟺ gate booléen » est **déjà FERMÉE** ce cycle (gardes `:105`/agrégat des fixes `85cd625`/`1992ee6` + notes `113134`). **Non-défaut** (déjà couvert).
|
||||||
|
- **`parser.py:101` `int(round(n))`** — parse la **quantité** de typologies (entier métier : pas de demi-unité) → bande `.5` sans signification. **Non-défaut** (domaine entier).
|
||||||
|
- **`audit_4big/scoring.py:8`** — **docstring** (formule §), pas du code ; scoring réel prouvé immune par `ea8458d` (score-au-seuil = quantité UNIQUE). **Non-défaut.**
|
||||||
|
|
||||||
|
**Teeth du détecteur (prouvé mordant).** Confirmé au runtime que `round()` EST banquier (`round(2.5)=2 · round(3.5)=4 · round(0.5)=0`) — l'axe n'est pas un homme de paille ; ET que la bande `.5` est réellement atteignable sur `scorer.py:57`. Le CLEAN n'est donc pas un faux-vert : la propriété testée existe, elle est simplement **découplée du verdict** partout.
|
||||||
|
|
||||||
|
**Pourquoi PAS un fix ni un gate ni une note in-situ (#5/#6).** Aucun défaut (8/8 non-défauts). Passer à `Decimal(ROUND_HALF_UP)` serait de la sur-ingénierie sur un effet cosmétique ±1 pt inatteignable au verdict, + casserait le déterminisme byte-repro. **Pas de note in-situ non plus** (contrairement à `rate()`/`_cond_documents`) : le trigger de ces notes était un Explore ayant *mal-classé* le locus — ici **aucun mis-flag**, et sur-annoter violerait `verify-uncovered`/#5. Comme les axes de déterminisme (`run-order`/`hash-seed`/`locale-tz`), c'est une **commande replayable**, occurrence-zéro, pas une matière à gate.
|
||||||
|
|
||||||
|
**Impact d'artefact : ZÉRO.** Sweep en lecture seule (repro dans le module, aucun fichier tracké touché hors ce journal). `regression_run.json`/`quality_report.json` inchangés (635/618/17 · 24×100).
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre inventé** (#6 — tous lus des artefacts commités), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : ce journal (+ mémoire agent `rounding-mode-bankers-sweep`, hors repo).
|
||||||
|
|
||||||
|
## Session `20260812_130149` — VÉRIF · chasse au défaut sur axe FRAIS (date/time + parsing chaîne money/percent) → 3 candidats RÉFUTÉS comme non-défauts + 1 note anti-récurrence in-situ sur `rate()` (commentaire pur · 0 impact artefact)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu de l'artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`, citations réalignées au HEAD par `4eb615b`). Canal stakeholder `daily_reports/2026-08-12.md` **à jour** (matrice 635, 4ᵉ vague · les commits `626c39d`→`4eb615b` sont vérif/currency à 0 impact matrice) → rien à ré-émettre.
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (axe FRAIS jamais balayé : date/time + parsing de chaînes money/percent dans les `*lib/*.py`).** Explore ciblé (brief strict anti-faux-positif : rejeter défensif-seul + domaine-borné · mémoires `verify-non-defects`/`docstring-vs-code-drift`). 3 candidats remontés, **TOUS réfutés au raisonnement domaine-borné** :
|
||||||
|
- **#2 `finlib/gate.py:153`** (`signature_date` en test de véracité) — exigerait une donnée **corrompue** `signature_date="0"` (chaîne « 0 » vraie en Python). Non atteignable : une date de signature est une chaîne ISO ou `null`, jamais « 0 ». Test de véracité **correct** pour des entrées réelles. **Non-défaut.**
|
||||||
|
- **#3 `banclib/finance.py:_pct`** — n'accepte que la **constante canonique** `« 52 % »` (mémoire `fix-vs-gate-transitively-protected-constant` : `finance._pct(CANONICAL)`), aucun séparateur, aucun free-text. **Non-défaut** (entrée contrôlée).
|
||||||
|
- **#1 `commlib/finance.py:47` `rate()`** — le candidat « CONFIRMÉ » de l'agent (`rate("1.234,56 %")` → `None` car `replace(",",".")` fabrique « 1.234.56 »). **Réfuté** : un taux de commission est **borné 0-100** (doctrine écrite de `_rate_label` lignes 76-77), donc jamais ≥ 1000 → **aucun séparateur de milliers** ne peut apparaître. La virgule est donc toujours **décimale** ici (« 3,5 % » → 3.5 = le cas français **voulu et géré**). Même classe que le `:g` de `_rate_label` (deux pièges inatteignables sur domaine borné). **Non-défaut** — cohérent avec le CLEAN de la session `060044` sur ce même fichier.
|
||||||
|
|
||||||
|
**Action = 1 note anti-récurrence in-situ sur `rate()` (commentaire pur · sœur des notes `090114`/`113134`).** Fait nouveau motivant l'action : un **Explore capable vient de mal-classer `#1` en « CONFIRMED DEFECT »** — preuve que le locus est **re-flaggable**. La fonction de **parsing** `rate()` ne portait **aucune** note (sa sœur `_rate_label` en porte une, mais sur le formatage `:g`, pas sur le parse à la virgule). Passe unique : ajout d'un commentaire *accurate* (vérifié) documentant (a) l'acceptation de la virgule décimale française et (b) l'inatteignabilité du séparateur de milliers (taux borné 0-100, renvoi à `_rate_label`), + interdiction de « corriger » vers `locale.atof` (#5/#6). Convertit le faux-positif futur en « déjà revu, sûr ».
|
||||||
|
|
||||||
|
**Pourquoi PAS un fix de code ni un gate (#5/#6).** Aucun défaut réel (3/3 réfutés) : passer à `locale.atof` serait de la sur-ingénierie sur un cas inatteignable + casserait le déterminisme (dépendance locale — cf. `locale-tz-determinism-sweep`). Robustesse portée par le domaine borné + les suites (∈ régression gatée). Commentaire sans marqueur `# N ·` d'invariant (pas de déclenchement du gate ledger).
|
||||||
|
|
||||||
|
**Impact d'artefact : ZÉRO (prouvé byte-level).** Commentaire pur → aucun changement de sortie. Rebuild de `commissions/out/` dans `/tmp` (avant ET après édit) → `diff -rq` = **BYTE-IDENTICAL**. `regression_run.json`/`quality_report.json` **inchangés** (635/618/17 · 24×100). Aucun compteur de fiche/README à réaligner (pas de test ajouté).
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). `finance.py` **compile** + **NFC-clean** (vérifié) · suite commissions OK + `validate` 10 invariants verts. **0 code moteur V18** (bloqué #6), **0 fix de code** (3/3 candidats réfutés), **0 chiffre/fixture inventé** (#6), **0 gate ajouté** (#5), **0 artefact modifié/reconstruit** (byte-identique), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M crm/commissions/commlib/finance.py` (1 note anti-récurrence `rate()`) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_113134` — VÉRIF · fermeture de la classe « percent == 100 ⟺ ok » sur les 4 conditions du gate financement + durcissement anti-récurrence des 2 conditions à domaine borné (commentaires purs · 0 impact artefact)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu de l'artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` à jour (session `103119`, matrice 635).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (angle : la classe « barre de progression == 100 % ⟺ soumissible » est-elle vraiment fermée après les 2 fixes `093114`+`110124` ?) → classe PROUVÉE FERMÉE, 0 nouveau défaut.** Les deux bugs réels de ce cycle (`093114` per-condition apport, `110124` agrégat `overall_percent`) partagent une classe : un percent user-facing (barre de progression) qui pouvait afficher **100.0 tout en BLOQUANT**. J'ai balayé toutes les surfaces susceptibles de porter cette classe.
|
||||||
|
|
||||||
|
**Balayage cross-module (Grep sur les artefacts commités).** Un seul module émet un champ `percent`/`overall_percent`/`progress` dans un `out/*.json` commité : `crm/financement_bancaire/out/gate_status_example.json`. Les autres scorers examinés dérivent leur verdict **directement** de la note, sans divergence possible : `audit_4big/scoring.py` — `verdict = "PASS" if score >= pass_score` où `score` EST la note affichée (pas deux quantités indépendantes) ; `bancable` — le `pe_brut` (point d'équilibre en unités) n'est pas une barre de complétion. **La classe est donc confinée au seul gate financement.**
|
||||||
|
|
||||||
|
**Audit exhaustif des 4 conditions du gate + agrégat (invariant : `percent == 100.0 ⟺ ok`).**
|
||||||
|
- **`_cond_apport`** — ratio USD **continu**, plafonné explicitement « non ok → 99.9 » par le fix `093114` ✓
|
||||||
|
- **`overall_percent`** (agrégat) — moyenne de 4, plafonnée explicitement par le fix `110124` ✓
|
||||||
|
- **`_cond_validation_wag`** — percent exact `100.0 if ok else 0.0`, aucune divergence ✓
|
||||||
|
- **`_cond_documents`** / **`_cond_autorisations`** — `pct = round1(validated/total*100)` **SANS** garde de plafond. Vérifié **empiriquement** : le dénominateur est une liste du contrat (`financement_spec.json`) — **13** documents `is_required` (max) et **4** autorisations. Or `round1((total-1)/total*100)` ne remonte à `[99.95 %, 100 %[` par le bas qu'à partir de **total ≥ 2000** (première valeur calculée par script : 2000, car `(total-1)/total ≥ 0.9995`). Domaines **bornés à 13/4 ≪ 2000** → `ok=False ⟹ percent < 100` tient **déjà** sans garde. Sœur exacte des 3 sites `:g`-domaine-borné (`090114`). Direction inverse triviale : `ok=True ⟹ validated==total ⟹ percent=100.0` exact.
|
||||||
|
|
||||||
|
**Conclusion : classe FERMÉE.** Les 2 conditions à ratio potentiellement continu (apport, agrégat) portent une garde explicite prouvée par test à dents ∈ régression gatée ; les 2 à domaine borné (documents ≤13, autorisations =4) sont sûres par construction ; wag est exact. Aucune 3ᵉ instance latente.
|
||||||
|
|
||||||
|
**Action = durcissement anti-récurrence (commentaires purs · sœur de `090114` « twin fix must sweep ALL surfaces »).** Fait nouveau motivant l'action : `_cond_documents`/`_cond_autorisations` sont les **seules** des 4 conditions à **ne porter NI garde NI note** expliquant leur sûreté — un audit futur posant « l'invariant est-il fermé pour les 4 ? » les re-flaggerait (cycle gaspillé) ou y ajouterait un plafond **redondant** (`#5`), exactement le problème que `090114` a réglé pour les `:g`. Passe unique : ajout à chacune d'une **note de justification in-situ** (domaine borné ≤13/4 ≪ seuil 2000 → invariant préservé sans garde · renvoi aux fixes `093114`/`110124` · « ne pas ajouter de plafond redondant · #5 »). Convertit le faux-positif futur en « déjà revu, sûr ».
|
||||||
|
|
||||||
|
**Pourquoi PAS un fix de code ni un gate (#5/#6).** Aucun défaut réel : ajouter un plafond aux 2 conditions bornées serait du **code mort** (inatteignable) — pas un fix. Robustesse portée par le domaine borné + les 2 tests à dents des conditions continues ; rien de nouveau à gater (#5). Commentaires sans marqueur `# N ·` d'invariant (pas de déclenchement du gate ledger).
|
||||||
|
|
||||||
|
**Impact d'artefact : ZÉRO (prouvé byte-level).** Commentaires purs → aucun changement de sortie. Rebuild de `financement_bancaire/out/` dans `/tmp` → `diff -rq out /tmp` = **BYTE-IDENTICAL** (7 fichiers). `regression_run.json`/`quality_report.json` **inchangés** (635/618/17 · 24×100). Aucun compteur de fiche/README à réaligner (pas de test ajouté).
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). `gate.py` **compile** + **NFC-clean** (vérifié) · suite module 37/37 OK. **0 code moteur V18** (bloqué #6), **0 fix de code** (classe prouvée fermée), **0 chiffre/fixture inventé** (#6), **0 gate ajouté** (#5), **0 artefact modifié/reconstruit** (byte-identique), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M crm/financement_bancaire/finlib/gate.py` (2 notes anti-récurrence) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_110124` — FIX correctness · `financement_bancaire` — même bug que `093114`, un niveau au-dessus : la barre GLOBALE `overall_percent` pouvait afficher 100.0 tout en BLOQUANT (l'invariant per-condition ne s'était PAS propagé à l'agrégat)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu de l'artefact commité · #6) = **634 exécutés · 617 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` à jour au HEAD `85cd625` (session `100114`).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (angle : la robustesse de la classe « affichage percent == 100 ⟺ soumissible » est-elle vraiment fermée après `093114` ?) → 1 candidat CONFIRMÉ au byte-level, latent mais réel.** Le fix `093114` (`gate.py:105`) a rétabli l'invariant **`percent == 100.0 ⟺ ok`** *par condition* (`_cond_apport` plafonné à 99.9 quand non ok). J'ai vérifié si l'**agrégat** `overall_percent` (la **barre GLOBALE** du frontend, `gate_status:171`) héritait de l'invariant — **il ne l'héritait pas**.
|
||||||
|
|
||||||
|
**Le défaut (même classe, un cran plus haut).** `overall = _round1(Σ percents / 4)` sur les 4 conditions. Cas limite atteignable : apport plafonné à **99.9** (non ok, exactement l'état laissé par le fix `093114` p. ex. 19 999/20 000 USD) **+** les 3 autres conditions **ok à 100.0** (documents tous validés WAG, autorisations toutes signées, validation WAG faite) → moyenne `(99.9 + 300)/4 = 99.975` → `round1` = **100.0**. Résultat user-facing : `overall_percent: 100.0` **tout en** `can_submit: False` (403). La **barre de progression GLOBALE** — celle que le client regarde en premier — afficherait « 100 % » sur un dossier refusé. **Exactement** la même auto-contradiction que le fix per-condition `093114`, mais à l'agrégat que ce fix n'a pas couvert. Reproduit **end-to-end** via le vrai `gate_status` (dossier résident RD 100 000 USD, apport 19 999) : `overall_percent=100.0`, `can_submit=False`.
|
||||||
|
|
||||||
|
**Pourquoi byte-repro est aveugle (preuve que ça échappe aux gates).** `gate_status_example.json` commité est à `overall_percent: 39.1` (`can_submit: false`) — **hors bande**. `check_artifacts` (rebuild == commité) le reproduit à l'identique éternellement. Bug **latent** : correct sur l'échantillon figé, faux dès qu'un dossier réel atteint la configuration limite. Même veine que `093114` (per-condition), `073055` (`base×taux`), `080104` (commissions).
|
||||||
|
|
||||||
|
**Fix (chirurgical, symétrique au fix `093114`, byte-repro préservé).** Après le calcul de `overall`, garde `if not can_submit and overall >= 100.0: overall = 99.9`. Invariant d'affichage rétabli au niveau agrégat : **`overall_percent == 100.0 ⟺ can_submit`**. L'exemple à 39.1 % **inchangé** → `financement/out/` rebuild **BYTE-IDENTIQUE** (`diff -rq out/ /tmp` = IDENTICAL).
|
||||||
|
|
||||||
|
**Test à dents (prouvé mordant).** Nouveau `test_overall_percent_agrege_ninclut_pas_100pct` (frère de `test_apport_quasi_complet_ninclut_pas_100pct`, mais à l'agrégat) : dossier avec apport 19 999/20 000 + 3 autres conditions ok → assert `can_submit=False`, les 3 autres conditions `ok=True` (isolation du cas limite), `overall_percent < 100.0` (== 99.9). **Sans le fix** → `AssertionError: 100.0 not less than 100.0` (mordu, vérifié par mutation) ; **avec** → OK. Suite financement **36 → 37**.
|
||||||
|
|
||||||
|
**Cascade artefacts (test count +1).** Régénéré `regression_run.json` (**634→635 / 617→618**), `regression_plan.json` + `MANIFEST.json` (build), `quality_report.json` (audit_4big **en dernier** · mémoire `audit4big-rebuild-after-doc-edits` — PASS 24/24, min 100). Fiches gatées re-dérivées (`check_readme_claims`) : QA `634/617→635/618`, Backend `634→635`, CRM financement `36→37` (le « Total CRM » du trio est source séparée · financement suivi à part ligne 44 · convention `fiche-accuracy-audit-closed`). Canal stakeholder `daily_reports/2026-08-12.md` prolongé (4ᵉ vague · matrice 634→635 · mémoire `two-logging-channels`).
|
||||||
|
|
||||||
|
**Pourquoi pas un nouveau gate (#5).** L'invariant `overall_percent == 100 ⟺ can_submit` est désormais **porté par le code + prouvé par un test à dents** ∈ régression gatée — comme son frère per-condition `093114`. Un gate statique dédié serait redondant (`verify-uncovered` : la suite couvre, mutation confirmée). Origine unique, un seul site.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers édités **compilent** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **1 module produit corrigé** (bug latent user-facing), **artefacts autoritaires régénérés déterministes** (échantillon byte-identique), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**, **0 invention de chiffre** (#6 — tous re-dérivés des artefacts). Fichiers : `finlib/gate.py`, `tests/test_financement_bancaire.py`, `qa/regression/out/{regression_run,regression_plan,MANIFEST}.json`, `qa/audit_4big/out/quality_report.json`, fiches `03_agents/{qa,erpnext_backend,crm}/AGENT.md`, `daily_reports/2026-08-12.md` + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_103119` — VÉRIF · nouvel axe « bien-formation structurelle du HTML généré » (parseur réel + entrées adverses FR/ES) → CLEAN teeth-prouvé, replayable, sans gate
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu depuis l'artefact commité · #6) = **634 exécutés · 617 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` re-réconcilié au HEAD par la session `100114` (matrice 634) → à jour ; cette session est **vérification-seule à 0 impact matrice**, rien à ré-émettre.
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (angle FRAIS jamais balayé = bien-formation *structurelle* du HTML généré).** Les ~40 sweeps antérieurs ont couvert l'octet (`runtime-encoding`), la forme NFC, le déterminisme (hash/locale/TZ/ordre), les schémas (7 axes), l'arithmétique dérivée, etc. — mais **jamais** la question « le HTML émis par les générateurs est-il structurellement bien-formé (balises équilibrées, attributs non cassés) sous une entrée réaliste FR/ES ». Distinct de la classe déjà fixée en publiciste `070054`/`073055` (mauvais encodeur de *contexte*) : ici on vérifie la **structure de sortie** au parseur.
|
||||||
|
|
||||||
|
**Méthode (à dents, replayable).** (1) Explore ciblé sur tous les chemins générant du markup (`publiciste/lib/generator.py` — pages HTML ; `frontend/chat_otoia/chatlib/frappe.py` — `mount_html`). (2) Parsing réel de la sortie avec un détecteur `html.parser` maison suivant une **pile de balises** (gestion des éléments void), flaggant unclosed-EOF / misnest / stray-close. Exécuté sur :
|
||||||
|
- La **sortie site commitée** (`render_site` sur `fixtures/data_room`, 2 projets P01/P02) → **len 6084, 0 erreur**.
|
||||||
|
- Un **projet adverse** (free-text injecté dans tous les champs éditables : `nom="Résidence «L'Été» & <Cie>"`, `localisation` avec guillemets doubles, `positionnement` avec `<b>`/`"`/`'`, `typologies[0].nom="T2 «Prestige» & <balcon>"`, `inclus` avec `&`/`"`/`'`/`<sud>`) → **0 erreur** : `_esc` (= `html.escape(quote=True)`) neutralise correctement tout le texte et les attributs.
|
||||||
|
- Le chemin **hero CSS `url('…')`** (le locus des fix `070054`/`073055`) reconfirmé bien-formé en page complète.
|
||||||
|
|
||||||
|
**Teeth du détecteur (prouvé mordant, mémoire `module-suite-teeth-mutation`).** 3 échantillons délibérément mal-formés **doivent** être flaggés : `<div><span>x</span>` → `unclosed EOF: div` ; `<div><span>x</div></span>` → `misnest`+`stray` ; `<p>x</p></p>` → `stray </p>`. Et un bien-formé `<div><span>x</span></div>` → `NONE`. **4/4 conformes** → le détecteur mord, le CLEAN n'est pas un faux-vert.
|
||||||
|
|
||||||
|
**Résultat = CLEAN, 0 défaut.** `_esc` est appliqué à **100 %** des insertions texte/attribut de `generator.py` (vérifié lignes 60/66/82/114/117/118-124), et le contexte CSS `url()` combine `quote()` (URL) puis `_esc()` (attribut HTML) — deux-étages correct. `frappe.py mount_html` reste non-échappé mais ses 4 entrées sont **provablement des enums/constantes** (`persona="Amélie"` figé, `portail_key`∈{ventes,construction,achat,compta,direction}, `css_class`/`id_prefix` du spec contrôlé) — aucun chemin free-text ne l'atteint → non-défaut (déjà statué 2× · `verify-non-defects`, non re-flaggé).
|
||||||
|
|
||||||
|
**Pourquoi pas un fix ni un gate (#5/#6).** Aucun défaut trouvé (structure saine end-to-end, teeth-prouvé). Comme les axes de déterminisme (`hash-seed`/`locale-tz`/`run-order`) et d'encodage (`runtime-encoding`/`unicode-nfc`), c'est une **commande replayable**, occurrence-zéro, pas une matière à gate — la robustesse structurelle vit dans `_esc`/`quote` (déjà couverts par la suite publiciste ∈ régression gatée). Aucun chiffre inventé (#6 — tous lus des artefacts commités).
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — sweep en lecture seule, 0 artefact touché). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : ce journal (+ mémoire agent `html-wellformedness-sweep`, hors repo).
|
||||||
|
|
||||||
|
## Session `20260812_100114` — CANAL STAKEHOLDER · re-réconciliation `daily_reports/2026-08-12.md` au HEAD courant (`e7cc3c4` → `85cd625`) · remontée du 4ᵉ fix de correctness (matrice `633→634`)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu depuis l'artefact commité · #6) = **634 exécutés · 617 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (2 axes frais, jamais audités en profondeur) → 0 défaut, surfaces confirmées CLEAN.**
|
||||||
|
- **Axe computationnel (Explore ciblé sur les 8 surfaces les moins récemment auditées** : `rbac/apply_plan/aggregator.py` · `qa/audit_5d/{controls,artifacts}.py` · `seo/{hreflang,schemaorg}.py` · `legal/confotur/{rbac_scan,builder}.py` · `demo/scenarios/{evidence,render}.py`**).** Brief strict anti-faux-positif (rejeter défensif-seul + domaine-borné · mémoires `verify-non-defects`/`docstring-vs-code-drift`). **Aucun défaut réel** : formatage bornes correct, échappement contextuel sûr sur entrées contrôlées, gardes de division présentes, bornes d'index correctes (`evidence.py:88` `-len(obj) <= idx < len(obj)`), tri déterministe, 0 défaut fabriqué.
|
||||||
|
- **Axe i18n frais (`faisabilite/bancable/banclib/i18n.py`, jamais balayé).** Vérifié la **parité des clés** FR/EN/ES du dictionnaire `LABELS` (une clé présente dans une langue mais absente d'une autre lèverait `KeyError` au rendu, byte-repro-aveugle si non exercée) → **les 3 langues ont des jeux de clés strictement identiques** (`missing: ∅ · extra: ∅` pour fr/en/es). CLEAN.
|
||||||
|
|
||||||
|
**Vrai locus = currency du canal stakeholder (mémoire `two-logging-channels` : garder les DEUX canaux à jour, chaque chiffre sourcé d'un artefact commité).** Le rapport `daily_reports/2026-08-12.md` avait été réconcilié en session `083114` au HEAD `e7cc3c4` (matrice **633/616**). Depuis, **2 commits worker** ont avancé l'arbre à `85cd625`, dont **un fix de correctness à impact matrice** que le snapshot ne reflétait pas :
|
||||||
|
- `cb8069b` (`090114`) — notes anti-récurrence `:g`-domaine-borné (commentaires purs · **0 impact matrice**).
|
||||||
|
- `85cd625` (`093114`) — **fix `financement_bancaire/finlib/gate.py`** : un dossier dans la bande [99.95 %, 100 %[ (p. ex. 19 999/20 000 USD) affichait `percent: 100.0` (barre frontend « 100 % ✓ ») tout en **bloquant** la soumission (403). Fix = garde `if not ok and pct >= 100.0: pct = 99.9` → invariant `percent == 100.0 ⟺ ok` rétabli · test à dents · suite financement **35→36**, matrice **633/616 → 634/617**.
|
||||||
|
|
||||||
|
**Fix = re-réconciliation (pas ré-émission) du rapport du jour.** Le rapport daté `2026-08-12.md` **est** le snapshot du jour ; le re-réconcilier au HEAD courant est le contrat, pas un doublon (#5). Porté : header `HEAD e7cc3c4 → 85cd625` + session `100114`, nouvelle section « Jalon du jour · 3ᵉ vague (`e7cc3c4` → `85cd625`) » (table du fix gate financement + opposabilité), `État courant` re-titré au HEAD `85cd625` avec **tous les chiffres re-dérivés de l'artefact commité** (matrice **634/617/17** · PASS 24× · bijection 24↔24), trajectoire de matrice étendue (633 → **+1** au `85cd625` = 634), §Portée actualisé (commit intercalaire `cb8069b` noté sans impact).
|
||||||
|
|
||||||
|
**Pourquoi pas un fix ni un gate (#5/#6).** Aucun défaut trouvé (Explore CLEAN + parité i18n + tree cohérent) ; le seul « écart » était la **staleness du canal stakeholder** (canal éditorial daté par design, comme un snapshot — pas un artefact byte-gaté). L'actualiser EST le contrat de `two-logging-channels`, pas une matière à gate.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rapport = document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (tous lus depuis l'artefact commité · #6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (re-réconciliation au HEAD courant) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_093114` — FIX correctness · `financement_bancaire` gate — un dossier incomplet pouvait afficher `percent: 100.0` (barre pleine ✓) tout en BLOQUANT (bug latent, byte-repro-aveugle)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **633 ran · 616 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Séquence moteur V18 toujours **bloquée** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6) → **aucun code moteur légitime**. Canal stakeholder `daily_reports/2026-08-12.md` **à jour** au HEAD (matrice stable — `cb8069b` commentaires purs).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (vein correctness, comme 070054/073055/080104) → 1 candidat CONFIRMÉ au byte-level, latent mais réel.** Explore ciblé sur l'arithmétique/parsing des générateurs (arrondis · taux · bornes · division). Le candidat remonté — `crm/financement_bancaire/finlib/gate.py:97` `_cond_apport` — est un **vrai défaut de cohérence d'affichage** confirmé par trace concrète, pas un cas théorique.
|
||||||
|
|
||||||
|
**Le défaut.** `pct = _round1(min(100.0, paid/required*100))` arrondit à 1 décimale, tandis que `ok = paid + 1e-9 >= required` est un test **exact**. Un versement dans la bande **[99.95 %, 100 %[** (p. ex. **19 999 / 20 000 USD**, 99.995 %) **remonte à `percent: 100.0`** par l'arrondi, alors que `ok=False` et `remaining=1.0 USD`. Conséquences user-facing :
|
||||||
|
- La **barre de progression frontend** (`GET /api/hypotheque/dossier/{id}/gate-status`, contrat DIRECTIVE) afficherait **« 100 % ✓ »** tout en refusant la soumission (403).
|
||||||
|
- La **raison de blocage** (`can_submit_dossier`, ligne 187) lirait « Apport initial incomplet : … (reste 1 USD · **100.0 %**) » — auto-contradictoire.
|
||||||
|
- Seul `_cond_apport` est **atteignable** : les deux autres conditions à `percent` continu-arrondi (`_cond_documents`/`_cond_autorisations`) sont des **ratios entiers** `validated/total` sur de **petites listes** (13 docs · 4 autorisations) — atteindre la bande [99.95 %,100 %[ avec `ok` faux exigerait `total ≥ 2000`, **impossible** ; leur `percent==100 ⟺ ok` tient structurellement. Non touchées (pas de sur-ingénierie · #5).
|
||||||
|
|
||||||
|
**Pourquoi byte-repro est aveugle (preuve que ça échappe aux gates).** Le `dossier_exemple` commité est à **67.8 %** (29 500/43 500 USD) — **hors bande**. `gate_status_example.json` échoue donc jamais le bug, et `check_artifacts` (rebuild==commité) le reproduit à l'identique éternellement. C'est un **bug latent** : correct sur l'échantillon figé, faux dès qu'un dossier réel atterrit dans la bande. Même classe que les fixes 073055 (`base×taux` cassait sur montants réels) et 080104 (commissions).
|
||||||
|
|
||||||
|
**Fix (chirurgical, byte-repro préservé).** `gate.py` : extraction de `ok`, puis garde `if not ok and pct >= 100.0: pct = 99.9` (plus grande valeur 1-décimale strictement < 100 · convention `_round1`). Invariant d'affichage rétabli : **`percent == 100.0 ⟺ ok`**. L'exemple à 67.8 % **inchangé** → `gate_status_example.json`/`out/` rebuild **BYTE-IDENTIQUE** (vérifié `diff -rq out/ /tmp` = IDENTICAL).
|
||||||
|
|
||||||
|
**Test à dents (prouvé mordant).** Nouveau `test_apport_quasi_complet_ninclut_pas_100pct` : 19 999/20 000 → `ok=False`, `remaining≈1.0`, `percent < 100.0` (== 99.9), et la raison **ne contient pas « 100 »**. **Sans le fix** → `AssertionError: 100.0 not less than 100.0` (mordu) ; **avec** → OK. Suite financement **35 → 36**.
|
||||||
|
|
||||||
|
**Cascade artefacts (test count +1).** Régénéré `regression_run.json` (**633→634 / 616→617**), `regression_plan.json` + `MANIFEST.json` (build), `quality_report.json` (audit_4big **en dernier** · mémoire `audit4big-rebuild-after-doc-edits` — PASS 24/24 ≥95, min 100). Fiches gatées re-dérivées : QA `634/617`, Backend `634`, CRM financement `35→36` (le « Total CRM 83 » du **trio** est source séparée · financement suivi à part ligne 40 · inchangé).
|
||||||
|
|
||||||
|
**Pourquoi pas un nouveau gate (#5).** L'invariant `percent==100 ⟺ ok` est désormais **porté par le code + prouvé par un test à dents dans la suite** (∈ régression gatée). Un gate statique dédié serait redondant (`verify-uncovered` : la suite couvre déjà, mutation confirmée). Origine unique, un seul site atteignable.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. **0 code moteur V18** (bloqué #6), **1 module produit corrigé** (bug latent user-facing), **artefacts autoritaires régénérés déterministes** (échantillon byte-identique), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**, **0 invention de chiffre** (#6 — tous re-dérivés des artefacts). Fichiers : `finlib/gate.py`, `tests/test_financement_bancaire.py`, `qa/regression/out/{regression_run,regression_plan,MANIFEST}.json`, `qa/audit_4big/out/quality_report.json`, fiches `03_agents/{qa,erpnext_backend,crm}/AGENT.md` + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_040034` — CLEANUP #5 · suppression d'un orphelin `out_fresh/` (doublon d'artefacts commité par accident, échappe à TOUS les gates)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 ran · 612 passés · 17 skippés · 0 échec** (PASS). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (directive `:90-91` « NE PAS coder avant l'audit » · D-06 approbation audit = gate d'entrée · #6) → **aucun code moteur légitime**. Dernier commit `031b5e7` = PNG `DASHBOARD_LUXURY_MODELE_REFERENCE` ajouté par Michel (référence design, hors production repo).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (2 axes) → 1 candidat écarté comme NON-DÉFAUT (refus de fabriquer · #6).** Explore very-thorough sur les modules produit hors `audit_4big`/`regression`/`publiciste` (déjà balayés). Le seul candidat remonté « confirmé » — `faisabilite/generator/genlib/model.py:104` `is_filled()` exigeant `"{{" in text and "}}" in text` pour classer un placeholder — a été **réfuté au byte-level** : le renderer (`renderer.py:23-42`) n'émet **jamais** que des placeholders `{{...}}` **complets** (deux accolades), `brief_incomplete.json` n'utilise **aucune** accolade pour marquer l'absence (il utilise `_PLACEHOLDER_ABSENT` : « tbd »/« todo »/… + champs omis), et **aucune** occurrence mono-accolade n'existe dans tout le module. Exiger les deux accolades est donc le choix **conservateur correct** (flaguer un `}}` isolé dans de la prose réelle serait un faux positif). L'Explore s'était sur-engagé sur un cas théorique inatteignable → **non touché** (`verify-non-defects`).
|
||||||
|
|
||||||
|
**Vrai locus trouvé = un ORPHELIN ACCUMULÉ (violation #5 « JAMAIS accumuler doublons »), pas un bug.** `05_deliverables_mvp/frontend/portails/out_fresh/` (2 fichiers : `workspace.json` + `MANIFEST.json`) est :
|
||||||
|
- **Tracké** (`git ls-files` le confirme) — mais commité **une seule fois**, dans la session auto `f954cc1` du **2026-08-02** (scratch-dir de comparaison « fresh rebuild » commité par accident).
|
||||||
|
- **Byte-identique** à `out/` (diff `json.tool` = IDENTICAL content).
|
||||||
|
- **Produit par rien** : le générateur `workspaces_gen.py` écrit **uniquement** dans `out/` (`_DEFAULT_OUT = os.path.join(_HERE, "out")` · `-o` défaut `./out`) — jamais `out_fresh`.
|
||||||
|
- **Référencé nulle part** : `git grep out_fresh` sur tout l'arbre tracké (hors le dossier lui-même) = **ZÉRO** (ni code, ni doc, ni CI, ni `.gitignore`, ni test).
|
||||||
|
|
||||||
|
**Pourquoi c'est un vrai défaut et pas un artefact by-design (preuve qu'il échappe à tout gate).** `check_artifacts.sh` découvre les cibles par `find -type d -name out` — un match **exact** sur `out`, donc `out_fresh` **n'entre dans aucun gate de reproductibilité**. C'est précisément pourquoi il a survécu au balayage `reverse-bijection-artifact-sweep` (qui ne vise que les répertoires `out/`). Conséquence : c'est une **bombe à retardement de staleness** — rien ne le régénère, donc dès que la source du générateur bouge, `out/` se met à jour tandis que `out_fresh/` **diverge silencieusement** en une copie **stale, non-possédée et trompeuse** (« laquelle fait autorité ? »). Dead weight ungated. C'est l'unique scratch-dir du genre dans tout le repo (`find` pour `out_new`/`out_old`/`out_tmp`/`out_bak`/`out_fresh` → seul `out_fresh` existe · occurrence isolée).
|
||||||
|
|
||||||
|
**Fix = suppression (`git rm -r out_fresh/`).** Sûr (réversible via l'historique git), et **je ne l'ai pas créé** (règle « avant de supprimer, regarde la cible » : contenu byte-identique à la copie autoritaire, aucun consommateur). La copie autoritaire `out/` est **intacte et se régénère BYTE-IDENTIQUE** (rebuild `-o /tmp` vs `out/` = identique, vérifié après suppression).
|
||||||
|
|
||||||
|
**Pourquoi pas un nouveau gate (#5).** Occurrence **isolée** (origine unique `f954cc1`, un seul module). Ajouter un gate « aucun répertoire `out_*` non-`out` » serait de la sur-ingénierie pour un événement-zéro. La discipline existante (générateur écrit dans `out/` · `.gitignore` documente que `out/` est commité) est le contrat ; l'orphelin était une déviation ponctuelle, corrigée à la source.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rien ne dépendait de `out_fresh/`). **0 code moteur V18** (bloqué #6), **0 module de production touché** (seul un doublon orphelin supprimé · char-class `model.py` laissée intacte car non-défaut), **0 artefact autoritaire modifié**, **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `- 05_deliverables_mvp/frontend/portails/out_fresh/{workspace,MANIFEST}.json` (supprimés) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_043034` — CANAL STAKEHOLDER · actualisation `daily_reports` au HEAD courant (`35de616` → `ee88d36`)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 ran · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 ok**. Séquence moteur V18 toujours **bloquée** en gouvernance (audit préalable produit mais non validé · D-06 = gate d'entrée · #6) → **aucun code moteur légitime**.
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (2 axes) → tree cohérent, aucun défaut candidat.** (1) Vérifié que les chiffres cités dans le journal `20260812_040034` (629/612/17) sont **byte-cohérents** avec le `regression_run.json` commité (identiques). (2) Vérifié que `README.md` **ne fige aucun total de tests** (il pointe l'artefact byte-gaté comme « autorité », prose « 24 suites gated / 24 modules » exacte vs `quality_report.json`) → **0 drift numérique README**.
|
||||||
|
|
||||||
|
**Vrai locus = currency du canal stakeholder (mémoire `two-logging-channels` : garder les DEUX canaux à jour, chaque chiffre sourcé d'un artefact commité).** Le dernier rapport `daily_reports/2026-08-11.md` était épinglé à `35de616` (currency `20260811_092842`) et citait la régression à **628/611**. Depuis, **3 commits** ont avancé l'arbre à `ee88d36`, et la matrice a **divergé d'un incrément** :
|
||||||
|
- `39def91` (`[DTP-Worker 20260811_095844] Auto exec`, **postérieur** à la currency du rapport 08-11) a ajouté un test verrou **NBSP U+00A0** dans `publiciste/tests/test_publiciste.py` (+15 lignes) → régression **628→629 / 611→612** + `regression_plan/run/MANIFEST` + `quality_report` re-générés du même coup. Le rapport 08-11 **ne le couvre pas** → chiffre `628/611` désormais **stale**.
|
||||||
|
- `031b5e7` = PNG `DASHBOARD_LUXURY_MODELE_REFERENCE` ajouté par Michel (asset de référence design, hors production repo, root-owned).
|
||||||
|
- `ee88d36` = jalon worker de la session précédente de ce jour (suppression orphelin `out_fresh/` · déjà journalisée ci-dessus).
|
||||||
|
|
||||||
|
**Fix = émission de `daily_reports/2026-08-12.md`.** Porte le canal `35de616 → ee88d36`, **re-dérive tous les chiffres depuis les artefacts commités** (jamais figés · #6) : gate 33/0/0, régression **629/612/17 PASS**, audit 4Big **PASS 24× 100/100**, bijection **24↔24 ok**. Attribue explicitement la seule évolution de matrice (628→629) à `39def91`, note que `031b5e7`/`ee88d36` **ne touchent aucun test** (matrice stable). Reprend le §« Chemin critique bloqué » (D-06/D-07/D-01 en attente Michel) + escalation.
|
||||||
|
|
||||||
|
**Pourquoi pas un nouveau gate (#5).** Aucun défaut trouvé : le tree est cohérent, le seul « écart » était la **staleness du canal stakeholder** (canal éditorial daté par design, comme un snapshot — pas un artefact byte-gaté). L'actualiser EST le contrat de la mémoire `two-logging-channels`, pas une matière à gate.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rapport = document, 0 dépendance). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `+ 05_deliverables_mvp/daily_reports/2026-08-12.md` (nouveau rapport stakeholder) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_050034` — VÉRIF · nouvel axe de déterminisme « ordre d'exécution des suites » (balayage CLEAN, replayable, sans gate)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 ran · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Moteur V18 toujours **bloqué** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6). Canal stakeholder `daily_reports/2026-08-12.md` **déjà à jour** au HEAD `ee88d36` (session précédente `043034`) → rien à ré-émettre.
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (2 axes) → 1 candidat Explore RÉFUTÉ comme non-défaut, tree propre.**
|
||||||
|
- **Axe prose/drift** (Explore very-thorough sur `daily_reports/*.md`, `04_roadmap/*.md`, `03_agents/**/AGENT.md`, liens Markdown). Le seul candidat « confirmé » — `03_agents/mobile/AGENT.md:81` « upgrade 54 tenté puis abandonné » vs `mobile_spec.json` `expo_sdk_major: 54` — a été **réfuté** par la mémoire `mobile-runtime-actual-vs-rebuild-target` **et** relecture byte : la ligne distingue explicitement le **runtime ACTUEL** (Expo 51, tentative 54 abandonnée le 2026-07-27) de la **cible de rebuild** (Expo 54 · S5). Le `54` du spec correspond exactement à « rebuild cible Expo 54 (S5) ». **Interne cohérent → non touché** (`verify-non-defects`).
|
||||||
|
- **Axe drift numérique README/artefacts** : déjà gaté (`check_readme_claims` ∈ run_ci = vert). 0 écart.
|
||||||
|
|
||||||
|
**Vrai locus = un axe de déterminisme JAMAIS balayé — l'ORDRE D'EXÉCUTION des 24 suites** (4ᵉ axe, frères : `hash-seed-determinism-sweep`, `locale-tz-determinism-sweep`, `forward-compat-warnings-sweep` — **aucun** ne varie l'ordre des suites). Hypothèse de risque : un couplage inter-suites (état global partagé, scratch `/tmp` à nom fixe, cwd partagé) rendrait la matrice `regression_run.json` dépendante de l'ordre → RED intermittent sur le runner.
|
||||||
|
|
||||||
|
**Expérience (replayable).** Monkeypatch `reglib.discovery.discover_suites` → `reversed(...)`, ré-exécution `builder.run_matrix(spec)`, diff par-suite (clé = `id`) + totaux vs `out/regression_run.json` commité :
|
||||||
|
- Totaux **byte-identiques** : 629 ran / 612 passés / 17 skippés / 0 échec.
|
||||||
|
- **24/24 suites** : tuples `(ran,passed,failures,errors,skipped,ok)` **identiques**. `PER-SUITE DIFFS: NONE`.
|
||||||
|
|
||||||
|
**Pourquoi c'est fermé par construction (teeth, pas chance).** `runner.run_suite` (`reglib/runner.py:98`) lance **chaque suite dans son propre subprocess** avec `cwd=abs_path` (le dossier de la suite) — aucun état module in-process partagé, aucun cwd partagé ; l'agrégation dans `run_matrix` est une **somme indépendante de l'ordre**. Le couplage inter-suites est donc empêché *by design* ; le run inversé est une **confirmation teeth**, pas une découverte. Mémoire : `run-order-determinism-sweep`.
|
||||||
|
|
||||||
|
**Pourquoi pas un nouveau gate (#5).** Résultat CLEAN, occurrence-zéro, l'isolation subprocess est déjà le contrat architectural. Comme les 3 autres axes de déterminisme, c'est une **commande replayable**, pas une matière à gate. Aucun chiffre inventé (#6 — tous lus depuis l'artefact commité).
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — expérience en lecture seule sur `/tmp`, 0 artefact touché). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié**, **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : ce journal (+ mémoire agent `run-order-determinism-sweep`, hors repo).
|
||||||
|
|
||||||
|
## Session `20260812_053034` — VÉRIF (nouvel angle i18n) · asymétrie de cardinalité SEO per-langue `fr=87·en=87·es=84` **prouvée NON-DÉFAUT** · note explicative au point de dedup (anti re-flag, sans gate)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629/612/17** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Moteur V18 toujours **bloqué** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6). Canal stakeholder `daily_reports/2026-08-12.md` **déjà à jour** au HEAD (session `043034`) → rien à ré-émettre.
|
||||||
|
|
||||||
|
**Angle FRAIS (jamais balayé) = cohérence tri-lingue FR/EN/ES du module `seo/`.** Aucun des ~40 balayages antérieurs n'avait attaqué l'i18n des mots-clés. Un Explore very-thorough a remonté un candidat « défaut confirmé » : `out/seo_keywords.json` = **fr 87 · en 87 · es 84**, l'espagnol « manquant » de 3 entrées projet P01 (generic/residence/appartement). J'ai **refusé de le prendre au mot** (#6, `verify-non-defects`) et **prouvé au byte-level que c'est un NON-DÉFAUT** — l'artefact étant byte-repro-gaté, tout écart reflète la LOGIQUE du générateur, pas une faute de saisie.
|
||||||
|
|
||||||
|
**Preuve (reproduction du générateur sans dedup, tracé collision→terme-gardé).** La génération est **symétrique par langue** (`keywords.py:58` `for lang in langs`) → **raw = 115/langue** identique. L'écart vient **uniquement** du dedup `(lang, term)` (`keywords.py:106-113`, « conserver la 1ʳᵉ occurrence ») :
|
||||||
|
- `country[es]` = **« Republica Dominicana »** est **exactement** la `localisation` générique de **5 projets** (P01/P02/P03/P06/P09 dans `fixtures/projets_master.json`). En espagnol, le mot-clé **global** `{type} Republica Dominicana` (émis avant les projets) est **byte-identique** au mot-clé **projet** `{type} {loc}` de P01 → la 2ᵉ occurrence (P01) est dédupliquée. Le terme **reste présent une fois**, en scope `global` : **rien n'est perdu**.
|
||||||
|
- En **fr/en**, `country` = « Republique Dominicaine » / « Dominican Republic » **≠** la localisation → pas de collision global↔projet ; P01 garde ses 7 termes et ce sont P02/03/06/09 (même loc) qui percutent P01. D'où **3 collisions global↔projet de plus en es** → 87 vs 84.
|
||||||
|
|
||||||
|
**Conclusion : sortie CORRECTE.** Chaque chaîne unique existe **exactement une fois** ; les invariants ne contraignent que `≥ min_keywords_per_lang` (50) + unicité `(term, lang)` (`seo_gen.py:88-98`, `test_seo.py:94-100`) — **jamais** l'égalité inter-langues, et à raison : forcer une symétrie ré-émettrait un **doublon strict**. Le chiffre `es=84` est d'ailleurs **déjà documenté et gaté** dans `README.md:15` (`258 (fr=87·en=87·es=84)`, re-calculé par `check_readme_claims`) → aucun drift, aucun « manquant ».
|
||||||
|
|
||||||
|
**Livrable = une NOTE explicative au point de dedup (`keywords.py:106`).** L'asymétrie est subtile et **non-évidente** : un Explore capable a failli la mal-classer en bug, et un futur balayage i18n refera l'enquête. J'ai ajouté un commentaire *accurate* (vérifié) expliquant la cause (coïncidence `country[es]` == localisation) et l'interdiction de « corriger » vers une symétrie. **Aucune** logique/fixture/claim-gaté touché (les gates byte visent `out/*.json`, pas la source `keywords.py`). Style aligné : ASCII sans accents + em-dash `—` U+2014 (convention existante lignes 5-6), NFC-clean.
|
||||||
|
|
||||||
|
**Pourquoi pas un fix ni un gate (#5/#6/`verify-non-defects`).** (1) Pas de fix logique : changer le tie-break dedup (préférer projet vs global) altèrerait l'artefact byte-gaté + le claim README pour « corriger » un comportement correct. (2) Pas de fix fixture : compléter la localisation générique des 5 projets serait **inventer** une donnée (#6) sans source. (3) Pas de gate d'égalité per-langue : ce serait une **fausse contrainte** (l'asymétrie est linguistiquement légitime) — le seuil `≥ min` par langue est déjà gaté et suffit.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — commentaire pur, artefact `seo/out/*.json` re-build byte-identique via `check_artifacts` ∈ run_ci). **0 code moteur V18** (bloqué #6), **0 logique de production modifiée** (commentaire seul), **0 artefact autoritaire modifié**, **0 fixture touchée** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/seo/seolib/keywords.py` (note by-design au dedup) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_060044` — HARDENING correctness · `criteria.py` — le 3ᵉ détecteur regex (le SEUL non durci) de la famille « détecter la forme, pas une graphie »
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629/612/17** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Moteur V18 toujours **bloqué** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6) → **aucun code moteur légitime**. Canal stakeholder `daily_reports/2026-08-12.md` déjà à jour au HEAD (session `043034`).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (3 axes computationnels, jamais audités en profondeur) → 2 modules confirmés CLEAN, 1 vrai locus de robustesse trouvé.**
|
||||||
|
- **`crm/commissions/comlib/finance.py`** (calcul commissions `montant = base × taux`) : logique **correcte** — `rate()` normalise nombre/« 3.5 % »/« 3,5 % » en fraction, `taux_pct` de sortie = fraction cohérente avec le libellé de formule, opérande manquant → `None` (jamais 0-inventé). **Non touché.**
|
||||||
|
- **`faisabilite/bancable/banclib/finance.py`** (cœur break-even, zone de risque D-07) : **correct** — point d'équilibre `⌈52 % × total_unites⌉` avec `52 %` re-dérivé du canonique #9 (jamais codé en dur, cf. fix `448c310`), agrégats catalogue `Σ(qté × prix)` gardés sur `all(... is not None)`, formule publiée avec chaque valeur. **Non touché.**
|
||||||
|
- **Vrai locus = `qa/audit_4big/q4lib/criteria.py:25`** — voir ci-dessous.
|
||||||
|
|
||||||
|
**Le locus : le 3ᵉ détecteur regex non durci d'une famille dont les 2 autres l'ont été.** Ce fichier est la source de **TROIS** fixes de correctness confirmés de la classe « détecter la **forme**, pas une **graphie** » : `ac595d0` (regex ASCII vs `\w` Unicode → test accentué sous-compté), `fd99362` (garde `__main__` littérale double-quotée → simple-quotée/Yoda/sans-espace loupées), `35de616` (`def ` figé vs `def\s+` de la source sœur byte-gatée → `def test_x(` loupé). Les deux détecteurs `_TEST_DEF_RE` (`def\s+…\w`) et `_MAIN_GUARD_RE` (`\s*==\s*`, deux styles de guillemets, ordre Yoda) sont explicitement durcis pour tolérer tout **Python valide** (doctrine articulée lignes 18-30). **Restait `_ADD_PARSER_RE`** — le 3ᵉ frère — figé sur `add_parser\(|add_subparsers\(` : il exige la parenthèse **collée** au nom de méthode, donc `add_subparsers ()` / `add_parser ("build")` (espace avant `(` — Python valide, PEP 8 déconseille sans l'imposer) sont **silencieusement loupés** → sous-scorage `−20 pts` du critère CLI d'un module sain, exactement le symptôme de `fd99362`.
|
||||||
|
|
||||||
|
**Preuve de dents (avant fix).** Ancien regex : `p.add_subparsers ()` → **False**, `sub.add_parser ("x")` → **False** ; nouveau `add_parser\s*\(|add_subparsers\s*\(` → **True** sur ces deux formes **et** True sur les formes normales `add_parser("build")` / `add_subparsers(dest=…)` (aucune régression de couverture).
|
||||||
|
|
||||||
|
**Pourquoi c'est un durcissement légitime et non une invention (#6).** (1) **Classe déjà jugée réelle 3×** dans CE fichier ; les 2 frères sont durcis, le 3ᵉ était l'anomalie — j'harmonise vers la doctrine **écrite du fichier lui-même** (« détecter la forme, pas une graphie »), pas une convention externe. (2) **Précédent direct** : `fd99362`/`35de616` ont durci pour des variantes de Python valide **pas nécessairement présentes** dans un module courant (guillemets simples, Yoda, `def\t`). (3) **Fix latent, artefact byte-identique** : `grep -rE 'add_(parser|subparsers)[ \t]+\('` sur tous les `.py` = **AUCUN** module n'utilise l'espace aujourd'hui → aucun re-scorage, `quality_report.json` re-build **byte-identique** (vérifié par `check_artifacts` ∈ run_ci, vert).
|
||||||
|
|
||||||
|
**Pourquoi pas un nouveau gate (#5).** Ce n'est pas une classe de drift à surveiller mais un durcissement one-shot d'un détecteur ; la robustesse est dans le code du critère, pas dans un gate. La famille des 3 détecteurs est désormais **cohérente end-to-end**.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). Fichier édité **compile** + **NFC-clean** (accents intentionnels, style aligné lignes 18-30). **0 code moteur V18** (bloqué #6), **0 artefact autoritaire modifié** (byte-identique), **0 fixture/chiffre inventé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/qa/audit_4big/q4lib/criteria.py` (regex `_ADD_PARSER_RE` durci + commentaire doctrine) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_063050` — CANAL STAKEHOLDER · complétion du rapport `daily_reports/2026-08-12.md` au HEAD du jour (`ee88d36` → `37c68ac`)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu depuis l'artefact commité, #6) = **629 exécutés · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Tous les arbitrages produit D-01→D-08 restent en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (2 axes, correctness fiscale jamais auditée en profondeur) → 0 défaut, modules confirmés CLEAN.**
|
||||||
|
- **`fiscal/ecf_dgii/ecflib/ncf.py`** (composition e-NCF DGII, Ley 32-23) : logique **correcte** — `ENCF_RE` = `E + tipo(2) + secuencia(10)` = 13 car. ; `compose_encf` publie sa formule, retourne `e_ncf: None` quand un opérande manque (jamais 0/valeur fabriquée · #6), `_secuencia` zero-padde `[0-9]{1,10}` → 10 chiffres sans jamais inventer de séquence. `is_valid_encf`/`parse_encf`/`compose_encf` cohérents (une valeur composée re-valide toujours). **Non touché.**
|
||||||
|
- **`fiscal/ecf_dgii/ecflib/builder.py`** (plan de config e-CF) : **correct** — aucun chiffre OTO fixé (RNC/ITBIS/TipoCambio/endpoints Compupar restent `null · a_confirmer` par design #6), `_a_confirmer_count` déterministe, tri stable partout → bundle byte-repro. **Non touché.** (`is_filled` partagé re-vérifié = choix conservateur correct, cf. session `040034`.)
|
||||||
|
|
||||||
|
**Vrai locus = currency du canal stakeholder (mémoire `two-logging-channels` : garder les DEUX canaux à jour, chaque chiffre sourcé d'un artefact commité).** Le rapport `daily_reports/2026-08-12.md` avait été émis en session `043034` au HEAD `ee88d36`. Depuis, **3 commits worker du même jour** ont avancé l'arbre à `37c68ac` sans que le snapshot stakeholder ne les reflète :
|
||||||
|
- `a17d86a` (`050034`) — 4ᵉ axe de déterminisme « ordre d'exécution des 24 suites » (CLEAN, replayable, sans gate).
|
||||||
|
- `1513e69` (`053034`) — vérif i18n SEO : asymétrie `fr=87·en=87·es=84` **prouvée NON-DÉFAUT** + note by-design au dedup.
|
||||||
|
- `37c68ac` (`060044`) — hardening `criteria.py` (3ᵉ détecteur regex durci de la famille « détecter la forme, pas une graphie »).
|
||||||
|
|
||||||
|
Les trois sont des travaux de vérif/hardening à **artefacts byte-identiques** (confirmé par `check_artifacts` ∈ `run_ci` vert) → **matrice stable à `629/612/17`**.
|
||||||
|
|
||||||
|
**Fix = complétion (pas ré-émission) du rapport du jour.** Le rapport daté `2026-08-12.md` **est** le snapshot du jour ; le compléter en fin de journée pour couvrir tout le travail du jour est le contrat, pas un doublon (#5). Porté : header `HEAD ee88d36 → 37c68ac` + session `063050`, nouvelle section « Jalons du jour depuis l'émission initiale (`ee88d36` → `37c68ac`) » (table des 3 commits + opposabilité au merge), `État courant` re-titré au HEAD `37c68ac` avec **tous les chiffres re-dérivés des artefacts commités** (629/612/17 · PASS 24× · bijection 24↔24 — inchangés, byte-identiques), note de stabilité de matrice étendue aux 3 jalons, §Portée actualisé.
|
||||||
|
|
||||||
|
**Pourquoi pas un fix ni un gate (#5/#6).** Aucun défaut trouvé : le tree est cohérent, le seul « écart » était la **staleness du canal stakeholder** (canal éditorial daté par design, comme un snapshot — pas un artefact byte-gaté). L'actualiser EST le contrat de `two-logging-channels`, pas une matière à gate.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rapport = document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (tous lus depuis l'artefact commité · #6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (complétion au HEAD du jour) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_070054` — FIX correctness (bug latent réel) · rendu HTML publiciste : URL de rendu percent-encodée dans le `url('…')` CSS (apostrophe/espace/accent cassait silencieusement le hero) + test de régression à dents
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 exécutés · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Tous les arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (large, 12 fichiers cœur / 7 modules jamais ré-audités ce cycle) → 11 CLEAN, 1 BUG LATENT RÉEL trouvé.** Un Explore ciblé sur `workflow_vente`, `dossier_vente`, `financement_bancaire` (gate+builder), `faisabilite/generator` (scorer+model), `seo` (schemaorg+hreflang+builder), `mobile`, `pie` a confirmé **0 défaut** (l'`epsilon 1e-9` du gate apport = tolérance flottante correcte, non-défaut). Puis audit manuel du **rendu HTML publiciste** (`generator.py`) — angle **output-encoding jamais balayé** (distinct des sweeps NFC/encoding octet).
|
||||||
|
|
||||||
|
**Le bug (rendu-correctness, entrée réaliste FR/ES).** `_hero_media` (`generator.py:92`) composait `src = f"/static/projets/{code}/{hero['fichier']}"` puis l'injectait dans `style="background-image:url('{_esc(src)}')"`. `_esc` (= `html.escape`) est le **mauvais encodeur pour un contexte CSS** : un nom de rendu réel — **« Vue d'ensemble.png »**, entièrement plausible dans un projet francophone en RD — devient `Vue d'ensemble.png`, que le navigateur **HTML-décode d'abord** en `Vue d'ensemble.png` ; l'apostrophe **referme alors la chaîne `url('…')` CSS** → `background-image` invalide → **le hero ne s'affiche pas, silencieusement**. Reproduit au byte-level (`html.unescape` de la sortie le prouve).
|
||||||
|
|
||||||
|
**Preuve de dents.** Nouveau test `test_hero_media_url_percent_encode` (`test_publiciste.py`) : rendu avec `fichier="Vue d'ensemble.png"` → assert la présence de `Vue%20d%27ensemble.png` **et** l'absence de l'apostrophe brute dans le `url()` HTML-décodé. Vérifié **teeth** : en revenant à `{hero['fichier']}` sans `quote`, le test **ÉCHOUE** ; avec le fix, **PASS**.
|
||||||
|
|
||||||
|
**Le fix (correct, sans dérive).** `src = f".../{quote(hero['fichier'])}"` (`urllib.parse.quote`) — percent-encode le segment de fichier : `'`→`%27`, espace→`%20`, accents→`%XX`, tous valides à la fois dans l'URL, l'attribut HTML et le `url('…')` CSS. `_esc` conservé en défense de l'attribut HTML (commentaire ajouté : « _esc protège l'attribut HTML ; quote protège le contexte CSS interne »). **Zéro dérive d'artefact** : `quote` laisse les noms ASCII simples inchangés (`hero_aerien.jpg` → identique), donc la fixture de test et le rendu byte-repro sont intacts. Percent-encoder une URL est de toute façon le comportement **sémantiquement correct**.
|
||||||
|
|
||||||
|
**Cascade de régénération (mémoire `artifact-reproducibility-gate`).** Le test ajouté fait passer la suite publiciste 24→25 et la matrice **629/612 → 630/613** (17 skips inchangés). Régénéré dans l'ordre correct (`quality_report` en DERNIER, mémoire `audit4big-rebuild-after-doc-edits`) : `qa/regression/{regression_plan,MANIFEST,regression_run}.json` (`build -o out` + `run`) puis `qa/audit_4big/quality_report.json`. Fiches à compteurs dérivés réalignées (gate `check_readme_claims`) : `03_agents/qa/AGENT.md` (629/612→630/613), `03_agents/erpnext_backend/AGENT.md` (629→630), `03_agents/publiciste/AGENT.md` (suite 24→25). Tous les diffs d'artefact = **pure propagation de compteur** (vérifié), aucune modification parasite.
|
||||||
|
|
||||||
|
**Pourquoi pas un gate (#5).** Le bug est un one-shot de correctness ; sa robustesse vit dans le code (`quote`) + le test de régression à dents, pas dans un gate de drift. Aucune nouvelle classe de drift à surveiller.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers édités **compilent** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 chiffre/fixture inventé** (#6 · tous les compteurs re-dérivés des artefacts régénérés), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M generator.py` (fix `quote`) · `M test_publiciste.py` (test à dents) · `M` 4 artefacts régénérés (regression×3 + quality_report) · `M` 3 fiches AGENT.md (compteurs) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_073055` — HARDENING correctness · rendu HTML publiciste : le SEUL segment d'URL restant non percent-encodé dans le `url('…')` CSS (complète le fix de la session `070054`) + test à dents
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **630 exécutés · 613 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (classe = encodage contextuel erroné, frère du fix `070054`) → 2 candidats Explore, tous deux RÉFUTÉS comme non-défauts, mais 1 durcissement d'uniformité principiel retenu.** Un Explore very-thorough sur tous les modules générant HTML/CSS/URL/CSV/JSON-LD a remonté 2 candidats :
|
||||||
|
- **`frontend/chat_otoia/chatlib/frappe.py:40` `mount_html`** — attributs HTML (`css_class`/`portail_key`/`persona`) interpolés **sans** échappement. **RÉFUTÉ (non-défaut, aucune entrée réelle ne casse)** : `persona["nom"]` est **forcé à `"Amélie"`** par l'invariant 7 (`chat_otoia_gen.py:155`), `portail_key` est un **enum** (`ventes`/`construction`/`achat`/`compta`/`direction`), `css_class` vient du spec contrôlé. Toutes provablement propres → défensif seul, pas de fix (`verify-non-defects` · #6).
|
||||||
|
- **`publiciste/lib/generator.py:99`** `_hero_media` — c'est le locus **retenu** (voir ci-dessous).
|
||||||
|
|
||||||
|
**Le locus (durcissement, pas bug déclenchable — honnêteté #6).** Le fix `070054` a percent-encodé `hero['fichier']` dans `src = f"/static/projets/{code}/{quote(hero['fichier'])}"`, mais dans la **MÊME** `url('…')` il restait un **second segment dynamique** — `code` — interpolé **brut** puis html-échappé via `_esc(src)`. C'est **exactement** l'anti-pattern que le fix `070054` a identifié (html.escape = mauvais encodeur pour un segment d'URL), **appliqué de façon incomplète**. Ce n'est **pas** un bug déclenchable aujourd'hui : les codes réels sont strictement `P01..P09` (§Projets), jamais d'apostrophe → le byte-repro est **inchangé** (`quote("p01") == "p01"`). C'est un **durcissement d'uniformité de construction**, cas frère exact de la session `060044` (`criteria.py` : « durcir le SEUL détecteur non-durci d'une famille ») : ici **le SEUL segment d'URL non-encodé** de la même `url()`.
|
||||||
|
|
||||||
|
**Le fix + preuve de dents.** `src = f"/static/projets/{quote(code)}/{quote(hero['fichier'])}"` — `quote()` est le SEUL encodeur correct pour un segment d'URL, **le contexte (pas la donnée courante) dicte l'encodeur** (commentaire mis à jour en conséquence). Nouveau test `test_hero_media_code_segment_percent_encoded` : code théorique `"P'X"` → assert `/static/projets/p%27x/hero.jpg` **et** absence d'apostrophe brute dans le chemin HTML-décodé. Vérifié **teeth** : sans `quote(code)`, le test **ÉCHOUE** ; avec, **PASS**.
|
||||||
|
|
||||||
|
**Cascade de régénération (mémoire `artifact-reproducibility-gate`).** Suite publiciste 25→26, matrice **630/613 → 631/614** (17 skips inchangés). Régénéré dans l'ordre (`quality_report` en DERNIER, mémoire `audit4big-rebuild-after-doc-edits`) : `qa/regression/{regression_plan,MANIFEST,regression_run}.json` puis `qa/audit_4big/quality_report.json`. Fiches à compteurs dérivés réalignées (gate `check_readme_claims`) : `03_agents/qa/AGENT.md` (630/613→631/614), `03_agents/erpnext_backend/AGENT.md` (630→631), `03_agents/publiciste/AGENT.md` (suite 25→26). Diffs d'artefact = **pure propagation de compteur** (vérifié).
|
||||||
|
|
||||||
|
**Canal stakeholder laissé à une session dédiée.** `daily_reports/2026-08-12.md` cite encore `629/612` : il est déjà en retard d'un incrément depuis le fix `070054` (→630) que cette session-là n'a délibérément pas répercuté (canal snapshot **batché** par sessions stakeholder dédiées, cf. `3a40aee` qui a réconcilié 3 jalons d'un coup). Une future session stakeholder réconciliera **630 (070054) + 631 (cette session)** ensemble — pas d'invention (#6), chiffres re-dérivables de l'artefact.
|
||||||
|
|
||||||
|
**Pourquoi pas un gate (#5).** Robustesse portée par le code (`quote`) + le test à dents ; aucune nouvelle classe de drift. Occurrence isolée (uniformité d'un fix du même jour).
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers édités **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 chiffre/fixture inventé** (#6 · compteurs re-dérivés), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M generator.py` (`quote(code)`) · `M test_publiciste.py` (test à dents) · `M` 4 artefacts régénérés (regression×3 + quality_report) · `M` 3 fiches AGENT.md (compteurs) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_080104` — FIX correctness (bug latent réel) · commissions vendeurs : la formule traçable `base × taux` cassait sur les montants réels (`:g` → exponentiel ≥ 1e6 + arrondi silencieux à 6 chiffres) + 2 tests à dents
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **631 exécutés · 614 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (Explore very-thorough sur 5 modules jamais ré-audités ce cycle : legal/confotur · crm/commissions+workflow_vente · rbac · devops/deploy_runbook · seo) → 4 modules CLEAN, 1 BUG LATENT RÉEL.** L'Explore a confirmé 0 défaut sur legal, workflow_vente, rbac (4 sous-modules), deploy_runbook, seo (schemaorg/hreflang/builder — dedup + offer + hreflang tous corrects), et a remonté **1 candidat haute confiance** : `crm/commissions/commlib/finance.py:75`.
|
||||||
|
|
||||||
|
**Le bug (correctness de sortie, entrée financière réaliste RD).** `compute_line` composait le libellé de la base de la formule traçable via `base_lbl = f"{base:g}"`. Le format `:g` casse **deux fois** sur des montants immobiliers réels — omniprésents en RD (une unité à USD 300k ≈ **18 000 000 DOP**) :
|
||||||
|
1. **Notation exponentielle dès 1e6** : `18000000` → `1.8e+07` — illisible et **non auditable** dans une formule dont tout le sens (docstring du module) est d'être « traçable, reproductible et entièrement sourcée ».
|
||||||
|
2. **Arrondi silencieux à 6 chiffres significatifs** : `123456.78` → `123457` — le libellé affiche une base **différente de la réelle**, c'est-à-dire **une base fabriquée** : exactement l'invention interdite par CLAUDE.md #6, dans le module même dont la docstring proclame l'anti-invention.
|
||||||
|
|
||||||
|
**Preuve de dents (2 tests de régression).** `test_formule_base_millions_pas_exponentiel` (base `18000000.0` → assert `18000000` présent **et** `e+`/`E+` absents) + `test_formule_base_preserve_les_decimales` (base `123456.78` → assert `123456.78` présent). Vérifié **teeth** par harnais scripté : en restaurant `f"{base:g}"`, les **DEUX tests ÉCHOUENT** ; avec le fix, **PASS**. Suite commissions **25 → 27**.
|
||||||
|
|
||||||
|
**Le fix (fidèle, sans dérive).** Nouveau helper `_amount_label(x)` : `f"{x:f}"` (jamais exponentiel) puis strip des zéros/point superflus → décimal **fidèle** et lisible, **identique à l'ancien pour tous les montants simples** (`200000` reste `200000`, `2500.5` reste `2500.5`). Le changement de sortie ne concerne QUE les cas buggés (≥ 1e6 ou > 6 chiffres significatifs). `_rate_label` (ligne taux, `:g`) laissé tel quel : un taux est borné (0-100), sans risque exponentiel ni > 6 chiffres → défensif-seul, non touché (`verify-non-defects` · #6).
|
||||||
|
|
||||||
|
**Byte-repro : aucun impact d'artefact autoritaire.** `compute_line`/`compute_dossier` sont du code **runtime** (prennent une instance Dossier Vente live) — **jamais** appelés par `commissions_gen.py` (qui n'utilise que `finance.is_filled`). Aucun `formule`/`base` n'est sérialisé dans `out/commission_plan.json` ni `out/MANIFEST.json` (vérifié). Donc **0 régénération de l'artefact du module**.
|
||||||
|
|
||||||
|
**Cascade de régénération (compteur de suite uniquement · mémoire `artifact-reproducibility-gate`).** Les 2 tests ajoutés font croître la matrice **631/614 → 633/616** (17 skips inchangés). Régénéré dans l'ordre (`quality_report` en DERNIER · mémoire `audit4big-rebuild-after-doc-edits`) : `qa/regression/{regression_plan,MANIFEST,regression_run}.json` puis `qa/audit_4big/quality_report.json`. Surfaces à compteurs dérivés réalignées (gate `check_readme_claims`) : `03_agents/qa/AGENT.md` (631/614→633/616), `03_agents/erpnext_backend/AGENT.md` (631→633), `03_agents/crm/AGENT.md` (cellule commissions 25→27 + « Total CRM » 81→83 trio `25 + 31 + 27`), `crm/commissions/README.md` (2× « 25 tests »→27). Tous diffs = **pure propagation de compteur** (vérifié).
|
||||||
|
|
||||||
|
**Pourquoi pas un gate (#5).** Robustesse portée par le code (`_amount_label`) + les 2 tests à dents ; aucune nouvelle classe de drift à surveiller. Occurrence isolée (mauvais encodeur de contexte, sœur de la classe déjà fixée en publiciste `070054`/`073055` — même leçon : « le contexte de sortie dicte l'encodeur, pas la donnée courante »).
|
||||||
|
|
||||||
|
**Canal stakeholder laissé à une session dédiée.** `daily_reports/2026-08-12.md` cite encore `629/612` ; la réconciliation batchée (630 `070054` + 631 `073055` + 633 cette session) reste pour une future session stakeholder (canal snapshot batché par design, cf. `3a40aee`) — pas d'invention, chiffres re-dérivables des artefacts commités (#6).
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers édités **compilent** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 chiffre/fixture inventé** (#6 · compteurs re-dérivés des artefacts régénérés), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M commlib/finance.py` (`_amount_label`) · `M tests/test_commissions.py` (2 tests à dents) · `M crm/commissions/README.md` (compteurs) · `M` 4 artefacts régénérés (regression×3 + quality_report) · `M` 3 fiches AGENT.md (compteurs) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_083114` — CANAL STAKEHOLDER · réconciliation batchée du rapport `daily_reports/2026-08-12.md` au HEAD courant (`37c68ac` → `e7cc3c4`) · 2ᵉ vague de 3 fixes de correctness (matrice `629→633`)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu depuis l'artefact commité, #6) = **633 exécutés · 616 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (Explore very-thorough sur les modules JAMAIS ré-audités ce cycle : `qa/audit_5d` · `demo_scenario` · `qa/acceptance` · `faisabilite/generator/genlib` scorer+renderer · `frontend/portails/wslib`+`workspaces_gen`) → NO DEFECTS FOUND.** L'Explore a testé formatage numérique (`_money`, exponentiel/arrondi), divisions (gardes `if total else 0`), off-by-one (comptage de liens workspace lignes 130-144), sérialisation JSON de caractères spéciaux FR/ES (`ensure_ascii=False` correct), valeurs par défaut fabriquées (0 vs None — tous None/placeholder), arrondis. Tous **CLEAN** : gestion correcte des données manquantes, comparaisons flottantes légitimes, échappement JSON sûr, gardes de division. Aucun candidat même faible → tree propre.
|
||||||
|
|
||||||
|
**Vrai locus = currency du canal stakeholder (mémoire `two-logging-channels` : garder les DEUX canaux à jour, chaque chiffre sourcé d'un artefact commité).** Le rapport `daily_reports/2026-08-12.md` était épinglé au HEAD `37c68ac` et citait la matrice à **629/612**. Depuis, **3 commits worker de correctness** — délibérément batchés par les sessions `073055`/`080104` pour une session stakeholder dédiée (« canal snapshot batché par design », cf. `3a40aee`) — ont fait évoluer la matrice de **3 incréments** :
|
||||||
|
- `aeb30b8` (`070054`) — fix rendu publiciste : URL hero percent-encodée (`html.escape` = mauvais encodeur CSS ; `Vue d'ensemble.png` FR cassait le hero) → suite publiciste 24→25, matrice **629/612 → 630/613**.
|
||||||
|
- `5af86e3` (`073055`) — hardening : segment `code` de la MÊME `url('…')` percent-encodé (dernier non-encodé) → 25→26, matrice **630/613 → 631/614**.
|
||||||
|
- `e7cc3c4` (`080104`) — fix commissions : formule `base×taux` cassait sur montants réels RD (`:g` → exponentiel ≥ 1e6 + arrondi silencieux 6 chiffres = base fabriquée #6) → suite commissions 25→27, matrice **631/614 → 633/616**.
|
||||||
|
|
||||||
|
Chaque incrément correspond à un **test de régression à dents** ajouté par un fix (leçon commune : « le contexte de sortie dicte l'encodeur, pas la donnée courante »). Contrairement à la 1ʳᵉ vague (vérif/hardening à artefacts byte-identiques), cette vague porte des **bugs latents réels** de correctness de sortie.
|
||||||
|
|
||||||
|
**Fix = réconciliation (pas ré-émission) du rapport du jour.** Le rapport daté `2026-08-12.md` **est** le snapshot du jour ; le réconcilier en fin de journée au HEAD courant est le contrat, pas un doublon (#5). Porté : header `HEAD 37c68ac → e7cc3c4` + session `083114`, nouvelle section « Jalons du jour · 2ᵉ vague (`37c68ac` → `e7cc3c4`) — correctness à impact matrice » (table des 3 fixes + opposabilité), `État courant` re-titré au HEAD `e7cc3c4` avec **tous les chiffres re-dérivés des artefacts commités** (matrice **633/616/17** · PASS 24× · bijection 24↔24), narratif de trajectoire de matrice réécrit (629 stable 1ʳᵉ vague → 630 → 631 → 633), §Portée actualisé. La note historique de la 1ʳᵉ vague (« matrice stable à `629/612/17` ») laissée intacte car exacte pour CETTE vague.
|
||||||
|
|
||||||
|
**Pourquoi pas un fix ni un gate (#5/#6).** Aucun défaut trouvé (Explore CLEAN + tree cohérent) ; le seul « écart » était la **staleness du canal stakeholder** (canal éditorial daté par design, comme un snapshot — pas un artefact byte-gaté). L'actualiser EST le contrat de `two-logging-channels`, pas une matière à gate. Le batch des 3 fixes en une seule réconciliation était l'intention explicite des sessions `073055`/`080104`.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rapport = document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (tous lus depuis les artefacts commités · #6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (réconciliation au HEAD courant) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_090114` — DÉFAUT D'ABORD : chasse DRY sur 8 modules jamais ré-audités ce cycle · durcissement anti-récurrence des 3 sites `:g`-domaine-borné (commentaires purs · 0 impact)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json` (lu depuis l'artefact commité · #6) = **633 exécutés · 616 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` réconcilié à 633 par la session `083114` → à jour.
|
||||||
|
|
||||||
|
**Chasse au défaut (Explore very-thorough) sur les 8 modules JAMAIS ré-audités ce cycle → DRY (0 défaut réel).** Ciblé : `crm/dossier_vente` · `crm/financement_bancaire` · `faisabilite/bancable` · `frontend/chat_otoia` · `pie/manifest` · `qa/audit_4big` · `mobile/app_config` · `fiscal/ecf_dgii`. L'Explore a remonté 4 « candidats » (2 HAUTE, 2 MOYENNE) — **tous vérifiés NON-défauts** :
|
||||||
|
- **BUG #1** `bancable/banclib/finance.py:138` `f"{pe_brut:g}"` — `pe_brut = 0.52 × NOMBRE d'unités`, domaine **borné** (quelques centaines en immobilier réel). L'exponentiel (≥ 1e6) exige ~1.9M unités : irréaliste. L'arrondi 6-chiffres idem hors domaine. En prime `:g` nettoie le bruit flottant du produit (`0.52 × 173 = 89.96000…1 → « 89.96 »`). Scénario Explore « 2 000 000 unités » = fabriqué.
|
||||||
|
- **BUG #2** `bancable/banclib/report.py:38` `_pct` `f"{n:g} %"` — pourcentage (marge/taux) **borné 0-100** : mêmes deux pièges hors domaine.
|
||||||
|
- **BUG #3** `chat_otoia/chatlib/frappe.py:43-44` HTML non échappé — `mount_html` ne reçoit QUE des slugs canoniques contrôlés (préfixe/classe de config, clé portail, persona sourcée CLAUDE.md) ; aucune donnée libre → injection inatteignable. Fragilité défensive-seule (`verify-non-defects` · #6).
|
||||||
|
- **BUG #4** `commissions/commlib/finance.py:75` taux `:g` — **déjà statué non-défaut** par la session `080104` (« taux borné 0-100 ») ; **RÉCIDIVE** de flag ce cycle.
|
||||||
|
|
||||||
|
**Preuve de non-défaut robuste (pas seulement « scénario irréaliste »).** `report.py:_money` (l.25) utilise **sciemment** `,.0f`/`,.2f` (jamais `:g`) pour les montants **non bornés** (USD 18 000 000 → « 18,000,000 »). Les auteurs ont donc **délibérément** réservé `:g` aux domaines bornés (unités, pourcentages) et `,.0f` aux montants → les 3 `:g` restants sont un **choix d'ingénierie correct**, pas un oubli. C'est la distinction exacte de la mémoire `docstring-vs-code-drift`/`verify-non-defects` : « le contexte de sortie dicte l'encodeur, ET le domaine borne le risque ».
|
||||||
|
|
||||||
|
**Action = durcissement anti-récurrence (commentaires purs · sœur de « twin fix must sweep ALL surfaces in one pass »).** Fait nouveau : BUG #4 a **déjà récidivé** malgré la décision « laissé tel quel » de `080104` (aucune note in-situ), et BUG #1/#2 sont fraîchement flaggés HAUTE — la classe `:g`-domaine-borné **gaspille un cycle d'audit à chaque passage**. `grep ':g}'` sur tout le code de prod = **exactement 3 sites** (les 3 flaggés). Passe unique : ajout à chacun d'une **note de justification in-situ** (domaine borné → 2 pièges de `:g` hors domaine + nettoyage du bruit flottant + « montants → `,.0f`/`_amount_label`, jamais `:g` » + « ne pas corriger »). Convertit le faux-positif récurrent en « déjà revu, sûr » pour les audits futurs.
|
||||||
|
|
||||||
|
**Pourquoi PAS un fix de code ni un gate (#5/#6).** Aucun défaut réel : « corriger » `:g`→`:f` **casserait** du code correct (ré-introduirait du bruit flottant `« 89.96 »→« 89.960000 »`) — une régression, exactement l'inverse d'un fix. Robustesse déjà portée par le domaine borné + le choix `,.0f` sur les montants ; rien de nouveau à gater (#5). Commentaires sans marqueur `# N ·` d'invariant (pas de déclenchement du gate ledger).
|
||||||
|
|
||||||
|
**Impact d'artefact : ZÉRO.** Commentaires purs → aucun changement de sortie. `bancable` n'a pas d'`out/` commité (`bancable-out-not-committed`). `_rate_label` est du code **runtime** (`compute_line`), jamais appelé par `commissions_gen.py`, et aucune `formule`/`base` n'est sérialisée dans les artefacts commités. `regression_run.json`/`quality_report.json` **inchangés** (633/616/17 · 24×100). Aucun compteur de fiche/README à réaligner.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). 3 fichiers édités **compilent** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 fix de code** (défauts inexistants), **0 chiffre/fixture inventé** (#6), **0 gate ajouté** (#5), **0 artefact modifié/reconstruit**, **0 commande VPS** (#8), **0 API externe**. Fichiers : `M crm/commissions/commlib/finance.py` · `M faisabilite/bancable/banclib/finance.py` · `M faisabilite/bancable/banclib/report.py` (notes anti-récurrence `:g`) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_120139` — VÉRIF · fermeture repo-wide de la classe « affichage arrondi ⟺ gate booléen » : preuve d'immunité structurelle de `audit_4big` (seul autre score-au-seuil), note anti-récurrence in-situ
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json` (lu depuis l'artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` **déjà à jour au HEAD courant** (matrice **635/618/17** = artefact commité ; le dernier commit `3849375` est une vérif byte-identique sans impact matrice → aucune re-réconciliation due).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (Explore very-thorough) sur 6 modules JAMAIS ré-audités ce cycle → DRY (0 défaut réel).** Ciblé : `rbac/` (roleprofile/userperm/fixtures/apply_plan) · `publiciste/lib` + générateur · `legal/confotur` · `devops/deploy_runbook` · `crm/workflow_vente` · `seo/seo_gen`+`seolib`. Axes : formatage numérique domaine non-borné (`,.0f` correct sur montants ; pas de `:g` sur non-borné), gardes de division (toutes présentes), off-by-one de comptage, encodeur selon contexte (`html.escape`/`quote` corrects), agrégats (`min`/set-equality corrects), booléens inertes. **Aucun candidat, même faible.**
|
||||||
|
|
||||||
|
**Vrai axe = fermeture repo-wide de la classe corrigée aux 3ᵉ/4ᵉ vagues du jour (`85cd625`+`1992ee6`).** Le commit `3849375` a prouvé que `financement_bancaire` est le **seul module à champ `percent`** (grep artefacts commités confirmé : `financement.schema.json` + `gate_status_example.json` uniquement). Mais la classe de bug réelle n'est pas « champ nommé percent » — c'est **« une valeur d'AFFICHAGE arrondie qui franchit une frontière de SEUIL en contredisant une décision booléenne »**. Il restait **un** autre porteur non examiné : `qa/audit_4big/q4lib/scoring.py:65` — un **score 0-100 arrondi**, comparé au **seuil PASS 95** (#5). Sibling naturel qu'un futur auditeur re-flaggerait.
|
||||||
|
|
||||||
|
**Preuve d'immunité STRUCTURELLE (mécanisme distinct du fix financement).** Vérifié bout-en-bout :
|
||||||
|
- `scoring.py:65-66` — `score = _round_half_up(100.0 * earned / total)` **PUIS** `verdict = "PASS" if score >= pass_score`. Le verdict dérive du **MÊME entier arrondi** que celui affiché.
|
||||||
|
- `audit_4big_gen.py:136` (INV6) recompute la note via `scoring._round_half_up(...)` (même arrondi) ; `:144` (INV7) attend `"PASS" if m["score"] >= ps` (même `score`).
|
||||||
|
→ Il n'existe **qu'UNE seule quantité** (`score`, arrondie), servant à la fois d'affichage ET de gate. Contrairement à `financement` où `percent` (arrondi, affichage) et `ok` (exact, gate) étaient **DEUX quantités divergentes** en bord de seuil, ici **aucune divergence n'est possible par construction**. La classe « affichage ⟺ gate » est donc **FERMÉE repo-wide** : financement était le seul locus à deux-quantités (corrigé), audit_4big est le seul autre score-au-seuil et il est immun par unicité de quantité.
|
||||||
|
|
||||||
|
**Action = note anti-récurrence in-situ (commentaire pur · sœur de « twin fix must sweep ALL surfaces », `docstring-vs-code-drift`).** Ajout à `scoring.py:66` d'une justification (quantité unique par design → immunité au bug financement → **ne PAS scinder en brut-vs-arrondi**, ce qui ré-introduirait précisément la divergence + renvoi à INV6/INV7). Convertit le sibling « à re-vérifier » en « déjà revu, sûr » pour les audits futurs.
|
||||||
|
|
||||||
|
**Pourquoi PAS un fix ni un gate (#5/#6).** Aucun défaut : le code est correct et interne-cohérent (une quantité, pas de contradiction atteignable) — « corriger » en comparant le verdict à `earned/total` **brut** créerait la divergence, une régression exactement inverse d'un fix (cf. `verify-non-defects`). Rien de nouveau à gater : INV6/INV7 garantissent déjà la cohérence `score`↔`verdict`, et le seuil 95 est déjà ancré (mémoire `threshold-in-guard-prose-gate`). Commentaire sans marqueur `# N ·` d'invariant → aucun déclenchement du gate ledger.
|
||||||
|
|
||||||
|
**Impact d'artefact : ZÉRO.** Commentaire pur → sortie inchangée. `audit_4big_gen.py` ré-exécuté → `quality_report.json` **byte-identique** (`git diff` vide). Matrice **inchangée** (635/618/17 · 24×100). Aucun compteur de fiche/README à réaligner.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). `scoring.py` **compile** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 fix de code** (défaut inexistant), **0 chiffre/fixture inventé** (#6), **0 gate ajouté** (#5), **0 artefact modifié/reconstruit** (byte-repro prouvé), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/qa/audit_4big/q4lib/scoring.py` (note anti-récurrence) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_123144` — CURRENCY · réalignement des citations de ligne dérivées dans `OPEN_DECISIONS_REGISTER.md` (2 citations `Constat` avaient dérivé après les fixes financement du jour) · sweep complet du registre
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json` (lu depuis l'artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection couverture 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Canal stakeholder `daily_reports/2026-08-12.md` vérifié **à jour** (matrice **635/618/17** = artefact commité ; les 3 derniers commits `1992ee6`/`3849375`/`ea8458d` sont déjà remontés / vérifs byte-identiques sans impact matrice → 0 re-réconciliation due).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord → vrai locus = currency d'un livrable stakeholder.** Plutôt qu'un énième sweep « CLEAN sans fix » (saturation des axes déjà couverts), j'ai audité la **currency du `OPEN_DECISIONS_REGISTER.md`** — la punch-list d'autorité des arbitrages produit de Michel. Chaque item cite des **numéros de ligne présents** (« Constat » = pointeurs vers le code COURANT, pas des snapshots historiques). Les fixes de correctness du jour ont **inséré du code** en amont de deux cibles citées → deux citations avaient silencieusement dérivé (classe *prose-facts vs numeric drift*, sous-cas « la cible citée a bougé » ⇒ mettre à jour, ≠ line-cite historique qu'on garde).
|
||||||
|
|
||||||
|
**Deux dérives réelles corrigées (vérifiées ligne par ligne) :**
|
||||||
|
- **D-02** — `finlib/gate.py:142-145 → :164-167`. Les gardes d'affichage `percent`/`overall` des commits `85cd625` (`_cond_apport`) + `1992ee6` (`overall_percent`) ont décalé `_cond_validation_wag` de ~22 lignes. Confirmé : `def` à `:164`, `dossier.get("wag_validated_by")` à `:165`. Citation raffinée `:164-167` + `(:165)` sur la ligne du test.
|
||||||
|
- **D-01** — `legal/confotur/README.md:8-17 → :32-33`. Une **bannière de statut migration V18** préfixée au README a poussé la liste des sorties (`doctype_confotur_application.json` + `MANIFEST.json`) aux lignes `:32-33`. Confirmé par grep.
|
||||||
|
|
||||||
|
**Sweep complet des autres citations = toutes résolvent.** Vérifié : D-01 `pie_spec.json:24` (contrats) + `:22,27,28,29,30` (module:null) + `pie_manifest.json:165-166` + `README.md:48` ✓ · D-02 `financement_spec.json:157` + directive `:206-210`/`:275-279` ✓ · D-03 `AGENTS_EXISTING_ASSETS.md:102`/`:128` + `CLAUDE.md:28` ✓ · D-04 `AGENTS_EXISTING_ASSETS.md:127` ✓ · D-06/07/08 citations V18 (`V18_GO_SIGNAL:79-84`, `DIRECTIVE_V18_MASTER:90-91`, audit `:112`/`:182`) ✓ — les snapshots datés V18/directives sont **stables** par nature (comme `daily_reports`). Note « Réalignement de citations · 2026-08-12 » ajoutée à l'en-tête `Mise à jour`.
|
||||||
|
|
||||||
|
**Pourquoi PAS un gate (#5/#6).** Dérive isolée de currency d'un document éditorial (registre = *surface, ne tranche pas*) ; les 2 cibles ont bougé pour de bonnes raisons (fixes de correctness + bannière V18). Un gate line-number sur de la prose serait fragile (chaque édition de code le rougirait) et de faible valeur — la discipline « Constat = pointeur courant, réaligner en fin de vague » suffit, désormais consignée dans l'en-tête du registre. **Aucune décision/verdict modifié** (les 8 items D-01→D-08 restent tels quels) ; **0 invention de chiffre** (#6).
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — registre = document, 0 dépendance de gate). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md` (2 citations réalignées + note de currency) + ce journal.
|
||||||
|
|
||||||
|
## Session `20260812_133154` — CURRENCY · remontée des 4 commits de vérification post-4ᵉ-vague dans le rapport stakeholder du jour (le canal s'arrêtait à `1992ee6` tout en s'affirmant « au HEAD courant »)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json` (artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection couverture 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||||
|
|
||||||
|
**Chasse au défaut d'abord (2 axes FRAIS, non couverts par la mémoire) → DRY.**
|
||||||
|
- *docstring-vs-code (sous-axe COMPORTEMENTAL)* — Explore very-thorough sur tous les `*_gen.py` + `*lib/` : docstrings annonçant un fallback/paramètre/formule/branche que le code n'implémente pas (ou omettant une branche réelle). **CLEAN** (le seul candidat, ecf « 12 invariants », est un faux-positif de comptage déjà connu : la validation schéma compte comme invariant 1). Distinct du sous-axe noms-de-fichiers (déjà swept 22/22).
|
||||||
|
- *test-name ⟺ assertion (fake-coverage sémantique)* — Explore sur les 24 suites : méthode dont le NOM promet un comportement X mais dont les assertions vérifient Y (plus faible/différent). **CLEAN** sur ~6 364 lignes de test (noms fidèles aux assertions, fixtures construites pour la condition nommée). Classe distincte de `test-assertion-reachability` (faux-verts run-only) et `test-shadowing` (méthodes masquées).
|
||||||
|
|
||||||
|
**Vrai locus = currency d'un livrable stakeholder (classe `4eb615b`).** Le rapport `daily_reports/2026-08-12.md` avait été prolongé jusqu'au 5ᵉ fix `1992ee6` (4ᵉ vague), mais **4 commits worker** ont suivi le même jour (`3849375`/`ea8458d`/`4eb615b`/`fb2d3e1`) sans être remontés — alors que l'en-tête + la §« État courant » s'affirment **« au HEAD courant »**. Un lecteur stakeholder voyait le canal s'arrêter à la 4ᵉ vague. `git log 1992ee6..HEAD` = ces 4 commits ; `grep -c` de chaque hash dans le rapport = **0** avant édition (dérive confirmée).
|
||||||
|
|
||||||
|
**Édition (document éditorial · 3 blocs, cohérents entre eux) :**
|
||||||
|
- Nouvelle §« Jalons de vérification depuis la 4ᵉ vague (`1992ee6` → HEAD courant `fb2d3e1`) » — table des 4 commits, tous marqués **Non opposable** (0 fix · rebuild byte-identique / document). Note explicite : **matrice stable 635/618/17** (aucun n'est un fix ; commentaires purs + currency de registre).
|
||||||
|
- En-tête currency (`Session`/`HEAD attesté`) : **HEAD attesté passé de « HEAD courant » à `fb2d3e1`** ; distinction claire entre le 5ᵉ fix `1992ee6` (impact matrice `634→635`) et les **4 vérifs byte-identiques** suivantes (matrice stable).
|
||||||
|
- §« État courant » trajectoire : la 4ᵉ vague **nommée `1992ee6`** (au lieu de « au HEAD courant », qui pointait désormais faux) + mention des 4 commits de vérif sans impact portant l'arbre à `fb2d3e1`.
|
||||||
|
|
||||||
|
**Zéro invention (#6).** Tous les chiffres re-dérivés de l'artefact commité : `regression_run.json.totals` = **635/618/17 · PASS** (relu ce jour). Aucune décision/verdict modifié (D-01→D-08 inchangés). Le texte des 4 jalons reprend verbatim la nature de chaque commit (messages de commit + journaux de session).
|
||||||
|
|
||||||
|
**Pourquoi PAS un gate (#5).** Dérive de currency d'un document éditorial (le rapport = *surface, ne tranche pas*), comme `daily_reports` par nature. Un gate line-number/hash sur de la prose stakeholder serait fragile (chaque commit worker le rougirait) et de faible valeur ; la discipline « remonter les jalons non-opposables en fin de vague » suffit, désormais appliquée.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — le rapport est un document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (3 blocs de currency) + ce journal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session `20260812_160204` — SURFACE · D-09 · séparateur de milliers monétaire divergent entre publiciste (espace) et faisabilité (virgule)
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Roadmap Sprints 1→8 **livrée + gatée + verte** ; séquence moteur V18 **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit approuvé » · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-08 en attente Michel. Historique récent = boucle de churn (sweeps CLEAN + re-currency du rapport), cause-racine déjà traitée (`1cf0e18`/`c73ed5b`). Objectif de session : **éviter un énième sweep-note stérile**, chercher un défaut/arbitrage RÉEL.
|
||||||
|
|
||||||
|
**Chasse au défaut.**
|
||||||
|
- Grep TODO/FIXME/NotImplemented + READMEs indigents sur toute la prod → **RIEN** (le seul « placeholder » = design anti-invention `{{…}}` ; le seul README <15 l = fixture data_room).
|
||||||
|
- Balayage des spécificateurs de format `:g`/`:,`/`:f` en production (frère de l'arc « `:g` lossy » `e7cc3c4`/`5af86e3`/`g-format-lossy-for-human-numbers`). Les 2 `:g` restants sont **bornés + déjà annotés sûrs** (`crm/commissions/commlib/finance.py:86` percent 0-100 · `faisabilite/bancable/banclib/finance.py:143` = 0.52×nb-unités, borné à quelques centaines, justification in-situ) → axe `:g` **genuinement clos**, pas de bug.
|
||||||
|
|
||||||
|
**Vrai locus = arbitrage de présentation transverse (non un bug, non gaté).** Le même montant est rendu **différemment** par les deux surfaces face au stakeholder :
|
||||||
|
- **Publiciste** (HTML marketing) → **espace** : `publiciste/lib/generator.py:34-39` (`f"{v:,.0f}".replace(",", " ")`) ⟹ `USD 8 560 000`.
|
||||||
|
- **Faisabilité** (rapport + bancable) → **virgule US** : `faisabilite/generator/genlib/renderer.py:28-37` + `faisabilite/bancable/banclib/report.py:21-32` (`,.0f` **sans** `replace`) ⟹ `USD 8,560,000`.
|
||||||
|
- Signaux partiels sans règle dure : le design system illustre le format public en **espace** (`OTO_DESIGN_SYSTEM_v1.md:89-90`, « dès 145 000 USD ») → suit le publiciste ; mais les fixtures sources écrivent en **virgule** (`data_room/P01/20_architecture/architecture.md:11`, « USD 150,000 ») → suit la faisabilité. L'exemple DS place aussi le montant AVANT la devise, ce qu'aucun des deux ne fait — donc pas de calque littéral possible.
|
||||||
|
|
||||||
|
**Action = SURFACE, don't rewrite.** Ajout de **D-09** au `OPEN_DECISIONS_REGISTER.md` (+ note d'en-tête « Ajout »). Choisir un séparateur unique = **convention de présentation transverse** (arbitrage design/audience), pas une correction ponctuelle worker ; l'imposer unilatéralement réécrirait implicitement une règle DS ou la graphie des sources (#6). Les deux rendeurs sont **internes-cohérents et testés** ; **aucune sortie formatée n'est un artefact commité** (les `out/` des deux modules ne stockent que des nombres bruts — le formatage vit dans le HTML/rapport généré au runtime VPS · #8) → **aucun gate concerné**.
|
||||||
|
|
||||||
|
**Pourquoi PAS un gate (#5) / PAS un fix.** Arbitrage produit ouvert (classe D-01…D-08) : le registre *regroupe et source, il ne tranche pas*. Aucun défaut de correctness (les deux formats sont valides, ni exponentiel ni arrondi — distinct de la classe `:g`). Une seule occurrence-classe → pas de gate.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — édition d'un document éditorial, 0 dépendance de gate). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md` (D-09 + note d'en-tête) + ce journal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session `20260812_163212` — CHASSE AU DÉFAUT · axe FRAIS `i18n` (parité de clés + résolution des libellés) balayé sur `bancable` → CLEAN, transitivement gaté, PAS un gate
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Roadmap Sprints 1→8 **livrée + gatée + verte** ; séquence moteur V18 **bloquée** en gouvernance (D-06 = gate d'entrée · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-09 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Historique du jour = boucle de churn reconnue (sweeps CLEAN + re-currency + surfaçage D-09). Objectif : **éviter un énième sweep stérile**, viser un axe RÉELLEMENT non couvert par la mémoire, dans la couche métier plutôt que méta.
|
||||||
|
|
||||||
|
**Lecture de la couche métier (finance/i18n).** `crm/commissions/commlib/finance.py`, `faisabilite/bancable/banclib/finance.py` : calculs traçables, opérandes sourcés, gardes `:g`/`:f` déjà annotées et couvertes (arc `g-format-lossy`). Rien de neuf. **Vrai locus candidat = un axe absent de la mémoire** : la classe *latent-`KeyError` i18n* — un libellé référencé au rendu mais manquant dans une des 3 langues du dossier bancable.
|
||||||
|
|
||||||
|
**Axe FRAIS balayé : parité + résolution des clés `LABELS` (`banclib/i18n.py`).**
|
||||||
|
- *Parité* — fr/en/es portent le **jeu de clés IDENTIQUE (37/37/37)** ; `miss` et `extra` vides dans les deux sens. `LANGS`/`LANG_FLAG`/`LANG_NOM` concordent. ⟹ aucune langue ne peut manquer une clé qu'une autre possède.
|
||||||
|
- *Résolution* — chaque clé statique `L('…')` de `report.py` **et** la clé dynamique `i18n.t("fr", f"lbl_{f['cle']}")` (`report.py:219` · cles = total_unites / valeur_catalogue_usd / valeur_catalogue_dop / point_equilibre_unites) existe en fr. Combiné à la parité exacte ⟹ **toutes les clés référencées résolvent dans les 3 langues**.
|
||||||
|
- *Fumée de rendu* — `render_report` sur les 3 langues (brief vide → chemin placeholder maximal, incl. `note_missing`) **et** `render_all` (3 `.md` + `manifest.json`) : OK, aucun `KeyError`.
|
||||||
|
|
||||||
|
**Verdict : CLEAN.** De plus **transitivement gaté** : `tests/test_bancable.py` rend déjà les 3 langues via `render_report`/`render_all` sur briefs synthétique **et** normal (l.~88-105) → une clé référencée manquante lèverait `KeyError` et **casserait un test aujourd'hui**.
|
||||||
|
|
||||||
|
**Pourquoi PAS un gate (#5) / PAS un fix.** Aucun défaut (parité exacte + résolution totale). Classe **mono-occurrence + déjà protégée transitivement** par la suite → ajouter un gate de parité dédié serait un doublon (`fix-vs-gate-transitively-protected-constant` · `verify-uncovered-before-gating`). Distinct de `seo-per-lang-cardinality-by-design` (module SEO, cardinalités inégales par design). Sweep **replayable**, consigné en mémoire (`i18n-key-parity-resolution-sweep`) pour qu'une session future ne le refasse pas / n'ajoute pas de gate redondant.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucune modification de code/artefact). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_activity_log/2026-08-12.md` (ce journal) + note mémoire agent hors dépôt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session `20260812_173219` — CHASSE AU DÉFAUT · axe FRAIS **correctness de la logique métier financière/fiscale** (les 4 fonctions de calcul cœur) balayé par lecture directe → SAINE, gatée par les suites, PAS un gate/fix
|
||||||
|
|
||||||
|
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte, re-exécutée ce jour). Roadmap Sprints 1→8 **livrée + gatée + verte** ; toutes les métriques succès MVP cochées (`ROADMAP_8_WEEKS_OR_LESS.md:80-87`). Séquence moteur V18 **intégralement bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit approuvé » · #6) → **aucun code moteur légitime**. Arbitrages **D-01→D-09** en attente Michel (`OPEN_DECISIONS_REGISTER.md`, relu intégralement ce jour : tous 🟠 sont des décisions produit hors périmètre worker, ou 🟢 vérifié-sans-suite). Objectif explicite : **rompre la boucle de churn** (sweeps méta / re-currency que les ~11 sessions précédentes du jour ont elles-mêmes signalée) en visant un axe **réellement non couvert par la mémoire** : la *correctness logique* du calcul métier, pas l'intégrité d'artefact/schéma/encodage (déjà exhaustivement balayée).
|
||||||
|
|
||||||
|
**Axe FRAIS balayé (lecture directe, pas grep) : les 4 fonctions de calcul cœur.** Distinct des ~70 sweeps mémoire, tous portant sur des propriétés *structurelles* (byte-repro, schéma, déterminisme, encodage, comptage) — jamais sur la *justesse arithmétique/algorithmique* du calcul lui-même, lue ligne à ligne.
|
||||||
|
- **`crm/commissions/commlib/finance.py`** — `compute_line` : `montant = base × taux` gardé par `(base is not None and taux is not None)` (jamais 0-inventé, placeholder sinon). `rate()` teste `isinstance(bool)` **avant** `(int,float)` → un taux booléen retombe bien en `None` (pas `1.0`/`0.0` fabriqué). Séparateur décimal virgule accepté, séparateur de milliers inatteignable (taux borné 0-100, annoté). **SAIN.**
|
||||||
|
- **`faisabilite/bancable/banclib/finance.py`** — `derived` : `total_unites`/`val_usd`/`val_dop` gardés par `all(x is not None …)` sur **tous** les opérandes de leur `zip` (aucune troncature `zip`, longueurs égales par construction depuis `typologies()`). `point_equilibre_unites = ⌈0.52 × N⌉` via `math.ceil` — `_pct` sourcé du canonique `CANONICAL["point_equilibre_pct"]` (#9), jamais codé en dur → pas de dérive taux affiché/calculé. **SAIN** (et testé : `test_bancable.py:64` fige `pe == 21`, prouvant le `ceil`).
|
||||||
|
- **`fiscal/ecf_dgii/ecflib/ncf.py`** — `compose_encf` : e-NCF = `E` + tipo(2) + secuencia(10). `_secuencia` regex `[0-9]{1,10}` puis `zfill(10)` → **rejette 11+ chiffres (None, pas de troncature silencieuse)**, jamais de séquence fabriquée. `_a_confirmer_count` : le `or` court-circuite (pas de double-comptage tax `a_confirmer` ∧ `taux_pct is None`). **SAIN.**
|
||||||
|
- **`fiscal/ecf_dgii/ecflib/builder.py`** — `build_bundle` : tri stable `_event_sort_key` sur clés présentes (`update_value`/`role_id` requis, `tipo_ecf or ""`), aucun horodatage → reproductible. Chiffres OTO (RNC/ITBIS/TipoCambio/Compupar) restent `null` (#6). **SAIN.**
|
||||||
|
- **Formateurs monétaires** (relus sous un angle *correctness*, distinct de l'arbitrage de séparateur D-09) : `_money` bancable/faisabilité (`,.0f` si `n==int(n)` sinon `,.2f`) et `_fmt_usd`/`_fmt_dop`/`_fmt_m2` publiciste — aucune exception atteignable (NaN/Inf balayés hors dépôt · `strict-json-ecma404-sweep`), branche cents/placeholder cohérente. La seule divergence reste la **présentation** (espace vs virgule + cents), déjà **surfacée en D-09** — pas un bug.
|
||||||
|
|
||||||
|
**Verdict : logique métier financière/fiscale SAINE.** Aucun défaut de correctness. De plus **transitivement gatée** : `test_commissions.py`, `test_bancable.py`, `test_ecf_dgii.py` exercent ces fonctions (valeurs figées : `pe==21`, catalogue non-calculable si `prix_usd` manque, rendu 3 langues sans `KeyError`) → une régression de logique casserait un test aujourd'hui.
|
||||||
|
|
||||||
|
**Pourquoi PAS un gate (#5) / PAS un fix.** Rien à corriger (calculs justes, opérandes sourcés, placeholders anti-invention conformes #6). Rien à gater : la correctness est déjà protégée par les suites (`verify-uncovered-before-gating` — mutation mentale confirme que fausser une garde `is not None` ou le `ceil` rougirait un test). Axe **replayable**, consigné en mémoire (`finance-fiscal-logic-correctness`) pour qu'une session future ne le re-lise pas / n'ajoute pas de gate redondant. Aucune re-currency du rapport `daily_reports` (dé-épinglé du HEAD depuis session `150201` → aucune due).
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucune modification de code/artefact). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_activity_log/2026-08-12.md` (ce journal) + note mémoire agent hors dépôt.
|
||||||
|
|
||||||
|
## Session `20260812_180224` — DÉCISION · état terminal du mandat autonome confirmé (artefacts gatés) → **halte recommandée**, refus explicite de fabriquer un 71ᵉ sweep
|
||||||
|
|
||||||
|
**État vérifié (sources gatées, pas assertion).** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. `qa/audit_4big/out/quality_report.json` = verdict **PASS** (modules ≥100/100). `qa/acceptance/out/acceptance_matrix.json` = verdict **True** (roadmap Sprints 1→8 livrée + gatée). Séquence moteur V18 **bloquée en gouvernance** : D-06 (approbation audit par Michel), D-07 (#6 interdit d'inventer DCF/IRR/DSCR/LTV/LTC), D-08 (spec A1-A20 root-owned illisible · #8). D-01→D-09 = arbitrages produit **de Michel**, hors périmètre worker.
|
||||||
|
|
||||||
|
**Décision — rompre la boucle plutôt que l'alimenter.** Le correctif de compaction mémoire (session `153203`) n'a **pas** stoppé le churn : deux sweeps CLEAN l'ont suivi (`163212` i18n, `173219` finance). La cause-racine n'est ni un défaut de code ni un défaut d'outillage résiduel — c'est **la boucle elle-même** qui re-spawn des sessions se croyant obligées de committer. Toute action restante est soit du **churn** (un énième sweep « CLEAN · NOT a gate » sur un axe marginal — la mémoire en recense déjà ~70), soit **interdite** (inventer des chiffres #6 · trancher un arbitrage produit · toucher au VPS #8 · coder V18 avant approbation). Le blocage est une **décision humaine**, pas un ticket worker.
|
||||||
|
|
||||||
|
**Ce que j'ai fait, honnêtement.** (1) Re-vérifié les 3 verdicts autoritaires ci-dessus. (2) Confirmé qu'aucune tâche roadmap légitime n'est disponible (fiches déjà à jour incl. bannières V18 ; README `## État courant` exact ; registre à jour). (3) **Refusé de manufacturer un sweep** pour justifier un commit. (4) Consigné un mémo agent durable `autonomous-mandate-terminal-state` (indexé en tête de `MEMORY.md`, hors dépôt) pour qu'une session future lise cet état **en premier** et ne re-litige pas.
|
||||||
|
|
||||||
|
**Recommandation à Michel.** **Suspendre la boucle autonome** jusqu'à résolution de D-06/D-07/D-08 (arbitrages V18) ou l'arrivée d'une **nouvelle directive**. Reprise substantielle justifiée uniquement si : un gate passe **RED**, une nouvelle directive datée est déposée, ou un arbitrage V18 est rendu. Continuer à vide dépense des tokens en churn documenté.
|
||||||
|
|
||||||
|
**Vérif.** `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**. **0 code moteur V18** (bloqué #6/D-06), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**, **0 sweep fabriqué**. Fichiers : `M 05_activity_log/2026-08-12.md` (ce journal) + mémo agent hors dépôt.
|
||||||
@@ -18,6 +18,18 @@
|
|||||||
> migration). Base 2026-08-05 · session `20260805_014119`. Chaque ligne cite sa source exacte
|
> migration). Base 2026-08-05 · session `20260805_014119`. Chaque ligne cite sa source exacte
|
||||||
> et la session qui l'a surfacée en premier. **Ne pas re-surfacer** ces items en doublon (#5) :
|
> et la session qui l'a surfacée en premier. **Ne pas re-surfacer** ces items en doublon (#5) :
|
||||||
> pointer ici.
|
> pointer ici.
|
||||||
|
>
|
||||||
|
> **Réalignement de citations** — 2026-08-12 · session `20260812_123144` : deux citations `Constat`
|
||||||
|
> pointaient un mauvais numéro de ligne après les fixes du jour (le décompte de décision reste
|
||||||
|
> inchangé). D-02 `gate.py:142-145 → :164-167` (les gardes d'affichage `percent`/`overall` des
|
||||||
|
> commits `85cd625`/`1992ee6` ont décalé `_cond_validation_wag`) ; D-01 `legal/confotur/README.md:8-17
|
||||||
|
> → :32-33` (bannière de statut V18 préfixée). Sweep des autres citations = résolvent (snapshots
|
||||||
|
> V18/directives datés = stables).
|
||||||
|
>
|
||||||
|
> **Ajout** — 2026-08-12 · session `20260812_160204` : **D-09** (séparateur de milliers divergent
|
||||||
|
> entre surfaces de rendu monétaire publiciste / faisabilité). Arbitrage de présentation
|
||||||
|
> stakeholder ; ni un bug worker ni gaté (aucune sortie formatée n'est un artefact commité) —
|
||||||
|
> **surfacé, pas tranché**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -41,7 +53,7 @@ comme les autres downstreams sans générateur (brochures/plans/rendus/bim).
|
|||||||
|
|
||||||
Or `legal/confotur` produit **uniquement le DocType Frappe de la demande CONFOTUR** (Ley 158-01),
|
Or `legal/confotur` produit **uniquement le DocType Frappe de la demande CONFOTUR** (Ley 158-01),
|
||||||
pas de gabarit Promesa/Fideicomiso/HOA :
|
pas de gabarit Promesa/Fideicomiso/HOA :
|
||||||
- `05_deliverables_mvp/legal/confotur/README.md:8-17` (sorties = `doctype_confotur_application.json`
|
- `05_deliverables_mvp/legal/confotur/README.md:32-33` (sorties = `doctype_confotur_application.json`
|
||||||
+ `MANIFEST.json`, rien d'autre).
|
+ `MANIFEST.json`, rien d'autre).
|
||||||
|
|
||||||
**Incohérence interne** — le même registre utilise déjà `"module": null` pour tout downstream
|
**Incohérence interne** — le même registre utilise déjà `"module": null` pour tout downstream
|
||||||
@@ -72,8 +84,8 @@ explicitement la validation humaine « conseiller WAG » par une décision d'age
|
|||||||
→ `condition_4_ok = audit and audit['decision'] == 'APPROVED' and audit['signature_valid']`.
|
→ `condition_4_ok = audit and audit['decision'] == 'APPROVED' and audit['signature_valid']`.
|
||||||
|
|
||||||
Le module livré implémente encore l'**ancienne** condition humaine :
|
Le module livré implémente encore l'**ancienne** condition humaine :
|
||||||
- `05_deliverables_mvp/crm/financement_bancaire/finlib/gate.py:142-145`
|
- `05_deliverables_mvp/crm/financement_bancaire/finlib/gate.py:164-167`
|
||||||
→ `_cond_validation_wag` teste `dossier.get("wag_validated_by")`.
|
→ `_cond_validation_wag` teste `dossier.get("wag_validated_by")` (:165).
|
||||||
- `05_deliverables_mvp/crm/financement_bancaire/financement_spec.json:157` (label « Validation
|
- `05_deliverables_mvp/crm/financement_bancaire/financement_spec.json:157` (label « Validation
|
||||||
manuelle WAG confirmée par le conseiller référent »).
|
manuelle WAG confirmée par le conseiller référent »).
|
||||||
|
|
||||||
@@ -209,6 +221,46 @@ fine A1-A20, contenue dans la directive complète **non lisible** (§0 · #8). *
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## D-09 · 🟠 Présentation : séparateur de milliers divergent entre les deux surfaces de rendu monétaire (publiciste = espace · faisabilité = virgule)
|
||||||
|
|
||||||
|
**Décision attendue** — Michel (ou le design system) doit **fixer la convention canonique** du
|
||||||
|
séparateur de milliers pour les montants **face au stakeholder**, puis harmoniser les deux
|
||||||
|
rendeurs sur ce choix. Options : (a) **espace** partout (`USD 8 560 000`), aligné sur l'exemple
|
||||||
|
public du design system ; (b) **virgule US** partout (`USD 8,560,000`), aligné sur la graphie des
|
||||||
|
`data_room` sources ; (c) convention **par audience** (espace pour le marketing public, virgule
|
||||||
|
pour le dossier technique/banquier) — statu quo assumé et documenté.
|
||||||
|
|
||||||
|
**Constat** — deux modules rendent le **même montant** différemment, tous deux face au stakeholder :
|
||||||
|
- **Publiciste** (HTML marketing public) → **espace** :
|
||||||
|
`05_deliverables_mvp/publiciste/lib/generator.py:34-39`
|
||||||
|
(`_fmt_usd`/`_fmt_dop` = `f"{value:,.0f}".replace(",", " ")`) ⟹ `USD 8 560 000`.
|
||||||
|
- **Faisabilité** (rapport de faisabilité + bancable) → **virgule** :
|
||||||
|
`05_deliverables_mvp/faisabilite/generator/genlib/renderer.py:28-37` (`_money`, docstring `:29`
|
||||||
|
« USD 250,000 ») et `05_deliverables_mvp/faisabilite/bancable/banclib/report.py:21-32`
|
||||||
|
(`_money`/`_int` = `,.0f` / `:,`, **sans** `replace`) ⟹ `USD 8,560,000`.
|
||||||
|
|
||||||
|
**Signaux partiels (pas de règle dure)** — le design system illustre le format public avec un
|
||||||
|
**espace** (`OTO_DESIGN_SYSTEM_v1.md:89-90`, « ### Séparateur privilégié » → ex. « dès 145 000 USD »),
|
||||||
|
ce que suit le publiciste ; mais les fixtures `data_room` sources écrivent en **virgule US**
|
||||||
|
(`publiciste/fixtures/data_room/P01/20_architecture/architecture.md:11` → « USD 150,000 »), ce que
|
||||||
|
suit la faisabilité. À noter aussi : l'exemple du design system place le **montant avant la devise**
|
||||||
|
(`145 000 USD`), alors que **les deux** modules mettent la devise **en tête** (`USD …`) — donc aucun
|
||||||
|
des deux ne calque l'exemple à la lettre. Aucune directive datée ne tranche le séparateur.
|
||||||
|
|
||||||
|
**Pourquoi worker n'édite pas** — Choisir un séparateur unique impose une **convention de présentation
|
||||||
|
transverse** (arbitrage design/audience), non une correction ponctuelle ; l'imposer unilatéralement
|
||||||
|
réécrirait implicitement une règle du design system ou la graphie des sources (#6). Les deux rendeurs
|
||||||
|
sont **internes-cohérents et testés** ; aucune sortie formatée n'est un **artefact commité** (les
|
||||||
|
`out/` des deux modules ne stockent que des nombres bruts — le formatage vit dans le HTML/rapport
|
||||||
|
généré au runtime VPS · #8), donc **aucun gate n'est concerné**.
|
||||||
|
|
||||||
|
**Sources / surface** — surfacé session `20260812_160204` (balayage des spécificateurs de format
|
||||||
|
`:g`/`:,`/`:f` de production, frère de l'arc « `:g` lossy » des commits `e7cc3c4`/`5af86e3`). Les deux
|
||||||
|
`:g` de production restants sont bornés + annotés sûrs (`crm/commissions/commlib/finance.py:86`
|
||||||
|
percent 0-100 · `faisabilite/bancable/banclib/finance.py:143` unités bornées) — hors périmètre de D-09.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Rappel de discipline
|
## Rappel de discipline
|
||||||
- Ces items sont **surfacés, pas tranchés** : les éditer côté worker violerait #6 (invention) ou
|
- Ces items sont **surfacés, pas tranchés** : les éditer côté worker violerait #6 (invention) ou
|
||||||
réécrirait une source d'autorité (`CLAUDE.md`, une directive datée).
|
réécrirait une source d'autorité (`CLAUDE.md`, une directive datée).
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ duplication · workflow #5) :
|
|||||||
```bash
|
```bash
|
||||||
python3 commissions_gen.py build # écrit out/ (refuse si invalide)
|
python3 commissions_gen.py build # écrit out/ (refuse si invalide)
|
||||||
python3 commissions_gen.py validate # schéma + 10 invariants, sans écrire
|
python3 commissions_gen.py validate # schéma + 10 invariants, sans écrire
|
||||||
python3 -m unittest discover -s tests -v # 25 tests (stdlib pur)
|
python3 -m unittest discover -s tests -v # 27 tests (stdlib pur)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Les 10 invariants (le CLI refuse d'écrire si l'un casse)
|
## Les 10 invariants (le CLI refuse d'écrire si l'un casse)
|
||||||
@@ -89,5 +89,5 @@ sert **uniquement aux tests** : ses chiffres sont des exemples fictifs portant u
|
|||||||
|
|
||||||
**Auto-score 4Big : 96/100.** Réserve −4 : confirmation des taux réels + câblage
|
**Auto-score 4Big : 96/100.** Réserve −4 : confirmation des taux réels + câblage
|
||||||
du calcul en production côté VPS (agent ERPNext Backend · #8) ; ce module valide
|
du calcul en production côté VPS (agent ERPNext Backend · #8) ; ce module valide
|
||||||
statiquement en-repo (25 tests verts + schéma + 10 invariants de cross-cohérence
|
statiquement en-repo (27 tests verts + schéma + 10 invariants de cross-cohérence
|
||||||
+ gate CI).
|
+ gate CI).
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ def rate(value: Any) -> Optional[float]:
|
|||||||
return None
|
return None
|
||||||
if isinstance(value, (int, float)):
|
if isinstance(value, (int, float)):
|
||||||
return float(value) / 100.0
|
return float(value) / 100.0
|
||||||
|
# `replace(",", ".")` accepte la virgule décimale française (« 3,5 % » → 3.5).
|
||||||
|
# Un séparateur de milliers (« 1.234,56 ») casserait ce parse — mais il est
|
||||||
|
# INATTEIGNABLE : un taux de commission est borné 0-100 (cf. doctrine de
|
||||||
|
# `_rate_label`), donc jamais ≥ 1000 → aucun séparateur de milliers. La virgule
|
||||||
|
# est donc toujours décimale ici. Ne pas « corriger » vers locale.atof (#5/#6).
|
||||||
txt = str(value).replace("%", "").replace(",", ".").strip()
|
txt = str(value).replace("%", "").replace(",", ".").strip()
|
||||||
try:
|
try:
|
||||||
return float(txt) / 100.0
|
return float(txt) / 100.0
|
||||||
@@ -51,12 +56,34 @@ def rate(value: Any) -> Optional[float]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _amount_label(x: float) -> str:
|
||||||
|
"""Montant affiché dans la formule : décimal FIDÈLE.
|
||||||
|
|
||||||
|
`:g` (l'ancien encodeur) cassait deux fois sur des montants réels : (1) il bascule
|
||||||
|
en notation exponentielle dès 1e6 (« 18000000 » → « 1.8e+07 » — illisible dans une
|
||||||
|
formule censée être auditable) et (2) il arrondit à 6 chiffres significatifs
|
||||||
|
(« 123456.78 » → « 123457 ») ce qui FABRIQUE une base différente de la réelle —
|
||||||
|
exactement l'invention interdite par CLAUDE.md #6. `:f` (jamais exponentiel) puis
|
||||||
|
strip des zéros/point superflus donne un décimal fidèle et lisible, identique à
|
||||||
|
l'ancien pour les montants simples (« 200000 » reste « 200000 »)."""
|
||||||
|
s = f"{x:f}" # décimal complet, jamais de notation exponentielle
|
||||||
|
if "." in s:
|
||||||
|
s = s.rstrip("0").rstrip(".")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def _rate_label(value: Any) -> str:
|
def _rate_label(value: Any) -> str:
|
||||||
"""Libellé du taux tel qu'affiché dans la formule (verbatim si texte)."""
|
"""Libellé du taux tel qu'affiché dans la formule (verbatim si texte).
|
||||||
|
|
||||||
|
NB : `:g` est VOLONTAIRE et sûr ici (≠ montants → `_amount_label`). Un taux de
|
||||||
|
commission est borné (0-100), donc jamais en notation exponentielle (< 1e6) et
|
||||||
|
jamais au-delà de 6 chiffres significatifs : les deux pièges de `:g` documentés
|
||||||
|
sur `_amount_label` sont inatteignables sur ce domaine, et `:g` nettoie en prime
|
||||||
|
le bruit flottant (« 3.0 » → « 3 »). Ne pas « corriger » en `:f` (audit récurrent)."""
|
||||||
if not is_filled(value):
|
if not is_filled(value):
|
||||||
return "{taux_pct}"
|
return "{taux_pct}"
|
||||||
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||||
return f"{value:g} %"
|
return f"{value:g} %" # borné 0-100 : sûr, cf. docstring
|
||||||
return str(value).strip()
|
return str(value).strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -72,7 +99,7 @@ def compute_line(dossier: dict, event: dict) -> dict:
|
|||||||
devise = dossier.get("devise")
|
devise = dossier.get("devise")
|
||||||
|
|
||||||
montant = base * taux if (base is not None and taux is not None) else None
|
montant = base * taux if (base is not None and taux is not None) else None
|
||||||
base_lbl = f"{base:g}" if base is not None else f"{{{base_field}}}"
|
base_lbl = _amount_label(base) if base is not None else f"{{{base_field}}}"
|
||||||
formule = f"{base_lbl} × {_rate_label(event.get('taux_pct'))}"
|
formule = f"{base_lbl} × {_rate_label(event.get('taux_pct'))}"
|
||||||
|
|
||||||
manquants: list[str] = []
|
manquants: list[str] = []
|
||||||
|
|||||||
@@ -214,6 +214,24 @@ class TraceableCalc(unittest.TestCase):
|
|||||||
self.assertIn("2.5", line["formule"])
|
self.assertIn("2.5", line["formule"])
|
||||||
self.assertEqual(line["devise"], "USD")
|
self.assertEqual(line["devise"], "USD")
|
||||||
|
|
||||||
|
def test_formule_base_millions_pas_exponentiel(self):
|
||||||
|
# Régression : `:g` cassait les bases réelles ≥ 1e6 en notation exponentielle
|
||||||
|
# (« 18000000 » → « 1.8e+07 ») — illisible et non auditable dans la formule.
|
||||||
|
ev = {"update_value": "contrat", "role_id": "ventes-conseiller",
|
||||||
|
"base_field": "montant_contrat", "taux_pct": self.taux}
|
||||||
|
line = finance.compute_line({"montant_contrat": 18000000.0, "devise": "DOP"}, ev)
|
||||||
|
self.assertIn("18000000", line["formule"])
|
||||||
|
self.assertNotIn("e+", line["formule"])
|
||||||
|
self.assertNotIn("E+", line["formule"])
|
||||||
|
|
||||||
|
def test_formule_base_preserve_les_decimales(self):
|
||||||
|
# Régression : `:g` arrondissait à 6 chiffres significatifs (« 123456.78 »
|
||||||
|
# → « 123457 »), fabriquant une base ≠ de la réelle (anti-invention #6).
|
||||||
|
ev = {"update_value": "contrat", "role_id": "ventes-conseiller",
|
||||||
|
"base_field": "montant_contrat", "taux_pct": self.taux}
|
||||||
|
line = finance.compute_line({"montant_contrat": 123456.78, "devise": "USD"}, ev)
|
||||||
|
self.assertIn("123456.78", line["formule"])
|
||||||
|
|
||||||
def test_none_si_taux_absent(self):
|
def test_none_si_taux_absent(self):
|
||||||
ev = {"update_value": "contrat", "role_id": "ventes-conseiller",
|
ev = {"update_value": "contrat", "role_id": "ventes-conseiller",
|
||||||
"base_field": "montant_contrat", "taux_pct": None}
|
"base_field": "montant_contrat", "taux_pct": None}
|
||||||
|
|||||||
@@ -94,9 +94,18 @@ def _cond_apport(dossier: dict, cfg: GateConfig) -> dict:
|
|||||||
required = apport_requis_usd(dossier.get("prix_usd", 0), residence, cfg)
|
required = apport_requis_usd(dossier.get("prix_usd", 0), residence, cfg)
|
||||||
paid = apport_verse_usd(dossier)
|
paid = apport_verse_usd(dossier)
|
||||||
remaining = round(max(0.0, required - paid), 2)
|
remaining = round(max(0.0, required - paid), 2)
|
||||||
|
ok = paid + 1e-9 >= required
|
||||||
pct = _round1(min(100.0, (paid / required * 100.0) if required > 0 else 100.0))
|
pct = _round1(min(100.0, (paid / required * 100.0) if required > 0 else 100.0))
|
||||||
|
# Un versement dans la bande [99.95 %, 100 %[ (p. ex. 19 999/20 000 USD)
|
||||||
|
# remonterait à 100.0 par l'arrondi 1-décimale, alors que le gate BLOQUE
|
||||||
|
# (ok=False, remaining>0). La barre de progression afficherait « 100 % ✓ »
|
||||||
|
# tout en refusant la soumission, et la raison lirait « reste 1 USD · 100 %».
|
||||||
|
# Invariant d'affichage : percent == 100.0 ⟺ ok — un dossier incomplet
|
||||||
|
# plafonne à 99.9 (plus grande valeur 1-décimale strictement < 100).
|
||||||
|
if not ok and pct >= 100.0:
|
||||||
|
pct = 99.9
|
||||||
return {
|
return {
|
||||||
"ok": paid + 1e-9 >= required,
|
"ok": ok,
|
||||||
"required": required,
|
"required": required,
|
||||||
"paid": paid,
|
"paid": paid,
|
||||||
"remaining": remaining,
|
"remaining": remaining,
|
||||||
@@ -117,6 +126,15 @@ def _cond_documents(dossier: dict, cfg: GateConfig) -> dict:
|
|||||||
if statut in cfg.statuts_recevables:
|
if statut in cfg.statuts_recevables:
|
||||||
validated += 1
|
validated += 1
|
||||||
total = len(required_ids)
|
total = len(required_ids)
|
||||||
|
# Contrairement à _cond_apport (ratio USD *continu*, cf. fix 093114) et à
|
||||||
|
# l'agrégat (moyenne de 4, cf. fix 110124), AUCUN plafond « non ok → 99.9 »
|
||||||
|
# n'est requis ici : le dénominateur est la liste de documents du contrat
|
||||||
|
# (≤ 13 is_required), un domaine BORNÉ. round1 ne peut remonter à
|
||||||
|
# [99.95 %, 100 %[ par le bas qu'à partir de total ≥ 2000 (car
|
||||||
|
# (total-1)/total ≥ 0.9995) — inatteignable pour une liste de documents.
|
||||||
|
# Donc ok=False ⟹ percent < 100 tient déjà : l'invariant d'affichage
|
||||||
|
# percent == 100.0 ⟺ ok est préservé sans garde. (même logique que les 3
|
||||||
|
# sites `:g`-domaine-borné · ne pas ajouter de plafond redondant · #5)
|
||||||
pct = _round1((validated / total * 100.0) if total > 0 else 100.0)
|
pct = _round1((validated / total * 100.0) if total > 0 else 100.0)
|
||||||
return {
|
return {
|
||||||
"ok": validated == total,
|
"ok": validated == total,
|
||||||
@@ -135,6 +153,10 @@ def _cond_autorisations(dossier: dict, cfg: GateConfig) -> dict:
|
|||||||
if (by_id.get(aid) or {}).get("signature_date"):
|
if (by_id.get(aid) or {}).get("signature_date"):
|
||||||
signed += 1
|
signed += 1
|
||||||
total = len(required_ids)
|
total = len(required_ids)
|
||||||
|
# Domaine BORNÉ (4 autorisations au contrat), a fortiori inférieur au seuil
|
||||||
|
# total ≥ 2000 requis pour que round1 remonte à [99.95 %, 100 %[ par le bas :
|
||||||
|
# pas de plafond « non ok → 99.9 » nécessaire ici (cf. note _cond_documents).
|
||||||
|
# L'invariant percent == 100.0 ⟺ ok tient sans garde. (ne pas ajouter · #5)
|
||||||
pct = _round1((signed / total * 100.0) if total > 0 else 100.0)
|
pct = _round1((signed / total * 100.0) if total > 0 else 100.0)
|
||||||
return {"ok": signed == total, "total": total, "signed": signed, "percent": pct}
|
return {"ok": signed == total, "total": total, "signed": signed, "percent": pct}
|
||||||
|
|
||||||
@@ -162,6 +184,14 @@ def gate_status(dossier: dict, cfg: GateConfig) -> dict:
|
|||||||
overall = _round1(
|
overall = _round1(
|
||||||
sum(conditions[k]["percent"] for k in CONDITION_KEYS) / len(CONDITION_KEYS)
|
sum(conditions[k]["percent"] for k in CONDITION_KEYS) / len(CONDITION_KEYS)
|
||||||
)
|
)
|
||||||
|
# Même invariant qu'au niveau condition (cf. _cond_apport) mais à l'AGRÉGAT :
|
||||||
|
# la moyenne de 4 percents peut arrondir à 100.0 alors qu'une condition reste
|
||||||
|
# bloquante — p. ex. apport plafonné à 99.9 (non ok) + les 3 autres à 100.0 →
|
||||||
|
# (99.9 + 300) / 4 = 99.975 → round1 = 100.0. La barre GLOBALE afficherait
|
||||||
|
# « 100 % » tout en refusant la soumission (403). On plafonne donc à 99.9.
|
||||||
|
# Invariant d'affichage : overall_percent == 100.0 ⟺ can_submit.
|
||||||
|
if not can_submit and overall >= 100.0:
|
||||||
|
overall = 99.9
|
||||||
return {
|
return {
|
||||||
"can_submit": can_submit,
|
"can_submit": can_submit,
|
||||||
"conditions_total": len(CONDITION_KEYS),
|
"conditions_total": len(CONDITION_KEYS),
|
||||||
|
|||||||
@@ -118,6 +118,39 @@ class GateCheckTest(unittest.TestCase):
|
|||||||
self.assertTrue(status["conditions"]["apport_initial_complet"]["ok"])
|
self.assertTrue(status["conditions"]["apport_initial_complet"]["ok"])
|
||||||
self.assertEqual(status["conditions"]["apport_initial_complet"]["percent"], 100.0)
|
self.assertEqual(status["conditions"]["apport_initial_complet"]["percent"], 100.0)
|
||||||
|
|
||||||
|
def test_apport_quasi_complet_ninclut_pas_100pct(self):
|
||||||
|
# Bande [99.95 %, 100 %[ : l'arrondi 1-décimale de 99.995 % remonte à
|
||||||
|
# 100.0, mais le gate BLOQUE (ok=False, reste 1 USD). Un dossier
|
||||||
|
# incomplet ne doit JAMAIS afficher 100 % (barre pleine ⇔ soumissible).
|
||||||
|
d = _dossier_complet(self.cfg, self.spec)
|
||||||
|
d["paiements"] = [{"montant_verse_usd": 19999}] # 20 000 requis → 99.995 %
|
||||||
|
c = gate.gate_status(d, self.cfg)["conditions"]["apport_initial_complet"]
|
||||||
|
self.assertFalse(c["ok"])
|
||||||
|
self.assertAlmostEqual(c["remaining"], 1.0)
|
||||||
|
self.assertLess(c["percent"], 100.0)
|
||||||
|
self.assertEqual(c["percent"], 99.9)
|
||||||
|
# La raison de blocage ne doit pas dire « 100 % ».
|
||||||
|
_, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||||
|
self.assertTrue(any("Apport initial incomplet" in r and "100" not in r for r in reasons))
|
||||||
|
|
||||||
|
def test_overall_percent_agrege_ninclut_pas_100pct(self):
|
||||||
|
# Invariant d'affichage AGRÉGAT (frère du test per-condition ci-dessus,
|
||||||
|
# mais au niveau de la barre GLOBALE) : overall_percent == 100.0 ⟺
|
||||||
|
# can_submit. Cas limite = apport plafonné à 99.9 (non ok) + les 3 autres
|
||||||
|
# conditions ok à 100.0 → moyenne (99.9 + 300) / 4 = 99.975, qui arrondit
|
||||||
|
# à 100.0 par _round1. Sans la garde, la barre globale afficherait
|
||||||
|
# « 100 % » tout en bloquant la soumission (403) — auto-contradiction.
|
||||||
|
d = _dossier_complet(self.cfg, self.spec)
|
||||||
|
d["paiements"] = [{"montant_verse_usd": 19999}] # 20 000 requis → apport non ok
|
||||||
|
status = gate.gate_status(d, self.cfg)
|
||||||
|
self.assertFalse(status["can_submit"])
|
||||||
|
# les 3 autres conditions sont bien complètes (isolation du cas limite)
|
||||||
|
self.assertTrue(status["conditions"]["documents_exiges"]["ok"])
|
||||||
|
self.assertTrue(status["conditions"]["autorisations_signees"]["ok"])
|
||||||
|
self.assertTrue(status["conditions"]["validation_wag"]["ok"])
|
||||||
|
self.assertLess(status["overall_percent"], 100.0)
|
||||||
|
self.assertEqual(status["overall_percent"], 99.9)
|
||||||
|
|
||||||
def test_apport_manquant_bloque(self):
|
def test_apport_manquant_bloque(self):
|
||||||
d = _dossier_complet(self.cfg, self.spec)
|
d = _dossier_complet(self.cfg, self.spec)
|
||||||
d["paiements"] = [{"montant_verse_usd": 19999}]
|
d["paiements"] = [{"montant_verse_usd": 19999}]
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# Daily Report · 2026-08-12 · Claude Code DTP Worker
|
||||||
|
|
||||||
|
**Session** : `20260812_150201` — rapport du jour, émis initialement en `20260812_043034`
|
||||||
|
puis enrichi au fil des sessions (la **trace horodatée** de chaque commit contributeur est
|
||||||
|
dans les §Jalons ci-dessous ; l'historique des ré-émissions ne se re-liste plus ici).
|
||||||
|
|
||||||
|
**Point d'attestation** — ce rapport **n'épingle volontairement AUCUN hash de HEAD**. Un
|
||||||
|
hash de HEAD périme à chaque commit worker : le ré-épingler à chaque session était la **cause
|
||||||
|
structurelle** de la boucle de re-currency qui a consommé ce canal (le HEAD était re-figé dans
|
||||||
|
l'en-tête, donc périmé au commit suivant, donc « réconcilié » à nouveau, indéfiniment). On
|
||||||
|
supprime la cause plutôt que de la surveiller : le HEAD courant se **lit** (`git log -1`), il
|
||||||
|
ne se **fige** pas. Ce qui **fait foi** pour le stakeholder est la **matrice de régression**,
|
||||||
|
dérivée d'artefacts commités et **stable à `635 / 618 / 17` · PASS** jusqu'au prochain fix de
|
||||||
|
correctness (aucun chiffre figé · #6). Les hashes cités dans les §Jalons sont des **jalons
|
||||||
|
historiques** (immuables), pas une attestation de HEAD courant.
|
||||||
|
**Phase roadmap** : transition **V12 → V18** (Master Institutional Feasibility &
|
||||||
|
Bankability Engine). La roadmap 8-semaines classique (Sprints 1→8) reste **livrée +
|
||||||
|
gatée + verte** au dépôt ; le **moteur de faisabilité** V18 est rouvert par directive
|
||||||
|
mais sa séquence reste **intégralement suspendue à des arbitrages Michel** (voir
|
||||||
|
§« Chemin critique bloqué »). Aucun code moteur V18 n'est légitime tant que l'audit
|
||||||
|
préalable n'est pas approuvé (D-06 · #6).
|
||||||
|
|
||||||
|
## Contexte — le canal stakeholder s'arrêtait au `2026-08-11` (HEAD `35de616`)
|
||||||
|
|
||||||
|
Le dernier rapport roadmap (`2026-08-11`, currency `20260811_092842`) était épinglé à
|
||||||
|
`35de616`. À l'**émission initiale** de ce rapport (session `043034`), trois commits
|
||||||
|
avaient depuis porté l'arbre à `ee88d36`, dont **deux jalons worker `[DTP-Worker]`** que
|
||||||
|
ce canal ne reflétait pas encore. Ce rapport a repris le canal stakeholder à partir de là
|
||||||
|
et **re-dérive tous les chiffres depuis les artefacts commités** (aucun chiffre figé, #6).
|
||||||
|
|
||||||
|
## Jalons depuis le dernier rapport (`35de616` → `ee88d36`)
|
||||||
|
|
||||||
|
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **TEETH publiciste NBSP** | `39def91` (`20260811_095844`) | Ajout d'un test de parsing prix : l'**espace insécable** U+00A0 (inséré par Word/InDesign FR/ES comme séparateur de milliers dans les prix réels des `data_room`) doit être capté par la char-class Unicode-whitespace de `lib/parser.py`. Le test **mord** si la capture est réduite à de l'ASCII pur (seul le 1er chiffre survivrait : `8.0` au lieu de `8 850 000`) — même racine « détecter la **forme**, pas une **graphie** » que l'arc de fixes `audit_4big`. NBSP en escape explicite ` ` (0 octet invisible). | **Opposable** : +1 test verrou ; régression **628/611 → 629/612** (voir §État courant). 0 artefact reconstruit hors les artefacts régression/audit re-générés du même coup. |
|
||||||
|
| **RÉFÉRENCE design (Michel)** | `031b5e7` | Ajout par Michel du PNG `DASHBOARD_LUXURY_MODELE_REFERENCE_20260811` (design canonique OTO CONTRACTOR, dark+doré · #4). **Hors production repo** (asset de référence root-owned mode 600). | **Non opposable** (asset de référence · 0 code · 0 artefact). |
|
||||||
|
| **CLEANUP #5 orphelin `out_fresh/`** | `ee88d36` (`20260812_040034`) | Suppression de `frontend/portails/out_fresh/` (`workspace.json` + `MANIFEST.json`) : scratch-dir **byte-identique** à `out/`, commité une seule fois par accident (`f954cc1`, 2026-08-02), **produit par rien** (le générateur n'écrit que dans `out/`) et **référencé nulle part** (`git grep out_fresh` = 0). Il **échappait à TOUS les gates** (`check_artifacts` découvre par `find -type d -name out` = match exact) → bombe à retardement de staleness. Fix = `git rm -r` (réversible via historique, non créé par le worker, copie autoritaire `out/` intacte + byte-repro vérifié). Occurrence isolée → **pas de nouveau gate** (#5). | **Opposable** : `run_ci` inchangé **33/0/0** ; **0 artefact autoritaire** modifié (seul un doublon orphelin retiré). |
|
||||||
|
|
||||||
|
## Jalons du jour depuis l'émission initiale (`ee88d36` → `37c68ac`)
|
||||||
|
|
||||||
|
Trois commits worker **du même jour** ont suivi l'émission initiale de ce rapport. Tous
|
||||||
|
sont des travaux de **vérification / durcissement** : **0 code moteur V18** (bloqué D-06),
|
||||||
|
**artefacts byte-identiques** (matrice **stable** à `629/612/17` — confirmé par
|
||||||
|
`check_artifacts` ∈ `run_ci` vert).
|
||||||
|
|
||||||
|
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Déterminisme · ordre d'exécution des suites** | `a17d86a` (`20260812_050034`) | 4ᵉ axe de déterminisme (frères : hash-seed · locale/TZ · forward-compat) : ré-exécution de la matrice avec l'ordre des 24 suites **inversé** → totaux **byte-identiques**, 24/24 suites identiques. Fermé *by design* (chaque suite tourne dans son propre subprocess `cwd=abs_path`, agrégation somme-indépendante-de-l'ordre). Balayage **replayable**, pas un gate (#5). | **Non opposable** (lecture seule `/tmp`, 0 artefact touché). |
|
||||||
|
| **Vérif i18n SEO · asymétrie `fr=87·en=87·es=84`** | `1513e69` (`20260812_053034`) | Angle i18n frais : l'écart per-langue **prouvé NON-DÉFAUT** au byte-level (`country[es]` = « Republica Dominicana » coïncide avec la `localisation` générique de 5 projets → 3 collisions global↔projet dédupliquées en plus qu'en fr/en ; chaque chaîne unique existe exactement une fois). Chiffre déjà gaté par `check_readme_claims`. Livrable = **note explicative** au point de dedup (`keywords.py:106`), anti re-flag ; 0 logique/fixture/artefact touché. | **Opposable** : commentaire pur, `seo/out/*.json` re-build **byte-identique**. |
|
||||||
|
| **Hardening correctness · `criteria.py`** | `37c68ac` (`20260812_060044`) | 3ᵉ détecteur regex `_ADD_PARSER_RE` durci `add_parser\(` → `add_parser\s*\(` (tolère l'espace avant `(`, Python valide) — dernier non durci de la famille « détecter la **forme**, pas une **graphie** » (frères `ac595d0`/`fd99362`/`35de616`). Fix **latent** : aucun module n'utilise l'espace aujourd'hui (`grep` = 0) → `quality_report.json` re-build **byte-identique**. Occurrence one-shot → pas de gate (#5). | **Opposable** : `run_ci` inchangé **33/0/0** ; **0 artefact autoritaire** modifié. |
|
||||||
|
|
||||||
|
## Jalons du jour · 2ᵉ vague (`37c68ac` → `e7cc3c4`) — correctness à impact matrice
|
||||||
|
|
||||||
|
Contrairement à la 1ʳᵉ vague (vérif/hardening à artefacts byte-identiques), ces trois
|
||||||
|
commits corrigent des **bugs latents réels** de correctness de sortie sur des **entrées
|
||||||
|
financières / francophones réalistes RD**. Chacun ajoute un **test de régression à dents**
|
||||||
|
(prouvé qu'il échoue sans le fix), ce qui fait **croître la matrice** `629 → 633`. Tous :
|
||||||
|
**0 code moteur V18** (bloqué D-06), leçon commune « **le contexte de sortie dicte
|
||||||
|
l'encodeur, pas la donnée courante** ».
|
||||||
|
|
||||||
|
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Fix · rendu publiciste : URL hero percent-encodée** | `aeb30b8` (`20260812_070054`) | `_hero_media` composait `url('/static/projets/{code}/{fichier}')` en CSS mais échappait le nom de fichier via `html.escape` — **mauvais encodeur pour un contexte CSS** : un rendu réel `Vue d'ensemble.png` (FR) devient `Vue d'ensemble.png`, HTML-décodé par le navigateur en `Vue d'ensemble.png` → l'apostrophe **referme la chaîne `url('…')`** → hero **invisible silencieusement**. Fix = `urllib.parse.quote(fichier)` (`'`→`%27`, espace→`%20`) — valide en URL/HTML/CSS. `_esc` conservé pour l'attribut HTML. Test à dents `test_hero_media_url_percent_encode`. Byte-repro intact (ASCII simple inchangé). | **Opposable** : bug de rendu réel corrigé ; suite publiciste **24→25**, matrice **629/612 → 630/613** ; 4 artefacts régénérés (regression×3 + quality_report) + 3 fiches à compteurs = **pure propagation**. |
|
||||||
|
| **Hardening · segment `code` de la même `url('…')`** | `5af86e3` (`20260812_073055`) | Le fix `070054` avait percent-encodé `fichier` mais laissé **le second segment dynamique `code`** brut dans la MÊME `url()` — dernier segment non-encodé (frère exact de `criteria.py` : durcir le SEUL non-durci d'une famille). **Non déclenchable aujourd'hui** (codes `P01..P09`, jamais d'apostrophe → byte-repro inchangé) mais durcissement d'uniformité. Fix = `quote(code)`. Test à dents `test_hero_media_code_segment_percent_encoded`. | **Opposable** : uniformité de construction ; suite publiciste **25→26**, matrice **630/613 → 631/614** ; artefacts = pure propagation de compteur. |
|
||||||
|
| **Fix · commissions : formule `base×taux` cassait sur montants réels** | `e7cc3c4` (`20260812_080104`) | `compute_line` composait le libellé de base via `f"{base:g}"` — casse **deux fois** sur des montants immobiliers RD (unité ≈ **18 000 000 DOP**) : (1) **exponentiel dès 1e6** (`18000000`→`1.8e+07`, non-auditable) ; (2) **arrondi silencieux à 6 chiffres sig.** (`123456.78`→`123457`) = **base fabriquée**, l'invention même interdite #6 dans le module dont la docstring proclame l'anti-invention. Fix = helper `_amount_label` (`f"{x:f}"` + strip zéros), fidèle et identique à l'ancien pour tout montant simple. Code **runtime** (jamais appelé par `commissions_gen.py`) → **0 régénération de l'artefact du module**. 2 tests à dents. | **Opposable** : bug de sortie financière corrigé ; suite commissions **25→27**, matrice **631/614 → 633/616** ; artefacts régression/audit régénérés = pure propagation. |
|
||||||
|
|
||||||
|
## Jalon du jour · 3ᵉ vague (`e7cc3c4` → `85cd625`) — correctness gate financement
|
||||||
|
|
||||||
|
Un 4ᵉ fix worker de correctness a suivi la 2ᵉ vague, dans la même veine (bug **latent
|
||||||
|
réel** sur entrée réaliste, byte-repro-aveugle, test à dents) : **0 code moteur V18**
|
||||||
|
(bloqué D-06).
|
||||||
|
|
||||||
|
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Fix · `financement_bancaire` : barre pleine ✓ mais dossier BLOQUÉ** | `85cd625` (`20260812_093114`) | `gate.py:97` `_cond_apport` : `pct = _round1(min(100.0, paid/required*100))` arrondit à 1 décimale, tandis que `ok = paid + 1e-9 >= required` est **exact**. Un versement dans la bande **[99.95 %, 100 %[** (p. ex. **19 999 / 20 000 USD** = 99.995 %) remonte `percent: 100.0` (barre frontend « 100 % ✓ ») **tout en bloquant** la soumission (403 · `remaining: 1.0 USD`) — raison de blocage auto-contradictoire. Seul `_cond_apport` est atteignable (les 2 autres conditions = ratios entiers sur petites listes, `percent==100 ⟺ ok` structurel). Fix = garde `if not ok and pct >= 100.0: pct = 99.9` → invariant d'affichage **`percent == 100.0 ⟺ ok`** rétabli. `dossier_exemple` commité à 67.8 % (hors bande) → `gate_status_example.json` **byte-identique**. Test à dents `test_apport_quasi_complet_ninclut_pas_100pct`. | **Opposable** : bug d'affichage user-facing corrigé ; suite financement **35→36**, matrice **633/616 → 634/617** ; artefacts régression/audit régénérés = pure propagation. |
|
||||||
|
|
||||||
|
## Jalon du jour · 4ᵉ vague (`85cd625` → HEAD courant) — même bug, un niveau au-dessus
|
||||||
|
|
||||||
|
Un **5ᵉ fix worker de correctness** prolonge exactement le 4ᵉ : la 3ᵉ vague avait rétabli
|
||||||
|
l'invariant **`percent == 100.0 ⟺ ok`** *au niveau de chaque condition* (`_cond_apport`),
|
||||||
|
mais l'**agrégat** `overall_percent` (la **barre GLOBALE**) le violait toujours — même
|
||||||
|
classe de bug, un cran plus haut, que le fix précédent avait manqué.
|
||||||
|
|
||||||
|
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Fix · `financement_bancaire` : barre GLOBALE 100 % mais dossier BLOQUÉ** | `20260812_110124` | `gate.py` `gate_status` : `overall = _round1(Σ percents / 4)`. Cas limite = apport plafonné à **99.9** (non ok, cf. fix 3ᵉ vague) **+** les 3 autres conditions à **100.0** (ok) → moyenne `(99.9 + 300)/4 = 99.975` → `round1` = **100.0**. La barre **globale** afficherait « 100 % » tout en refusant la soumission (403) — même auto-contradiction que le fix `_cond_apport`, mais à l'agrégat. Fix = garde `if not can_submit and overall >= 100.0: overall = 99.9` → invariant d'affichage **`overall_percent == 100.0 ⟺ can_submit`**. `gate_status_example.json` commité à `overall_percent: 39.1` (hors bande) → **byte-identique**. Test à dents `test_overall_percent_agrege_ninclut_pas_100pct` (mord `100.0 not less than 100.0` sans la garde). | **Opposable** : bug d'affichage user-facing corrigé ; suite financement **36→37**, matrice **634/617 → 635/618** ; artefacts régénérés = pure propagation. |
|
||||||
|
|
||||||
|
## Jalons de vérification depuis la 4ᵉ vague (depuis le 5ᵉ fix `1992ee6` · au fil de l'eau)
|
||||||
|
|
||||||
|
> **Table roulante.** Après le 5ᵉ (et dernier) fix à impact matrice (`1992ee6`), **tout**
|
||||||
|
> commit worker du jour est **non opposable par construction** — vérification / durcissement
|
||||||
|
> anti-récurrence (commentaire pur), currency de canal, ou sweep en lecture seule. Aucun
|
||||||
|
> n'est un fix de code produit. La matrice reste donc **stable à 635/618/17** quel que soit
|
||||||
|
> le nombre de lignes ci-dessous ; elle ne bougera qu'au **prochain fix de correctness**.
|
||||||
|
> Cette section liste ces jalons au fil de l'eau (**six** à ce jour) sans re-figer le HEAD
|
||||||
|
> ailleurs — chiffres re-dérivés de l'artefact commité, jamais figés (#6) ; `check_artifacts`
|
||||||
|
> ∈ `run_ci` vert · rebuild **byte-identique** de tous les artefacts.
|
||||||
|
|
||||||
|
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Classe « percent==100 ⟺ ok » FERMÉE (4 conditions)** | `3849375` (`20260812_113134`) | Preuve que `financement_bancaire` est le **seul module à champ `percent`** ; les 2 conditions à domaine borné (documents ≤ 13 · autorisations = 4 ≪ seuil 2000) durcies par note anti-récurrence. Commentaires purs. | **Non opposable** (aucun fix · rebuild byte-identique). |
|
||||||
|
| **Classe « affichage arrondi ⟺ gate booléen » FERMÉE repo-wide** | `ea8458d` (`20260812_120139`) | Preuve d'**immunité structurelle** de `audit_4big` (score-au-seuil 95 = quantité UNIQUE affichage+verdict, ≠ financement à 2 quantités divergentes) + note in-situ `scoring.py`. Commentaire pur. | **Non opposable** (aucun fix · rebuild byte-identique). |
|
||||||
|
| **CURRENCY · réalignement de 2 citations `OPEN_DECISIONS_REGISTER.md`** | `4eb615b` (`20260812_123144`) | D-02 `gate.py:142-145 → :164-167` (les gardes des fixes du jour ont décalé `_cond_validation_wag`) · D-01 confotur `README:8-17 → :32-33` (bannière V18 préfixée) ; sweep complet = autres citations résolvent. Document éditorial. | **Non opposable** (0 code · 0 artefact). |
|
||||||
|
| **Chasse au défaut axe FRAIS date/time + parsing money/percent** | `fb2d3e1` (`20260812_130149`) | 3 candidats **RÉFUTÉS** domaine-borné (`signature_date="0"` corrompu · `banclib._pct` constante canonique · `rate()` séparateur-milliers inatteignable, taux borné 0-100) + note anti-récurrence in-situ sur `rate()` (un Explore l'avait mal-classé CONFIRMED). Commentaire pur. | **Non opposable** (aucun fix · rebuild byte-identique). |
|
||||||
|
| **CURRENCY · remontée des 4 jalons post-4ᵉ-vague dans ce rapport** | `e15129b` (`20260812_133154`) | Le canal s'arrêtait à `1992ee6` tout en s'affirmant « au HEAD courant » (grep-count 0 des 4 hashes) → §« Jalons de vérification » créée + en-tête `HEAD attesté` réaligné. Document éditorial. | **Non opposable** (0 code · 0 artefact). |
|
||||||
|
| **Nouvel axe FRAIS · mode d'arrondi (banquier / round-half-to-even)** | `4a13557` (`20260812_140156`) | Balayage repo-wide des **8 sites `round()`** de production → CLEAN teeth-prouvé, 0 défaut : `scorer.py:57` verdict-immune (`complete ⟹ filled==total ⟹ pts==20` exact) · `:81` immune par inclusion `champs_manquants` · `gate.py` montants au cent + percents déjà gardés (`85cd625`/`1992ee6`) · `parser.py` domaine entier · `audit_4big` immune (`ea8458d`). Sweep en lecture seule (journal only, **aucun mis-flag → pas de note in-situ**). | **Non opposable** (aucun fix · aucun artefact touché). |
|
||||||
|
|
||||||
|
## Chemin critique bloqué — séquence moteur V18 (inchangé)
|
||||||
|
|
||||||
|
La directive V18 (`DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md`) impose une
|
||||||
|
séquence *audit → validation Michel → Phase 1 → validation → Phases 2-15*. L'audit
|
||||||
|
préalable obligatoire (`OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md`) est **produit**
|
||||||
|
mais **en attente de validation** ; rien côté moteur n'est légitime avant l'approbation
|
||||||
|
D-06 (`OPEN_DECISIONS_REGISTER.md`). Les arbitrages ouverts restent :
|
||||||
|
|
||||||
|
| Décision | Objet | Statut |
|
||||||
|
|---|---|---|
|
||||||
|
| **D-06** 🔴 | Approbation de l'audit V18 (gate d'entrée Phase 1) | En attente Michel |
|
||||||
|
| **D-07** 🔴 | Formules financières canoniques (Master Data Model) | En attente Michel |
|
||||||
|
| **D-01** 🟠 | Périmètre juridique Section 13 (CONFOTUR seul, ou aussi Promesa/Fideicomiso/HOA) | Arbitrage de périmètre (aucun code ne génère ces contrats aujourd'hui) |
|
||||||
|
|
||||||
|
> **Action Michel** — l'approbation D-06 + les formules D-07 débloquent la Phase 1
|
||||||
|
> (Master Project Intake / Data Model). Escalation : WhatsApp `+18296296385` ·
|
||||||
|
> `mroy@worldactivitiesrd.com`.
|
||||||
|
|
||||||
|
## État courant · chiffres re-dérivés d'artefacts commités (HEAD courant)
|
||||||
|
|
||||||
|
| Métrique | Valeur | Source (re-calculée ce jour) |
|
||||||
|
|---|---|---|
|
||||||
|
| Gate de merge | **33 PASS · 0 FAIL · 0 SKIP** | `run_ci.sh` (= 8 statiques + 25 suites) |
|
||||||
|
| Gates statiques | **8** | `git ls-files 'ci/*.sh'` hors `lib.sh` |
|
||||||
|
| Suites de modules | **25** exécutées / **24** gatées régression | jobs `*-tests` de `gate.needs` (`.gitea/workflows/ci.yml`) ; la régression s'auto-exclut de son propre recensement |
|
||||||
|
| Matrice de régression | **635 exécutés · 618 passés · 17 skippés · 0 échec** — **PASS** | `qa/regression/out/regression_run.json` (`totals`) |
|
||||||
|
| Audit 4Big | **verdict PASS · 24 modules · score min 100/100** | `qa/audit_4big/out/quality_report.json` (`verdict` · `modules` · `coverage`) |
|
||||||
|
| Bijection couverture | **24 CI ↔ 24 registre** (`ok: true`, 0 manquant) | `quality_report.json` (`coverage`) |
|
||||||
|
|
||||||
|
Les **17 skips** sont l'oracle `jsonschema` OPTIONNEL neutralisé par `python -S` (par
|
||||||
|
design ; lecture stakeholder = **618 passés + 17 skippés**, **pas** 635 — voir baseline
|
||||||
|
régression). Trajectoire de la matrice depuis le rapport précédent (`628/611` à
|
||||||
|
`35de616`) : **+1** au `39def91` (test verrou NBSP publiciste, 628→**629**) ; stable à
|
||||||
|
`629/612` sur la 1ʳᵉ vague de vérif/hardening (`a17d86a`/`1513e69`/`37c68ac`, artefacts
|
||||||
|
byte-identiques) ; puis **2ᵉ vague de correctness** — **+1** au `aeb30b8` (630/613), **+1**
|
||||||
|
au `5af86e3` (631/614), **+2** au `e7cc3c4` (633/616) ; puis **3ᵉ vague** — **+1** au
|
||||||
|
`85cd625` (fix gate financement per-condition, **634/617**) ; puis **4ᵉ vague** — **+1**
|
||||||
|
au `1992ee6` (fix gate financement **agrégat** `overall_percent`, **635/618**) ; puis **six
|
||||||
|
commits de vérification / currency** (`3849375`/`ea8458d`/`4eb615b`/`fb2d3e1`/`e15129b`/`4a13557`)
|
||||||
|
**sans impact matrice** (commentaires purs + currency, rebuild byte-identique · trace dans la
|
||||||
|
table roulante ci-dessus, non ré-épinglée à un hash). Chaque incrément = un **test de
|
||||||
|
régression à dents** ajouté par un fix ; **aucun nouveau gate** (une divergence de compte
|
||||||
|
serait déjà structurellement rougie par `check_readme_claims`, ∈ `run_ci`). Les commits
|
||||||
|
non-test (`031b5e7` PNG · `ee88d36` cleanup orphelin · `cb8069b` notes `:g` pures) **ne
|
||||||
|
touchent aucun test**. Matrice courante **635/618/17** au HEAD courant.
|
||||||
|
|
||||||
|
## Portée / anti-churn (session `20260812_100114` · re-réconcilie `083114` au HEAD `85cd625`)
|
||||||
|
|
||||||
|
- **0 commande VPS** (#8) · **0 API externe payante** · **0 code moteur V18** (séquence
|
||||||
|
suspendue à Michel · #6).
|
||||||
|
- **0 fichier de production édité** cette session, **0 artefact reconstruit**, **0 gate
|
||||||
|
ajouté** (#5) : le livrable est la **re-réconciliation de ce rapport stakeholder** au
|
||||||
|
HEAD courant (`85cd625`) — currency du canal + remontée du **4ᵉ fix de correctness**
|
||||||
|
(`633→634`, gate `financement_bancaire`, commité après la réconciliation `083114`) +
|
||||||
|
chemin critique bloqué inchangé — + le journal `05_activity_log/`. Le commit intercalaire
|
||||||
|
`cb8069b` (notes `:g` pures · session `090114`) est sans impact matrice.
|
||||||
|
- Tous les chiffres ci-dessus dérivent d'artefacts **commités**, jamais figés (#6).
|
||||||
|
|
||||||
|
**Gate au dépôt** : `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**.
|
||||||
@@ -134,6 +134,11 @@ def derived(brief: dict) -> list[dict]:
|
|||||||
if total_unites is not None and pe_pct is not None:
|
if total_unites is not None and pe_pct is not None:
|
||||||
pe_brut = pe_pct * total_unites
|
pe_brut = pe_pct * total_unites
|
||||||
pe_unites = float(math.ceil(pe_brut))
|
pe_unites = float(math.ceil(pe_brut))
|
||||||
|
# `:g` sûr ici : pe_brut = 0.52 × NOMBRE d'unités (domaine borné à quelques
|
||||||
|
# centaines en immobilier réel) — jamais ≥ 1e6 (pas d'exponentiel) ni > 6
|
||||||
|
# chiffres significatifs, les deux pièges de `:g`. En prime `:g` nettoie le
|
||||||
|
# bruit flottant du produit (0.52 × 173 = 89.96000…1 → « 89.96 »). Les
|
||||||
|
# MONTANTS non bornés, eux, passent par _money (`,.0f`), jamais par `:g`.
|
||||||
pe_formule = (f"{CANONICAL['point_equilibre_pct']} × {int(total_unites)} = "
|
pe_formule = (f"{CANONICAL['point_equilibre_pct']} × {int(total_unites)} = "
|
||||||
f"{pe_brut:g}")
|
f"{pe_brut:g}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ def _int(n: Optional[float], placeholder: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _pct(n: Optional[float], placeholder: str) -> str:
|
def _pct(n: Optional[float], placeholder: str) -> str:
|
||||||
|
# `:g` sûr : un pourcentage (marge, taux) est borné (0-100), donc jamais en
|
||||||
|
# exponentiel (< 1e6) ni > 6 chiffres significatifs — les deux pièges de `:g`
|
||||||
|
# sont hors domaine ; `:g` nettoie en prime le bruit flottant. Les MONTANTS
|
||||||
|
# passent par _money (`,.0f`/`,.2f`), jamais par `:g`. Ne pas « corriger ».
|
||||||
if n is None:
|
if n is None:
|
||||||
return "{{" + placeholder + "}}"
|
return "{{" + placeholder + "}}"
|
||||||
return f"{n:g} %"
|
return f"{n:g} %"
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Artefacts de génération locale (jamais commités — produits à la demande).
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
build/
|
||||||
|
out/
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# V18 · Master Project Intake / Master Data Model (Phase 1)
|
||||||
|
|
||||||
|
**Moteur :** V18 · Phase 1/15 (première du plan « ORDRE DÉVELOPPEMENT » de la
|
||||||
|
[directive V18](../../../DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md)).
|
||||||
|
**Débloqué par :** [V18_GO_PHASE_1_20260812.md](../../../V18_GO_PHASE_1_20260812.md)
|
||||||
|
(D-06 APPROVED) + [V18_DECISIONS_D07_D08_20260812.md](../../../V18_DECISIONS_D07_D08_20260812.md)
|
||||||
|
(D-08 : sur-ensemble STRICT `brief.json` V12).
|
||||||
|
**Fondé sur :** [OTO V18 · Migration & Architecture Audit](../../../OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md)
|
||||||
|
§4 (data model) · §5 (provenance) · §11 (séquence).
|
||||||
|
**Statut :** livrable **prêt pour CP0** (Document Completeness) · en attente de la
|
||||||
|
validation Michel AVANT toute Phase 2 (séquence GO_PHASE_1 §Validation à chaque
|
||||||
|
étape). **Auto-score 4Big cible : ≥ 95/100** (doc + contrat schéma + couverture
|
||||||
|
de tests + CLI reproductible), aligné CLAUDE.md #5.
|
||||||
|
|
||||||
|
## Ce que livre la Phase 1 (et ce qu'elle NE livre PAS)
|
||||||
|
|
||||||
|
Phase 1 = **le data model, pas un moteur métier.** Conformément à l'audit §11
|
||||||
|
(« Zéro moteur métier tant que le data model n'est pas validé »), ce module ne
|
||||||
|
calcule AUCUNE faisabilité, AUCUN DCF/IRR/DSCR (ceux-là arrivent aux moteurs 4/8,
|
||||||
|
formules Big4 débloquées D-07), n'écrit rien sur le VPS (#8) et n'invente aucune
|
||||||
|
donnée (#6).
|
||||||
|
|
||||||
|
Il fournit le **contrat** du Master Dataset — « One Master Dataset · Multiple
|
||||||
|
Outputs » :
|
||||||
|
|
||||||
|
| Brique | Fichier | Rôle |
|
||||||
|
|---|---|---|
|
||||||
|
| Schéma maître | [`master_intake.schema.json`](master_intake.schema.json) | Sur-ensemble strict de [`brief.schema.json`](../generator/brief.schema.json) V12, inclus via `allOf`/`$ref` ; ajoute `dataset_version`, les registres et les défs `data_status`/`data_category`/`data_point`. |
|
||||||
|
| Modèle de données | [`mdmlib/master_data_model.py`](mdmlib/master_data_model.py) | Les **11 statuts** + **5 catégories** + `DataPoint` traçable + garde des canoniques #9/#10. |
|
||||||
|
| Evidence Register | [`mdmlib/evidence_register.py`](mdmlib/evidence_register.py) | Registre documentaire (id · version · date) + liens donnée→pièce + détection de provenance pendouillante. |
|
||||||
|
| CLI | [`master_intake_gen.py`](master_intake_gen.py) | `validate` (contrat + intégrité) et `maturity` (complétude honnête). |
|
||||||
|
| Fixture pilote | [`fixtures/intake_P01_coralis.json`](fixtures/intake_P01_coralis.json) | P01 Coralis · majoritairement `PENDING` (aucun chiffre inventé). |
|
||||||
|
| Tests | [`tests/test_master_intake.py`](tests/test_master_intake.py) | 32 tests · contrat, rétro-compat V12, garde canonique, intégrité preuves. |
|
||||||
|
|
||||||
|
## Les 11 statuts de donnée (ordre canonique)
|
||||||
|
|
||||||
|
`VERIFIED` · `CONFIRMED` · `SOURCE_BASED` · `CALCULATED` · `ESTIMATED` ·
|
||||||
|
`ASSUMPTION` · `TARGET` · `BANK_REQUIREMENT` · `PENDING` · `MISSING` ·
|
||||||
|
`NOT_APPLICABLE`
|
||||||
|
|
||||||
|
Une donnée n'est jamais un chiffre nu : c'est un `DataPoint` portant sa
|
||||||
|
provenance (`source_document` · `source_date` · `status` · `confidence` ·
|
||||||
|
`validated_by`). C'est la généralisation du patron `{formule + opérandes
|
||||||
|
sourcés/null}` déjà éprouvé dans `banclib/finance.py` (audit §5).
|
||||||
|
|
||||||
|
## Les 5 catégories de séparation
|
||||||
|
|
||||||
|
`INPUT` · `TARGET` · `ASSUMPTION` · `BANK_REQUIREMENT` · `CALCULATED` — le RÔLE de
|
||||||
|
la donnée dans le modèle (distinct du statut, qui dit sa fiabilité).
|
||||||
|
|
||||||
|
## Rétro-compatibilité V12 (D-08)
|
||||||
|
|
||||||
|
- Toute clé du `brief.json` V12 conserve son nom exact (le parser Publiciste
|
||||||
|
continue de lire les données V18, ignorant silencieusement les champs V18).
|
||||||
|
- `brief.schema.json` V12 est **inclus** (jamais recopié) via `allOf`/`$ref` sur
|
||||||
|
son `$id` canonique → source unique, zéro duplication.
|
||||||
|
- Les champs V18 sont ajoutés en **extension**, jamais en remplacement (test
|
||||||
|
`test_no_v12_key_shadowed_with_conflicting_type`).
|
||||||
|
- `dataset_version` distingue V12 vs V18.
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 master_intake_gen.py validate fixtures/intake_P01_coralis.json
|
||||||
|
python3 master_intake_gen.py maturity fixtures/intake_P01_coralis.json
|
||||||
|
python3 -m unittest discover -s tests -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Les lacunes `PENDING`/`MISSING` **ne sont pas des erreurs** : elles mesurent la
|
||||||
|
complétude (audit §4 · « la fraction de champs absents explosera au démarrage…
|
||||||
|
ce n'est pas un bug »). `validate` ne sort en erreur que sur une **violation de
|
||||||
|
contrat** (statut/catégorie inconnu, canonique saisi, preuve pendouillante).
|
||||||
|
|
||||||
|
## Anti-invention & angle mort déclaré (#6 · #8)
|
||||||
|
|
||||||
|
Les **A1-A20 fins** vivent dans la directive complète root-owned (57 chapitres),
|
||||||
|
illisible par le worker (audit §0). Ce module livre donc le **cadre** ouvert
|
||||||
|
(`intake_sections` clés `A1..A20`, contenu à confronter au CP0) — il ne fabrique
|
||||||
|
aucun champ A1-A20. Toute divergence avec la spec fine de Michel sera résolue à
|
||||||
|
la validation Phase 1.
|
||||||
|
|
||||||
|
## Prochain incrément (post-CP0)
|
||||||
|
|
||||||
|
Après validation Michel du data model (CP0), le module sera câblé au gate CI
|
||||||
|
(`.gitea/workflows/ci.yml` + régénération des méta-artefacts régression/4Big) et
|
||||||
|
Phase 2 (Document / Evidence Engine · généralisation `champs_manquants` → gate
|
||||||
|
CP0 automatisé) pourra démarrer.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"projet": "P01",
|
||||||
|
"nom": "Coralis",
|
||||||
|
"synthetique": true,
|
||||||
|
"dataset_version": "V18",
|
||||||
|
"sources": [
|
||||||
|
"CLAUDE.md §Projets (code projet canonique)",
|
||||||
|
"V18_GO_PHASE_1_20260812.md §Projet pilote (P01 Coralis)"
|
||||||
|
],
|
||||||
|
"masterplan": {
|
||||||
|
"terrain_m2": null,
|
||||||
|
"nb_unites": null,
|
||||||
|
"nb_phases": null
|
||||||
|
},
|
||||||
|
"intake_sections": {
|
||||||
|
"A1": { "titre": "Identification projet", "statut": "amorce" }
|
||||||
|
},
|
||||||
|
"document_register": [
|
||||||
|
{
|
||||||
|
"doc_id": "DOC-CLAUDE-MD",
|
||||||
|
"title": "CLAUDE.md — constitution du mandat (§Projets)",
|
||||||
|
"filename": "CLAUDE.md",
|
||||||
|
"version": "2026-08-05",
|
||||||
|
"source_date": "2026-08-05"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"data_register": [
|
||||||
|
{
|
||||||
|
"key": "projet.code",
|
||||||
|
"value": "P01",
|
||||||
|
"category": "INPUT",
|
||||||
|
"status": "SOURCE_BASED",
|
||||||
|
"source_document": "DOC-CLAUDE-MD",
|
||||||
|
"source_date": "2026-08-05",
|
||||||
|
"confidence": 1.0,
|
||||||
|
"validated_by": "Michel Roy",
|
||||||
|
"unit": null,
|
||||||
|
"note": "Code projet canonique ancré CLAUDE.md §Projets."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "programme.nb_unites",
|
||||||
|
"value": null,
|
||||||
|
"category": "INPUT",
|
||||||
|
"status": "PENDING",
|
||||||
|
"source_document": null,
|
||||||
|
"source_date": null,
|
||||||
|
"confidence": null,
|
||||||
|
"validated_by": null,
|
||||||
|
"unit": "unités",
|
||||||
|
"note": "En attente du brief maître P01 (CP0 Document Completeness)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "financier.dscr_cible",
|
||||||
|
"value": null,
|
||||||
|
"category": "BANK_REQUIREMENT",
|
||||||
|
"status": "PENDING",
|
||||||
|
"source_document": null,
|
||||||
|
"source_date": null,
|
||||||
|
"confidence": null,
|
||||||
|
"validated_by": null,
|
||||||
|
"unit": "ratio",
|
||||||
|
"note": "Exigence bancaire DSCR — formules Big4 débloquées D-07 ; valeur à confirmer par la banque (moteur 8 · D-02)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"evidence_register": [
|
||||||
|
{ "data_key": "projet.code", "doc_ids": ["DOC-CLAUDE-MD"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "https://oto.dtp/schemas/faisabilite/master_intake.schema.json",
|
||||||
|
"title": "OTO V18 · Master Project Intake (A1-A20) · Master Data Model",
|
||||||
|
"description": "Formulaire d'intake maître V18 qui remplace le Formulaire Briefing V12. D-08 (V18_DECISIONS_D07_D08_20260812.md) : SUR-ENSEMBLE STRICT de brief.json V12 — le contrat V12 brief.schema.json est INCLUS tel quel via allOf/$ref (toute clé V12 conserve son nom exact, rétro-compat parser Publiciste), et les champs V18 sont ajoutés en EXTENSION, jamais en remplacement. Le champ dataset_version distingue V12 vs V18. Anti-invention #6 : les A1-A20 fins restent ouverts tant que la spec root-owned de Michel (#8) n'est pas confrontée — ce schéma livre le CADRE (statuts · catégories · registres), pas des champs A1-A20 fabriqués.",
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [
|
||||||
|
{ "$ref": "https://oto.dtp/schemas/faisabilite/brief.schema.json" }
|
||||||
|
],
|
||||||
|
"required": ["projet", "dataset_version"],
|
||||||
|
"properties": {
|
||||||
|
"dataset_version": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["V12", "V18"],
|
||||||
|
"description": "Distingue un dataset legacy V12 d'un intake V18 (D-08 règle 6). Un intake maître V18 porte « V18 »."
|
||||||
|
},
|
||||||
|
"intake_sections": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Slots A1-A20 du Master Project Intake (directive V18 §Master Project Intake). Contenu FIN volontairement OUVERT (additionalProperties) : les 20 sections détaillées vivent dans la directive complète root-owned (#8, audit §0) et seront confrontées par Michel au CP0 — les fabriquer ici serait une invention (#6). Chaque clé est un id de section A1..A20.",
|
||||||
|
"propertyNames": { "pattern": "^A([1-9]|1[0-9]|20)$" },
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"document_register": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Catalogue des pièces probantes (Evidence Room · audit §18). Register documents / files / versions (V18_GO_PHASE_1).",
|
||||||
|
"items": { "$ref": "#/definitions/document_ref" }
|
||||||
|
},
|
||||||
|
"data_register": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Le Master Dataset : chaque donnée AVEC sa provenance (statut · source · confiance · validateur). One Master Dataset · Multiple Outputs — les 3 sorties (Bank/Full/Ops) sont des projections de ce registre.",
|
||||||
|
"items": { "$ref": "#/definitions/data_point" }
|
||||||
|
},
|
||||||
|
"evidence_register": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Liens donnée→pièce : rattache un data_key à un ou plusieurs doc_id du document_register (intégrité référentielle : pas de provenance fantôme).",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["data_key", "doc_ids"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"data_key": { "type": "string" },
|
||||||
|
"doc_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"data_status": {
|
||||||
|
"description": "Les 11 statuts obligatoires (V18_GO_PHASE_1 §Data status). Ordre canonique = du plus fiable au sans-objet ; miroir de mdmlib.master_data_model.STATUSES.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"VERIFIED",
|
||||||
|
"CONFIRMED",
|
||||||
|
"SOURCE_BASED",
|
||||||
|
"CALCULATED",
|
||||||
|
"ESTIMATED",
|
||||||
|
"ASSUMPTION",
|
||||||
|
"TARGET",
|
||||||
|
"BANK_REQUIREMENT",
|
||||||
|
"PENDING",
|
||||||
|
"MISSING",
|
||||||
|
"NOT_APPLICABLE"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data_category": {
|
||||||
|
"description": "Les 5 catégories de séparation (V18_GO_PHASE_1 §Séparation). Le RÔLE de la donnée dans le modèle ; miroir de mdmlib.master_data_model.CATEGORIES.",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"INPUT",
|
||||||
|
"TARGET",
|
||||||
|
"ASSUMPTION",
|
||||||
|
"BANK_REQUIREMENT",
|
||||||
|
"CALCULATED"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data_point": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Une donnée du Master Dataset avec sa provenance intégrale.",
|
||||||
|
"required": ["key", "category", "status"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"key": { "type": "string", "minLength": 1 },
|
||||||
|
"value": {},
|
||||||
|
"category": { "$ref": "#/definitions/data_category" },
|
||||||
|
"status": { "$ref": "#/definitions/data_status" },
|
||||||
|
"source_document": { "type": ["string", "null"] },
|
||||||
|
"source_date": { "type": ["string", "null"] },
|
||||||
|
"confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 1 },
|
||||||
|
"validated_by": { "type": ["string", "null"] },
|
||||||
|
"unit": { "type": ["string", "null"] },
|
||||||
|
"note": { "type": ["string", "null"] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"document_ref": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "Une pièce probante cataloguée (id unique · version · date).",
|
||||||
|
"required": ["doc_id", "title"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"doc_id": { "type": "string", "minLength": 1 },
|
||||||
|
"title": { "type": "string", "minLength": 1 },
|
||||||
|
"filename": { "type": ["string", "null"] },
|
||||||
|
"version": { "type": ["string", "null"] },
|
||||||
|
"source_date": { "type": ["string", "null"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""master_intake_gen · CLI Phase 1 du Master Project Intake V18.
|
||||||
|
|
||||||
|
PÉRIMÈTRE STRICT Phase 1 (audit §11 · « Zéro moteur métier tant que le data
|
||||||
|
model n'est pas validé ») : ce CLI NE GÉNÈRE aucune faisabilité, aucun chiffre
|
||||||
|
financier, aucun rendu. Il VALIDE un intake maître contre le contrat V18
|
||||||
|
(statuts · catégories · provenance · intégrité référentielle des preuves) et
|
||||||
|
imprime une MATURITÉ DATA honnête (présents vs lacunes) — la mesure de
|
||||||
|
complétude qui alimentera le CP0 (Document Completeness) au moteur 2.
|
||||||
|
|
||||||
|
Usage :
|
||||||
|
python3 master_intake_gen.py validate <intake.json>
|
||||||
|
python3 master_intake_gen.py maturity <intake.json>
|
||||||
|
|
||||||
|
Sortie : code 0 si le contrat est respecté (les lacunes PENDING/MISSING ne sont
|
||||||
|
PAS des erreurs — c'est la complétude, pas un échec) ; code 1 si le contrat est
|
||||||
|
violé (statut/catégorie inconnu · preuve pendouillante · JSON mal formé) ;
|
||||||
|
code 2 si l'usage est incorrect.
|
||||||
|
|
||||||
|
stdlib pur. L'oracle `jsonschema` est utilisé s'il est présent (avec
|
||||||
|
brief.schema.json V12 dans le store de résolution du $ref) ; sinon la validation
|
||||||
|
structurelle du contrat reste assurée par mdmlib (en mémoire).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections import Counter
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if _HERE not in sys.path:
|
||||||
|
sys.path.insert(0, _HERE)
|
||||||
|
|
||||||
|
from mdmlib import ( # noqa: E402
|
||||||
|
CATEGORIES,
|
||||||
|
STATUSES,
|
||||||
|
DataPoint,
|
||||||
|
Document,
|
||||||
|
DocumentRegister,
|
||||||
|
EvidenceRegister,
|
||||||
|
MasterDataError,
|
||||||
|
)
|
||||||
|
|
||||||
|
SCHEMA_PATH = os.path.join(_HERE, "master_intake.schema.json")
|
||||||
|
BRIEF_SCHEMA_PATH = os.path.join(_HERE, "..", "generator", "brief.schema.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_json(path: str) -> Any:
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
|
||||||
|
|
||||||
|
def build_registers(intake: dict) -> tuple[list[DataPoint], EvidenceRegister]:
|
||||||
|
"""Reconstruit le Master Dataset en mémoire — lève MasterDataError si le
|
||||||
|
contrat est violé (statut/catégorie/canonique/provenance fantôme)."""
|
||||||
|
docs = DocumentRegister()
|
||||||
|
for d in intake.get("document_register", []) or []:
|
||||||
|
docs.add(
|
||||||
|
Document(
|
||||||
|
doc_id=d.get("doc_id", ""),
|
||||||
|
title=d.get("title", ""),
|
||||||
|
filename=d.get("filename"),
|
||||||
|
version=d.get("version"),
|
||||||
|
source_date=d.get("source_date"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
points: list[DataPoint] = []
|
||||||
|
for dp in intake.get("data_register", []) or []:
|
||||||
|
points.append(
|
||||||
|
DataPoint(
|
||||||
|
key=dp.get("key", ""),
|
||||||
|
value=dp.get("value"),
|
||||||
|
category=dp.get("category", ""),
|
||||||
|
status=dp.get("status", ""),
|
||||||
|
source_document=dp.get("source_document"),
|
||||||
|
source_date=dp.get("source_date"),
|
||||||
|
confidence=dp.get("confidence"),
|
||||||
|
validated_by=dp.get("validated_by"),
|
||||||
|
unit=dp.get("unit"),
|
||||||
|
note=dp.get("note"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ev = EvidenceRegister(docs)
|
||||||
|
for link in intake.get("evidence_register", []) or []:
|
||||||
|
for doc_id in link.get("doc_ids", []) or []:
|
||||||
|
ev.link(link.get("data_key", ""), doc_id)
|
||||||
|
return points, ev
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_validate(intake: dict) -> list[str]:
|
||||||
|
"""Validation structurelle via l'oracle jsonschema (best-effort).
|
||||||
|
|
||||||
|
Renvoie [] si valide OU si l'oracle est absent (skip silencieux, comme les
|
||||||
|
17 skips « par design » de la matrice régression sous `python -S`)."""
|
||||||
|
try:
|
||||||
|
import jsonschema # type: ignore
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
schema = _load_json(SCHEMA_PATH)
|
||||||
|
brief = _load_json(BRIEF_SCHEMA_PATH)
|
||||||
|
store = {
|
||||||
|
schema.get("$id", ""): schema,
|
||||||
|
brief.get("$id", ""): brief,
|
||||||
|
}
|
||||||
|
resolver = jsonschema.RefResolver.from_schema(schema, store=store)
|
||||||
|
validator = jsonschema.Draft7Validator(schema, resolver=resolver)
|
||||||
|
return [e.message for e in validator.iter_errors(intake)]
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_validate(path: str) -> int:
|
||||||
|
intake = _load_json(path)
|
||||||
|
schema_errors = _schema_validate(intake)
|
||||||
|
try:
|
||||||
|
points, ev = build_registers(intake)
|
||||||
|
except MasterDataError as exc:
|
||||||
|
print(f"❌ Contrat Master Data Model violé : {exc}")
|
||||||
|
return 1
|
||||||
|
dangling = ev.dangling_evidence(points)
|
||||||
|
|
||||||
|
print(f"Intake : {path}")
|
||||||
|
print(f" dataset_version : {intake.get('dataset_version', '(absent)')}")
|
||||||
|
print(f" projet : {intake.get('projet', '(absent)')}")
|
||||||
|
print(f" pièces : {len(ev.documents)}")
|
||||||
|
print(f" données : {len(points)}")
|
||||||
|
if schema_errors:
|
||||||
|
print("❌ Non conforme au schéma master_intake.schema.json :")
|
||||||
|
for m in schema_errors:
|
||||||
|
print(f" · {m}")
|
||||||
|
return 1
|
||||||
|
if dangling:
|
||||||
|
print("❌ Preuve(s) pendouillante(s) (SOURCE_BASED sans pièce résolue) :")
|
||||||
|
for k in dangling:
|
||||||
|
print(f" · {k}")
|
||||||
|
return 1
|
||||||
|
print("✅ Contrat V18 respecté (statuts · catégories · provenance intègres).")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_maturity(path: str) -> int:
|
||||||
|
intake = _load_json(path)
|
||||||
|
try:
|
||||||
|
points, _ = build_registers(intake)
|
||||||
|
except MasterDataError as exc:
|
||||||
|
print(f"❌ Contrat Master Data Model violé : {exc}")
|
||||||
|
return 1
|
||||||
|
total = len(points)
|
||||||
|
present = sum(1 for p in points if p.present)
|
||||||
|
by_status: Counter[str] = Counter(p.status for p in points)
|
||||||
|
by_cat: Counter[str] = Counter(p.category for p in points)
|
||||||
|
|
||||||
|
pct = (100.0 * present / total) if total else 0.0
|
||||||
|
print(f"Maturité data · {path}")
|
||||||
|
print(f" données présentes : {present}/{total} ({pct:.1f} %)")
|
||||||
|
print(" par statut :")
|
||||||
|
for s in STATUSES:
|
||||||
|
if by_status.get(s):
|
||||||
|
print(f" {s:<16} {by_status[s]}")
|
||||||
|
print(" par catégorie :")
|
||||||
|
for c in CATEGORIES:
|
||||||
|
if by_cat.get(c):
|
||||||
|
print(f" {c:<16} {by_cat[c]}")
|
||||||
|
print(" (les lacunes PENDING/MISSING sont attendues au démarrage · audit §4)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
if len(argv) < 2 or argv[1] in ("-h", "--help"):
|
||||||
|
print(__doc__)
|
||||||
|
return 0 if (len(argv) >= 2 and argv[1] in ("-h", "--help")) else 2
|
||||||
|
cmd = argv[1]
|
||||||
|
if cmd in ("validate", "maturity"):
|
||||||
|
if len(argv) != 3:
|
||||||
|
print(f"usage : master_intake_gen.py {cmd} <intake.json>", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
return (cmd_validate if cmd == "validate" else cmd_maturity)(argv[2])
|
||||||
|
print(f"commande inconnue « {cmd} » (attendu : validate | maturity)", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv))
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Master Data Model V18 — bibliothèque (Phase 1 · Master Project Intake).
|
||||||
|
|
||||||
|
Débloquée par `V18_GO_PHASE_1_20260812.md` (D-06 APPROVED) +
|
||||||
|
`V18_DECISIONS_D07_D08_20260812.md` (D-08). Voir `../README.md`.
|
||||||
|
|
||||||
|
Expose le contrat en mémoire du Master Dataset (One Master Dataset · Multiple
|
||||||
|
Outputs) : statuts, catégories, DataPoint traçable, registres documentaires.
|
||||||
|
Aucun moteur métier ici (Zéro moteur tant que le data model n'est pas validé au
|
||||||
|
CP0 · audit §11).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .master_data_model import (
|
||||||
|
CANONICAL,
|
||||||
|
CATEGORIES,
|
||||||
|
STATUSES,
|
||||||
|
DataPoint,
|
||||||
|
MasterDataError,
|
||||||
|
assert_not_canonical,
|
||||||
|
is_filled,
|
||||||
|
is_present,
|
||||||
|
)
|
||||||
|
from .evidence_register import (
|
||||||
|
Document,
|
||||||
|
DocumentRegister,
|
||||||
|
EvidenceRegister,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CANONICAL",
|
||||||
|
"CATEGORIES",
|
||||||
|
"STATUSES",
|
||||||
|
"DataPoint",
|
||||||
|
"MasterDataError",
|
||||||
|
"assert_not_canonical",
|
||||||
|
"is_filled",
|
||||||
|
"is_present",
|
||||||
|
"Document",
|
||||||
|
"DocumentRegister",
|
||||||
|
"EvidenceRegister",
|
||||||
|
]
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"""Evidence Register V18 · registre documentaire + traçabilité des preuves.
|
||||||
|
|
||||||
|
Phase 1 · brique « Data Register + Evidence Register (traçabilité) » de
|
||||||
|
`V18_GO_PHASE_1_20260812.md` : « Register documents / files / versions ». C'est
|
||||||
|
l'amorce en mémoire du futur moteur 2 (Document / Evidence Engine · audit §11) —
|
||||||
|
ici on livre le REGISTRE et ses invariants d'intégrité, pas encore l'extraction.
|
||||||
|
|
||||||
|
Deux registres complémentaires :
|
||||||
|
|
||||||
|
* DocumentRegister · catalogue des pièces probantes (id · titre · fichier ·
|
||||||
|
version · date). Une pièce = une source citable, versionnée (audit §4.3 :
|
||||||
|
baseline vs actual = vues horodatées, jamais un écrasement).
|
||||||
|
* EvidenceRegister · lie chaque `DataPoint.key` à la (aux) pièce(s) qui la
|
||||||
|
justifie(nt). Ferme la « provenance fantôme » : un DataPoint SOURCE_BASED
|
||||||
|
dont le `source_document` ne résout à AUCUNE pièce enregistrée est un lien
|
||||||
|
pendouillant → détecté ici (`dangling_evidence`).
|
||||||
|
|
||||||
|
Anti-invention (#6) : le registre n'invente aucune pièce ; il ne fait que
|
||||||
|
cataloguer ce qui est déclaré et VÉRIFIER la cohérence référentielle. stdlib pur,
|
||||||
|
aucune écriture VPS (#8).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
|
from .master_data_model import DataPoint, MasterDataError
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Document:
|
||||||
|
"""Une pièce probante cataloguée (audit §18 · Evidence Room)."""
|
||||||
|
|
||||||
|
doc_id: str
|
||||||
|
title: str
|
||||||
|
filename: Optional[str] = None
|
||||||
|
version: Optional[str] = None
|
||||||
|
source_date: Optional[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.doc_id or not str(self.doc_id).strip():
|
||||||
|
raise MasterDataError("Document.doc_id ne peut être vide.")
|
||||||
|
if not self.title or not str(self.title).strip():
|
||||||
|
raise MasterDataError("Document.title ne peut être vide.")
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentRegister:
|
||||||
|
"""Catalogue des pièces probantes, indexé par `doc_id` unique."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._docs: dict[str, Document] = {}
|
||||||
|
|
||||||
|
def add(self, doc: Document) -> Document:
|
||||||
|
if doc.doc_id in self._docs:
|
||||||
|
raise MasterDataError(
|
||||||
|
f"doc_id dupliqué « {doc.doc_id} » — l'id de pièce doit être "
|
||||||
|
f"unique (registre = source citable non ambiguë)."
|
||||||
|
)
|
||||||
|
self._docs[doc.doc_id] = doc
|
||||||
|
return doc
|
||||||
|
|
||||||
|
def get(self, doc_id: str) -> Optional[Document]:
|
||||||
|
return self._docs.get(doc_id)
|
||||||
|
|
||||||
|
def has(self, doc_id: str) -> bool:
|
||||||
|
return doc_id in self._docs
|
||||||
|
|
||||||
|
def __len__(self) -> int:
|
||||||
|
return len(self._docs)
|
||||||
|
|
||||||
|
def ids(self) -> list[str]:
|
||||||
|
return sorted(self._docs)
|
||||||
|
|
||||||
|
|
||||||
|
class EvidenceRegister:
|
||||||
|
"""Lie les DataPoint à leurs pièces + vérifie l'intégrité référentielle."""
|
||||||
|
|
||||||
|
def __init__(self, documents: Optional[DocumentRegister] = None) -> None:
|
||||||
|
self.documents: DocumentRegister = documents or DocumentRegister()
|
||||||
|
# key du DataPoint -> ensemble ordonné de doc_id justificatifs
|
||||||
|
self._links: dict[str, list[str]] = {}
|
||||||
|
|
||||||
|
def link(self, data_key: str, doc_id: str) -> None:
|
||||||
|
"""Rattache la donnée `data_key` à la pièce `doc_id` (déjà cataloguée)."""
|
||||||
|
if not self.documents.has(doc_id):
|
||||||
|
raise MasterDataError(
|
||||||
|
f"lien vers une pièce inconnue « {doc_id} » — la cataloguer "
|
||||||
|
f"d'abord dans le DocumentRegister (pas de preuve fantôme)."
|
||||||
|
)
|
||||||
|
bucket = self._links.setdefault(data_key, [])
|
||||||
|
if doc_id not in bucket:
|
||||||
|
bucket.append(doc_id)
|
||||||
|
|
||||||
|
def evidence_for(self, data_key: str) -> list[str]:
|
||||||
|
"""Pièces justificatives (doc_id) déclarées pour cette donnée."""
|
||||||
|
return list(self._links.get(data_key, ()))
|
||||||
|
|
||||||
|
def dangling_evidence(self, data_points: Iterable[DataPoint]) -> list[str]:
|
||||||
|
"""Clés de DataPoint SOURCE_BASED dont la pièce citée n'est PAS cataloguée.
|
||||||
|
|
||||||
|
C'est le cœur de l'intégrité : un DataPoint qui prétend venir d'un
|
||||||
|
document (`status == SOURCE_BASED`) doit pouvoir résoudre ce document —
|
||||||
|
soit via un lien Evidence, soit via un `source_document` == doc_id connu.
|
||||||
|
Toute clé retournée est une provenance non résoluble (à corriger).
|
||||||
|
"""
|
||||||
|
missing: list[str] = []
|
||||||
|
for dp in data_points:
|
||||||
|
if dp.status != "SOURCE_BASED":
|
||||||
|
continue
|
||||||
|
linked = self.evidence_for(dp.key)
|
||||||
|
resolves = any(self.documents.has(d) for d in linked) or (
|
||||||
|
dp.source_document is not None
|
||||||
|
and self.documents.has(dp.source_document)
|
||||||
|
)
|
||||||
|
if not resolves:
|
||||||
|
missing.append(dp.key)
|
||||||
|
return sorted(missing)
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
"""Master Data Model V18 · statuts de donnée + catégories + DataPoint.
|
||||||
|
|
||||||
|
Phase 1 du moteur V18 (`DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md`
|
||||||
|
« ORDRE DÉVELOPPEMENT · 1. Master Project Intake / Data Model »), débloquée par
|
||||||
|
`V18_GO_PHASE_1_20260812.md` (D-06 APPROVED) et `V18_DECISIONS_D07_D08_20260812.md`
|
||||||
|
(D-08 : Master Intake A1-A20 = sur-ensemble STRICT de `brief.json` V12).
|
||||||
|
|
||||||
|
Principe « One Master Dataset · Multiple Outputs » : une donnée n'est jamais un
|
||||||
|
chiffre nu. C'est un `DataPoint` qui porte SA PROVENANCE — statut de fiabilité,
|
||||||
|
document source, date, confiance, validateur. Les 3 sorties V18 (Bank Package /
|
||||||
|
Full Feasibility / Ops) sont des PROJECTIONS de ce dataset, jamais des copies
|
||||||
|
éditables (audit §4/§7).
|
||||||
|
|
||||||
|
Anti-invention (CLAUDE.md #6 · directive V18 « NE PAS inventer données ») : le
|
||||||
|
modèle n'attribue AUCUNE valeur ni statut par défaut « optimiste ». Une donnée
|
||||||
|
absente est explicitement `MISSING`/`PENDING` — la complétude devient une mesure
|
||||||
|
honnête (audit §4, « la fraction de champs absents explosera au démarrage… ce
|
||||||
|
n'est pas un bug »).
|
||||||
|
|
||||||
|
Ce module ne FAIT rien d'externe : ni I/O réseau, ni écriture VPS (#8). stdlib
|
||||||
|
pur. Il définit le contrat en mémoire ; le schéma machine miroir est
|
||||||
|
`../master_intake.schema.json` (défs `data_status` / `data_category` /
|
||||||
|
`data_point`), tenu identique par la suite de tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Les 11 STATUTS de donnée (V18_GO_PHASE_1 §« Data status obligatoire »).
|
||||||
|
# ORDRE CANONIQUE = du plus fiable (prouvé/vérifié) au moins engageant
|
||||||
|
# (non applicable). Cet ordre est un CONTRAT : le schéma `data_status.enum` et la
|
||||||
|
# suite de tests le rejouent à l'identique (aucune ré-ordonnance silencieuse).
|
||||||
|
#
|
||||||
|
# VERIFIED · vérifié contre une pièce probante (audit/tiers)
|
||||||
|
# CONFIRMED · confirmé par une partie prenante autorisée
|
||||||
|
# SOURCE_BASED · repris verbatim d'un document source daté
|
||||||
|
# CALCULATED · dérivé par formule traçable d'opérandes sourcés
|
||||||
|
# ESTIMATED · estimation méthodologique déclarée (non un fait)
|
||||||
|
# ASSUMPTION · hypothèse de travail explicite (à valider)
|
||||||
|
# TARGET · objectif fixé (cible), pas une donnée constatée
|
||||||
|
# BANK_REQUIREMENT · exigence imposée par la banque / le prêteur
|
||||||
|
# PENDING · attendu, en cours de collecte
|
||||||
|
# MISSING · absent — devrait exister, non fourni
|
||||||
|
# NOT_APPLICABLE · sans objet pour ce projet
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
STATUSES: tuple[str, ...] = (
|
||||||
|
"VERIFIED",
|
||||||
|
"CONFIRMED",
|
||||||
|
"SOURCE_BASED",
|
||||||
|
"CALCULATED",
|
||||||
|
"ESTIMATED",
|
||||||
|
"ASSUMPTION",
|
||||||
|
"TARGET",
|
||||||
|
"BANK_REQUIREMENT",
|
||||||
|
"PENDING",
|
||||||
|
"MISSING",
|
||||||
|
"NOT_APPLICABLE",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Statuts qui dénotent une donnée EXPLOITABLE (présente et défendable) vs une
|
||||||
|
# lacune. Sert au futur gate CP0 (Document Completeness, moteur 2 · audit §6).
|
||||||
|
_ABSENT_STATUSES: frozenset[str] = frozenset({"PENDING", "MISSING"})
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Les 5 CATÉGORIES de séparation (V18_GO_PHASE_1 §« Séparation INPUT / TARGET /
|
||||||
|
# ASSUMPTION / BANK_REQUIREMENT / CALCULATED »). La CATÉGORIE dit le RÔLE de la
|
||||||
|
# donnée dans le modèle ; le STATUT dit sa FIABILITÉ. (TARGET / ASSUMPTION /
|
||||||
|
# BANK_REQUIREMENT / CALCULATED existent dans les deux axes à dessein : un même
|
||||||
|
# libellé peut être un rôle ET un état — ils ne sont pas confondus.)
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
CATEGORIES: tuple[str, ...] = (
|
||||||
|
"INPUT", # donnée d'entrée fournie (le brief V12 vit ici)
|
||||||
|
"TARGET", # objectif projet (ex. absorption cible)
|
||||||
|
"ASSUMPTION", # hypothèse de modélisation
|
||||||
|
"BANK_REQUIREMENT", # contrainte bancaire imposée
|
||||||
|
"CALCULATED", # sortie dérivée du moteur (jamais saisie)
|
||||||
|
)
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Paramètres CANONIQUES CLAUDE.md #9/#10 — RÉIMPOSÉS à l'identique du générateur
|
||||||
|
# V12 (`generator/genlib/model.py CANONICAL`), conformément à l'audit §8 non-
|
||||||
|
# négociable #9 : « À réimposer identiques dans le Master Data Model V18 (ne pas
|
||||||
|
# les rendre saisissables) ». Ils ne sont JAMAIS des DataPoint d'intake : ils
|
||||||
|
# sont IMPOSÉS par le moteur pour tous les projets → aucune invention projet par
|
||||||
|
# projet possible. La suite de tests re-vérifie l'égalité stricte avec la valeur
|
||||||
|
# du générateur (single source of truth : toute dérive est rouge).
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
CANONICAL: dict[str, str] = {
|
||||||
|
"frais_edition_pct": "3 %",
|
||||||
|
"marketing_pct": "8.5 %",
|
||||||
|
"point_equilibre_pct": "52 %",
|
||||||
|
"devises": "USD + DOP",
|
||||||
|
"paiement": "Cardnet",
|
||||||
|
"format_doc": "Letter US",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clés réservées : ne peuvent JAMAIS être une donnée d'intake saisissable.
|
||||||
|
_CANONICAL_KEYS: frozenset[str] = frozenset(CANONICAL)
|
||||||
|
|
||||||
|
|
||||||
|
class MasterDataError(ValueError):
|
||||||
|
"""Violation du contrat Master Data Model (statut/catégorie/canonique)."""
|
||||||
|
|
||||||
|
|
||||||
|
def is_present(status: str) -> bool:
|
||||||
|
"""True si le statut dénote une donnée exploitable (ni PENDING ni MISSING)."""
|
||||||
|
return status in STATUSES and status not in _ABSENT_STATUSES
|
||||||
|
|
||||||
|
|
||||||
|
def assert_not_canonical(key: str) -> None:
|
||||||
|
"""Rejette toute tentative de saisir un paramètre canonique comme intake.
|
||||||
|
|
||||||
|
Les 6 constantes #9/#10 sont IMPOSÉES par le moteur, jamais fournies par le
|
||||||
|
brief → les capter comme DataPoint rouvrirait la porte à l'invention projet
|
||||||
|
par projet (audit §8 · l'invariant de migration le plus important).
|
||||||
|
"""
|
||||||
|
if key in _CANONICAL_KEYS:
|
||||||
|
raise MasterDataError(
|
||||||
|
f"« {key} » est un paramètre CANONIQUE imposé (#9/#10) — "
|
||||||
|
f"interdit comme donnée d'intake saisissable (audit §8)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DataPoint:
|
||||||
|
"""Une donnée du Master Dataset AVEC sa provenance (audit §4/§5).
|
||||||
|
|
||||||
|
Le patron « valeur = {donnée + provenance + statut} » généralise le
|
||||||
|
`{formule + opérandes sourcés/null}` déjà éprouvé dans `banclib/finance.py`
|
||||||
|
(audit §5 : « exactement le bon patron pour un moteur financier auditable »).
|
||||||
|
|
||||||
|
Champs de traçabilité (V18_GO_PHASE_1 §« Data Register + Evidence Register :
|
||||||
|
chaque donnée: source_document · source_date · status · confidence ·
|
||||||
|
validated_by »).
|
||||||
|
"""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
value: Any
|
||||||
|
category: str
|
||||||
|
status: str
|
||||||
|
source_document: Optional[str] = None
|
||||||
|
source_date: Optional[str] = None
|
||||||
|
confidence: Optional[float] = None
|
||||||
|
validated_by: Optional[str] = None
|
||||||
|
unit: Optional[str] = None
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if not self.key or not str(self.key).strip():
|
||||||
|
raise MasterDataError("DataPoint.key ne peut être vide.")
|
||||||
|
assert_not_canonical(self.key)
|
||||||
|
if self.category not in CATEGORIES:
|
||||||
|
raise MasterDataError(
|
||||||
|
f"catégorie inconnue « {self.category} » "
|
||||||
|
f"(attendu ∈ {CATEGORIES})."
|
||||||
|
)
|
||||||
|
if self.status not in STATUSES:
|
||||||
|
raise MasterDataError(
|
||||||
|
f"statut inconnu « {self.status} » (attendu ∈ {STATUSES})."
|
||||||
|
)
|
||||||
|
if self.confidence is not None:
|
||||||
|
if not isinstance(self.confidence, (int, float)) or isinstance(
|
||||||
|
self.confidence, bool
|
||||||
|
):
|
||||||
|
raise MasterDataError("confidence doit être un nombre 0..1 ou None.")
|
||||||
|
if not 0.0 <= float(self.confidence) <= 1.0:
|
||||||
|
raise MasterDataError("confidence doit être dans [0, 1].")
|
||||||
|
# Cohérence provenance ⟺ statut : une donnée SOURCE_BASED prétend venir
|
||||||
|
# d'un document. Sa provenance peut être fournie SOIT en ligne
|
||||||
|
# (source_document), SOIT par un lien de l'Evidence Register — un
|
||||||
|
# DataPoint isolé ne connaît pas le registre, donc la complétude de
|
||||||
|
# provenance est une propriété du DATASET (EvidenceRegister.
|
||||||
|
# dangling_evidence), pas du point pris seul. On ne la vérifie donc PAS
|
||||||
|
# ici (sinon on interdirait le chemin « preuve par lien »).
|
||||||
|
#
|
||||||
|
# Symétriquement, un trou (PENDING/MISSING) ne peut pas porter une
|
||||||
|
# valeur non nulle — ce serait une donnée déguisée en lacune.
|
||||||
|
if self.status in _ABSENT_STATUSES and is_filled(self.value):
|
||||||
|
raise MasterDataError(
|
||||||
|
f"« {self.key} » est {self.status} mais porte une valeur — "
|
||||||
|
f"une lacune ne peut pas contenir de donnée."
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def present(self) -> bool:
|
||||||
|
"""Raccourci : la donnée est-elle exploitable (pas une lacune) ?"""
|
||||||
|
return is_present(self.status)
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
"""Projection sérialisable (ordre de clés stable pour byte-repro)."""
|
||||||
|
return {
|
||||||
|
"key": self.key,
|
||||||
|
"value": self.value,
|
||||||
|
"category": self.category,
|
||||||
|
"status": self.status,
|
||||||
|
"source_document": self.source_document,
|
||||||
|
"source_date": self.source_date,
|
||||||
|
"confidence": self.confidence,
|
||||||
|
"validated_by": self.validated_by,
|
||||||
|
"unit": self.unit,
|
||||||
|
"note": self.note,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_filled(value: Any) -> bool:
|
||||||
|
"""True si `value` est une donnée réelle (pas None ni placeholder vide).
|
||||||
|
|
||||||
|
Aligné sur `generator/genlib/model.py is_filled` : un vrai 0 numérique est
|
||||||
|
une donnée valide ; None / chaîne vide / liste vide ne le sont pas.
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return False
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return True
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return True
|
||||||
|
if isinstance(value, (list, tuple, dict)):
|
||||||
|
return len(value) > 0
|
||||||
|
return bool(str(value).strip())
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
"""Suite Phase 1 · Master Project Intake / Master Data Model V18.
|
||||||
|
|
||||||
|
Vérifie le CONTRAT (pas un moteur métier — il n'y en a pas encore, audit §11) :
|
||||||
|
statuts/catégories canoniques · DataPoint traçable · garde canonique #9/#10 ·
|
||||||
|
intégrité de l'Evidence Register · rétro-compat STRICTE avec brief.json V12
|
||||||
|
(D-08) · cohérence schéma ↔ code. stdlib pur ; l'oracle jsonschema est utilisé
|
||||||
|
s'il est présent, sinon ignoré (comme les 17 skips « par design » du dépôt).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_MODULE = os.path.dirname(_HERE)
|
||||||
|
for p in (_MODULE,):
|
||||||
|
if p not in sys.path:
|
||||||
|
sys.path.insert(0, p)
|
||||||
|
|
||||||
|
from mdmlib import ( # noqa: E402
|
||||||
|
CANONICAL,
|
||||||
|
CATEGORIES,
|
||||||
|
STATUSES,
|
||||||
|
DataPoint,
|
||||||
|
Document,
|
||||||
|
DocumentRegister,
|
||||||
|
EvidenceRegister,
|
||||||
|
MasterDataError,
|
||||||
|
assert_not_canonical,
|
||||||
|
is_present,
|
||||||
|
)
|
||||||
|
import master_intake_gen as cli # noqa: E402
|
||||||
|
|
||||||
|
SCHEMA_PATH = os.path.join(_MODULE, "master_intake.schema.json")
|
||||||
|
BRIEF_SCHEMA_PATH = os.path.join(_MODULE, "..", "generator", "brief.schema.json")
|
||||||
|
FIXTURE = os.path.join(_MODULE, "fixtures", "intake_P01_coralis.json")
|
||||||
|
GEN_CANONICAL_PATH = os.path.join(
|
||||||
|
_MODULE, "..", "generator", "genlib", "model.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load(path: str) -> dict:
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
|
return json.load(fh)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStatusesCategories(unittest.TestCase):
|
||||||
|
def test_exactly_11_statuses_in_canonical_order(self):
|
||||||
|
self.assertEqual(len(STATUSES), 11)
|
||||||
|
self.assertEqual(
|
||||||
|
STATUSES,
|
||||||
|
(
|
||||||
|
"VERIFIED", "CONFIRMED", "SOURCE_BASED", "CALCULATED",
|
||||||
|
"ESTIMATED", "ASSUMPTION", "TARGET", "BANK_REQUIREMENT",
|
||||||
|
"PENDING", "MISSING", "NOT_APPLICABLE",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_exactly_5_categories(self):
|
||||||
|
self.assertEqual(
|
||||||
|
CATEGORIES,
|
||||||
|
("INPUT", "TARGET", "ASSUMPTION", "BANK_REQUIREMENT", "CALCULATED"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_is_present_only_false_for_pending_and_missing(self):
|
||||||
|
absent = {s for s in STATUSES if not is_present(s)}
|
||||||
|
self.assertEqual(absent, {"PENDING", "MISSING"})
|
||||||
|
|
||||||
|
|
||||||
|
class TestSchemaMirrorsCode(unittest.TestCase):
|
||||||
|
"""Le schéma machine et le code Python doivent porter les MÊMES enums (pas
|
||||||
|
de dérive silencieuse entre les deux surfaces du contrat)."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.schema = _load(SCHEMA_PATH)
|
||||||
|
|
||||||
|
def test_schema_is_draft07(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.schema["$schema"], "http://json-schema.org/draft-07/schema#"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_status_enum_matches_code(self):
|
||||||
|
enum = self.schema["definitions"]["data_status"]["enum"]
|
||||||
|
self.assertEqual(tuple(enum), STATUSES)
|
||||||
|
|
||||||
|
def test_category_enum_matches_code(self):
|
||||||
|
enum = self.schema["definitions"]["data_category"]["enum"]
|
||||||
|
self.assertEqual(tuple(enum), CATEGORIES)
|
||||||
|
|
||||||
|
def test_dataset_version_enum(self):
|
||||||
|
self.assertEqual(
|
||||||
|
self.schema["properties"]["dataset_version"]["enum"], ["V12", "V18"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRetroCompatV12(unittest.TestCase):
|
||||||
|
"""D-08 : master_intake = SUR-ENSEMBLE STRICT du brief.json V12."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.master = _load(SCHEMA_PATH)
|
||||||
|
self.brief = _load(BRIEF_SCHEMA_PATH)
|
||||||
|
|
||||||
|
def test_master_includes_brief_by_ref(self):
|
||||||
|
refs = [a.get("$ref") for a in self.master.get("allOf", [])]
|
||||||
|
self.assertIn(self.brief["$id"], refs,
|
||||||
|
"brief.schema.json V12 doit être inclus via allOf/$ref (D-08 règle 2)")
|
||||||
|
|
||||||
|
def test_no_v12_key_shadowed_with_conflicting_type(self):
|
||||||
|
# Toute clé V12 réintroduite dans les properties V18 doit garder un type
|
||||||
|
# compatible (rétro-compat parser Publiciste · D-08 règle 1). Ici V18
|
||||||
|
# n'en réintroduit AUCUNE (extension pure) — on le prouve.
|
||||||
|
v12_keys = set(self.brief.get("properties", {}))
|
||||||
|
v18_keys = set(self.master.get("properties", {}))
|
||||||
|
self.assertEqual(
|
||||||
|
v12_keys & v18_keys, set(),
|
||||||
|
"les champs V18 sont ajoutés en EXTENSION, jamais en remplacement (D-08 règle 3)",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_dataset_version_field_present(self):
|
||||||
|
self.assertIn("dataset_version", self.master["properties"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestDataPoint(unittest.TestCase):
|
||||||
|
def test_valid_point(self):
|
||||||
|
dp = DataPoint(key="k", value=1, category="INPUT", status="VERIFIED")
|
||||||
|
self.assertTrue(dp.present)
|
||||||
|
self.assertEqual(dp.as_dict()["key"], "k")
|
||||||
|
|
||||||
|
def test_unknown_status_rejected(self):
|
||||||
|
with self.assertRaises(MasterDataError):
|
||||||
|
DataPoint(key="k", value=1, category="INPUT", status="NOPE")
|
||||||
|
|
||||||
|
def test_unknown_category_rejected(self):
|
||||||
|
with self.assertRaises(MasterDataError):
|
||||||
|
DataPoint(key="k", value=1, category="NOPE", status="VERIFIED")
|
||||||
|
|
||||||
|
def test_empty_key_rejected(self):
|
||||||
|
with self.assertRaises(MasterDataError):
|
||||||
|
DataPoint(key=" ", value=1, category="INPUT", status="VERIFIED")
|
||||||
|
|
||||||
|
def test_confidence_out_of_range_rejected(self):
|
||||||
|
with self.assertRaises(MasterDataError):
|
||||||
|
DataPoint(key="k", value=1, category="INPUT", status="VERIFIED",
|
||||||
|
confidence=1.5)
|
||||||
|
|
||||||
|
def test_confidence_bool_rejected(self):
|
||||||
|
with self.assertRaises(MasterDataError):
|
||||||
|
DataPoint(key="k", value=1, category="INPUT", status="VERIFIED",
|
||||||
|
confidence=True)
|
||||||
|
|
||||||
|
def test_source_based_constructs_without_inline_doc(self):
|
||||||
|
# La provenance d'un SOURCE_BASED peut venir d'un lien Evidence : le
|
||||||
|
# point seul se construit (la complétude est vérifiée au niveau dataset,
|
||||||
|
# cf. TestEvidenceRegister.test_dangling_evidence_detected).
|
||||||
|
dp = DataPoint(key="k", value="x", category="INPUT", status="SOURCE_BASED")
|
||||||
|
self.assertTrue(dp.present)
|
||||||
|
DataPoint(key="k", value="x", category="INPUT", status="SOURCE_BASED",
|
||||||
|
source_document="DOC-1")
|
||||||
|
|
||||||
|
def test_absent_status_cannot_carry_value(self):
|
||||||
|
with self.assertRaises(MasterDataError):
|
||||||
|
DataPoint(key="k", value=42, category="INPUT", status="PENDING")
|
||||||
|
# None est OK pour une lacune
|
||||||
|
DataPoint(key="k", value=None, category="INPUT", status="MISSING")
|
||||||
|
|
||||||
|
|
||||||
|
class TestCanonicalGuard(unittest.TestCase):
|
||||||
|
"""Les 6 canoniques #9/#10 ne peuvent jamais devenir un intake saisissable."""
|
||||||
|
|
||||||
|
def test_each_canonical_key_rejected(self):
|
||||||
|
for key in CANONICAL:
|
||||||
|
with self.assertRaises(MasterDataError):
|
||||||
|
assert_not_canonical(key)
|
||||||
|
with self.assertRaises(MasterDataError):
|
||||||
|
DataPoint(key=key, value="x", category="INPUT", status="VERIFIED")
|
||||||
|
|
||||||
|
def test_canonical_values_match_generator_v12(self):
|
||||||
|
# Single source of truth : les valeurs canoniques V18 sont IDENTIQUES à
|
||||||
|
# celles du générateur V12 (audit §8 « réimposer identiques »). On les
|
||||||
|
# relit dans generator/genlib/model.py sans l'importer (évite tout effet
|
||||||
|
# de bord d'import du module de prod) et on exige l'égalité stricte.
|
||||||
|
ns: dict = {}
|
||||||
|
with open(GEN_CANONICAL_PATH, encoding="utf-8") as fh:
|
||||||
|
src = fh.read()
|
||||||
|
# extrait le littéral CANONICAL = {...}
|
||||||
|
start = src.index("CANONICAL = {")
|
||||||
|
end = src.index("}", start) + 1
|
||||||
|
exec(src[start:end], ns) # noqa: S102 — littéral de constantes contrôlé
|
||||||
|
self.assertEqual(CANONICAL, ns["CANONICAL"])
|
||||||
|
|
||||||
|
|
||||||
|
class TestEvidenceRegister(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.docs = DocumentRegister()
|
||||||
|
self.docs.add(Document(doc_id="D1", title="Pièce 1"))
|
||||||
|
|
||||||
|
def test_duplicate_doc_id_rejected(self):
|
||||||
|
with self.assertRaises(MasterDataError):
|
||||||
|
self.docs.add(Document(doc_id="D1", title="doublon"))
|
||||||
|
|
||||||
|
def test_link_to_unknown_doc_rejected(self):
|
||||||
|
ev = EvidenceRegister(self.docs)
|
||||||
|
with self.assertRaises(MasterDataError):
|
||||||
|
ev.link("k", "GHOST")
|
||||||
|
|
||||||
|
def test_dangling_evidence_detected(self):
|
||||||
|
ev = EvidenceRegister(self.docs)
|
||||||
|
# SOURCE_BASED dont la pièce n'est pas cataloguée → pendouillant
|
||||||
|
dp = DataPoint(key="k", value="x", category="INPUT",
|
||||||
|
status="SOURCE_BASED", source_document="GHOST")
|
||||||
|
self.assertEqual(ev.dangling_evidence([dp]), ["k"])
|
||||||
|
|
||||||
|
def test_resolved_evidence_not_dangling(self):
|
||||||
|
ev = EvidenceRegister(self.docs)
|
||||||
|
dp = DataPoint(key="k", value="x", category="INPUT",
|
||||||
|
status="SOURCE_BASED", source_document="D1")
|
||||||
|
self.assertEqual(ev.dangling_evidence([dp]), [])
|
||||||
|
|
||||||
|
def test_link_resolves_evidence(self):
|
||||||
|
ev = EvidenceRegister(self.docs)
|
||||||
|
dp = DataPoint(key="k", value="x", category="INPUT",
|
||||||
|
status="SOURCE_BASED", source_document=None)
|
||||||
|
# pas de source_document mais un lien Evidence vers D1 → résout
|
||||||
|
ev.link("k", "D1")
|
||||||
|
self.assertEqual(ev.dangling_evidence([dp]), [])
|
||||||
|
|
||||||
|
|
||||||
|
class TestPilotFixture(unittest.TestCase):
|
||||||
|
"""Le fixture pilote P01 respecte le contrat et est un vrai sur-ensemble V12."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.intake = _load(FIXTURE)
|
||||||
|
|
||||||
|
def test_is_superset_of_v12_brief(self):
|
||||||
|
# champ requis V12 présent + version V18 + champs V18 d'extension
|
||||||
|
self.assertEqual(self.intake["projet"], "P01")
|
||||||
|
self.assertEqual(self.intake["dataset_version"], "V18")
|
||||||
|
self.assertIn("data_register", self.intake)
|
||||||
|
|
||||||
|
def test_synthetique_flagged(self):
|
||||||
|
self.assertTrue(self.intake.get("synthetique"),
|
||||||
|
"un fixture de test doit être marqué synthetique (jamais publiable)")
|
||||||
|
|
||||||
|
def test_contract_holds_and_no_dangling(self):
|
||||||
|
points, ev = cli.build_registers(self.intake)
|
||||||
|
self.assertEqual(ev.dangling_evidence(points), [])
|
||||||
|
# honnêteté : la majorité des données sont des lacunes au démarrage
|
||||||
|
present = [p for p in points if p.present]
|
||||||
|
self.assertTrue(len(present) >= 1)
|
||||||
|
|
||||||
|
def test_cli_validate_returns_zero(self):
|
||||||
|
self.assertEqual(cli.cmd_validate(FIXTURE), 0)
|
||||||
|
|
||||||
|
def test_cli_maturity_returns_zero(self):
|
||||||
|
self.assertEqual(cli.cmd_maturity(FIXTURE), 0)
|
||||||
|
|
||||||
|
def test_jsonschema_oracle_if_present(self):
|
||||||
|
try:
|
||||||
|
import jsonschema # noqa: F401
|
||||||
|
except Exception:
|
||||||
|
self.skipTest("jsonschema absent — oracle structurel ignoré (par design)")
|
||||||
|
errors = cli._schema_validate(self.intake)
|
||||||
|
self.assertEqual(errors, [], f"fixture non conforme : {errors}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestCliContractViolations(unittest.TestCase):
|
||||||
|
def test_bad_status_fixture_fails_validate(self):
|
||||||
|
import tempfile
|
||||||
|
bad = {
|
||||||
|
"projet": "P02",
|
||||||
|
"dataset_version": "V18",
|
||||||
|
"data_register": [
|
||||||
|
{"key": "x", "value": 1, "category": "INPUT", "status": "BOGUS"}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False,
|
||||||
|
encoding="utf-8") as fh:
|
||||||
|
json.dump(bad, fh)
|
||||||
|
path = fh.name
|
||||||
|
try:
|
||||||
|
self.assertEqual(cli.cmd_validate(path), 1)
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
{
|
|
||||||
"generated_from": "rbac_50_roles.json (permissions_cibles + portail par rôle)",
|
|
||||||
"spec_version": "1.0.0",
|
|
||||||
"source_version": "1.0.0",
|
|
||||||
"roadmap_ref": "04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md · Sprint 4 l.49 « Frontend Console : 5 portails (Ventes/Construction/Achat/Compta/Direction) »",
|
|
||||||
"counts": {
|
|
||||||
"workspaces": 5,
|
|
||||||
"roles_couverts": 44,
|
|
||||||
"liens_total": 53,
|
|
||||||
"raccourcis_total": 20
|
|
||||||
},
|
|
||||||
"brand": {
|
|
||||||
"note": "Tokens de marque luxury dark+doré (CLAUDE.md #4). Appliqués par la couche thème desk (Website Theme / CSS custom) déposée par l'agent Frontend sur le VPS — HORS de ce worker (#8). Repris ici pour le hand-off, jamais fabriqués.",
|
|
||||||
"fond": {
|
|
||||||
"valeur": "#0a0a12",
|
|
||||||
"source": "CLAUDE.md #4"
|
|
||||||
},
|
|
||||||
"accent": {
|
|
||||||
"valeur": "#f0b429",
|
|
||||||
"source": "CLAUDE.md #4"
|
|
||||||
},
|
|
||||||
"polices": [
|
|
||||||
{
|
|
||||||
"valeur": "Fraunces",
|
|
||||||
"source": "CLAUDE.md #4"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"valeur": "Cormorant Garamond",
|
|
||||||
"source": "CLAUDE.md #4"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"module_note": "Workspace.module laisse a None (a_confirmer) : l'agent ERPNext Backend le fixe au module de l'app OTO a l'import (#8). Le worker ne fabrique pas de nom d'app.",
|
|
||||||
"doctypes_custom_a_creer": [
|
|
||||||
"CONFOTUR Application",
|
|
||||||
"Faisabilité",
|
|
||||||
"Publiciste Log"
|
|
||||||
],
|
|
||||||
"workspaces": [
|
|
||||||
{
|
|
||||||
"portail": "ventes",
|
|
||||||
"workspace": "OTO Ventes",
|
|
||||||
"nb_cartes": 4,
|
|
||||||
"nb_raccourcis": 4,
|
|
||||||
"nb_liens": 11,
|
|
||||||
"nb_roles": 12,
|
|
||||||
"doctypes_custom": [
|
|
||||||
"CONFOTUR Application",
|
|
||||||
"Publiciste Log"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"portail": "construction",
|
|
||||||
"workspace": "OTO Construction",
|
|
||||||
"nb_cartes": 4,
|
|
||||||
"nb_raccourcis": 4,
|
|
||||||
"nb_liens": 9,
|
|
||||||
"nb_roles": 10,
|
|
||||||
"doctypes_custom": [
|
|
||||||
"Faisabilité"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"portail": "achat",
|
|
||||||
"workspace": "OTO Achat",
|
|
||||||
"nb_cartes": 3,
|
|
||||||
"nb_raccourcis": 4,
|
|
||||||
"nb_liens": 8,
|
|
||||||
"nb_roles": 5,
|
|
||||||
"doctypes_custom": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"portail": "compta",
|
|
||||||
"workspace": "OTO Compta",
|
|
||||||
"nb_cartes": 4,
|
|
||||||
"nb_raccourcis": 4,
|
|
||||||
"nb_liens": 11,
|
|
||||||
"nb_roles": 8,
|
|
||||||
"doctypes_custom": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"portail": "direction",
|
|
||||||
"workspace": "OTO Direction",
|
|
||||||
"nb_cartes": 4,
|
|
||||||
"nb_raccourcis": 4,
|
|
||||||
"nb_liens": 14,
|
|
||||||
"nb_roles": 9,
|
|
||||||
"doctypes_custom": [
|
|
||||||
"CONFOTUR Application",
|
|
||||||
"Faisabilité"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|||||||
import html
|
import html
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from . import branding
|
from . import branding
|
||||||
|
|
||||||
@@ -88,8 +89,18 @@ def _hero_media(projet: dict[str, Any]) -> str:
|
|||||||
if not hero:
|
if not hero:
|
||||||
return ' <div class="media"></div>'
|
return ' <div class="media"></div>'
|
||||||
# Convention de chemin public (miroir de /opt/oto/sites/static/projets/pXX/).
|
# Convention de chemin public (miroir de /opt/oto/sites/static/projets/pXX/).
|
||||||
|
# Le nom de rendu réel peut contenir espaces/apostrophes/accents (« Vue
|
||||||
|
# d'ensemble.png », « façade nord.webp ») : on percent-encode CHAQUE segment
|
||||||
|
# dynamique de l'URL (code ET fichier) pour qu'elle reste valide DANS le
|
||||||
|
# url('…') CSS. Sans cela, une apostrophe — une fois l'attribut style
|
||||||
|
# HTML-décodé par le navigateur — referme la chaîne CSS et casse
|
||||||
|
# silencieusement le background-image. quote() est le SEUL encodeur correct
|
||||||
|
# pour un segment d'URL (html.escape ne l'est pas) : `code` est un identifiant
|
||||||
|
# contrôlé (P01..P09) aujourd'hui, mais on l'encode par uniformité de
|
||||||
|
# construction — le contexte, pas la donnée courante, dicte l'encodeur.
|
||||||
|
# (_esc protège l'attribut HTML ; quote protège le contexte CSS interne.)
|
||||||
code = projet["code"].lower()
|
code = projet["code"].lower()
|
||||||
src = f"/static/projets/{code}/{hero['fichier']}"
|
src = f"/static/projets/{quote(code)}/{quote(hero['fichier'])}"
|
||||||
style = f"background-image:url('{_esc(src)}')"
|
style = f"background-image:url('{_esc(src)}')"
|
||||||
return f' <div class="media" style="{style}" role="img" aria-label="{_esc(projet.get("nom",""))}"></div>'
|
return f' <div class="media" style="{style}" role="img" aria-label="{_esc(projet.get("nom",""))}"></div>'
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,42 @@ class TestGenerator(unittest.TestCase):
|
|||||||
self.assertNotIn("USD", p02, "aucun prix ne doit apparaître pour P02")
|
self.assertNotIn("USD", p02, "aucun prix ne doit apparaître pour P02")
|
||||||
self.assertNotIn("prix-depuis", p02)
|
self.assertNotIn("prix-depuis", p02)
|
||||||
|
|
||||||
|
def test_hero_media_url_percent_encode(self):
|
||||||
|
# Un nom de rendu réel peut contenir apostrophe/espace/accents (FR/ES) :
|
||||||
|
# l'URL doit être percent-encodée pour rester valide DANS le url('…') CSS.
|
||||||
|
# Sinon l'apostrophe, une fois l'attribut style HTML-décodé, referme la
|
||||||
|
# chaîne CSS et casse le background-image (régression silencieuse).
|
||||||
|
projet = {
|
||||||
|
"code": "P07",
|
||||||
|
"nom": "Aqua Terra",
|
||||||
|
"statut": "disponible",
|
||||||
|
"rendus": [{"fichier": "Vue d'ensemble.png", "hero": True}],
|
||||||
|
}
|
||||||
|
media = generator._hero_media(projet)
|
||||||
|
self.assertIn("Vue%20d%27ensemble.png", media)
|
||||||
|
# L'apostrophe brute ne doit JAMAIS survivre dans le url('…').
|
||||||
|
import html as _h
|
||||||
|
self.assertNotIn("Vue d'ensemble", _h.unescape(media))
|
||||||
|
|
||||||
|
def test_hero_media_code_segment_percent_encoded(self):
|
||||||
|
# Le segment `code` est l'AUTRE segment dynamique de la même url('…').
|
||||||
|
# Un code contenant un caractère spécial (théorique : codes réels = P01..P09)
|
||||||
|
# doit lui aussi être percent-encodé — quote() est le SEUL encodeur correct
|
||||||
|
# pour un segment d'URL. Sinon _esc(html) laisserait l'apostrophe, une fois
|
||||||
|
# l'attribut style HTML-décodé, refermer la chaîne CSS (même bug que fichier).
|
||||||
|
projet = {
|
||||||
|
"code": "P'X",
|
||||||
|
"nom": "Théorique",
|
||||||
|
"statut": "disponible",
|
||||||
|
"rendus": [{"fichier": "hero.jpg", "hero": True}],
|
||||||
|
}
|
||||||
|
media = generator._hero_media(projet)
|
||||||
|
self.assertIn("/static/projets/p%27x/hero.jpg", media)
|
||||||
|
import html as _h
|
||||||
|
# Après HTML-décodage de l'attribut style, aucune apostrophe brute ne survit
|
||||||
|
# dans le segment de chemin (elle refermerait le url('…')).
|
||||||
|
self.assertNotIn("projets/p'x", _h.unescape(media))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _article(html: str, code: str) -> str:
|
def _article(html: str, code: str) -> str:
|
||||||
start = html.index(f'data-code="{code}"')
|
start = html.index(f'data-code="{code}"')
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
"criterion": "TESTS",
|
"criterion": "TESTS",
|
||||||
"weight": 25,
|
"weight": 25,
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"evidence": "24 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
"evidence": "26 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"criterion": "CLI",
|
"criterion": "CLI",
|
||||||
@@ -366,7 +366,7 @@
|
|||||||
"criterion": "TESTS",
|
"criterion": "TESTS",
|
||||||
"weight": 25,
|
"weight": 25,
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"evidence": "25 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
"evidence": "27 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"criterion": "CLI",
|
"criterion": "CLI",
|
||||||
@@ -450,7 +450,7 @@
|
|||||||
"criterion": "TESTS",
|
"criterion": "TESTS",
|
||||||
"weight": 25,
|
"weight": 25,
|
||||||
"passed": true,
|
"passed": true,
|
||||||
"evidence": "35 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
"evidence": "37 méthodes test_* dans 1 fichier(s) (seuil 8)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"criterion": "CLI",
|
"criterion": "CLI",
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ import re
|
|||||||
# autorité byte-gatée de la colonne « Tests » des fiches — les deux compteurs du
|
# autorité byte-gatée de la colonne « Tests » des fiches — les deux compteurs du
|
||||||
# MÊME concept doivent coïncider (sinon dérive d'évidence #6, cf. le fix ASCII).
|
# MÊME concept doivent coïncider (sinon dérive d'évidence #6, cf. le fix ASCII).
|
||||||
_TEST_DEF_RE = re.compile(r"^\s*def\s+(test_\w+)\s*\(", re.MULTILINE)
|
_TEST_DEF_RE = re.compile(r"^\s*def\s+(test_\w+)\s*\(", re.MULTILINE)
|
||||||
_ADD_PARSER_RE = re.compile(r"add_parser\(|add_subparsers\(")
|
# `\s*` avant `(` : `add_subparsers ()` / `add_parser ("build")` sont du Python
|
||||||
|
# valide (PEP 8 déconseille l'espace mais le langage l'autorise). Même doctrine
|
||||||
|
# que ses détecteurs sœurs `_TEST_DEF_RE` / `_MAIN_GUARD_RE` — détecter la forme,
|
||||||
|
# pas une graphie (sinon sous-scorage silencieux d'un CLI sain · #6).
|
||||||
|
_ADD_PARSER_RE = re.compile(r"add_parser\s*\(|add_subparsers\s*\(")
|
||||||
# Garde `if __name__ == "__main__":` — les DEUX styles de guillemets sont du
|
# Garde `if __name__ == "__main__":` — les DEUX styles de guillemets sont du
|
||||||
# Python valide (PEP 8 n'impose rien), l'ordre des opérandes peut être inversé
|
# Python valide (PEP 8 n'impose rien), l'ordre des opérandes peut être inversé
|
||||||
# (« Yoda »), et l'espacement autour de `==` est libre. Un littéral figé
|
# (« Yoda »), et l'espacement autour de `==` est libre. Un littéral figé
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ def score_module(spec: dict, module: dict) -> dict:
|
|||||||
f"Archétype {archetype} sans critère applicable — spec incohérent.")
|
f"Archétype {archetype} sans critère applicable — spec incohérent.")
|
||||||
|
|
||||||
score = _round_half_up(100.0 * earned / total)
|
score = _round_half_up(100.0 * earned / total)
|
||||||
|
# Quantité UNIQUE par design : le verdict dérive du MÊME `score` arrondi que
|
||||||
|
# celui affiché (jamais d'une note brute parallèle). Immunité structurelle au
|
||||||
|
# bug « barre à 100 % ⟺ gate ouvert » corrigé côté financement_bancaire (là,
|
||||||
|
# `percent` arrondi et `ok` exact étaient DEUX quantités divergentes en bord de
|
||||||
|
# seuil). Ici affichage et gate sont le même entier → ne PAS scinder en
|
||||||
|
# brut-vs-arrondi (ré-introduirait précisément la divergence). Idem INV6/INV7
|
||||||
|
# du générateur, qui recomputent verdict sur ce `score` (pas sur earned/total brut).
|
||||||
verdict = "PASS" if score >= thresholds["pass_score"] else "FAIL"
|
verdict = "PASS" if score >= thresholds["pass_score"] else "FAIL"
|
||||||
return {
|
return {
|
||||||
"id": module["id"],
|
"id": module["id"],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"verdict": "PASS",
|
"verdict": "PASS",
|
||||||
"suites": 24,
|
"suites": 24,
|
||||||
"test_methods": 629,
|
"test_methods": 635,
|
||||||
"coverage_ok": true,
|
"coverage_ok": true,
|
||||||
"artifacts": [
|
"artifacts": [
|
||||||
"regression_plan.json"
|
"regression_plan.json"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
"in_gate": true,
|
"in_gate": true,
|
||||||
"has_tests_dir": true,
|
"has_tests_dir": true,
|
||||||
"test_files": 1,
|
"test_files": 1,
|
||||||
"test_methods": 25
|
"test_methods": 27
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "crm.dossier_vente",
|
"id": "crm.dossier_vente",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"in_gate": true,
|
"in_gate": true,
|
||||||
"has_tests_dir": true,
|
"has_tests_dir": true,
|
||||||
"test_files": 1,
|
"test_files": 1,
|
||||||
"test_methods": 35
|
"test_methods": 37
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "crm.workflow_vente",
|
"id": "crm.workflow_vente",
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
"in_gate": true,
|
"in_gate": true,
|
||||||
"has_tests_dir": true,
|
"has_tests_dir": true,
|
||||||
"test_files": 1,
|
"test_files": 1,
|
||||||
"test_methods": 24
|
"test_methods": 26
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "qa.acceptance",
|
"id": "qa.acceptance",
|
||||||
@@ -295,7 +295,7 @@
|
|||||||
"totals": {
|
"totals": {
|
||||||
"suites": 24,
|
"suites": 24,
|
||||||
"test_files": 24,
|
"test_files": 24,
|
||||||
"test_methods": 629,
|
"test_methods": 635,
|
||||||
"min_methods": 10,
|
"min_methods": 10,
|
||||||
"under_threshold": 0
|
"under_threshold": 0
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
{
|
{
|
||||||
"id": "crm.commissions",
|
"id": "crm.commissions",
|
||||||
"path": "crm/commissions",
|
"path": "crm/commissions",
|
||||||
"expected_methods": 25,
|
"expected_methods": 27,
|
||||||
"ran": 25,
|
"ran": 27,
|
||||||
"passed": 24,
|
"passed": 26,
|
||||||
"failures": 0,
|
"failures": 0,
|
||||||
"errors": 0,
|
"errors": 0,
|
||||||
"skipped": 1,
|
"skipped": 1,
|
||||||
@@ -30,9 +30,9 @@
|
|||||||
{
|
{
|
||||||
"id": "crm.financement_bancaire",
|
"id": "crm.financement_bancaire",
|
||||||
"path": "crm/financement_bancaire",
|
"path": "crm/financement_bancaire",
|
||||||
"expected_methods": 35,
|
"expected_methods": 37,
|
||||||
"ran": 35,
|
"ran": 37,
|
||||||
"passed": 35,
|
"passed": 37,
|
||||||
"failures": 0,
|
"failures": 0,
|
||||||
"errors": 0,
|
"errors": 0,
|
||||||
"skipped": 0,
|
"skipped": 0,
|
||||||
@@ -174,9 +174,9 @@
|
|||||||
{
|
{
|
||||||
"id": "publiciste",
|
"id": "publiciste",
|
||||||
"path": "publiciste",
|
"path": "publiciste",
|
||||||
"expected_methods": 24,
|
"expected_methods": 26,
|
||||||
"ran": 24,
|
"ran": 26,
|
||||||
"passed": 22,
|
"passed": 24,
|
||||||
"failures": 0,
|
"failures": 0,
|
||||||
"errors": 0,
|
"errors": 0,
|
||||||
"skipped": 2,
|
"skipped": 2,
|
||||||
@@ -296,8 +296,8 @@
|
|||||||
"suites": 24,
|
"suites": 24,
|
||||||
"green": 24,
|
"green": 24,
|
||||||
"red": 0,
|
"red": 0,
|
||||||
"ran": 629,
|
"ran": 635,
|
||||||
"passed": 612,
|
"passed": 618,
|
||||||
"failures": 0,
|
"failures": 0,
|
||||||
"errors": 0,
|
"errors": 0,
|
||||||
"skipped": 17
|
"skipped": 17
|
||||||
|
|||||||
@@ -103,6 +103,21 @@ def generate(spec: dict, master: dict) -> list[dict]:
|
|||||||
|
|
||||||
# --- Dedup (term, lang) en conservant la premiere occurrence, puis tri
|
# --- Dedup (term, lang) en conservant la premiere occurrence, puis tri
|
||||||
# deterministe.
|
# deterministe.
|
||||||
|
#
|
||||||
|
# NOTE — asymetrie de cardinalite PAR LANGUE (by-design, pas un bug).
|
||||||
|
# Le nombre de mots-cles UNIQUES peut differer d'une langue a l'autre
|
||||||
|
# (ex. fr=87, en=87, es=84) sans qu'aucun mot-cle ne soit "manquant".
|
||||||
|
# Cause : un projet dont la `localisation` coincide, dans une langue,
|
||||||
|
# avec le `country[lang]` produit un mot-cle projet `{type} {loc}`
|
||||||
|
# BYTE-IDENTIQUE au mot-cle global `{type} {country}` (emis avant) ;
|
||||||
|
# la 2e occurrence est donc dedupliquee — le terme reste present une
|
||||||
|
# fois (scope `global`). En espagnol `country[es]` == "Republica
|
||||||
|
# Dominicana" == la localisation generique de 5 projets, d'ou 3
|
||||||
|
# collisions global<->projet de plus qu'en fr/en (ou "Republique
|
||||||
|
# Dominicaine"/"Dominican Republic" != la localisation). Les invariants
|
||||||
|
# ne contraignent que `>= min_keywords_per_lang` + unicite (term, lang),
|
||||||
|
# jamais l'egalite inter-langues : ne pas "corriger" en forcant une
|
||||||
|
# symetrie (ce serait emettre un doublon strict).
|
||||||
seen: set[tuple[str, str]] = set()
|
seen: set[tuple[str, str]] = set()
|
||||||
deduped: list[dict] = []
|
deduped: list[dict] = []
|
||||||
for k in acc:
|
for k in acc:
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
@@ -0,0 +1,70 @@
|
|||||||
|
# 🎯 V18 · Décisions Michel · D-07 + D-08 tranchées
|
||||||
|
**Date:** 2026-08-12 · **Autorisation:** Michel Roy · **Publication:** Gitea
|
||||||
|
|
||||||
|
## D-07 · APPROUVÉ · Formules standards Big4
|
||||||
|
|
||||||
|
**Décision Michel:** Claude Code DTP-Worker est autorisé à utiliser les **formules standards Big4** (Deloitte / KPMG / PwC / EY) pour toutes les métriques financières V18:
|
||||||
|
|
||||||
|
- **DCF** (Discounted Cash Flow) — standard NPV + terminal value + WACC
|
||||||
|
- **IRR / VAN** (Internal Rate of Return · Net Present Value) — standard time-value-of-money
|
||||||
|
- **DSCR** (Debt Service Coverage Ratio) — Net Operating Income / Debt Service annuel
|
||||||
|
- **LTV** (Loan-to-Value) — Loan Amount / Property Value
|
||||||
|
- **LTC** (Loan-to-Cost) — Loan Amount / Total Development Cost
|
||||||
|
- **Break-even analysis** — Fixed Costs / (Price - Variable Cost)
|
||||||
|
- **Sensitivity analysis** — ±10% swing sur variables clés (prix vente · CAPEX · absorption)
|
||||||
|
- **Downside / Stress case** — Monte Carlo simplifié 3 scénarios
|
||||||
|
|
||||||
|
**Sources autorisées:**
|
||||||
|
- Deloitte Real Estate Feasibility Standards
|
||||||
|
- KPMG Real Estate Valuation Handbook
|
||||||
|
- PwC Real Estate Investor Guidelines
|
||||||
|
- EY Global Real Estate Trends
|
||||||
|
- ULI (Urban Land Institute) standards
|
||||||
|
- FIABCI Real Estate Standards
|
||||||
|
|
||||||
|
**Contrainte:** chaque formule utilisée dans un livrable V18 doit être **sourcée explicitement** en note de bas de page ou annexe (nom de la source + année).
|
||||||
|
|
||||||
|
**Débloqué:** Moteurs Phase 4 (Financial Engine) et Phase 8 (Bankability Engine).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D-08 · APPROUVÉ · Master Intake A1-A20 = sur-ensemble STRICT brief.json V12
|
||||||
|
|
||||||
|
**Décision Michel:** OUI · le Master Project Intake V18 (A1-A20) est un **sur-ensemble strict** du `brief.json` V12.
|
||||||
|
|
||||||
|
**Règles de migration:**
|
||||||
|
|
||||||
|
1. **Tout champ V12 existant** conserve son nom/clé exact dans V18 (rétro-compat parser Publiciste)
|
||||||
|
2. `brief.schema.json` (V12) devient un **sous-schéma inclus** dans `master_intake.schema.json` (V18)
|
||||||
|
3. **Nouveaux champs V18** ajoutés en extension · jamais en remplacement
|
||||||
|
4. **Parser V12 continue de fonctionner** sur les données V18 (lecture partielle · les champs V18 ignorés silencieusement par le vieux parser)
|
||||||
|
5. **Aucune migration destructive** — projets V12 existants restent lisibles/utilisables
|
||||||
|
6. **Le champ `dataset_version`** distingue V12 vs V18 pour éviter confusion
|
||||||
|
|
||||||
|
**Bénéfice:** transition douce · aucune interruption du pipeline V12 pendant la construction V18 · rétro-compat garantie sur brief.json déjà en production.
|
||||||
|
|
||||||
|
**Débloqué:** Phase 1 Master Data Model peut finaliser le schema fusionné.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Impact global sur Phase 1
|
||||||
|
|
||||||
|
Après ces 2 décisions:
|
||||||
|
- Phase 1 Master Project Intake peut être livré avec schema V12 rétro-compat ✅
|
||||||
|
- Phase 4 Financial Engine peut démarrer avec formules Big4 ✅
|
||||||
|
- Phase 8 Bankability Engine peut démarrer avec métriques Big4 ✅
|
||||||
|
- Phases 2, 3, 5, 6, 7 restent autonomes (pas de blocage supplémentaire)
|
||||||
|
|
||||||
|
## Décisions restantes OUVERTES (non bloquantes)
|
||||||
|
|
||||||
|
- **D-01** · PIE contrats · à trancher avant Phase 13 (Legal)
|
||||||
|
- **D-02** · Financement condition #4 · à trancher avant Phase 11 (Commercial + Bankability)
|
||||||
|
- **D-03** · Inventaire otoia/capabilities/chat.py · info technique · non urgent
|
||||||
|
- **D-04** · Inventaire config/projets_editor.py · info technique · non urgent
|
||||||
|
- **D-09** · Séparateur milliers · à trancher au polissage final (Phase 11+)
|
||||||
|
|
||||||
|
## Publication
|
||||||
|
|
||||||
|
Commit à venir · Gitea `oto-enterprise-os-dtp`
|
||||||
|
Memory: mise à jour registre décisions
|
||||||
|
Notification: DTP-Worker doit lire au prochain cycle et intégrer ces 2 décisions dans le développement Phase 1 + planification Phases 4/8
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# 🚀 GO SIGNAL · V18 PHASE 1 · DÉBLOCAGE OFFICIEL
|
||||||
|
|
||||||
|
**Date:** 2026-08-12 15:35 UTC
|
||||||
|
**Autorisation:** Michel Roy · Co-Président World Activities Group
|
||||||
|
**Statut:** OFFICIEL · EXÉCUTION IMMÉDIATE AUTORISÉE
|
||||||
|
|
||||||
|
## Décision Michel
|
||||||
|
|
||||||
|
> "V y option B" · approbation de l'audit V18 tel quel · trancher D-06 seulement pour débloquer immédiatement · autres décisions tranchées au fur et à mesure.
|
||||||
|
|
||||||
|
## D-06 · APPROBATION AUDIT V18
|
||||||
|
|
||||||
|
**Statut:** 🟢 **APPROUVÉ**
|
||||||
|
|
||||||
|
Michel approuve `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` tel qu'il est.
|
||||||
|
|
||||||
|
Le DTP-Worker est officiellement **DÉBLOQUÉ** pour démarrer la construction V18.
|
||||||
|
|
||||||
|
## Décisions restantes · à trancher au fur et à mesure
|
||||||
|
|
||||||
|
D-01, D-02, D-03, D-04, D-05, D-07, D-08, D-09 restent 🟠 OUVERT · sans bloquer Phase 1.
|
||||||
|
|
||||||
|
Chaque décision sera tranchée quand sa phase respective sera atteinte.
|
||||||
|
|
||||||
|
## Phase 1 · Master Project Intake / Master Data Model
|
||||||
|
|
||||||
|
**Livrables attendus:**
|
||||||
|
|
||||||
|
1. **Master Project Intake V18** (formulaire A1-A20 selon directive)
|
||||||
|
- Remplace définitivement le formulaire briefing V12
|
||||||
|
- Sur-ensemble strict de `brief.json` V12 (rétro-compat)
|
||||||
|
- Alimente Master Project Dataset unique
|
||||||
|
|
||||||
|
2. **Master Data Model** (schema unique)
|
||||||
|
- `master_intake.schema.json` incluant `brief.schema.json` V12
|
||||||
|
- Data status obligatoire (VERIFIED / CONFIRMED / SOURCE_BASED / CALCULATED / ESTIMATED / ASSUMPTION / TARGET / BANK_REQUIREMENT / PENDING / MISSING / NOT_APPLICABLE)
|
||||||
|
- Séparation INPUT / TARGET / ASSUMPTION / BANK_REQUIREMENT / CALCULATED
|
||||||
|
|
||||||
|
3. **Data Register + Evidence Register** (traçabilité)
|
||||||
|
- Chaque donnée: source_document · source_date · status · confidence · validated_by
|
||||||
|
- Register documents / files / versions
|
||||||
|
|
||||||
|
## Contraintes rappelées
|
||||||
|
|
||||||
|
- **Zéro invention** de données
|
||||||
|
- **One Master Dataset · Multiple Outputs** — pas de duplication
|
||||||
|
- **Zéro écrasement silencieux** de baseline
|
||||||
|
- **Toute donnée traçable** jusqu'à sa source
|
||||||
|
- **Style institutionnel** Sec 60 · HELIOS RD SRL only externement
|
||||||
|
- **OTOv7 = système de gestion** OK · OTOAI/Claude/AI = interdit externement
|
||||||
|
|
||||||
|
## Validation à chaque étape
|
||||||
|
|
||||||
|
Après Phase 1 · Michel valide (CP0 Document Completeness) avant Phase 2.
|
||||||
|
|
||||||
|
**Séquence:** Phase 1 → validation Michel → Phase 2 → validation → Phase 3 → etc. jusqu'à Phase 15.
|
||||||
|
|
||||||
|
## Projet pilote
|
||||||
|
|
||||||
|
**P01 Coralis** = premier projet à tester Phase 1
|
||||||
|
|
||||||
|
Après validation Phase 1-15 sur P01 · réplication P02 Coral del Sur puis P03 Nakua.
|
||||||
|
|
||||||
|
## Publication
|
||||||
|
|
||||||
|
Commit à venir · Gitea `oto-enterprise-os-dtp`
|
||||||
|
Memory: mise à jour `otov7-v18-master-feasibility-engine`
|
||||||
|
Notification: le DTP-Worker doit lire ce fichier au prochain cycle et démarrer Phase 1
|
||||||
|
|
||||||
|
## Prochaine action attendue de Claude Code DTP-Worker
|
||||||
|
|
||||||
|
Passer du mode QA (vérifications qualité perpétuelles) au **mode BUILDER V18 Phase 1**:
|
||||||
|
1. Créer scaffold `master_intake.schema.json`
|
||||||
|
2. Créer scaffold `master_data_model.py` avec 11 statuts
|
||||||
|
3. Créer scaffold `evidence_register.py`
|
||||||
|
4. Créer test suite Phase 1
|
||||||
|
5. Ne pas toucher aux modules production existants
|
||||||
|
6. Committer avec préfixe `[V18-P1]` chaque incrément
|
||||||
|
7. Notifier Michel quand Phase 1 est prête pour CP0 validation
|
||||||
|
|
||||||
|
**Zéro dépassement de scope** · rester strictement dans Phase 1 · attendre validation avant Phase 2.
|
||||||
Reference in New Issue
Block a user