Compare commits

..

2 Commits

Author SHA1 Message Date
Claude Code DTP Worker 15eebb41ca [DTP-Worker 20260813_020316] Auto exec · session 20260813_020316
CI / Contraintes NON-NÉGOCIABLES (CLAUDE.md) (push) Has been cancelled
CI / Validation JSON (schémas Faisabilité) (push) Has been cancelled
CI / Qualité documentaire (liens + 4Big) (push) Has been cancelled
CI / Reproductibilité des artefacts out/ (build == commité) (push) Has been cancelled
CI / Fraîcheur matrice de régression (run == commité) (push) Has been cancelled
CI / Intégrité du câblage CI (gate agrège tout · gates statiques verrouillés) (push) Has been cancelled
CI / Intégrité des chiffres du README (valeur == artefact cité · (push) Has been cancelled
CI / Intégrité mobile-build.yml (gating portable · activation différée · (push) Has been cancelled
CI / Publiciste · parser + schéma + generator (unittest) (push) Has been cancelled
CI / RBAC · 50 rôles + schéma (unittest) (push) Has been cancelled
CI / Faisabilité · générateur 4 volets + round-trip (unittest) (push) Has been cancelled
CI / RBAC · fixtures ERPNext (Role + Custom DocPerm) (push) Has been cancelled
CI / RBAC · plan User Permission (row-level) (push) Has been cancelled
CI / RBAC · Role Profile (bundles par portail) (push) Has been cancelled
CI / RBAC · run-book d'application unifié (agrégat 3 volets) (push) Has been cancelled
CI / Faisabilité · dossier bancable trilingue FR/EN/ES (push) Has been cancelled
CI / CRM · workflow vente ERPNext (lead → CONFOTUR) (push) Has been cancelled
CI / CRM · DocType porteur OTO Dossier Vente (push) Has been cancelled
CI / CRM · barème commissions vendeurs (push) Has been cancelled
CI / CRM · Financement Bancaire (gate hypothécaire RD) (push) Has been cancelled
CI / Fiscal · e-CF DGII (Compupar) (push) Has been cancelled
CI / Frontend · Workspaces 5 portails rôle (push) Has been cancelled
CI / Legal · DocType CONFOTUR Application (push) Has been cancelled
CI / QA · Audit 5D conformité (push) Has been cancelled
CI / SEO · mots-clés trilingues + schema.org + hreflang (push) Has been cancelled
CI / Chat OTOIA · montage par portail (Custom Block) (push) Has been cancelled
CI / QA · Audit 4Big (95+/100 sur 100% deliverables) (push) Has been cancelled
CI / Démo · Scénarios (run-sheet P07 banquier / P05 client) (push) Has been cancelled
CI / QA · Matrice de régression exhaustive (Sprint 8) (push) Has been cancelled
CI / DevOps · Run-book de déploiement VPS unifié (Sprint 8) (push) Has been cancelled
CI / QA · Matrice d'acceptation / traçabilité MVP (Sprint 8) (push) Has been cancelled
CI / Mobile · config app Expo/EAS (navigation par rôle) (push) Has been cancelled
CI / PIE · manifest de dépendances (Annexe 12 · V10.1) (push) Has been cancelled
CI / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
2026-08-13 02:18:16 +00:00
Claude Code DTP Worker c1e5fd9a6e V18 DECISIONS · Michel tranche D-07 (formules standards Big4) + D-08 (Master Intake sur-ensemble strict brief.json V12) · débloque moteurs 4 Financial + 8 Bankability + Phase 1 schema finale
CI / Contraintes NON-NÉGOCIABLES (CLAUDE.md) (push) Has been cancelled
CI / Validation JSON (schémas Faisabilité) (push) Has been cancelled
CI / Qualité documentaire (liens + 4Big) (push) Has been cancelled
CI / Reproductibilité des artefacts out/ (build == commité) (push) Has been cancelled
CI / Fraîcheur matrice de régression (run == commité) (push) Has been cancelled
CI / Intégrité du câblage CI (gate agrège tout · gates statiques verrouillés) (push) Has been cancelled
CI / Intégrité des chiffres du README (valeur == artefact cité · (push) Has been cancelled
CI / Intégrité mobile-build.yml (gating portable · activation différée · (push) Has been cancelled
CI / Publiciste · parser + schéma + generator (unittest) (push) Has been cancelled
CI / RBAC · 50 rôles + schéma (unittest) (push) Has been cancelled
CI / Faisabilité · générateur 4 volets + round-trip (unittest) (push) Has been cancelled
CI / RBAC · fixtures ERPNext (Role + Custom DocPerm) (push) Has been cancelled
CI / RBAC · plan User Permission (row-level) (push) Has been cancelled
CI / RBAC · Role Profile (bundles par portail) (push) Has been cancelled
CI / RBAC · run-book d'application unifié (agrégat 3 volets) (push) Has been cancelled
CI / Faisabilité · dossier bancable trilingue FR/EN/ES (push) Has been cancelled
CI / CRM · workflow vente ERPNext (lead → CONFOTUR) (push) Has been cancelled
CI / CRM · DocType porteur OTO Dossier Vente (push) Has been cancelled
CI / CRM · barème commissions vendeurs (push) Has been cancelled
CI / CRM · Financement Bancaire (gate hypothécaire RD) (push) Has been cancelled
CI / Fiscal · e-CF DGII (Compupar) (push) Has been cancelled
CI / Frontend · Workspaces 5 portails rôle (push) Has been cancelled
CI / Legal · DocType CONFOTUR Application (push) Has been cancelled
CI / QA · Audit 5D conformité (push) Has been cancelled
CI / SEO · mots-clés trilingues + schema.org + hreflang (push) Has been cancelled
CI / Chat OTOIA · montage par portail (Custom Block) (push) Has been cancelled
CI / QA · Audit 4Big (95+/100 sur 100% deliverables) (push) Has been cancelled
CI / Démo · Scénarios (run-sheet P07 banquier / P05 client) (push) Has been cancelled
CI / QA · Matrice de régression exhaustive (Sprint 8) (push) Has been cancelled
CI / DevOps · Run-book de déploiement VPS unifié (Sprint 8) (push) Has been cancelled
CI / QA · Matrice d'acceptation / traçabilité MVP (Sprint 8) (push) Has been cancelled
CI / Mobile · config app Expo/EAS (navigation par rôle) (push) Has been cancelled
CI / PIE · manifest de dépendances (Annexe 12 · V10.1) (push) Has been cancelled
CI / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
2026-08-13 01:56:05 +00:00
10 changed files with 1198 additions and 0 deletions
@@ -0,0 +1,5 @@
# Artefacts de génération locale (jamais commités — produits à la demande).
__pycache__/
*.pyc
build/
out/
@@ -0,0 +1,87 @@
# V18 · Master Project Intake / Master Data Model (Phase 1)
**Moteur :** V18 · Phase 1/15 (première du plan « ORDRE DÉVELOPPEMENT » de la
[directive V18](../../../DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md)).
**Débloqué par :** [V18_GO_PHASE_1_20260812.md](../../../V18_GO_PHASE_1_20260812.md)
(D-06 APPROVED) + [V18_DECISIONS_D07_D08_20260812.md](../../../V18_DECISIONS_D07_D08_20260812.md)
(D-08 : sur-ensemble STRICT `brief.json` V12).
**Fondé sur :** [OTO V18 · Migration & Architecture Audit](../../../OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md)
§4 (data model) · §5 (provenance) · §11 (séquence).
**Statut :** livrable **prêt pour CP0** (Document Completeness) · en attente de la
validation Michel AVANT toute Phase 2 (séquence GO_PHASE_1 §Validation à chaque
étape). **Auto-score 4Big cible : ≥ 95/100** (doc + contrat schéma + couverture
de tests + CLI reproductible), aligné CLAUDE.md #5.
## Ce que livre la Phase 1 (et ce qu'elle NE livre PAS)
Phase 1 = **le data model, pas un moteur métier.** Conformément à l'audit §11
(« Zéro moteur métier tant que le data model n'est pas validé »), ce module ne
calcule AUCUNE faisabilité, AUCUN DCF/IRR/DSCR (ceux-là arrivent aux moteurs 4/8,
formules Big4 débloquées D-07), n'écrit rien sur le VPS (#8) et n'invente aucune
donnée (#6).
Il fournit le **contrat** du Master Dataset — « One Master Dataset · Multiple
Outputs » :
| Brique | Fichier | Rôle |
|---|---|---|
| Schéma maître | [`master_intake.schema.json`](master_intake.schema.json) | Sur-ensemble strict de [`brief.schema.json`](../generator/brief.schema.json) V12, inclus via `allOf`/`$ref` ; ajoute `dataset_version`, les registres et les défs `data_status`/`data_category`/`data_point`. |
| Modèle de données | [`mdmlib/master_data_model.py`](mdmlib/master_data_model.py) | Les **11 statuts** + **5 catégories** + `DataPoint` traçable + garde des canoniques #9/#10. |
| Evidence Register | [`mdmlib/evidence_register.py`](mdmlib/evidence_register.py) | Registre documentaire (id · version · date) + liens donnée→pièce + détection de provenance pendouillante. |
| CLI | [`master_intake_gen.py`](master_intake_gen.py) | `validate` (contrat + intégrité) et `maturity` (complétude honnête). |
| Fixture pilote | [`fixtures/intake_P01_coralis.json`](fixtures/intake_P01_coralis.json) | P01 Coralis · majoritairement `PENDING` (aucun chiffre inventé). |
| Tests | [`tests/test_master_intake.py`](tests/test_master_intake.py) | 32 tests · contrat, rétro-compat V12, garde canonique, intégrité preuves. |
## Les 11 statuts de donnée (ordre canonique)
`VERIFIED` · `CONFIRMED` · `SOURCE_BASED` · `CALCULATED` · `ESTIMATED` ·
`ASSUMPTION` · `TARGET` · `BANK_REQUIREMENT` · `PENDING` · `MISSING` ·
`NOT_APPLICABLE`
Une donnée n'est jamais un chiffre nu : c'est un `DataPoint` portant sa
provenance (`source_document` · `source_date` · `status` · `confidence` ·
`validated_by`). C'est la généralisation du patron `{formule + opérandes
sourcés/null}` déjà éprouvé dans `banclib/finance.py` (audit §5).
## Les 5 catégories de séparation
`INPUT` · `TARGET` · `ASSUMPTION` · `BANK_REQUIREMENT` · `CALCULATED` — le RÔLE de
la donnée dans le modèle (distinct du statut, qui dit sa fiabilité).
## Rétro-compatibilité V12 (D-08)
- Toute clé du `brief.json` V12 conserve son nom exact (le parser Publiciste
continue de lire les données V18, ignorant silencieusement les champs V18).
- `brief.schema.json` V12 est **inclus** (jamais recopié) via `allOf`/`$ref` sur
son `$id` canonique → source unique, zéro duplication.
- Les champs V18 sont ajoutés en **extension**, jamais en remplacement (test
`test_no_v12_key_shadowed_with_conflicting_type`).
- `dataset_version` distingue V12 vs V18.
## Utilisation
```bash
python3 master_intake_gen.py validate fixtures/intake_P01_coralis.json
python3 master_intake_gen.py maturity fixtures/intake_P01_coralis.json
python3 -m unittest discover -s tests -v
```
Les lacunes `PENDING`/`MISSING` **ne sont pas des erreurs** : elles mesurent la
complétude (audit §4 · « la fraction de champs absents explosera au démarrage…
ce n'est pas un bug »). `validate` ne sort en erreur que sur une **violation de
contrat** (statut/catégorie inconnu, canonique saisi, preuve pendouillante).
## Anti-invention & angle mort déclaré (#6 · #8)
Les **A1-A20 fins** vivent dans la directive complète root-owned (57 chapitres),
illisible par le worker (audit §0). Ce module livre donc le **cadre** ouvert
(`intake_sections` clés `A1..A20`, contenu à confronter au CP0) — il ne fabrique
aucun champ A1-A20. Toute divergence avec la spec fine de Michel sera résolue à
la validation Phase 1.
## Prochain incrément (post-CP0)
Après validation Michel du data model (CP0), le module sera câblé au gate CI
(`.gitea/workflows/ci.yml` + régénération des méta-artefacts régression/4Big) et
Phase 2 (Document / Evidence Engine · généralisation `champs_manquants` → gate
CP0 automatisé) pourra démarrer.
@@ -0,0 +1,68 @@
{
"projet": "P01",
"nom": "Coralis",
"synthetique": true,
"dataset_version": "V18",
"sources": [
"CLAUDE.md §Projets (code projet canonique)",
"V18_GO_PHASE_1_20260812.md §Projet pilote (P01 Coralis)"
],
"masterplan": {
"terrain_m2": null,
"nb_unites": null,
"nb_phases": null
},
"intake_sections": {
"A1": { "titre": "Identification projet", "statut": "amorce" }
},
"document_register": [
{
"doc_id": "DOC-CLAUDE-MD",
"title": "CLAUDE.md — constitution du mandat (§Projets)",
"filename": "CLAUDE.md",
"version": "2026-08-05",
"source_date": "2026-08-05"
}
],
"data_register": [
{
"key": "projet.code",
"value": "P01",
"category": "INPUT",
"status": "SOURCE_BASED",
"source_document": "DOC-CLAUDE-MD",
"source_date": "2026-08-05",
"confidence": 1.0,
"validated_by": "Michel Roy",
"unit": null,
"note": "Code projet canonique ancré CLAUDE.md §Projets."
},
{
"key": "programme.nb_unites",
"value": null,
"category": "INPUT",
"status": "PENDING",
"source_document": null,
"source_date": null,
"confidence": null,
"validated_by": null,
"unit": "unités",
"note": "En attente du brief maître P01 (CP0 Document Completeness)."
},
{
"key": "financier.dscr_cible",
"value": null,
"category": "BANK_REQUIREMENT",
"status": "PENDING",
"source_document": null,
"source_date": null,
"confidence": null,
"validated_by": null,
"unit": "ratio",
"note": "Exigence bancaire DSCR — formules Big4 débloquées D-07 ; valeur à confirmer par la banque (moteur 8 · D-02)."
}
],
"evidence_register": [
{ "data_key": "projet.code", "doc_ids": ["DOC-CLAUDE-MD"] }
]
}
@@ -0,0 +1,112 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://oto.dtp/schemas/faisabilite/master_intake.schema.json",
"title": "OTO V18 · Master Project Intake (A1-A20) · Master Data Model",
"description": "Formulaire d'intake maître V18 qui remplace le Formulaire Briefing V12. D-08 (V18_DECISIONS_D07_D08_20260812.md) : SUR-ENSEMBLE STRICT de brief.json V12 — le contrat V12 brief.schema.json est INCLUS tel quel via allOf/$ref (toute clé V12 conserve son nom exact, rétro-compat parser Publiciste), et les champs V18 sont ajoutés en EXTENSION, jamais en remplacement. Le champ dataset_version distingue V12 vs V18. Anti-invention #6 : les A1-A20 fins restent ouverts tant que la spec root-owned de Michel (#8) n'est pas confrontée — ce schéma livre le CADRE (statuts · catégories · registres), pas des champs A1-A20 fabriqués.",
"type": "object",
"allOf": [
{ "$ref": "https://oto.dtp/schemas/faisabilite/brief.schema.json" }
],
"required": ["projet", "dataset_version"],
"properties": {
"dataset_version": {
"type": "string",
"enum": ["V12", "V18"],
"description": "Distingue un dataset legacy V12 d'un intake V18 (D-08 règle 6). Un intake maître V18 porte « V18 »."
},
"intake_sections": {
"type": "object",
"description": "Slots A1-A20 du Master Project Intake (directive V18 §Master Project Intake). Contenu FIN volontairement OUVERT (additionalProperties) : les 20 sections détaillées vivent dans la directive complète root-owned (#8, audit §0) et seront confrontées par Michel au CP0 — les fabriquer ici serait une invention (#6). Chaque clé est un id de section A1..A20.",
"propertyNames": { "pattern": "^A([1-9]|1[0-9]|20)$" },
"additionalProperties": true
},
"document_register": {
"type": "array",
"description": "Catalogue des pièces probantes (Evidence Room · audit §18). Register documents / files / versions (V18_GO_PHASE_1).",
"items": { "$ref": "#/definitions/document_ref" }
},
"data_register": {
"type": "array",
"description": "Le Master Dataset : chaque donnée AVEC sa provenance (statut · source · confiance · validateur). One Master Dataset · Multiple Outputs — les 3 sorties (Bank/Full/Ops) sont des projections de ce registre.",
"items": { "$ref": "#/definitions/data_point" }
},
"evidence_register": {
"type": "array",
"description": "Liens donnée→pièce : rattache un data_key à un ou plusieurs doc_id du document_register (intégrité référentielle : pas de provenance fantôme).",
"items": {
"type": "object",
"required": ["data_key", "doc_ids"],
"additionalProperties": false,
"properties": {
"data_key": { "type": "string" },
"doc_ids": {
"type": "array",
"items": { "type": "string" },
"minItems": 1
}
}
}
}
},
"definitions": {
"data_status": {
"description": "Les 11 statuts obligatoires (V18_GO_PHASE_1 §Data status). Ordre canonique = du plus fiable au sans-objet ; miroir de mdmlib.master_data_model.STATUSES.",
"type": "string",
"enum": [
"VERIFIED",
"CONFIRMED",
"SOURCE_BASED",
"CALCULATED",
"ESTIMATED",
"ASSUMPTION",
"TARGET",
"BANK_REQUIREMENT",
"PENDING",
"MISSING",
"NOT_APPLICABLE"
]
},
"data_category": {
"description": "Les 5 catégories de séparation (V18_GO_PHASE_1 §Séparation). Le RÔLE de la donnée dans le modèle ; miroir de mdmlib.master_data_model.CATEGORIES.",
"type": "string",
"enum": [
"INPUT",
"TARGET",
"ASSUMPTION",
"BANK_REQUIREMENT",
"CALCULATED"
]
},
"data_point": {
"type": "object",
"description": "Une donnée du Master Dataset avec sa provenance intégrale.",
"required": ["key", "category", "status"],
"additionalProperties": false,
"properties": {
"key": { "type": "string", "minLength": 1 },
"value": {},
"category": { "$ref": "#/definitions/data_category" },
"status": { "$ref": "#/definitions/data_status" },
"source_document": { "type": ["string", "null"] },
"source_date": { "type": ["string", "null"] },
"confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 1 },
"validated_by": { "type": ["string", "null"] },
"unit": { "type": ["string", "null"] },
"note": { "type": ["string", "null"] }
}
},
"document_ref": {
"type": "object",
"description": "Une pièce probante cataloguée (id unique · version · date).",
"required": ["doc_id", "title"],
"additionalProperties": false,
"properties": {
"doc_id": { "type": "string", "minLength": 1 },
"title": { "type": "string", "minLength": 1 },
"filename": { "type": ["string", "null"] },
"version": { "type": ["string", "null"] },
"source_date": { "type": ["string", "null"] }
}
}
}
}
@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""master_intake_gen · CLI Phase 1 du Master Project Intake V18.
PÉRIMÈTRE STRICT Phase 1 (audit §11 · « Zéro moteur métier tant que le data
model n'est pas validé ») : ce CLI NE GÉNÈRE aucune faisabilité, aucun chiffre
financier, aucun rendu. Il VALIDE un intake maître contre le contrat V18
(statuts · catégories · provenance · intégrité référentielle des preuves) et
imprime une MATURITÉ DATA honnête (présents vs lacunes) — la mesure de
complétude qui alimentera le CP0 (Document Completeness) au moteur 2.
Usage :
python3 master_intake_gen.py validate <intake.json>
python3 master_intake_gen.py maturity <intake.json>
Sortie : code 0 si le contrat est respecté (les lacunes PENDING/MISSING ne sont
PAS des erreurs — c'est la complétude, pas un échec) ; code 1 si le contrat est
violé (statut/catégorie inconnu · preuve pendouillante · JSON mal formé) ;
code 2 si l'usage est incorrect.
stdlib pur. L'oracle `jsonschema` est utilisé s'il est présent (avec
brief.schema.json V12 dans le store de résolution du $ref) ; sinon la validation
structurelle du contrat reste assurée par mdmlib (en mémoire).
"""
from __future__ import annotations
import json
import os
import sys
from collections import Counter
from typing import Any
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
from mdmlib import ( # noqa: E402
CATEGORIES,
STATUSES,
DataPoint,
Document,
DocumentRegister,
EvidenceRegister,
MasterDataError,
)
SCHEMA_PATH = os.path.join(_HERE, "master_intake.schema.json")
BRIEF_SCHEMA_PATH = os.path.join(_HERE, "..", "generator", "brief.schema.json")
def _load_json(path: str) -> Any:
with open(path, encoding="utf-8") as fh:
return json.load(fh)
def build_registers(intake: dict) -> tuple[list[DataPoint], EvidenceRegister]:
"""Reconstruit le Master Dataset en mémoire — lève MasterDataError si le
contrat est violé (statut/catégorie/canonique/provenance fantôme)."""
docs = DocumentRegister()
for d in intake.get("document_register", []) or []:
docs.add(
Document(
doc_id=d.get("doc_id", ""),
title=d.get("title", ""),
filename=d.get("filename"),
version=d.get("version"),
source_date=d.get("source_date"),
)
)
points: list[DataPoint] = []
for dp in intake.get("data_register", []) or []:
points.append(
DataPoint(
key=dp.get("key", ""),
value=dp.get("value"),
category=dp.get("category", ""),
status=dp.get("status", ""),
source_document=dp.get("source_document"),
source_date=dp.get("source_date"),
confidence=dp.get("confidence"),
validated_by=dp.get("validated_by"),
unit=dp.get("unit"),
note=dp.get("note"),
)
)
ev = EvidenceRegister(docs)
for link in intake.get("evidence_register", []) or []:
for doc_id in link.get("doc_ids", []) or []:
ev.link(link.get("data_key", ""), doc_id)
return points, ev
def _schema_validate(intake: dict) -> list[str]:
"""Validation structurelle via l'oracle jsonschema (best-effort).
Renvoie [] si valide OU si l'oracle est absent (skip silencieux, comme les
17 skips « par design » de la matrice régression sous `python -S`)."""
try:
import jsonschema # type: ignore
except Exception:
return []
schema = _load_json(SCHEMA_PATH)
brief = _load_json(BRIEF_SCHEMA_PATH)
store = {
schema.get("$id", ""): schema,
brief.get("$id", ""): brief,
}
resolver = jsonschema.RefResolver.from_schema(schema, store=store)
validator = jsonschema.Draft7Validator(schema, resolver=resolver)
return [e.message for e in validator.iter_errors(intake)]
def cmd_validate(path: str) -> int:
intake = _load_json(path)
schema_errors = _schema_validate(intake)
try:
points, ev = build_registers(intake)
except MasterDataError as exc:
print(f"❌ Contrat Master Data Model violé : {exc}")
return 1
dangling = ev.dangling_evidence(points)
print(f"Intake : {path}")
print(f" dataset_version : {intake.get('dataset_version', '(absent)')}")
print(f" projet : {intake.get('projet', '(absent)')}")
print(f" pièces : {len(ev.documents)}")
print(f" données : {len(points)}")
if schema_errors:
print("❌ Non conforme au schéma master_intake.schema.json :")
for m in schema_errors:
print(f" · {m}")
return 1
if dangling:
print("❌ Preuve(s) pendouillante(s) (SOURCE_BASED sans pièce résolue) :")
for k in dangling:
print(f" · {k}")
return 1
print("✅ Contrat V18 respecté (statuts · catégories · provenance intègres).")
return 0
def cmd_maturity(path: str) -> int:
intake = _load_json(path)
try:
points, _ = build_registers(intake)
except MasterDataError as exc:
print(f"❌ Contrat Master Data Model violé : {exc}")
return 1
total = len(points)
present = sum(1 for p in points if p.present)
by_status: Counter[str] = Counter(p.status for p in points)
by_cat: Counter[str] = Counter(p.category for p in points)
pct = (100.0 * present / total) if total else 0.0
print(f"Maturité data · {path}")
print(f" données présentes : {present}/{total} ({pct:.1f} %)")
print(" par statut :")
for s in STATUSES:
if by_status.get(s):
print(f" {s:<16} {by_status[s]}")
print(" par catégorie :")
for c in CATEGORIES:
if by_cat.get(c):
print(f" {c:<16} {by_cat[c]}")
print(" (les lacunes PENDING/MISSING sont attendues au démarrage · audit §4)")
return 0
def main(argv: list[str]) -> int:
if len(argv) < 2 or argv[1] in ("-h", "--help"):
print(__doc__)
return 0 if (len(argv) >= 2 and argv[1] in ("-h", "--help")) else 2
cmd = argv[1]
if cmd in ("validate", "maturity"):
if len(argv) != 3:
print(f"usage : master_intake_gen.py {cmd} <intake.json>", file=sys.stderr)
return 2
return (cmd_validate if cmd == "validate" else cmd_maturity)(argv[2])
print(f"commande inconnue « {cmd} » (attendu : validate | maturity)", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
@@ -0,0 +1,40 @@
"""Master Data Model V18 — bibliothèque (Phase 1 · Master Project Intake).
Débloquée par `V18_GO_PHASE_1_20260812.md` (D-06 APPROVED) +
`V18_DECISIONS_D07_D08_20260812.md` (D-08). Voir `../README.md`.
Expose le contrat en mémoire du Master Dataset (One Master Dataset · Multiple
Outputs) : statuts, catégories, DataPoint traçable, registres documentaires.
Aucun moteur métier ici (Zéro moteur tant que le data model n'est pas validé au
CP0 · audit §11).
"""
from .master_data_model import (
CANONICAL,
CATEGORIES,
STATUSES,
DataPoint,
MasterDataError,
assert_not_canonical,
is_filled,
is_present,
)
from .evidence_register import (
Document,
DocumentRegister,
EvidenceRegister,
)
__all__ = [
"CANONICAL",
"CATEGORIES",
"STATUSES",
"DataPoint",
"MasterDataError",
"assert_not_canonical",
"is_filled",
"is_present",
"Document",
"DocumentRegister",
"EvidenceRegister",
]
@@ -0,0 +1,118 @@
"""Evidence Register V18 · registre documentaire + traçabilité des preuves.
Phase 1 · brique « Data Register + Evidence Register (traçabilité) » de
`V18_GO_PHASE_1_20260812.md` : « Register documents / files / versions ». C'est
l'amorce en mémoire du futur moteur 2 (Document / Evidence Engine · audit §11) —
ici on livre le REGISTRE et ses invariants d'intégrité, pas encore l'extraction.
Deux registres complémentaires :
* DocumentRegister · catalogue des pièces probantes (id · titre · fichier ·
version · date). Une pièce = une source citable, versionnée (audit §4.3 :
baseline vs actual = vues horodatées, jamais un écrasement).
* EvidenceRegister · lie chaque `DataPoint.key` à la (aux) pièce(s) qui la
justifie(nt). Ferme la « provenance fantôme » : un DataPoint SOURCE_BASED
dont le `source_document` ne résout à AUCUNE pièce enregistrée est un lien
pendouillant → détecté ici (`dangling_evidence`).
Anti-invention (#6) : le registre n'invente aucune pièce ; il ne fait que
cataloguer ce qui est déclaré et VÉRIFIER la cohérence référentielle. stdlib pur,
aucune écriture VPS (#8).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Iterable, Optional
from .master_data_model import DataPoint, MasterDataError
@dataclass(frozen=True)
class Document:
"""Une pièce probante cataloguée (audit §18 · Evidence Room)."""
doc_id: str
title: str
filename: Optional[str] = None
version: Optional[str] = None
source_date: Optional[str] = None
def __post_init__(self) -> None:
if not self.doc_id or not str(self.doc_id).strip():
raise MasterDataError("Document.doc_id ne peut être vide.")
if not self.title or not str(self.title).strip():
raise MasterDataError("Document.title ne peut être vide.")
class DocumentRegister:
"""Catalogue des pièces probantes, indexé par `doc_id` unique."""
def __init__(self) -> None:
self._docs: dict[str, Document] = {}
def add(self, doc: Document) -> Document:
if doc.doc_id in self._docs:
raise MasterDataError(
f"doc_id dupliqué « {doc.doc_id} » — l'id de pièce doit être "
f"unique (registre = source citable non ambiguë)."
)
self._docs[doc.doc_id] = doc
return doc
def get(self, doc_id: str) -> Optional[Document]:
return self._docs.get(doc_id)
def has(self, doc_id: str) -> bool:
return doc_id in self._docs
def __len__(self) -> int:
return len(self._docs)
def ids(self) -> list[str]:
return sorted(self._docs)
class EvidenceRegister:
"""Lie les DataPoint à leurs pièces + vérifie l'intégrité référentielle."""
def __init__(self, documents: Optional[DocumentRegister] = None) -> None:
self.documents: DocumentRegister = documents or DocumentRegister()
# key du DataPoint -> ensemble ordonné de doc_id justificatifs
self._links: dict[str, list[str]] = {}
def link(self, data_key: str, doc_id: str) -> None:
"""Rattache la donnée `data_key` à la pièce `doc_id` (déjà cataloguée)."""
if not self.documents.has(doc_id):
raise MasterDataError(
f"lien vers une pièce inconnue « {doc_id} » — la cataloguer "
f"d'abord dans le DocumentRegister (pas de preuve fantôme)."
)
bucket = self._links.setdefault(data_key, [])
if doc_id not in bucket:
bucket.append(doc_id)
def evidence_for(self, data_key: str) -> list[str]:
"""Pièces justificatives (doc_id) déclarées pour cette donnée."""
return list(self._links.get(data_key, ()))
def dangling_evidence(self, data_points: Iterable[DataPoint]) -> list[str]:
"""Clés de DataPoint SOURCE_BASED dont la pièce citée n'est PAS cataloguée.
C'est le cœur de l'intégrité : un DataPoint qui prétend venir d'un
document (`status == SOURCE_BASED`) doit pouvoir résoudre ce document —
soit via un lien Evidence, soit via un `source_document` == doc_id connu.
Toute clé retournée est une provenance non résoluble (à corriger).
"""
missing: list[str] = []
for dp in data_points:
if dp.status != "SOURCE_BASED":
continue
linked = self.evidence_for(dp.key)
resolves = any(self.documents.has(d) for d in linked) or (
dp.source_document is not None
and self.documents.has(dp.source_document)
)
if not resolves:
missing.append(dp.key)
return sorted(missing)
@@ -0,0 +1,222 @@
"""Master Data Model V18 · statuts de donnée + catégories + DataPoint.
Phase 1 du moteur V18 (`DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md`
« ORDRE DÉVELOPPEMENT · 1. Master Project Intake / Data Model »), débloquée par
`V18_GO_PHASE_1_20260812.md` (D-06 APPROVED) et `V18_DECISIONS_D07_D08_20260812.md`
(D-08 : Master Intake A1-A20 = sur-ensemble STRICT de `brief.json` V12).
Principe « One Master Dataset · Multiple Outputs » : une donnée n'est jamais un
chiffre nu. C'est un `DataPoint` qui porte SA PROVENANCE — statut de fiabilité,
document source, date, confiance, validateur. Les 3 sorties V18 (Bank Package /
Full Feasibility / Ops) sont des PROJECTIONS de ce dataset, jamais des copies
éditables (audit §4/§7).
Anti-invention (CLAUDE.md #6 · directive V18 « NE PAS inventer données ») : le
modèle n'attribue AUCUNE valeur ni statut par défaut « optimiste ». Une donnée
absente est explicitement `MISSING`/`PENDING` — la complétude devient une mesure
honnête (audit §4, « la fraction de champs absents explosera au démarrage… ce
n'est pas un bug »).
Ce module ne FAIT rien d'externe : ni I/O réseau, ni écriture VPS (#8). stdlib
pur. Il définit le contrat en mémoire ; le schéma machine miroir est
`../master_intake.schema.json` (défs `data_status` / `data_category` /
`data_point`), tenu identique par la suite de tests.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Optional
# --------------------------------------------------------------------------- #
# Les 11 STATUTS de donnée (V18_GO_PHASE_1 §« Data status obligatoire »).
# ORDRE CANONIQUE = du plus fiable (prouvé/vérifié) au moins engageant
# (non applicable). Cet ordre est un CONTRAT : le schéma `data_status.enum` et la
# suite de tests le rejouent à l'identique (aucune ré-ordonnance silencieuse).
#
# VERIFIED · vérifié contre une pièce probante (audit/tiers)
# CONFIRMED · confirmé par une partie prenante autorisée
# SOURCE_BASED · repris verbatim d'un document source daté
# CALCULATED · dérivé par formule traçable d'opérandes sourcés
# ESTIMATED · estimation méthodologique déclarée (non un fait)
# ASSUMPTION · hypothèse de travail explicite (à valider)
# TARGET · objectif fixé (cible), pas une donnée constatée
# BANK_REQUIREMENT · exigence imposée par la banque / le prêteur
# PENDING · attendu, en cours de collecte
# MISSING · absent — devrait exister, non fourni
# NOT_APPLICABLE · sans objet pour ce projet
# --------------------------------------------------------------------------- #
STATUSES: tuple[str, ...] = (
"VERIFIED",
"CONFIRMED",
"SOURCE_BASED",
"CALCULATED",
"ESTIMATED",
"ASSUMPTION",
"TARGET",
"BANK_REQUIREMENT",
"PENDING",
"MISSING",
"NOT_APPLICABLE",
)
# Statuts qui dénotent une donnée EXPLOITABLE (présente et défendable) vs une
# lacune. Sert au futur gate CP0 (Document Completeness, moteur 2 · audit §6).
_ABSENT_STATUSES: frozenset[str] = frozenset({"PENDING", "MISSING"})
# --------------------------------------------------------------------------- #
# Les 5 CATÉGORIES de séparation (V18_GO_PHASE_1 §« Séparation INPUT / TARGET /
# ASSUMPTION / BANK_REQUIREMENT / CALCULATED »). La CATÉGORIE dit le RÔLE de la
# donnée dans le modèle ; le STATUT dit sa FIABILITÉ. (TARGET / ASSUMPTION /
# BANK_REQUIREMENT / CALCULATED existent dans les deux axes à dessein : un même
# libellé peut être un rôle ET un état — ils ne sont pas confondus.)
# --------------------------------------------------------------------------- #
CATEGORIES: tuple[str, ...] = (
"INPUT", # donnée d'entrée fournie (le brief V12 vit ici)
"TARGET", # objectif projet (ex. absorption cible)
"ASSUMPTION", # hypothèse de modélisation
"BANK_REQUIREMENT", # contrainte bancaire imposée
"CALCULATED", # sortie dérivée du moteur (jamais saisie)
)
# --------------------------------------------------------------------------- #
# Paramètres CANONIQUES CLAUDE.md #9/#10 — RÉIMPOSÉS à l'identique du générateur
# V12 (`generator/genlib/model.py CANONICAL`), conformément à l'audit §8 non-
# négociable #9 : « À réimposer identiques dans le Master Data Model V18 (ne pas
# les rendre saisissables) ». Ils ne sont JAMAIS des DataPoint d'intake : ils
# sont IMPOSÉS par le moteur pour tous les projets → aucune invention projet par
# projet possible. La suite de tests re-vérifie l'égalité stricte avec la valeur
# du générateur (single source of truth : toute dérive est rouge).
# --------------------------------------------------------------------------- #
CANONICAL: dict[str, str] = {
"frais_edition_pct": "3 %",
"marketing_pct": "8.5 %",
"point_equilibre_pct": "52 %",
"devises": "USD + DOP",
"paiement": "Cardnet",
"format_doc": "Letter US",
}
# Clés réservées : ne peuvent JAMAIS être une donnée d'intake saisissable.
_CANONICAL_KEYS: frozenset[str] = frozenset(CANONICAL)
class MasterDataError(ValueError):
"""Violation du contrat Master Data Model (statut/catégorie/canonique)."""
def is_present(status: str) -> bool:
"""True si le statut dénote une donnée exploitable (ni PENDING ni MISSING)."""
return status in STATUSES and status not in _ABSENT_STATUSES
def assert_not_canonical(key: str) -> None:
"""Rejette toute tentative de saisir un paramètre canonique comme intake.
Les 6 constantes #9/#10 sont IMPOSÉES par le moteur, jamais fournies par le
brief → les capter comme DataPoint rouvrirait la porte à l'invention projet
par projet (audit §8 · l'invariant de migration le plus important).
"""
if key in _CANONICAL_KEYS:
raise MasterDataError(
f"« {key} » est un paramètre CANONIQUE imposé (#9/#10) — "
f"interdit comme donnée d'intake saisissable (audit §8)."
)
@dataclass(frozen=True)
class DataPoint:
"""Une donnée du Master Dataset AVEC sa provenance (audit §4/§5).
Le patron « valeur = {donnée + provenance + statut} » généralise le
`{formule + opérandes sourcés/null}` déjà éprouvé dans `banclib/finance.py`
(audit §5 : « exactement le bon patron pour un moteur financier auditable »).
Champs de traçabilité (V18_GO_PHASE_1 §« Data Register + Evidence Register :
chaque donnée: source_document · source_date · status · confidence ·
validated_by »).
"""
key: str
value: Any
category: str
status: str
source_document: Optional[str] = None
source_date: Optional[str] = None
confidence: Optional[float] = None
validated_by: Optional[str] = None
unit: Optional[str] = None
note: Optional[str] = None
def __post_init__(self) -> None:
if not self.key or not str(self.key).strip():
raise MasterDataError("DataPoint.key ne peut être vide.")
assert_not_canonical(self.key)
if self.category not in CATEGORIES:
raise MasterDataError(
f"catégorie inconnue « {self.category} » "
f"(attendu ∈ {CATEGORIES})."
)
if self.status not in STATUSES:
raise MasterDataError(
f"statut inconnu « {self.status} » (attendu ∈ {STATUSES})."
)
if self.confidence is not None:
if not isinstance(self.confidence, (int, float)) or isinstance(
self.confidence, bool
):
raise MasterDataError("confidence doit être un nombre 0..1 ou None.")
if not 0.0 <= float(self.confidence) <= 1.0:
raise MasterDataError("confidence doit être dans [0, 1].")
# Cohérence provenance ⟺ statut : une donnée SOURCE_BASED prétend venir
# d'un document. Sa provenance peut être fournie SOIT en ligne
# (source_document), SOIT par un lien de l'Evidence Register — un
# DataPoint isolé ne connaît pas le registre, donc la complétude de
# provenance est une propriété du DATASET (EvidenceRegister.
# dangling_evidence), pas du point pris seul. On ne la vérifie donc PAS
# ici (sinon on interdirait le chemin « preuve par lien »).
#
# Symétriquement, un trou (PENDING/MISSING) ne peut pas porter une
# valeur non nulle — ce serait une donnée déguisée en lacune.
if self.status in _ABSENT_STATUSES and is_filled(self.value):
raise MasterDataError(
f"« {self.key} » est {self.status} mais porte une valeur — "
f"une lacune ne peut pas contenir de donnée."
)
@property
def present(self) -> bool:
"""Raccourci : la donnée est-elle exploitable (pas une lacune) ?"""
return is_present(self.status)
def as_dict(self) -> dict[str, Any]:
"""Projection sérialisable (ordre de clés stable pour byte-repro)."""
return {
"key": self.key,
"value": self.value,
"category": self.category,
"status": self.status,
"source_document": self.source_document,
"source_date": self.source_date,
"confidence": self.confidence,
"validated_by": self.validated_by,
"unit": self.unit,
"note": self.note,
}
def is_filled(value: Any) -> bool:
"""True si `value` est une donnée réelle (pas None ni placeholder vide).
Aligné sur `generator/genlib/model.py is_filled` : un vrai 0 numérique est
une donnée valide ; None / chaîne vide / liste vide ne le sont pas.
"""
if value is None:
return False
if isinstance(value, bool):
return True
if isinstance(value, (int, float)):
return True
if isinstance(value, (list, tuple, dict)):
return len(value) > 0
return bool(str(value).strip())
@@ -0,0 +1,290 @@
"""Suite Phase 1 · Master Project Intake / Master Data Model V18.
Vérifie le CONTRAT (pas un moteur métier — il n'y en a pas encore, audit §11) :
statuts/catégories canoniques · DataPoint traçable · garde canonique #9/#10 ·
intégrité de l'Evidence Register · rétro-compat STRICTE avec brief.json V12
(D-08) · cohérence schéma ↔ code. stdlib pur ; l'oracle jsonschema est utilisé
s'il est présent, sinon ignoré (comme les 17 skips « par design » du dépôt).
"""
from __future__ import annotations
import json
import os
import sys
import unittest
_HERE = os.path.dirname(os.path.abspath(__file__))
_MODULE = os.path.dirname(_HERE)
for p in (_MODULE,):
if p not in sys.path:
sys.path.insert(0, p)
from mdmlib import ( # noqa: E402
CANONICAL,
CATEGORIES,
STATUSES,
DataPoint,
Document,
DocumentRegister,
EvidenceRegister,
MasterDataError,
assert_not_canonical,
is_present,
)
import master_intake_gen as cli # noqa: E402
SCHEMA_PATH = os.path.join(_MODULE, "master_intake.schema.json")
BRIEF_SCHEMA_PATH = os.path.join(_MODULE, "..", "generator", "brief.schema.json")
FIXTURE = os.path.join(_MODULE, "fixtures", "intake_P01_coralis.json")
GEN_CANONICAL_PATH = os.path.join(
_MODULE, "..", "generator", "genlib", "model.py"
)
def _load(path: str) -> dict:
with open(path, encoding="utf-8") as fh:
return json.load(fh)
class TestStatusesCategories(unittest.TestCase):
def test_exactly_11_statuses_in_canonical_order(self):
self.assertEqual(len(STATUSES), 11)
self.assertEqual(
STATUSES,
(
"VERIFIED", "CONFIRMED", "SOURCE_BASED", "CALCULATED",
"ESTIMATED", "ASSUMPTION", "TARGET", "BANK_REQUIREMENT",
"PENDING", "MISSING", "NOT_APPLICABLE",
),
)
def test_exactly_5_categories(self):
self.assertEqual(
CATEGORIES,
("INPUT", "TARGET", "ASSUMPTION", "BANK_REQUIREMENT", "CALCULATED"),
)
def test_is_present_only_false_for_pending_and_missing(self):
absent = {s for s in STATUSES if not is_present(s)}
self.assertEqual(absent, {"PENDING", "MISSING"})
class TestSchemaMirrorsCode(unittest.TestCase):
"""Le schéma machine et le code Python doivent porter les MÊMES enums (pas
de dérive silencieuse entre les deux surfaces du contrat)."""
def setUp(self):
self.schema = _load(SCHEMA_PATH)
def test_schema_is_draft07(self):
self.assertEqual(
self.schema["$schema"], "http://json-schema.org/draft-07/schema#"
)
def test_status_enum_matches_code(self):
enum = self.schema["definitions"]["data_status"]["enum"]
self.assertEqual(tuple(enum), STATUSES)
def test_category_enum_matches_code(self):
enum = self.schema["definitions"]["data_category"]["enum"]
self.assertEqual(tuple(enum), CATEGORIES)
def test_dataset_version_enum(self):
self.assertEqual(
self.schema["properties"]["dataset_version"]["enum"], ["V12", "V18"]
)
class TestRetroCompatV12(unittest.TestCase):
"""D-08 : master_intake = SUR-ENSEMBLE STRICT du brief.json V12."""
def setUp(self):
self.master = _load(SCHEMA_PATH)
self.brief = _load(BRIEF_SCHEMA_PATH)
def test_master_includes_brief_by_ref(self):
refs = [a.get("$ref") for a in self.master.get("allOf", [])]
self.assertIn(self.brief["$id"], refs,
"brief.schema.json V12 doit être inclus via allOf/$ref (D-08 règle 2)")
def test_no_v12_key_shadowed_with_conflicting_type(self):
# Toute clé V12 réintroduite dans les properties V18 doit garder un type
# compatible (rétro-compat parser Publiciste · D-08 règle 1). Ici V18
# n'en réintroduit AUCUNE (extension pure) — on le prouve.
v12_keys = set(self.brief.get("properties", {}))
v18_keys = set(self.master.get("properties", {}))
self.assertEqual(
v12_keys & v18_keys, set(),
"les champs V18 sont ajoutés en EXTENSION, jamais en remplacement (D-08 règle 3)",
)
def test_dataset_version_field_present(self):
self.assertIn("dataset_version", self.master["properties"])
class TestDataPoint(unittest.TestCase):
def test_valid_point(self):
dp = DataPoint(key="k", value=1, category="INPUT", status="VERIFIED")
self.assertTrue(dp.present)
self.assertEqual(dp.as_dict()["key"], "k")
def test_unknown_status_rejected(self):
with self.assertRaises(MasterDataError):
DataPoint(key="k", value=1, category="INPUT", status="NOPE")
def test_unknown_category_rejected(self):
with self.assertRaises(MasterDataError):
DataPoint(key="k", value=1, category="NOPE", status="VERIFIED")
def test_empty_key_rejected(self):
with self.assertRaises(MasterDataError):
DataPoint(key=" ", value=1, category="INPUT", status="VERIFIED")
def test_confidence_out_of_range_rejected(self):
with self.assertRaises(MasterDataError):
DataPoint(key="k", value=1, category="INPUT", status="VERIFIED",
confidence=1.5)
def test_confidence_bool_rejected(self):
with self.assertRaises(MasterDataError):
DataPoint(key="k", value=1, category="INPUT", status="VERIFIED",
confidence=True)
def test_source_based_constructs_without_inline_doc(self):
# La provenance d'un SOURCE_BASED peut venir d'un lien Evidence : le
# point seul se construit (la complétude est vérifiée au niveau dataset,
# cf. TestEvidenceRegister.test_dangling_evidence_detected).
dp = DataPoint(key="k", value="x", category="INPUT", status="SOURCE_BASED")
self.assertTrue(dp.present)
DataPoint(key="k", value="x", category="INPUT", status="SOURCE_BASED",
source_document="DOC-1")
def test_absent_status_cannot_carry_value(self):
with self.assertRaises(MasterDataError):
DataPoint(key="k", value=42, category="INPUT", status="PENDING")
# None est OK pour une lacune
DataPoint(key="k", value=None, category="INPUT", status="MISSING")
class TestCanonicalGuard(unittest.TestCase):
"""Les 6 canoniques #9/#10 ne peuvent jamais devenir un intake saisissable."""
def test_each_canonical_key_rejected(self):
for key in CANONICAL:
with self.assertRaises(MasterDataError):
assert_not_canonical(key)
with self.assertRaises(MasterDataError):
DataPoint(key=key, value="x", category="INPUT", status="VERIFIED")
def test_canonical_values_match_generator_v12(self):
# Single source of truth : les valeurs canoniques V18 sont IDENTIQUES à
# celles du générateur V12 (audit §8 « réimposer identiques »). On les
# relit dans generator/genlib/model.py sans l'importer (évite tout effet
# de bord d'import du module de prod) et on exige l'égalité stricte.
ns: dict = {}
with open(GEN_CANONICAL_PATH, encoding="utf-8") as fh:
src = fh.read()
# extrait le littéral CANONICAL = {...}
start = src.index("CANONICAL = {")
end = src.index("}", start) + 1
exec(src[start:end], ns) # noqa: S102 — littéral de constantes contrôlé
self.assertEqual(CANONICAL, ns["CANONICAL"])
class TestEvidenceRegister(unittest.TestCase):
def setUp(self):
self.docs = DocumentRegister()
self.docs.add(Document(doc_id="D1", title="Pièce 1"))
def test_duplicate_doc_id_rejected(self):
with self.assertRaises(MasterDataError):
self.docs.add(Document(doc_id="D1", title="doublon"))
def test_link_to_unknown_doc_rejected(self):
ev = EvidenceRegister(self.docs)
with self.assertRaises(MasterDataError):
ev.link("k", "GHOST")
def test_dangling_evidence_detected(self):
ev = EvidenceRegister(self.docs)
# SOURCE_BASED dont la pièce n'est pas cataloguée → pendouillant
dp = DataPoint(key="k", value="x", category="INPUT",
status="SOURCE_BASED", source_document="GHOST")
self.assertEqual(ev.dangling_evidence([dp]), ["k"])
def test_resolved_evidence_not_dangling(self):
ev = EvidenceRegister(self.docs)
dp = DataPoint(key="k", value="x", category="INPUT",
status="SOURCE_BASED", source_document="D1")
self.assertEqual(ev.dangling_evidence([dp]), [])
def test_link_resolves_evidence(self):
ev = EvidenceRegister(self.docs)
dp = DataPoint(key="k", value="x", category="INPUT",
status="SOURCE_BASED", source_document=None)
# pas de source_document mais un lien Evidence vers D1 → résout
ev.link("k", "D1")
self.assertEqual(ev.dangling_evidence([dp]), [])
class TestPilotFixture(unittest.TestCase):
"""Le fixture pilote P01 respecte le contrat et est un vrai sur-ensemble V12."""
def setUp(self):
self.intake = _load(FIXTURE)
def test_is_superset_of_v12_brief(self):
# champ requis V12 présent + version V18 + champs V18 d'extension
self.assertEqual(self.intake["projet"], "P01")
self.assertEqual(self.intake["dataset_version"], "V18")
self.assertIn("data_register", self.intake)
def test_synthetique_flagged(self):
self.assertTrue(self.intake.get("synthetique"),
"un fixture de test doit être marqué synthetique (jamais publiable)")
def test_contract_holds_and_no_dangling(self):
points, ev = cli.build_registers(self.intake)
self.assertEqual(ev.dangling_evidence(points), [])
# honnêteté : la majorité des données sont des lacunes au démarrage
present = [p for p in points if p.present]
self.assertTrue(len(present) >= 1)
def test_cli_validate_returns_zero(self):
self.assertEqual(cli.cmd_validate(FIXTURE), 0)
def test_cli_maturity_returns_zero(self):
self.assertEqual(cli.cmd_maturity(FIXTURE), 0)
def test_jsonschema_oracle_if_present(self):
try:
import jsonschema # noqa: F401
except Exception:
self.skipTest("jsonschema absent — oracle structurel ignoré (par design)")
errors = cli._schema_validate(self.intake)
self.assertEqual(errors, [], f"fixture non conforme : {errors}")
class TestCliContractViolations(unittest.TestCase):
def test_bad_status_fixture_fails_validate(self):
import tempfile
bad = {
"projet": "P02",
"dataset_version": "V18",
"data_register": [
{"key": "x", "value": 1, "category": "INPUT", "status": "BOGUS"}
],
}
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False,
encoding="utf-8") as fh:
json.dump(bad, fh)
path = fh.name
try:
self.assertEqual(cli.cmd_validate(path), 1)
finally:
os.unlink(path)
if __name__ == "__main__":
unittest.main(verbosity=2)
+70
View File
@@ -0,0 +1,70 @@
# 🎯 V18 · Décisions Michel · D-07 + D-08 tranchées
**Date:** 2026-08-12 · **Autorisation:** Michel Roy · **Publication:** Gitea
## D-07 · APPROUVÉ · Formules standards Big4
**Décision Michel:** Claude Code DTP-Worker est autorisé à utiliser les **formules standards Big4** (Deloitte / KPMG / PwC / EY) pour toutes les métriques financières V18:
- **DCF** (Discounted Cash Flow) — standard NPV + terminal value + WACC
- **IRR / VAN** (Internal Rate of Return · Net Present Value) — standard time-value-of-money
- **DSCR** (Debt Service Coverage Ratio) — Net Operating Income / Debt Service annuel
- **LTV** (Loan-to-Value) — Loan Amount / Property Value
- **LTC** (Loan-to-Cost) — Loan Amount / Total Development Cost
- **Break-even analysis** — Fixed Costs / (Price - Variable Cost)
- **Sensitivity analysis** — ±10% swing sur variables clés (prix vente · CAPEX · absorption)
- **Downside / Stress case** — Monte Carlo simplifié 3 scénarios
**Sources autorisées:**
- Deloitte Real Estate Feasibility Standards
- KPMG Real Estate Valuation Handbook
- PwC Real Estate Investor Guidelines
- EY Global Real Estate Trends
- ULI (Urban Land Institute) standards
- FIABCI Real Estate Standards
**Contrainte:** chaque formule utilisée dans un livrable V18 doit être **sourcée explicitement** en note de bas de page ou annexe (nom de la source + année).
**Débloqué:** Moteurs Phase 4 (Financial Engine) et Phase 8 (Bankability Engine).
---
## D-08 · APPROUVÉ · Master Intake A1-A20 = sur-ensemble STRICT brief.json V12
**Décision Michel:** OUI · le Master Project Intake V18 (A1-A20) est un **sur-ensemble strict** du `brief.json` V12.
**Règles de migration:**
1. **Tout champ V12 existant** conserve son nom/clé exact dans V18 (rétro-compat parser Publiciste)
2. `brief.schema.json` (V12) devient un **sous-schéma inclus** dans `master_intake.schema.json` (V18)
3. **Nouveaux champs V18** ajoutés en extension · jamais en remplacement
4. **Parser V12 continue de fonctionner** sur les données V18 (lecture partielle · les champs V18 ignorés silencieusement par le vieux parser)
5. **Aucune migration destructive** — projets V12 existants restent lisibles/utilisables
6. **Le champ `dataset_version`** distingue V12 vs V18 pour éviter confusion
**Bénéfice:** transition douce · aucune interruption du pipeline V12 pendant la construction V18 · rétro-compat garantie sur brief.json déjà en production.
**Débloqué:** Phase 1 Master Data Model peut finaliser le schema fusionné.
---
## Impact global sur Phase 1
Après ces 2 décisions:
- Phase 1 Master Project Intake peut être livré avec schema V12 rétro-compat ✅
- Phase 4 Financial Engine peut démarrer avec formules Big4 ✅
- Phase 8 Bankability Engine peut démarrer avec métriques Big4 ✅
- Phases 2, 3, 5, 6, 7 restent autonomes (pas de blocage supplémentaire)
## Décisions restantes OUVERTES (non bloquantes)
- **D-01** · PIE contrats · à trancher avant Phase 13 (Legal)
- **D-02** · Financement condition #4 · à trancher avant Phase 11 (Commercial + Bankability)
- **D-03** · Inventaire otoia/capabilities/chat.py · info technique · non urgent
- **D-04** · Inventaire config/projets_editor.py · info technique · non urgent
- **D-09** · Séparateur milliers · à trancher au polissage final (Phase 11+)
## Publication
Commit à venir · Gitea `oto-enterprise-os-dtp`
Memory: mise à jour registre décisions
Notification: DTP-Worker doit lire au prochain cycle et intégrer ces 2 décisions dans le développement Phase 1 + planification Phases 4/8