48276ee916
Défaut réel/reproductible (pas stale, byte-repro vert) : finlib/builder.py:89
faisait [roles_resolved[k] for k in sorted(roles_resolved)], itérant les 4 SLOTS
logiques de spec["roles"] — 2 slots (conseiller_wag ET validation_dossier) pointent
le même rôle ventes-conseiller → 4 entrées pour 3 rôles distincts. Preuve bug (pas
per-slot voulu) : la sortie drope la clé de slot (cle), donc le doublon ne porte
aucune info distinctive ; sémantique documentée README.md:54 « rôles RBAC utilisés »
+ ci/README.md:1100 « ensemble distinct » ; les 3 modules frères produisent tous un
ensemble distinct (confotur 3/3, commissions 4/4, workflow_vente 7/7) — financement
seul outlier 4/3.
Fix (aligné idiome commissions) : ensemble distinct trié par role_id via
sorted({rr["role_id"] for rr in roles_resolved.values()}). Artefact régénéré → 3/3
distinct. 0 gate ajouté (#5 — champ lu par AUCUN gate financement ni consommateur
externe ; convention distinctness déjà tenue par les frères). 0 chiffre inventé (#6).
run_ci 33 PASS · 0 FAIL · 0 SKIP (byte-repro vert avec nouvel artefact, 35 tests verts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
4.5 KiB
Python
110 lines
4.5 KiB
Python
"""Assemblage du bundle d'artefacts Financement Bancaire depuis le contrat.
|
|
|
|
Entrée : `financement_spec.json` + un `RoleResolver` (id → nom Frappe issu de
|
|
rbac_50_roles.json). Sortie : un bundle déterministe (tri stable, aucun
|
|
horodatage) → reproductible bit-à-bit à contrat constant, donc diffable et
|
|
re-générable en CI (`ci/check_artifacts.sh`).
|
|
|
|
Le builder n'invente rien (#6) : banques, documents, autorisations, sections,
|
|
conditions du gate proviennent du contrat ; les noms de rôle proviennent du
|
|
contrat RBAC ; l'exemple de gate-status est CALCULÉ par `finlib.gate` depuis le
|
|
dossier d'exemple du contrat (donc auditable, pas fabriqué).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from . import gate
|
|
from .rbac import RoleResolver
|
|
|
|
|
|
def build_bundle(spec: dict, resolver: RoleResolver) -> dict[str, Any]:
|
|
"""Transforme le contrat en bundle d'artefacts prêts à consommer."""
|
|
cfg = gate.GateConfig.from_spec(spec)
|
|
|
|
# --- rôles résolus (id → nom Frappe) --------------------------------------
|
|
roles_resolved = {
|
|
cle: {"role_id": rid, "erpnext_role_name": resolver.erpnext_name(rid)}
|
|
for cle, rid in sorted(spec["roles"].items())
|
|
}
|
|
|
|
# --- banques (ordre du contrat = ordre d'affichage) -----------------------
|
|
banques = list(spec["banques"])
|
|
|
|
# --- sections triées par `ordre` -----------------------------------------
|
|
sections = sorted(spec["sections"], key=lambda s: s["ordre"])
|
|
|
|
documents = list(spec["documents"])
|
|
autorisations = list(spec["autorisations"])
|
|
|
|
# --- spec du gate (conditions + bannière + endpoints + rôle validation) ---
|
|
gate_spec = {
|
|
"endpoint": spec["gate"]["endpoint"],
|
|
"submit_endpoint": spec["gate"]["submit_endpoint"],
|
|
"condition_keys": list(gate.CONDITION_KEYS),
|
|
"conditions": sorted(spec["gate"]["conditions"], key=lambda c: c["ordre"]),
|
|
"banniere": spec["gate"]["banniere"],
|
|
"role_validation": roles_resolved["validation_dossier"],
|
|
"workflow_suivi_banque": list(spec["workflow_suivi_banque"]),
|
|
}
|
|
|
|
# --- exemple de gate-status CALCULÉ (auditable, pas inventé) ---------------
|
|
exemple = spec["dossier_exemple"]
|
|
can_submit, reasons = gate.can_submit_dossier(exemple, cfg)
|
|
gate_status_example = {
|
|
"dossier_id": exemple["id"],
|
|
"projet": exemple["projet"],
|
|
"unite": exemple["unite"],
|
|
"banque_choisie": exemple["banque_choisie"],
|
|
"gate_status": gate.gate_status(exemple, cfg),
|
|
"can_submit": can_submit,
|
|
"reasons": reasons,
|
|
}
|
|
|
|
# --- comptes des documents requis par type de résidence -------------------
|
|
docs_requis = {
|
|
rt["code"]: len(gate.required_document_ids(rt["code"], cfg))
|
|
for rt in spec["apport"]["residence_types"]
|
|
}
|
|
|
|
manifest = {
|
|
"generated_from": "financement_spec.json",
|
|
"source_directive": spec["source_directive"],
|
|
"rbac_source": "rbac_50_roles.json",
|
|
"source_version": spec["version"],
|
|
"module": spec["module"],
|
|
"titre": spec["titre"],
|
|
"counts": {
|
|
"banques": len(banques),
|
|
"sections": len(sections),
|
|
"documents": len(documents),
|
|
"documents_requis_resident": docs_requis.get("resident_rd", 0),
|
|
"documents_requis_etranger": docs_requis.get("etranger", 0),
|
|
"autorisations": len(autorisations),
|
|
"gate_conditions": len(gate_spec["conditions"]),
|
|
"banniere_lignes": len(gate_spec["banniere"]["lignes"]),
|
|
"workflow_suivi_etapes": len(gate_spec["workflow_suivi_banque"]),
|
|
},
|
|
# Ensemble DISTINCT des rôles RBAC réellement utilisés. Plusieurs slots
|
|
# logiques peuvent pointer le même rôle (ex. conseiller_wag ET
|
|
# validation_dossier == ventes-conseiller) : on dédoublonne par role_id
|
|
# et on trie par role_id, comme les modules frères (commissions,
|
|
# confotur, workflow_vente). Sans ce dédoublonnage, la liste répétait le
|
|
# rôle et contredisait sa sémantique « rôles utilisés » (ensemble).
|
|
"roles_rbac_utilises": [
|
|
{"role_id": rid, "erpnext_role_name": resolver.erpnext_name(rid)}
|
|
for rid in sorted({rr["role_id"] for rr in roles_resolved.values()})
|
|
],
|
|
}
|
|
|
|
return {
|
|
"manifest": manifest,
|
|
"banques": banques,
|
|
"sections": sections,
|
|
"documents": documents,
|
|
"autorisations": autorisations,
|
|
"gate_spec": gate_spec,
|
|
"gate_status_example": gate_status_example,
|
|
}
|