[DTP-Worker] Sprint 8 · buffer · Fiscal/e-CF DGII : gate d'IDENTITÉ des ÉVÈNEMENTS D'ÉMISSION (« le cœur du livrable ») — ENSEMBLE émetteurs {réservation,contrat} ⨯ DISJONCTION des brouillons (jamais de facture sur brouillon) ⨯ base_field ⨯ rôle unique compta-fiscaliste-ecf double-ancré RBAC portail compta ⨯ FormaPago défaut 3/Tarjeta/Cardnet #10, recomputés d'ecf_plan.json

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-01 14:14:18 +00:00
parent 915be3cb77
commit 8667cdf4de
3 changed files with 250 additions and 1 deletions
+63
View File
@@ -1508,3 +1508,66 @@ taux_a_confirmer` périmé 5→4 (c seul) · promesse anti-invention retirée du
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; la
confirmation réelle des taux + le câblage du calcul restent côté Direction/VPS).
- **Auto-score 4Big** : 96/100.
## Session `20260801_140334` · Buffer S8 · Domaine Fiscal/e-CF DGII : l'**IDENTITÉ des ÉVÈNEMENTS D'ÉMISSION** — la section §Cross-cohérence présentée comme **« le cœur du livrable »** (README:40-59) — était transcrite À LA MAIN SANS aucun gate d'identité. Le SEUL gate fiscal existant couvre le **contrat de format e-NCF** (`E+tipo(2)+seq(10)=13`) + le compte de tests ; le bloc racine, le seul agrégat « e-CF 39 tests ». RIEN ne confrontait à l'artefact byte-gaté QUEL évènement du pipeline émet un e-CF, sur quel `base_field`, par quel rôle Compta, ni la FormaPago défaut.
**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement
close ; poursuite de la série anti-dérive (CLAUDE.md #6). Même classe « identité
d'un ENSEMBLE » que la colonne « Entité » de la SPEC §4, l'énumération des onglets
Mobile ou les transitions à séparation des pouvoirs du workflow — appliquée à la
**surface data-derived la plus critique du module fiscal** : les `emission_events`
d'`ecf_plan.json`, jamais gatés hors le format e-NCF et le compte de tests.
**Dérive silencieuse fermée** :
- `05_deliverables_mvp/fiscal/ecf_dgii/README.md:40-59` (§Cross-cohérence, « le cœur
du livrable ») — transcrit À LA MAIN : (L48) l'ENSEMBLE des évènements émetteurs
(« seulement réservation et contrat ») + l'ENSEMBLE exclu des brouillons (« on ne
facture pas un brouillon (lead/visite/devis) ») ; (L50) l'ENSEMBLE des `base_field`
(`montant_reservation`/`montant_contrat`) ; (L53-54) le rôle UNIQUE
(`compta-fiscaliste-ecf` · « OTO Compta Fiscaliste eCF » · portail compta) ;
(L59) la FormaPago défaut (« = 3 (Tarjeta) » · Cardnet #10).
- Source faisant autorité (byte-gatée par `check_artifacts`) :
`out/ecf_plan.json` — `emission_events[]` (`update_value`/`base_field`/`role_id`/
`erpnext_role_name`), `forma_pago_defaut` + catalogue `formas_pago`. Double
ancrage du rôle sur `rbac/rbac_50_roles.json` (id → `portail` compta → nom ERPNext).
- Piège #6/#5, la dérive la plus dommageable ici : **AJOUTER un `emission_event` sur
un état BROUILLON** (ex. `devis`) — l'anti-pattern EXACT que le module interdit
(« on ne facture pas un brouillon ») — pendant que L48 dit encore « seulement
réservation et contrat » → l'agent ERPNext Backend câblerait une **facture émise
avant soumission/encaissement**. Aucune suite `tests/` (FONCTIONS de résolution/
composition, jamais la prose vs l'artefact) ne l'attrape.
- **État courant** : émetteurs `{contrat, reservation}`, base_field
`{montant_contrat, montant_reservation}`, rôle unique `compta-fiscaliste-ecf`
(portail compta · RBAC concordant), FormaPago défaut `3`/Tarjeta/Cardnet —
tout recoupe l'artefact EXACTEMENT (anti-invention #6, rien à réécrire). Le
défaut est la **surface ungated**.
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Fiscal · e-CF DGII —
IDENTITÉ des ÉVÈNEMENTS D'ÉMISSION » avant `sys.exit`) : (0) **cohérence interne** —
rôle UNIQUE sur tous les évènements · défaut ∈ catalogue FormaPago ; (1) **ENSEMBLE
émetteurs** — set-diff README `seulement …` == `update_value` de l'artefact (ni
fantôme ni manquant · accents/casse normalisés) ; (2) **DISJONCTION** — l'ensemble
exclu `(lead/visite/devis)` ∩ émetteurs = ∅ (jamais de facture sur brouillon — mord
un `devis` émetteur ET la self-contradiction du README) ; (3) **ENSEMBLE `base_field`**
— set-diff README == artefact ; (4) **rôle unique** cité backtické + nom ERPNext,
**double-ancré** sur `rbac_50_roles` (id → portail compta → nom concordant) ; (5)
**FormaPago défaut** — code == artefact · label ∈ catalogue · motif cite Cardnet (#10).
Un claim absent échoue AUSSI (traçabilité #6).
**6 morsures vérifiées** : artefact facture un BROUILLON (`devis` émetteur → mismatch
d'ensemble **ET** clash de disjonction · 2 morsures en une) · artefact renomme un
`base_field` (`montant_reservation→montant_acompte`) · artefact change FormaPago défaut
`3→1` (Efectivo) · artefact casse le rôle unique (2ᵉ évènement → `compta-comptable`) ·
README périmé « seulement contrat » (réservation retiré) · motif du défaut ne cite
plus « Cardnet » (#10) ; restauré = green : émetteurs · disjonction · base_field ·
rôle == RBAC · FormaPago == artefact · exit 0. Working tree byte-restauré
(`git checkout --`, **JAMAIS** `git clean`, interdit absolu) · **7 gates re-verts**
(`check_readme_claims`/`check_docs`/`guard_constraints`/`check_artifacts`/
`check_ci_integrity`/`validate_json`/`check_regression`).
- `ci/README.md` (clause récap du pipeline `check-readme-claims` + paragraphe détaillé
« IDENTITÉ des évènements d'émission e-CF ancrée sur `ecf_plan.json` ») mis à jour.
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; la connexion
Compupar réelle + l'émission e-CF en production restent côté ERPNext Backend/VPS).
- **Auto-score 4Big** : 96/100.
+27 -1
View File
File diff suppressed because one or more lines are too long
+160
View File
@@ -5557,6 +5557,166 @@ if _rp_src is not None:
bad(f"RunPod endpoint · AUCUNE fiche/master prompt ne mentionne RunPod — "
f"la surface d'identité de l'endpoint GPU s'est évaporée (régression #6)")
# ============================================================================
# Fiscal · e-CF DGII — IDENTITÉ des ÉVÈNEMENTS D'ÉMISSION (le « cœur du livrable »)
# ----------------------------------------------------------------------------
# La section §Cross-cohérence du README (05_deliverables_mvp/fiscal/ecf_dgii/
# README.md:40-59) — présentée comme « le cœur du livrable » — transcrit À LA MAIN
# l'IDENTITÉ des emission_events d'out/ecf_plan.json (byte-gaté par check_artifacts
# ⇒ artefact faisant autorité) :
# • L48 l'ENSEMBLE des évènements qui émettent un e-CF (« seulement réservation
# et contrat ») + l'ENSEMBLE exclu « on ne facture pas un brouillon
# (lead/visite/devis) » — la RAISON D'ÊTRE du module : jamais de facture
# sur un brouillon (états SOUMIS uniquement · doc_status=1)
# • L50 l'ENSEMBLE des base_field (« montant_reservation, montant_contrat »)
# • L53 le rôle UNIQUE (« compta-fiscaliste-ecf » · « OTO Compta Fiscaliste
# eCF » · portail compta)
# • L59 la FormaPago défaut (« = 3 (Tarjeta) » · Cardnet #10)
# Le SEUL gate fiscal existant couvre le CONTRAT DE FORMAT e-NCF + le compte de
# tests ; le bloc racine, le seul agrégat « e-CF 39 tests ». RIEN ne confronte
# cette identité à l'artefact. Dérive la plus grave, #6/#5 : AJOUTER un
# emission_event sur un état BROUILLON (ex. `devis`) — l'anti-pattern EXACT que le
# module interdit (« on ne facture pas un brouillon ») — pendant que L48 dit encore
# « seulement réservation et contrat » → l'agent ERPNext Backend câblerait une
# facturation sur brouillon (e-CF émis avant soumission/encaissement). Aucune suite
# tests/ (FONCTIONS de résolution/composition, jamais la prose vs l'artefact) ne
# l'attrape — classe « vert trompeur » notée en mémoire.
#
# On RECOMPUTE les ENSEMBLES depuis ecf_plan.json (jamais une liste à la main · #6)
# et EXIGE que le README les reflète EXACTEMENT (set-diff : ni fantôme ni manquant ;
# accents/casse normalisés). Le rôle est en outre DOUBLE-ANCRÉ sur rbac_50_roles
# (id → portail compta → nom ERPNext concordant). Cohérences croisées en bonus
# (mordent un plan INTERNEMENT incohérent) : rôle UNIQUE sur tous les évènements ·
# défaut ∈ catalogue FormaPago · ensembles NON VIDES. Un claim absent échoue AUSSI
# (traçabilité #6).
# ============================================================================
ECF_RD2 = f"{D}/fiscal/ecf_dgii/README.md"
try:
_ecfp = load("fiscal/ecf_dgii/out/ecf_plan.json")
_ecf_ev = _ecfp["emission_events"]
if not _ecf_ev:
raise ValueError("emission_events vide")
ecf_updvals = {e["update_value"] for e in _ecf_ev}
ecf_bases = {e["base_field"] for e in _ecf_ev}
ecf_roleids = {e["role_id"] for e in _ecf_ev}
ecf_rolenms = {e["erpnext_role_name"] for e in _ecf_ev}
_ecf_fpd = _ecfp["forma_pago_defaut"]
_ecf_fpcat = {str(f["code"]): f["label"] for f in _ecfp["formas_pago"]}
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError) as e:
bad(f"Fiscal e-CF évènements · ecf_plan.json illisible/inattendu : {e}")
_ecf_ev = None
if _ecf_ev is not None:
def _ecf_ai(s): # accent-insensible · minuscule · sans espaces
return "".join(c for c in unicodedata.normalize("NFKD", str(s))
if unicodedata.category(c) != "Mn").lower().replace(" ", "")
try:
ecf_doc = re.sub(r"\s+", " ", open(ECF_RD2, encoding="utf-8").read())
except OSError as e:
bad(f"Fiscal e-CF évènements · {ECF_RD2} illisible : {e}"); ecf_doc = None
# (0) cohérence interne du plan : rôle UNIQUE + défaut ∈ catalogue FormaPago
if len(ecf_roleids) != 1 or len(ecf_rolenms) != 1:
bad(f"Fiscal e-CF évènements · plan INCOHÉRENT : rôle non unique sur les "
f"évènements (ids={sorted(ecf_roleids)} noms={sorted(ecf_rolenms)}) — "
f"le README promet un « rôle dédié » unique")
_fp_code = str(_ecf_fpd.get("code"))
if _fp_code not in _ecf_fpcat:
bad(f"Fiscal e-CF évènements · forma_pago_defaut.code={_fp_code!r} ABSENT "
f"du catalogue formas_pago {sorted(_ecf_fpcat)} (plan incohérent)")
if ecf_doc is not None:
# (1) ENSEMBLE des évènements émetteurs — L48 « seulement X et Y »
m_bill = re.search(r"seulement\s+([^.]+?)\.", ecf_doc)
want = {_ecf_ai(x) for x in ecf_updvals}
if m_bill is None:
bad(f"Fiscal e-CF évènements · {ECF_RD2} — énumération « seulement … » "
f"INTROUVABLE (attendu {sorted(ecf_updvals)})")
else:
bill = {_ecf_ai(x) for x in re.split(r"\s+et\s+|,\s*", m_bill.group(1)) if x.strip()}
if bill == want:
good(f"Fiscal e-CF évènements · émetteurs README == ecf_plan "
f"({sorted(ecf_updvals)})")
else:
bad(f"Fiscal e-CF évènements · émetteurs README {sorted(bill)} ≠ "
f"ecf_plan {sorted(want)} (fantôme={sorted(bill-want)} "
f"manquant={sorted(want-bill)}) → régénérer le README")
# (2) ENSEMBLE exclu (brouillons) DISJOINT des émetteurs — anti-facture-brouillon
m_excl = re.search(r"on ne facture pas un brouillon\s*\(([^)]+)\)", ecf_doc)
if m_excl is None:
bad(f"Fiscal e-CF évènements · {ECF_RD2} — clause « on ne facture pas un "
f"brouillon (…) » INTROUVABLE (raison d'être #6 · traçabilité)")
else:
excl = {_ecf_ai(x) for x in re.split(r"[/,]", m_excl.group(1)) if x.strip()}
clash = excl & want
if clash:
bad(f"Fiscal e-CF évènements · un état déclaré BROUILLON {sorted(clash)} "
f"est ÉMETTEUR dans ecf_plan → FACTURE SUR BROUILLON (l'anti-pattern "
f"que le module interdit · #6) OU README auto-contradictoire")
else:
good(f"Fiscal e-CF évènements · brouillons {sorted(excl)} ∩ émetteurs = ∅ "
f"(jamais de facture sur brouillon)")
# (3) ENSEMBLE des base_field — L50 « (montant_reservation, montant_contrat) »
m_base = re.search(r"champ \*\*Currency réel\*\*[^(]*\(([^)]+)\)", ecf_doc)
if m_base is None:
bad(f"Fiscal e-CF évènements · {ECF_RD2} — énumération des base_field "
f"INTROUVABLE (attendu {sorted(ecf_bases)})")
else:
docbases = set(re.findall(r"`([^`]+)`", m_base.group(1)))
if docbases == ecf_bases:
good(f"Fiscal e-CF évènements · base_field README == ecf_plan "
f"({sorted(ecf_bases)})")
else:
bad(f"Fiscal e-CF évènements · base_field README {sorted(docbases)} ≠ "
f"ecf_plan {sorted(ecf_bases)} (fantôme={sorted(docbases-ecf_bases)} "
f"manquant={sorted(ecf_bases-docbases)})")
# (4) rôle UNIQUE : backtické + nom ERPNext + DOUBLE-ANCRÉ portail compta (RBAC)
_rid = next(iter(ecf_roleids)); _rnm = next(iter(ecf_rolenms))
if f"`{_rid}`" not in ecf_doc:
bad(f"Fiscal e-CF évènements · rôle dédié `{_rid}` (== ecf_plan) NON cité "
f"backtické dans le README")
elif _rnm not in ecf_doc:
bad(f"Fiscal e-CF évènements · nom ERPNext « {_rnm} » (== ecf_plan) ABSENT "
f"du README")
else:
try:
_rbac = load("rbac/rbac_50_roles.json")["roles"]
_rrec = next((r for r in _rbac if r.get("id") == _rid), None)
except (OSError, KeyError, TypeError, json.JSONDecodeError) as e:
bad(f"Fiscal e-CF évènements · rbac_50_roles illisible : {e}"); _rrec = "ERR"
if _rrec is None:
bad(f"Fiscal e-CF évènements · rôle `{_rid}` (ecf_plan) INTROUVABLE dans "
f"rbac_50_roles → rôle Compta fantôme")
elif _rrec == "ERR":
pass
elif _rrec.get("portail") != "compta":
bad(f"Fiscal e-CF évènements · rôle `{_rid}` portail={_rrec.get('portail')!r} "
f"≠ « compta » (le README exige le portail compta)")
elif _rrec.get("erpnext_role_name") != _rnm:
bad(f"Fiscal e-CF évènements · nom ERPNext discordant ecf_plan « {_rnm} » "
f"vs RBAC « {_rrec.get('erpnext_role_name')} »")
else:
good(f"Fiscal e-CF évènements · rôle unique `{_rid}` (« {_rnm} ») == "
f"ecf_plan == RBAC (portail compta)")
# (5) FormaPago défaut — L59 « = 3 (Tarjeta) » · Cardnet #10
m_fp = re.search(r"FormaPago défaut\s*=\s*(\d+)\s*\(([^)]+)\)", ecf_doc)
_lbl = _ecf_fpcat.get(_fp_code, "")
if m_fp is None:
bad(f"Fiscal e-CF évènements · {ECF_RD2} — « FormaPago défaut = N (…) » "
f"INTROUVABLE (attendu {_fp_code} / {_lbl})")
elif m_fp.group(1) != _fp_code:
bad(f"Fiscal e-CF évènements · FormaPago défaut README ={m_fp.group(1)} "
f"MAIS ecf_plan dit ={_fp_code} ({_lbl}) → défaut périmé")
elif _ecf_ai(m_fp.group(2)) not in _ecf_ai(_lbl):
bad(f"Fiscal e-CF évènements · FormaPago défaut label README "
f"« {m_fp.group(2)} » incohérent avec le catalogue « {_lbl} » (code {_fp_code})")
elif "cardnet" not in _ecf_ai(_ecf_fpd.get("motif", "")):
bad(f"Fiscal e-CF évènements · forma_pago_defaut.motif ne cite plus "
f"« Cardnet » (#10) → justification du défaut évaporée")
else:
good(f"Fiscal e-CF évènements · FormaPago défaut = {_fp_code} "
f"({_lbl}) · Cardnet #10 == ecf_plan")
sys.exit(1 if FAIL else 0)
PY
rc=$?