[DTP-Worker 20260812_093114] fix correctness · financement_bancaire gate — un dossier incomplet pouvait afficher percent 100.0 (barre pleine ✓) tout en BLOQUANT

Défaut latent, byte-repro-aveugle : _cond_apport arrondit percent à 1 décimale
(round) tandis que ok est un test exact. Un versement dans la bande [99.95%,100%[
(ex. 19 999/20 000 USD = 99.995%) remonte à percent:100.0 alors que ok=False,
remaining=1 USD → barre frontend « 100% ✓ » qui refuse la soumission + raison
auto-contradictoire « reste 1 USD · 100.0% ». Seul _cond_apport atteignable
(montant continu) ; documents/autorisations = ratios entiers sur listes 13/4,
bande inatteignable.

Fix chirurgical : garde `if not ok and pct>=100.0: pct=99.9` → invariant
d'affichage percent==100.0 ⟺ ok. Échantillon commité à 67.8% (hors bande) →
gate_status_example.json/out rebuild BYTE-IDENTIQUE.

Test à dents test_apport_quasi_complet_ninclut_pas_100pct (mordu sans le fix).
Cascade +1 test : régression 633→634/616→617, plan+MANIFEST+quality_report
régénérés, fiches QA/Backend/CRM re-dérivées. run_ci 33/0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-12 09:43:10 +00:00
parent cb8069b83c
commit 85cd625dac
10 changed files with 60 additions and 13 deletions
+1 -1
View File
@@ -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` | 36 |
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
+1 -1
View File
@@ -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 : **633 tests · 24 suites** · verdict `PASS`, source de régression du repo : **634 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)
+1 -1
View File
@@ -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 · 633 tests · 616 passés · 0 échec · 0 erreur → verdict `PASS`.** **24 suites gated · 634 tests · 617 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*
+23
View File
@@ -1,5 +1,28 @@
# Activity Log · 2026-08-12 · Claude Code DTP Worker # Activity Log · 2026-08-12 · Claude Code DTP Worker
## 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) ## 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). **É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).
@@ -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,
@@ -118,6 +118,21 @@ 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_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}]
@@ -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": "36 méthodes test_* dans 1 fichier(s) (seuil 8)"
}, },
{ {
"criterion": "CLI", "criterion": "CLI",
@@ -5,7 +5,7 @@
"version": "1.0", "version": "1.0",
"verdict": "PASS", "verdict": "PASS",
"suites": 24, "suites": 24,
"test_methods": 633, "test_methods": 634,
"coverage_ok": true, "coverage_ok": true,
"artifacts": [ "artifacts": [
"regression_plan.json" "regression_plan.json"
@@ -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": 36
}, },
{ {
"id": "crm.workflow_vente", "id": "crm.workflow_vente",
@@ -295,7 +295,7 @@
"totals": { "totals": {
"suites": 24, "suites": 24,
"test_files": 24, "test_files": 24,
"test_methods": 633, "test_methods": 634,
"min_methods": 10, "min_methods": 10,
"under_threshold": 0 "under_threshold": 0
}, },
@@ -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": 36,
"ran": 35, "ran": 36,
"passed": 35, "passed": 36,
"failures": 0, "failures": 0,
"errors": 0, "errors": 0,
"skipped": 0, "skipped": 0,
@@ -296,8 +296,8 @@
"suites": 24, "suites": 24,
"green": 24, "green": 24,
"red": 0, "red": 0,
"ran": 633, "ran": 634,
"passed": 616, "passed": 617,
"failures": 0, "failures": 0,
"errors": 0, "errors": 0,
"skipped": 17 "skipped": 17