[DTP-Worker] Sprint 8 · Générateur Run-book de déploiement VPS unifié (7 phases · 20 modules gated · couverture bijective vs CI) (DevOps · roadmap L73)
Agrégateur de méta-niveau au-dessus du run-book RBAC : ordonne le déploiement VPS de TOUS les livrables gated en 7 phases (Prérequis → DocTypes → RBAC → Workflow/métier → Frontend → Contenu → Vérification QA), avec graphe de dépendances inter-phases acyclique et confirmations préalables sourcées. Anti-invention (#6) : périmètre dérivé du CI (parse_ci réutilisé), couverture bijective module→phase (un module gated non planifié OU un module planifié non gated → refus), SoD (auto-exclusion), zéro chiffre métier (confirmations sourcées via audit_5d D1.1/D1.2/D1.3/D2.3 + endpoint OTOIA). Vérifs : 29/29 tests module (dont 14 injections négatives) · audit_4big PASS 20/20 à 100 · régression run 20/20 suites · 503 tests · 0 échec · gate CI local vert. CI job devops-deploy-runbook-tests + gate ; enregistrement audit_4big (19→20) ; plan régression régénéré (19→20 suites). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
"""Assemblage du run-book de déploiement VPS à partir du spec + de la réalité CI.
|
||||
|
||||
Le run-book ordonne le déploiement de TOUS les livrables gated du mandat en un
|
||||
plan de phases. La liste des modules à déployer n'est PAS écrite à la main : elle
|
||||
est dérivée de `.gitea/workflows/ci.yml` (via q4lib.registry.parse_ci, réutilisé)
|
||||
puis mise en correspondance BIJECTIVE avec le `module_phase` du spec.
|
||||
|
||||
Fonctions PURES (aucune I/O, aucun horodatage) → sortie déterministe, diffable,
|
||||
re-générable bit-à-bit. Le CLI leur injecte le spec chargé et le résultat de
|
||||
`parse_ci()` ; les tests injectent des dictionnaires synthétiques (facile à
|
||||
mettre en défaut → tests négatifs).
|
||||
|
||||
Anti-invention (#6) : aucun chiffre métier ; les paramètres non confirmés restent
|
||||
des CONFIRMATIONS sourcées (owner + source), jamais une valeur fabriquée.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
def gated_modules(ci: dict, self_module: str) -> list[str]:
|
||||
"""Chemins de modules gated par le CI, hors le run-book lui-même (SoD)."""
|
||||
paths = set(ci.get("job_to_path", {}).values())
|
||||
paths.discard(self_module)
|
||||
return sorted(paths)
|
||||
|
||||
|
||||
def _path_to_job(ci: dict) -> dict[str, str]:
|
||||
"""Inverse job→path en path→job. Suppose l'unicité (vérifiée en invariant)."""
|
||||
out: dict[str, str] = {}
|
||||
for job, path in ci.get("job_to_path", {}).items():
|
||||
# En cas de collision on garde le job le plus petit (déterministe) ; la
|
||||
# collision elle-même est signalée par l'invariant INV9 du CLI.
|
||||
if path not in out or job < out[path]:
|
||||
out[path] = job
|
||||
return out
|
||||
|
||||
|
||||
def build_runbook(spec: dict, ci: dict) -> dict[str, Any]:
|
||||
"""Consolide spec + CI en run-book de phases + manifeste agrégé.
|
||||
|
||||
Ne lève pas sur une incohérence de couverture (module manquant / en trop) :
|
||||
elle est REMONTÉE dans `manifest.coverage` pour que le CLI/les tests la
|
||||
signalent explicitement (un déploiement ne doit pas oublier silencieusement
|
||||
un livrable).
|
||||
"""
|
||||
self_module = spec["self_module"]
|
||||
module_phase = spec["module_phase"]
|
||||
phases_meta = spec["phases"]
|
||||
catalogue = spec["confirmations"]
|
||||
|
||||
gated = gated_modules(ci, self_module)
|
||||
gated_set = set(gated)
|
||||
mapped_set = set(module_phase)
|
||||
p2j = _path_to_job(ci)
|
||||
|
||||
# Couverture bijective : les modules mappés == les modules gated (hors self).
|
||||
missing_in_map = sorted(gated_set - mapped_set) # gated mais non planifié
|
||||
extra_in_map = sorted(mapped_set - gated_set) # planifié mais non gated
|
||||
bijective = not missing_in_map and not extra_in_map
|
||||
|
||||
phase_ids = [p["id"] for p in phases_meta]
|
||||
phase_id_set = set(phase_ids)
|
||||
|
||||
# Modules dont la phase déclarée n'existe pas dans `phases` (spec incohérent).
|
||||
unknown_phase = sorted(
|
||||
m for m, ph in module_phase.items() if ph not in phase_id_set
|
||||
)
|
||||
|
||||
# Assemblage du run-book : une phase = métadonnées + modules gated assignés +
|
||||
# confirmations résolues depuis le catalogue.
|
||||
runbook: list[dict[str, Any]] = []
|
||||
for p in phases_meta:
|
||||
mods = sorted(
|
||||
m for m, ph in module_phase.items()
|
||||
if ph == p["id"] and m in gated_set
|
||||
)
|
||||
modules = [{"module": m, "ci_job": p2j.get(m)} for m in mods]
|
||||
confs = [
|
||||
{
|
||||
"id": cid,
|
||||
"libelle": catalogue.get(cid, {}).get("libelle"),
|
||||
"owner": catalogue.get(cid, {}).get("owner"),
|
||||
"source": catalogue.get(cid, {}).get("source"),
|
||||
}
|
||||
for cid in p.get("confirmations", [])
|
||||
]
|
||||
runbook.append({
|
||||
"order": p["order"],
|
||||
"id": p["id"],
|
||||
"titre": p["titre"],
|
||||
"responsable": p["responsable"],
|
||||
"rationale": p["rationale"],
|
||||
"depends_on": list(p.get("depends_on", [])),
|
||||
"modules": modules,
|
||||
"confirmations": confs,
|
||||
})
|
||||
|
||||
# Confirmations effectivement référencées par ≥1 phase (détection d'orphelin).
|
||||
referenced: set[str] = set()
|
||||
for p in phases_meta:
|
||||
referenced.update(p.get("confirmations", []))
|
||||
orphan_confirmations = sorted(set(catalogue) - referenced)
|
||||
unknown_confirmations = sorted(referenced - set(catalogue))
|
||||
|
||||
manifest = {
|
||||
"generated_from": f"deploy_spec.json v{spec.get('version')}",
|
||||
"reference_cadre": spec.get("reference_cadre"),
|
||||
"cible_portage": spec.get("cible_portage"),
|
||||
"self_module": self_module,
|
||||
"counts": {
|
||||
"phases": len(phases_meta),
|
||||
"modules": len(gated),
|
||||
"modules_mapped": len(mapped_set),
|
||||
"confirmations": len(catalogue),
|
||||
"confirmations_referenced": len(referenced),
|
||||
},
|
||||
"coverage": {
|
||||
"ci_modules_count": len(gated),
|
||||
"mapped_modules_count": len(mapped_set),
|
||||
"bijective": bijective,
|
||||
"missing_in_map": missing_in_map,
|
||||
"extra_in_map": extra_in_map,
|
||||
"unknown_phase": unknown_phase,
|
||||
"self_module_excluded": self_module,
|
||||
},
|
||||
"graph": {
|
||||
"phase_order": phase_ids,
|
||||
"orphan_confirmations": orphan_confirmations,
|
||||
"unknown_confirmations": unknown_confirmations,
|
||||
},
|
||||
"confirmations_ouvertes": sorted(catalogue),
|
||||
}
|
||||
|
||||
return {"runbook": runbook, "manifest": manifest}
|
||||
Reference in New Issue
Block a user