Files
Claude Code DTP Worker cac65386ab [DTP-Worker] Sprint 7 · Générateur Scénarios démo (run-sheet P07 banquier / P05 client) (CRM+Faisabilité · roadmap L68)
Orchestrateur méta-niveau : compose les hand-offs out/ des 10 modules déjà
livrés en un run-sheet de démo jouable, via pointeurs RFC 6901 résolus à la
construction (zéro chiffre en dur · #6). Réutilise le validateur Publiciste et
la preuve de couverture CI de l'auditeur 4Big (#5) — une démo ne s'appuie que
sur des modules gated. Enregistre demo/scenarios dans l'audit 4Big (17→18,
verdict PASS 18/18 à 100). 32 tests (9 injections négatives) · régression 474
tests verts · guards CI verts · builds déterministes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 10:39:24 +00:00

113 lines
4.0 KiB
Python

"""Cœur anti-invention (#6) : résolution des preuves depuis les hand-offs amont.
Un scénario démo ne contient AUCUN chiffre en dur. Chaque « evidence » du spec est
un pointeur `(module, file, pointer)` vers un artefact `out/` déjà produit par un
module livré. Ce fichier :
1. charge l'artefact JSON (avec cache) et signale toute absence / JSON invalide ;
2. résout un pointeur RFC 6901 (`/0/counts/fields`) vers sa valeur verbatim ;
3. quand la valeur n'existe pas, renvoie un PLACEHOLDER `{{module:file#pointer}}`
— jamais un chiffre fabriqué, jamais un `0` posé.
La résolution est PURE et rejouable : l'invariant `INV_ANTI_INVENTION` du CLI
re-résout indépendamment chaque pointeur et compare à ce qui figure dans le
run-sheet — impossible de « poser » une valeur à la main.
"""
from __future__ import annotations
import json
import os
from typing import Any
from .deps import DELIVERABLES_ROOT
# Sentinelle « non résolu » — un placeholder lisible, jamais confondable avec une
# vraie valeur (ni un nombre, ni une chaîne métier).
_MISSING = object()
def placeholder(module: str, file: str, pointer: str) -> str:
return f"{{{{{module}:{file}#{pointer}}}}}"
def is_placeholder(value: Any) -> bool:
return isinstance(value, str) and value.startswith("{{") and value.endswith("}}")
def artifact_path(module: str, file: str) -> str:
"""Chemin absolu de l'artefact `out/` (jamais hors des livrables)."""
return os.path.join(DELIVERABLES_ROOT, module, file)
class Loader:
"""Charge et met en cache les artefacts amont pour une passe de build."""
def __init__(self) -> None:
self._cache: dict[str, Any] = {}
self._errors: dict[str, str] = {}
def load(self, module: str, file: str) -> Any:
key = f"{module}/{file}"
if key in self._cache or key in self._errors:
return self._cache.get(key, _MISSING)
path = artifact_path(module, file)
if not os.path.isfile(path):
self._errors[key] = "artefact absent"
return _MISSING
try:
with open(path, encoding="utf-8") as fh:
obj = json.load(fh)
except (OSError, ValueError) as exc:
self._errors[key] = f"JSON invalide : {exc}"
return _MISSING
self._cache[key] = obj
return obj
def errors(self) -> dict[str, str]:
return dict(self._errors)
def _split_pointer(pointer: str) -> list[str]:
"""Découpe un pointeur RFC 6901 (`/a/0/b`) en tokens déséchappés."""
if pointer == "":
return []
if not pointer.startswith("/"):
raise ValueError(f"pointeur RFC 6901 invalide (doit commencer par '/') : {pointer!r}")
tokens = pointer.split("/")[1:]
return [t.replace("~1", "/").replace("~0", "~") for t in tokens]
def _descend(obj: Any, token: str) -> Any:
"""Un pas de descente : clé d'objet ou index de liste (jamais d'erreur)."""
if isinstance(obj, dict):
return obj.get(token, _MISSING)
if isinstance(obj, list):
if token.lstrip("-").isdigit():
idx = int(token)
if -len(obj) <= idx < len(obj):
return obj[idx]
return _MISSING
return _MISSING
def resolve(loader: Loader, module: str, file: str, pointer: str) -> tuple[Any, bool]:
"""Résout un pointeur en `(valeur, resolu)`.
`resolu=False` ⇒ `valeur` est un placeholder `{{…}}` (artefact absent / JSON
invalide / chemin inexistant). `resolu=True` ⇒ valeur verbatim non `None`.
Une valeur littérale `null` dans l'artefact est traitée comme NON résolue :
un chiffre « à confirmer » ne doit jamais alimenter un pitch (#6).
"""
root = loader.load(module, file)
if root is _MISSING:
return placeholder(module, file, pointer), False
node: Any = root
for token in _split_pointer(pointer):
node = _descend(node, token)
if node is _MISSING:
return placeholder(module, file, pointer), False
if node is None:
return placeholder(module, file, pointer), False
return node, True