From 4f31d0c8c56de21a9c0ac88e17d992b764d81ed1 Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Mon, 3 Aug 2026 09:52:14 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker=2020260803=5F093713]=20Auto=20exec?= =?UTF-8?q?=20=C2=B7=20session=2020260803=5F093713?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 23 +- .../demo/scenarios/out/run_sheet.json | 2 +- .../demo/scenarios/out/run_sheet.md | 2 +- .../devops/deploy_runbook/deploy_spec.json | 1 + .../devops/deploy_runbook/out/MANIFEST.json | 8 +- .../deploy_runbook/out/deploy_runbook.json | 4 + 05_deliverables_mvp/pie/manifest/.gitignore | 4 + 05_deliverables_mvp/pie/manifest/README.md | 78 ++++ .../pie/manifest/out/MANIFEST.json | 37 ++ .../pie/manifest/out/pie_manifest.json | 369 ++++++++++++++++++ .../pie/manifest/pie.schema.json | 107 +++++ .../pie/manifest/pie_manifest_gen.py | 248 ++++++++++++ .../pie/manifest/pie_spec.json | 75 ++++ .../pie/manifest/pielib/__init__.py | 5 + .../pie/manifest/pielib/builder.py | 100 +++++ .../pie/manifest/pielib/deps.py | 60 +++ .../pie/manifest/tests/test_pie_manifest.py | 194 +++++++++ .../qa/acceptance/acceptance_spec.json | 6 +- .../qa/acceptance/out/MANIFEST.json | 12 +- .../qa/acceptance/out/acceptance_matrix.json | 11 +- .../qa/audit_4big/out/MANIFEST.json | 2 +- .../qa/audit_4big/out/quality_report.json | 134 ++++--- .../qa/audit_4big/quality_spec.json | 3 +- .../qa/regression/out/MANIFEST.json | 4 +- .../qa/regression/out/regression_plan.json | 21 +- 25 files changed, 1441 insertions(+), 69 deletions(-) create mode 100644 05_deliverables_mvp/pie/manifest/.gitignore create mode 100644 05_deliverables_mvp/pie/manifest/README.md create mode 100644 05_deliverables_mvp/pie/manifest/out/MANIFEST.json create mode 100644 05_deliverables_mvp/pie/manifest/out/pie_manifest.json create mode 100644 05_deliverables_mvp/pie/manifest/pie.schema.json create mode 100644 05_deliverables_mvp/pie/manifest/pie_manifest_gen.py create mode 100644 05_deliverables_mvp/pie/manifest/pie_spec.json create mode 100644 05_deliverables_mvp/pie/manifest/pielib/__init__.py create mode 100644 05_deliverables_mvp/pie/manifest/pielib/builder.py create mode 100644 05_deliverables_mvp/pie/manifest/pielib/deps.py create mode 100644 05_deliverables_mvp/pie/manifest/tests/test_pie_manifest.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index a91b392..c54866b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -542,10 +542,31 @@ jobs: - name: Tests générateur config app mobile run: python3 -m unittest discover -s tests -v + # -------------------------------------------------------------------------- + # PIE · manifest de dépendances (Annexe 12 · V10.1 · directive Project Identity + # Engine du 2026-08-03). La faisabilité = SSOT : ce module livre le P0 (schéma + # Project Master Data + graphe faisabilité→livrables downstream, §88). Zéro + # invention (#6) : contenu transcrit de la directive ; codes projets ancrés à + # CLAUDE.md §Projets ; annexe/version verbatim dans la directive ; modules + # downstream « gated » = répertoires de livrables réels (FS). Le worker n'écrit + # PAS le storage VPS /opt/oto/data/pie/{code}/ (#8). Schéma + 10 invariants. + # stdlib pur, zéro pip. + # -------------------------------------------------------------------------- + pie-manifest-tests: + name: PIE · manifest de dépendances (Annexe 12 · V10.1) + runs-on: ubuntu-latest + defaults: + run: + working-directory: 05_deliverables_mvp/pie/manifest + steps: + - uses: actions/checkout@v4 + - name: Tests générateur manifest PIE + run: python3 -m unittest discover -s tests -v + gate: name: Gate qualité (agrégat) runs-on: ubuntu-latest - needs: [constraints-guard, validate-json, check-docs, check-artifacts, check-regression, check-ci-integrity, check-readme-claims, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests, bancable-tests, crm-workflow-vente-tests, crm-dossier-vente-tests, crm-commissions-tests, crm-financement-bancaire-tests, fiscal-ecf-tests, frontend-portails-tests, legal-confotur-tests, qa-audit-5d-tests, seo-tests, chat-otoia-tests, qa-audit-4big-tests, demo-scenario-tests, qa-regression-tests, devops-deploy-runbook-tests, qa-acceptance-tests, mobile-app-config-tests] + needs: [constraints-guard, validate-json, check-docs, check-artifacts, check-regression, check-ci-integrity, check-readme-claims, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests, bancable-tests, crm-workflow-vente-tests, crm-dossier-vente-tests, crm-commissions-tests, crm-financement-bancaire-tests, fiscal-ecf-tests, frontend-portails-tests, legal-confotur-tests, qa-audit-5d-tests, seo-tests, chat-otoia-tests, qa-audit-4big-tests, demo-scenario-tests, qa-regression-tests, devops-deploy-runbook-tests, qa-acceptance-tests, mobile-app-config-tests, pie-manifest-tests] steps: - name: Résultat run: echo "✅ Gate qualité 4Big franchi — tous les checks verts." diff --git a/05_deliverables_mvp/demo/scenarios/out/run_sheet.json b/05_deliverables_mvp/demo/scenarios/out/run_sheet.json index b00183e..997e24e 100644 --- a/05_deliverables_mvp/demo/scenarios/out/run_sheet.json +++ b/05_deliverables_mvp/demo/scenarios/out/run_sheet.json @@ -149,7 +149,7 @@ "file": "out/quality_report.json", "pointer": "/coverage/ci_modules_count", "label": "Modules sous gate qualité", - "valeur": 22, + "valeur": 24, "resolu": true } ] diff --git a/05_deliverables_mvp/demo/scenarios/out/run_sheet.md b/05_deliverables_mvp/demo/scenarios/out/run_sheet.md index 8f02e97..01743a5 100644 --- a/05_deliverables_mvp/demo/scenarios/out/run_sheet.md +++ b/05_deliverables_mvp/demo/scenarios/out/run_sheet.md @@ -58,7 +58,7 @@ | À dire | Chiffre | Source (preuve) | |---|---|---| | Seuil bloquant 4Big | 95 | `qa/audit_4big` · `out/quality_report.json#/pass_score` | -| Modules sous gate qualité | 22 | `qa/audit_4big` · `out/quality_report.json#/coverage/ci_modules_count` | +| Modules sous gate qualité | 24 | `qa/audit_4big` · `out/quality_report.json#/coverage/ci_modules_count` | ### 6. Le dossier de vente, pièce bancable finale · 2 min diff --git a/05_deliverables_mvp/devops/deploy_runbook/deploy_spec.json b/05_deliverables_mvp/devops/deploy_runbook/deploy_spec.json index e4f2834..3cfcf45 100644 --- a/05_deliverables_mvp/devops/deploy_runbook/deploy_spec.json +++ b/05_deliverables_mvp/devops/deploy_runbook/deploy_spec.json @@ -89,6 +89,7 @@ "faisabilite/bancable": "contenu", "seo": "contenu", "demo/scenarios": "contenu", + "pie/manifest": "contenu", "qa/audit_5d": "verification-qa", "qa/audit_4big": "verification-qa", "qa/regression": "verification-qa", diff --git a/05_deliverables_mvp/devops/deploy_runbook/out/MANIFEST.json b/05_deliverables_mvp/devops/deploy_runbook/out/MANIFEST.json index 7d3e6c5..5d79208 100644 --- a/05_deliverables_mvp/devops/deploy_runbook/out/MANIFEST.json +++ b/05_deliverables_mvp/devops/deploy_runbook/out/MANIFEST.json @@ -5,14 +5,14 @@ "self_module": "devops/deploy_runbook", "counts": { "phases": 7, - "modules": 23, - "modules_mapped": 23, + "modules": 24, + "modules_mapped": 24, "confirmations": 7, "confirmations_referenced": 7 }, "coverage": { - "ci_modules_count": 23, - "mapped_modules_count": 23, + "ci_modules_count": 24, + "mapped_modules_count": 24, "bijective": true, "missing_in_map": [], "extra_in_map": [], diff --git a/05_deliverables_mvp/devops/deploy_runbook/out/deploy_runbook.json b/05_deliverables_mvp/devops/deploy_runbook/out/deploy_runbook.json index 65f509d..5f14274 100644 --- a/05_deliverables_mvp/devops/deploy_runbook/out/deploy_runbook.json +++ b/05_deliverables_mvp/devops/deploy_runbook/out/deploy_runbook.json @@ -188,6 +188,10 @@ "module": "faisabilite/generator", "ci_job": "faisabilite-gen-tests" }, + { + "module": "pie/manifest", + "ci_job": "pie-manifest-tests" + }, { "module": "publiciste", "ci_job": "publiciste-tests" diff --git a/05_deliverables_mvp/pie/manifest/.gitignore b/05_deliverables_mvp/pie/manifest/.gitignore new file mode 100644 index 0000000..1df2c8d --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/.gitignore @@ -0,0 +1,4 @@ +# Caches Python +__pycache__/ +*.pyc +# Le dossier out/ EST commité (manifest de dépendances versionné) — voir README. diff --git a/05_deliverables_mvp/pie/manifest/README.md b/05_deliverables_mvp/pie/manifest/README.md new file mode 100644 index 0000000..87c87a5 --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/README.md @@ -0,0 +1,78 @@ +# PIE · Manifest de dépendances · Annexe 12 · V10.1 + +**Directive** : [`DIRECTIVE_PIE_PROJECT_IDENTITY_ENGINE_20260803.md`](../../../DIRECTIVE_PIE_PROJECT_IDENTITY_ENGINE_20260803.md) +(Michel · 2026-08-03) · **Auto-score 4Big : 96/100** (≥95 requis · CLAUDE.md #5). + +## Rôle + +Le **Project Identity Engine (PIE)** pose la faisabilité comme **Project DNA · +source unique de vérité (SSOT)** : aucun livrable downstream (brochure, kit +banquier, site, app, contrat…) ne doit être recréé à la main si la donnée existe +déjà dans la faisabilité, et toute modification de la faisabilité régénère +sélectivement les livrables concernés. + +Ce module livre le **P0** de la directive : le **schéma Project Master Data** + +le **manifest de dépendances** (§88 de la directive) — le graphe +`faisabilité → livrables downstream` versionnable, cross-vérifié. + +Ce que ce worker **ne fait pas** (côté agents · VPS · CLAUDE.md #8) : le storage +réel `/opt/oto/data/pie/{code}/`, la génération effective des Brand Books / logos +(Flux) / PDF, et l'écriture dans ERPNext / Speckle. Ici : uniquement le contrat +de dépendances, à consommer par ces agents. + +## Sortie (`out/`, commité) + +| Fichier | Contenu | +|---|---| +| `pie_manifest.json` | 12 groupes Master Data · 4 règles de synchronisation · 13 livrables downstream · workflow 10 étapes · 9 marques · storage layout | +| `MANIFEST.json` | Traçabilité : sources d'ancrage + comptes dérivés + références des modules gated | + +## Contenu (transcrit VERBATIM de la directive — zéro invention · #6) + +- **Project Master Data** — 12 groupes (§16-29) : identité, localisation, + architecture, unités, budget, marketing, BIM, finances, juridique, images, + services, calendrier. +- **Règles de synchronisation** — 4 déclencheurs (§82-86) : + `prix` → brochures · kit banquier · contrats · site · app · + `typologie` → plans · rendus · fiche unité · BIM · + `amenities` → brochures · site · slogan · brand book · + `livraison` → calendrier · brochures · trackers chantier. +- **Workflow** — 10 étapes (§108-133), de la validation faisabilité à la publication. +- **Marques** — 9 projets (§90-102), codes ancrés à `CLAUDE.md §Projets`. + +### Downstream déjà gated par un module MVP (4/13) + +| Livrable | Module implémenteur | +|---|---| +| Kit banquier | [`faisabilite/bancable`](../../faisabilite/bancable/README.md) | +| Contrats types | [`legal/confotur`](../../legal/confotur/README.md) | +| Page projet (site) | [`publiciste`](../../publiciste/README.md) | +| Section app mobile | [`mobile/app_config`](../../mobile/app_config/README.md) | + +Les 9 autres (brand book, logos, brochures PDF, plans, rendus, fiche unité, BIM, +slogan, calendrier, trackers chantier) sont marqués `a_construire` (P1-P4 de la +directive) — aucun sur-engagement. + +## Invariants (schéma + 10, ré-affichés à l'exécution) + +1. Schéma draft-07 de `pie_manifest.json`. +2. 12 groupes Master Data, clés uniques == ensemble canonique (§16-29). +3. 4 règles sync, triggers == `{prix, typologie, amenities, livraison}` (§82-86). +4. Intégrité référentielle : chaque `downstream` + `trigger_group` pointe du réel. +5. Registre downstream : `statut` cohérent (`gated` ⇔ module non nul). +6. Chaque module downstream non nul **est un répertoire de livrable réel** (FS). +7. Workflow = étapes `1..N` contiguës et uniques. +8. Codes marques ⊆ `CLAUDE.md §Projets` (ancrage constitution). +9. `annexe` (12) + `version_workflow` (V10.1) **verbatim dans la directive**. +10. Storage layout hors-repo (`/opt/oto/data/pie/…`) — jamais le `out/` du module (#8). + +## Usage + +```bash +python3 pie_manifest_gen.py validate # schéma + 10 invariants, sans écrire +python3 pie_manifest_gen.py build # écrit out/pie_manifest.json + out/MANIFEST.json +python3 -m unittest discover -s tests -v +``` + +Sortie **déterministe** (tri stable, aucun horodatage) → re-générable +byte-identique, gaté par `ci/check_artifacts.sh`. diff --git a/05_deliverables_mvp/pie/manifest/out/MANIFEST.json b/05_deliverables_mvp/pie/manifest/out/MANIFEST.json new file mode 100644 index 0000000..9a7b5af --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/out/MANIFEST.json @@ -0,0 +1,37 @@ +{ + "generated_from": "pie_spec.json", + "claude_md_source": "CLAUDE.md", + "directive_source": "DIRECTIVE_PIE_PROJECT_IDENTITY_ENGINE_20260803.md", + "engine": "OTO Project Identity Engine (PIE)", + "annexe": 12, + "version_workflow": "V10.1", + "ssot": "faisabilite", + "counts": { + "master_data_groups": 12, + "downstream_total": 13, + "downstream_gated": 4, + "downstream_a_construire": 9, + "sync_rules": 4, + "workflow_steps": 10, + "brands": 9 + }, + "modules_gated_references": [ + { + "cle": "app", + "module": "mobile/app_config" + }, + { + "cle": "contrats", + "module": "legal/confotur" + }, + { + "cle": "kit_banquier", + "module": "faisabilite/bancable" + }, + { + "cle": "site", + "module": "publiciste" + } + ], + "note_anti_invention": "Toute donnée est transcrite de la directive PIE (SSOT · #6) : aucune valeur projet (prix, surfaces, marges) n'est fabriquée ici — le PIE les EXTRAIT de la faisabilité validée côté VPS (/opt/oto/data/pie/{code}/, jamais écrit par ce worker · #8). Les codes projets sont ancrés à CLAUDE.md §Projets ; annexe/version à la directive ; les modules downstream « gated » à des répertoires de livrables réels." +} diff --git a/05_deliverables_mvp/pie/manifest/out/pie_manifest.json b/05_deliverables_mvp/pie/manifest/out/pie_manifest.json new file mode 100644 index 0000000..6400287 --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/out/pie_manifest.json @@ -0,0 +1,369 @@ +{ + "engine": "OTO Project Identity Engine (PIE)", + "annexe": 12, + "version_workflow": "V10.1", + "ssot": "faisabilite", + "source_spec": "pie_spec.json", + "master_data_groups": [ + { + "cle": "architecture", + "libelle": "Architecture", + "champs": [ + "style", + "matériaux", + "ambiances", + "palette matériaux" + ] + }, + { + "cle": "bim", + "libelle": "BIM", + "champs": [ + "IFC/GLB", + "plans", + "coupes", + "panoramas" + ] + }, + { + "cle": "budget", + "libelle": "Budget", + "champs": [ + "CAPEX", + "coûts construction", + "marges", + "break-even", + "apport promoteur" + ] + }, + { + "cle": "calendrier", + "libelle": "Calendrier", + "champs": [ + "phasage travaux", + "jalons", + "livraison" + ] + }, + { + "cle": "finances", + "libelle": "Finances", + "champs": [ + "plan de financement", + "fideicomiso", + "CONFOTUR", + "exonérations" + ] + }, + { + "cle": "identite", + "libelle": "Identité", + "champs": [ + "nom", + "type", + "concept", + "vision", + "mission", + "valeurs" + ] + }, + { + "cle": "images", + "libelle": "Images", + "champs": [ + "rendus extérieurs", + "intérieurs", + "aériens", + "ambiances" + ] + }, + { + "cle": "juridique", + "libelle": "Juridique", + "champs": [ + "titres", + "permis", + "licences", + "contrats types" + ] + }, + { + "cle": "localisation", + "libelle": "Localisation", + "champs": [ + "adresse exacte", + "coordonnées GPS", + "province", + "ville", + "quartier" + ] + }, + { + "cle": "marketing", + "libelle": "Marketing", + "champs": [ + "positionnement", + "clientèle cible", + "USPs", + "canaux" + ] + }, + { + "cle": "services", + "libelle": "Services", + "champs": [ + "amenities", + "conciergerie", + "services inclus" + ] + }, + { + "cle": "unites", + "libelle": "Unités", + "champs": [ + "typologies", + "surfaces", + "orientations", + "quantités", + "prix", + "disponibilités" + ] + } + ], + "downstream_registry": [ + { + "cle": "app", + "libelle": "Section app mobile (portail Client)", + "module": "mobile/app_config", + "statut": "gated" + }, + { + "cle": "bim", + "libelle": "IFC + GLB + Speckle", + "module": null, + "statut": "a_construire" + }, + { + "cle": "brand_book", + "libelle": "Brand Book PDF (~30-50 pages)", + "module": null, + "statut": "a_construire" + }, + { + "cle": "brochures", + "libelle": "Brochures PDF FR/EN/ES", + "module": null, + "statut": "a_construire" + }, + { + "cle": "calendrier", + "libelle": "Calendrier / phasage travaux", + "module": null, + "statut": "a_construire" + }, + { + "cle": "contrats", + "libelle": "Contrats types (Promesa · Fideicomiso · HOA)", + "module": "legal/confotur", + "statut": "gated" + }, + { + "cle": "fiche_unite", + "libelle": "Fiche unité", + "module": null, + "statut": "a_construire" + }, + { + "cle": "kit_banquier", + "libelle": "Kit banquier PDF trilingue", + "module": "faisabilite/bancable", + "statut": "gated" + }, + { + "cle": "plans", + "libelle": "Plans (générés depuis BIM)", + "module": null, + "statut": "a_construire" + }, + { + "cle": "rendus", + "libelle": "Rendus extérieurs/intérieurs/aériens", + "module": null, + "statut": "a_construire" + }, + { + "cle": "site", + "libelle": "Page projet vente.otov7.com/projets/{slug}/", + "module": "publiciste", + "statut": "gated" + }, + { + "cle": "slogan", + "libelle": "Slogan / tagline", + "module": null, + "statut": "a_construire" + }, + { + "cle": "trackers_chantier", + "libelle": "Trackers chantier", + "module": null, + "statut": "a_construire" + } + ], + "sync_rules": [ + { + "trigger": "amenities", + "trigger_group": "services", + "downstream": [ + "brochures", + "site", + "slogan", + "brand_book" + ] + }, + { + "trigger": "livraison", + "trigger_group": "calendrier", + "downstream": [ + "calendrier", + "brochures", + "trackers_chantier" + ] + }, + { + "trigger": "prix", + "trigger_group": "unites", + "downstream": [ + "brochures", + "kit_banquier", + "contrats", + "site", + "app" + ] + }, + { + "trigger": "typologie", + "trigger_group": "unites", + "downstream": [ + "plans", + "rendus", + "fiche_unite", + "bim" + ] + } + ], + "workflow": [ + { + "etape": 1, + "libelle": "Validation Faisabilité (par OTO Auditeur Faisabilité IA)" + }, + { + "etape": 2, + "libelle": "Extraction Project Master Data (master.json)" + }, + { + "etape": 3, + "libelle": "Brand Book génération" + }, + { + "etape": 4, + "libelle": "Logos génération (Flux + variantes SVG)" + }, + { + "etape": 5, + "libelle": "Thème appliqué (CSS variables + design tokens)" + }, + { + "etape": 6, + "libelle": "Documents commerciaux (brochures · kit banquier · Trousse · contrats)" + }, + { + "etape": 7, + "libelle": "Portails web (page projet · viewer 3D · portail client)" + }, + { + "etape": 8, + "libelle": "Applications mobiles (section projet + tab dédié)" + }, + { + "etape": 9, + "libelle": "Synchronisation (ERPNext · CRM · BIM · OTO AI · OFAC/PEP/FinCEN)" + }, + { + "etape": 10, + "libelle": "Publication (site · app · portail admin · ERPNext · Speckle · GSC)" + } + ], + "brands": [ + { + "code": "P01", + "marque": "Coralis", + "localisation": "San Cristóbal" + }, + { + "code": "P02", + "marque": "Coral del Sur", + "localisation": "San Cristóbal" + }, + { + "code": "P03", + "marque": "Nakua", + "localisation": "Bávaro" + }, + { + "code": "P04", + "marque": "Xamaná Cantiles", + "localisation": "Palenque" + }, + { + "code": "P05", + "marque": "Las Colinas", + "localisation": "Najayo" + }, + { + "code": "P06", + "marque": "Coco Real", + "localisation": "Punta Cana Bávaro" + }, + { + "code": "P07", + "marque": "Aqua Terra", + "localisation": "Las Terrenas" + }, + { + "code": "P08", + "marque": "Fasano Espirilla", + "localisation": "Santo Domingo Espirilla" + }, + { + "code": "P09", + "marque": "Résidence Gazcue", + "localisation": "Santo Domingo centre historique" + } + ], + "storage_layout": { + "racine": "/opt/oto/data/pie/{code}/", + "artefacts": [ + { + "chemin": "master.json", + "role": "Project Master Data (SSOT extrait de la faisabilité)" + }, + { + "chemin": "brand/", + "role": "Brand Book assets" + }, + { + "chemin": "logos/", + "role": "Logos SVG/PNG + variantes" + }, + { + "chemin": "documents/", + "role": "PDF générés (brochures · kit banquier · contrats)" + }, + { + "chemin": "manifest.json", + "role": "Manifest de dépendances + checksums (régénération sélective)" + } + ] + } +} diff --git a/05_deliverables_mvp/pie/manifest/pie.schema.json b/05_deliverables_mvp/pie/manifest/pie.schema.json new file mode 100644 index 0000000..9168b66 --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/pie.schema.json @@ -0,0 +1,107 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://oto.dtp/schemas/pie/pie_manifest.schema.json", + "title": "PIE · manifest de dépendances (pie_manifest.json)", + "description": "Sortie déterministe du Project Identity Engine (Annexe 12 · V10.1). Décrit le graphe de dépendances faisabilité(SSOT)→livrables downstream. Zéro invention (#6) : contenus transcrits de la directive PIE ; codes projets ancrés à CLAUDE.md §Projets.", + "type": "object", + "required": ["engine", "annexe", "version_workflow", "ssot", "source_spec", "master_data_groups", "downstream_registry", "sync_rules", "workflow", "brands", "storage_layout"], + "additionalProperties": false, + "properties": { + "engine": { "type": "string", "minLength": 1 }, + "annexe": { "type": "integer" }, + "version_workflow": { "type": "string", "minLength": 1 }, + "ssot": { "type": "string", "minLength": 1 }, + "source_spec": { "type": "string", "minLength": 1 }, + "master_data_groups": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["cle", "libelle", "champs"], + "additionalProperties": false, + "properties": { + "cle": { "type": "string", "minLength": 1 }, + "libelle": { "type": "string", "minLength": 1 }, + "champs": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } } + } + } + }, + "downstream_registry": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["cle", "libelle", "module", "statut"], + "additionalProperties": false, + "properties": { + "cle": { "type": "string", "minLength": 1 }, + "libelle": { "type": "string", "minLength": 1 }, + "module": { "type": ["string", "null"] }, + "statut": { "type": "string", "enum": ["gated", "a_construire"] } + } + } + }, + "sync_rules": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["trigger", "trigger_group", "downstream"], + "additionalProperties": false, + "properties": { + "trigger": { "type": "string", "minLength": 1 }, + "trigger_group": { "type": "string", "minLength": 1 }, + "downstream": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } } + } + } + }, + "workflow": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["etape", "libelle"], + "additionalProperties": false, + "properties": { + "etape": { "type": "integer", "minimum": 1 }, + "libelle": { "type": "string", "minLength": 1 } + } + } + }, + "brands": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["code", "marque", "localisation"], + "additionalProperties": false, + "properties": { + "code": { "type": "string", "pattern": "^P0[1-9]$" }, + "marque": { "type": "string", "minLength": 1 }, + "localisation": { "type": "string", "minLength": 1 } + } + } + }, + "storage_layout": { + "type": "object", + "required": ["racine", "artefacts"], + "additionalProperties": false, + "properties": { + "racine": { "type": "string", "minLength": 1 }, + "artefacts": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["chemin", "role"], + "additionalProperties": false, + "properties": { + "chemin": { "type": "string", "minLength": 1 }, + "role": { "type": "string", "minLength": 1 } + } + } + } + } + } + } +} diff --git a/05_deliverables_mvp/pie/manifest/pie_manifest_gen.py b/05_deliverables_mvp/pie/manifest/pie_manifest_gen.py new file mode 100644 index 0000000..c118926 --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/pie_manifest_gen.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +"""Générateur du manifest de dépendances PIE · Annexe 12 · V10.1. + +Directive : `DIRECTIVE_PIE_PROJECT_IDENTITY_ENGINE_20260803.md` (Michel · 2026-08-03). +« La faisabilité devient le Project DNA · source unique de vérité (SSOT). » + +Ce module livre le **P0** du PIE : le schéma Project Master Data + le manifest de +dépendances (§88 de la directive) qui liste, pour chaque déclencheur de la +faisabilité, les livrables downstream à régénérer — et lesquels sont DÉJÀ gated +par un module MVP existant vs restent à construire (P1-P4). + +Ce worker n'écrit JAMAIS sur le VPS (#8) : le storage réel du PIE +(`/opt/oto/data/pie/{code}/`) et la génération effective des Brand Books / logos / +PDF restent côté agents (Flux, ERPNext, OTOIA). Ici : uniquement le contrat de +dépendances versionnable, cross-vérifié contre CLAUDE.md, la directive et le FS. + +Sous-commandes : + build [-o OUT] → écrit pie_manifest.json + MANIFEST.json + validate → (re)génère en mémoire, valide schéma + 10 invariants ; + sort en erreur sinon. + +Sortie déterministe (tri stable, aucun horodatage) → diffable + re-générable. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..")) # 05_deliverables_mvp/ + +sys.path.insert(0, _HERE) +sys.path.insert(0, os.path.join(_DELIVERABLES, "publiciste")) + +from pielib import builder, deps # noqa: E402 +from lib import validator as maison # type: ignore # noqa: E402 + +_SPEC_PATH = os.path.join(_HERE, "pie_spec.json") +_SCHEMA_PATH = os.path.join(_HERE, "pie.schema.json") +_DEFAULT_OUT = os.path.join(_HERE, "out") + +# Ensemble documenté des déclencheurs de synchronisation (directive §82-86). +_DECLENCHEURS = {"prix", "typologie", "amenities", "livraison"} +# Clés canoniques des 12 groupes Project Master Data (directive §16-29). +_GROUPES_CANONIQUES = { + "identite", "localisation", "architecture", "unites", "budget", "marketing", + "bim", "finances", "juridique", "images", "services", "calendrier", +} + + +def _eprint(*args) -> None: + print(*args, file=sys.stderr) + + +def _load(path: str) -> dict: + with open(path, encoding="utf-8") as fh: + return json.load(fh) + + +def _write_json(path: str, data) -> None: + with open(path, "w", encoding="utf-8") as fh: + json.dump(data, fh, ensure_ascii=False, indent=2) + fh.write("\n") + + +def _validate(bundle: dict) -> list[str]: + """Schéma de sortie + 10 invariants (SSOT · ancrages CLAUDE.md/directive/FS). + + Ledger des invariants (le compte « 10 » est ré-affiché à l'exécution) : + 1 · schéma draft-07 de pie_manifest.json + 2 · 12 groupes Master Data, clés uniques == ensemble canonique (§16-29) + 3 · 4 règles sync, triggers == {prix, typologie, amenities, livraison} + 4 · chaque downstream référencé existe dans le registre ; trigger_group réel + 5 · registre downstream : statut cohérent avec présence d'un module + 6 · chaque module downstream non nul est un répertoire de livrable réel (FS) + 7 · workflow = étapes 1..N contiguës et uniques + 8 · codes marques ⊆ CLAUDE.md §Projets (ancrage constitution) + 9 · annexe (12) + version_workflow (V10.1) verbatim dans la directive + 10 · storage layout hors-repo (/opt/oto/data/...) — jamais le out/ du module (#8) + """ + schema = _load(_SCHEMA_PATH) + errors: list[str] = [] + + man = bundle["manifest"] + trace = bundle["traceability"] + + # 1 · schéma de sortie. + errors += list(maison.validate(man, schema)) + + # 2 · Project Master Data : 12 groupes, clés uniques, == ensemble canonique. + groups = man["master_data_groups"] + cles = [g["cle"] for g in groups] + if len(cles) != 12: + errors.append(f"master_data_groups : {len(cles)} groupes (attendu 12 · §16-29)") + if len(set(cles)) != len(cles): + errors.append("master_data_groups : clés dupliquées") + if set(cles) != _GROUPES_CANONIQUES: + manquants = _GROUPES_CANONIQUES - set(cles) + surplus = set(cles) - _GROUPES_CANONIQUES + errors.append(f"master_data_groups : clés ≠ ensemble canonique " + f"(manquants={sorted(manquants)} · surplus={sorted(surplus)})") + + # 3 · règles de synchronisation : 4 triggers == ensemble documenté (§82-86). + triggers = [r["trigger"] for r in man["sync_rules"]] + if set(triggers) != _DECLENCHEURS: + errors.append(f"sync_rules : triggers {sorted(set(triggers))} ≠ " + f"{sorted(_DECLENCHEURS)} (§82-86)") + if len(set(triggers)) != len(triggers): + errors.append("sync_rules : trigger dupliqué") + + # 4 · intégrité référentielle : downstream + trigger_group pointent du réel. + registry_keys = {d["cle"] for d in man["downstream_registry"]} + group_keys = set(cles) + for r in man["sync_rules"]: + if r["trigger_group"] not in group_keys: + errors.append(f"sync_rule {r['trigger']!r} : trigger_group " + f"{r['trigger_group']!r} absent des Master Data groups") + for t in r["downstream"]: + if t not in registry_keys: + errors.append(f"sync_rule {r['trigger']!r} : downstream {t!r} " + f"absent du downstream_registry") + + # 5 · registre downstream : statut cohérent (gated ⇔ module non nul). + for d in man["downstream_registry"]: + attendu = "gated" if d["module"] else "a_construire" + if d["statut"] != attendu: + errors.append(f"downstream {d['cle']!r} : statut {d['statut']!r} " + f"incohérent avec module {d['module']!r}") + + # 6 · chaque module downstream non nul EST un répertoire de livrable réel (FS). + for d in man["downstream_registry"]: + if d["module"] and not deps.module_exists(d["module"]): + errors.append(f"downstream {d['cle']!r} : module {d['module']!r} " + f"introuvable sous 05_deliverables_mvp/") + + # 7 · workflow : étapes 1..N contiguës et uniques. + etapes = [s["etape"] for s in man["workflow"]] + if etapes != list(range(1, len(etapes) + 1)): + errors.append(f"workflow : étapes non contiguës/desordonnées : {etapes}") + + # 8 · codes marques ⊆ CLAUDE.md §Projets (ancrage constitution · #6). + codes = {b["code"] for b in man["brands"]} + canoniques = deps.claude_md_project_codes() + if not canoniques: + errors.append("CLAUDE.md §Projets : aucun code Pxx lu (ancrage impossible)") + elif not codes <= canoniques: + errors.append(f"brands : codes {sorted(codes - canoniques)} absents de " + f"CLAUDE.md §Projets") + + # 9 · annexe + version verbatim dans la directive (ancrage directive · #6). + decl = deps.directive_declares(trace["directive_source"], + f"Annexe {man['annexe']}", man["version_workflow"]) + for needle, present in decl.items(): + if not present: + errors.append(f"directive {trace['directive_source']!r} : " + f"« {needle} » introuvable (ancrage rompu)") + + # 10 · storage layout hors-repo : aucun artefact PIE n'atterrit dans le out/ + # du module (le worker n'écrit JAMAIS le storage VPS · #8). + racine = man["storage_layout"]["racine"] + if not racine.startswith("/opt/oto/data/pie/"): + errors.append(f"storage_layout.racine {racine!r} hors /opt/oto/data/pie/") + if os.path.abspath(deps.module_out_dir()).startswith(os.path.abspath("/opt/oto/data")): + errors.append("out/ du module confondu avec le storage VPS (#8)") + + # Traçabilité : comptes du manifeste cohérents avec les tableaux. + c = trace["counts"] + if c["master_data_groups"] != len(groups): + errors.append("counts.master_data_groups incohérent") + if c["downstream_total"] != len(man["downstream_registry"]): + errors.append("counts.downstream_total incohérent") + if c["sync_rules"] != len(man["sync_rules"]): + errors.append("counts.sync_rules incohérent") + if c["workflow_steps"] != len(man["workflow"]): + errors.append("counts.workflow_steps incohérent") + if c["brands"] != len(man["brands"]): + errors.append("counts.brands incohérent") + gated = sum(1 for d in man["downstream_registry"] if d["statut"] == "gated") + if c["downstream_gated"] != gated: + errors.append("counts.downstream_gated incohérent") + + return errors + + +def _build() -> dict: + return builder.build_bundle(_load(_SPEC_PATH)) + + +def cmd_build(args: argparse.Namespace) -> int: + bundle = _build() + errors = _validate(bundle) + if errors: + _eprint("❌ Manifest invalide — génération refusée (anti-régression) :") + for e in errors: + _eprint(f" - {e}") + return 1 + + out = os.path.abspath(args.out) + os.makedirs(out, exist_ok=True) + _write_json(os.path.join(out, "pie_manifest.json"), bundle["manifest"]) + _write_json(os.path.join(out, "MANIFEST.json"), bundle["traceability"]) + + c = bundle["traceability"]["counts"] + print(f"✅ Manifest PIE généré dans {out}") + print(f" pie_manifest.json : {c['master_data_groups']} groupes Master Data · " + f"{c['sync_rules']} règles sync · {c['downstream_total']} livrables downstream " + f"({c['downstream_gated']} gated · {c['downstream_a_construire']} à construire) · " + f"{c['workflow_steps']} étapes workflow · {c['brands']} marques") + print(" ⚠ Storage PIE réel (/opt/oto/data/pie/{code}/) + génération Brand Book / " + "logos / PDF côté agents (Flux · ERPNext · OTOIA · VPS · #8).") + return 0 + + +def cmd_validate(args: argparse.Namespace) -> int: + bundle = _build() + errors = _validate(bundle) + if errors: + _eprint("❌ Validation KO :") + for e in errors: + _eprint(f" - {e}") + return 1 + c = bundle["traceability"]["counts"] + print(f"✅ Validation OK — PIE Annexe {bundle['manifest']['annexe']} · " + f"{bundle['manifest']['version_workflow']} : schéma + 10 invariants verts " + f"({c['downstream_total']} downstream · {c['brands']} marques).") + return 0 + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Générateur du manifest de dépendances PIE (Annexe 12 · V10.1).") + sub = p.add_subparsers(dest="cmd", required=True) + + pb = sub.add_parser("build", help="génère pie_manifest.json / MANIFEST.json") + pb.add_argument("-o", "--out", default=_DEFAULT_OUT, help="dossier de sortie (défaut: ./out)") + pb.set_defaults(func=cmd_build) + + pv = sub.add_parser("validate", help="valide le bundle (schéma + 10 invariants) sans écrire") + pv.set_defaults(func=cmd_validate) + + args = p.parse_args(argv) + return args.func(args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/05_deliverables_mvp/pie/manifest/pie_spec.json b/05_deliverables_mvp/pie/manifest/pie_spec.json new file mode 100644 index 0000000..5664902 --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/pie_spec.json @@ -0,0 +1,75 @@ +{ + "engine": "OTO Project Identity Engine (PIE)", + "annexe": 12, + "version_workflow": "V10.1", + "ssot": "faisabilite", + "directive": "DIRECTIVE_PIE_PROJECT_IDENTITY_ENGINE_20260803.md", + "master_data_groups": [ + { "cle": "identite", "libelle": "Identité", "champs": ["nom", "type", "concept", "vision", "mission", "valeurs"] }, + { "cle": "localisation","libelle": "Localisation","champs": ["adresse exacte", "coordonnées GPS", "province", "ville", "quartier"] }, + { "cle": "architecture","libelle": "Architecture","champs": ["style", "matériaux", "ambiances", "palette matériaux"] }, + { "cle": "unites", "libelle": "Unités", "champs": ["typologies", "surfaces", "orientations", "quantités", "prix", "disponibilités"] }, + { "cle": "budget", "libelle": "Budget", "champs": ["CAPEX", "coûts construction", "marges", "break-even", "apport promoteur"] }, + { "cle": "marketing", "libelle": "Marketing", "champs": ["positionnement", "clientèle cible", "USPs", "canaux"] }, + { "cle": "bim", "libelle": "BIM", "champs": ["IFC/GLB", "plans", "coupes", "panoramas"] }, + { "cle": "finances", "libelle": "Finances", "champs": ["plan de financement", "fideicomiso", "CONFOTUR", "exonérations"] }, + { "cle": "juridique", "libelle": "Juridique", "champs": ["titres", "permis", "licences", "contrats types"] }, + { "cle": "images", "libelle": "Images", "champs": ["rendus extérieurs", "intérieurs", "aériens", "ambiances"] }, + { "cle": "services", "libelle": "Services", "champs": ["amenities", "conciergerie", "services inclus"] }, + { "cle": "calendrier", "libelle": "Calendrier", "champs": ["phasage travaux", "jalons", "livraison"] } + ], + "downstream_registry": [ + { "cle": "brochures", "libelle": "Brochures PDF FR/EN/ES", "module": null }, + { "cle": "kit_banquier", "libelle": "Kit banquier PDF trilingue", "module": "faisabilite/bancable" }, + { "cle": "contrats", "libelle": "Contrats types (Promesa · Fideicomiso · HOA)","module": "legal/confotur" }, + { "cle": "site", "libelle": "Page projet vente.otov7.com/projets/{slug}/","module": "publiciste" }, + { "cle": "app", "libelle": "Section app mobile (portail Client)", "module": "mobile/app_config" }, + { "cle": "plans", "libelle": "Plans (générés depuis BIM)", "module": null }, + { "cle": "rendus", "libelle": "Rendus extérieurs/intérieurs/aériens", "module": null }, + { "cle": "fiche_unite", "libelle": "Fiche unité", "module": null }, + { "cle": "bim", "libelle": "IFC + GLB + Speckle", "module": null }, + { "cle": "slogan", "libelle": "Slogan / tagline", "module": null }, + { "cle": "brand_book", "libelle": "Brand Book PDF (~30-50 pages)", "module": null }, + { "cle": "calendrier", "libelle": "Calendrier / phasage travaux", "module": null }, + { "cle": "trackers_chantier", "libelle": "Trackers chantier", "module": null } + ], + "sync_rules": [ + { "trigger": "prix", "trigger_group": "unites", "downstream": ["brochures", "kit_banquier", "contrats", "site", "app"] }, + { "trigger": "typologie", "trigger_group": "unites", "downstream": ["plans", "rendus", "fiche_unite", "bim"] }, + { "trigger": "amenities", "trigger_group": "services", "downstream": ["brochures", "site", "slogan", "brand_book"] }, + { "trigger": "livraison", "trigger_group": "calendrier", "downstream": ["calendrier", "brochures", "trackers_chantier"] } + ], + "workflow": [ + { "etape": 1, "libelle": "Validation Faisabilité (par OTO Auditeur Faisabilité IA)" }, + { "etape": 2, "libelle": "Extraction Project Master Data (master.json)" }, + { "etape": 3, "libelle": "Brand Book génération" }, + { "etape": 4, "libelle": "Logos génération (Flux + variantes SVG)" }, + { "etape": 5, "libelle": "Thème appliqué (CSS variables + design tokens)" }, + { "etape": 6, "libelle": "Documents commerciaux (brochures · kit banquier · Trousse · contrats)" }, + { "etape": 7, "libelle": "Portails web (page projet · viewer 3D · portail client)" }, + { "etape": 8, "libelle": "Applications mobiles (section projet + tab dédié)" }, + { "etape": 9, "libelle": "Synchronisation (ERPNext · CRM · BIM · OTO AI · OFAC/PEP/FinCEN)" }, + { "etape": 10, "libelle": "Publication (site · app · portail admin · ERPNext · Speckle · GSC)" } + ], + "brands": [ + { "code": "P01", "marque": "Coralis", "localisation": "San Cristóbal" }, + { "code": "P02", "marque": "Coral del Sur", "localisation": "San Cristóbal" }, + { "code": "P03", "marque": "Nakua", "localisation": "Bávaro" }, + { "code": "P04", "marque": "Xamaná Cantiles", "localisation": "Palenque" }, + { "code": "P05", "marque": "Las Colinas", "localisation": "Najayo" }, + { "code": "P06", "marque": "Coco Real", "localisation": "Punta Cana Bávaro" }, + { "code": "P07", "marque": "Aqua Terra", "localisation": "Las Terrenas" }, + { "code": "P08", "marque": "Fasano Espirilla", "localisation": "Santo Domingo Espirilla" }, + { "code": "P09", "marque": "Résidence Gazcue", "localisation": "Santo Domingo centre historique" } + ], + "storage_layout": { + "racine": "/opt/oto/data/pie/{code}/", + "artefacts": [ + { "chemin": "master.json", "role": "Project Master Data (SSOT extrait de la faisabilité)" }, + { "chemin": "brand/", "role": "Brand Book assets" }, + { "chemin": "logos/", "role": "Logos SVG/PNG + variantes" }, + { "chemin": "documents/", "role": "PDF générés (brochures · kit banquier · contrats)" }, + { "chemin": "manifest.json", "role": "Manifest de dépendances + checksums (régénération sélective)" } + ] + } +} diff --git a/05_deliverables_mvp/pie/manifest/pielib/__init__.py b/05_deliverables_mvp/pie/manifest/pielib/__init__.py new file mode 100644 index 0000000..49bac3d --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/pielib/__init__.py @@ -0,0 +1,5 @@ +"""PIE · manifest de dépendances · bibliothèque interne. + + - deps → ancrages hors-module (CLAUDE.md §Projets · directive PIE · FS des livrables) + - builder → assemblage déterministe du manifest + du MANIFEST de traçabilité +""" diff --git a/05_deliverables_mvp/pie/manifest/pielib/builder.py b/05_deliverables_mvp/pie/manifest/pielib/builder.py new file mode 100644 index 0000000..3d7ff03 --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/pielib/builder.py @@ -0,0 +1,100 @@ +"""Assemblage déterministe du manifest PIE depuis `pie_spec.json`. + +Entrée : le contrat PIE (taxonomie Project Master Data, registre downstream, +règles de synchronisation, workflow 10 étapes, marques, storage layout) — +transcrit VERBATIM de la directive PIE (SSOT · #6, aucune valeur inventée). +Sortie : un bundle déterministe (tri stable, aucun horodatage) → reproductible +bit-à-bit à contrat constant, donc diffable et re-générable en CI +([[artifact-reproducibility-gate]]). + +Le builder n'ajoute AUCUN chiffre propre à OTO : il résout le `statut` de chaque +livrable downstream (« gated » si un module de livrado réel l'implémente déjà, +sinon « a_construire ») et un manifeste de traçabilité (comptes dérivés). +""" + +from __future__ import annotations + +from typing import Any + +from . import deps + + +def _statut(module: str | None) -> str: + """« gated » si un module réel implémente déjà le downstream, sinon « a_construire ».""" + return "gated" if module else "a_construire" + + +def build_bundle(spec: dict) -> dict[str, Any]: + """Transforme le contrat PIE en manifest de dépendances + manifeste de traçabilité.""" + groups = sorted(spec["master_data_groups"], key=lambda g: g["cle"]) + + registry = [ + { + "cle": d["cle"], + "libelle": d["libelle"], + "module": d["module"], + "statut": _statut(d["module"]), + } + for d in sorted(spec["downstream_registry"], key=lambda d: d["cle"]) + ] + + sync_rules = [ + { + "trigger": r["trigger"], + "trigger_group": r["trigger_group"], + "downstream": list(r["downstream"]), # ordre directive préservé (§82-86) + } + for r in sorted(spec["sync_rules"], key=lambda r: r["trigger"]) + ] + + workflow = sorted(spec["workflow"], key=lambda s: s["etape"]) + brands = sorted(spec["brands"], key=lambda b: b["code"]) + + manifest_out = { + "engine": spec["engine"], + "annexe": spec["annexe"], + "version_workflow": spec["version_workflow"], + "ssot": spec["ssot"], + "source_spec": "pie_spec.json", + "master_data_groups": groups, + "downstream_registry": registry, + "sync_rules": sync_rules, + "workflow": workflow, + "brands": brands, + "storage_layout": spec["storage_layout"], + } + + gated = [d for d in registry if d["statut"] == "gated"] + a_construire = [d for d in registry if d["statut"] == "a_construire"] + + traceability = { + "generated_from": "pie_spec.json", + "claude_md_source": "CLAUDE.md", + "directive_source": spec["directive"], + "engine": spec["engine"], + "annexe": spec["annexe"], + "version_workflow": spec["version_workflow"], + "ssot": spec["ssot"], + "counts": { + "master_data_groups": len(groups), + "downstream_total": len(registry), + "downstream_gated": len(gated), + "downstream_a_construire": len(a_construire), + "sync_rules": len(sync_rules), + "workflow_steps": len(workflow), + "brands": len(brands), + }, + "modules_gated_references": [ + {"cle": d["cle"], "module": d["module"]} for d in gated + ], + "note_anti_invention": ( + "Toute donnée est transcrite de la directive PIE (SSOT · #6) : aucune " + "valeur projet (prix, surfaces, marges) n'est fabriquée ici — le PIE " + "les EXTRAIT de la faisabilité validée côté VPS (/opt/oto/data/pie/{code}/, " + "jamais écrit par ce worker · #8). Les codes projets sont ancrés à " + "CLAUDE.md §Projets ; annexe/version à la directive ; les modules " + "downstream « gated » à des répertoires de livrables réels." + ), + } + + return {"manifest": manifest_out, "traceability": traceability} diff --git a/05_deliverables_mvp/pie/manifest/pielib/deps.py b/05_deliverables_mvp/pie/manifest/pielib/deps.py new file mode 100644 index 0000000..9bc5e5c --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/pielib/deps.py @@ -0,0 +1,60 @@ +"""Ancrages hors-module du PIE manifest (anti-dérive · anti-invention #6). + +Le manifest PIE ne se valide pas seulement contre lui-même : trois faits sont +RE-DÉRIVÉS de sources de vérité EXTERNES au module, jamais recopiés à la main — + + 1. Les codes projets (`brands[].code`) sont confrontés à `CLAUDE.md §Projets` + (la constitution · [[claude-md-constant-anchor-gate]]). Une marque PIE dont + le code ne figure pas dans la constitution est une invention refusée. + 2. `annexe` (12) et `version_workflow` (V10.1) doivent apparaître VERBATIM dans + la directive PIE. Renuméroter l'annexe dans la directive sans régénérer ici + casse la validation ([[roadmap-anchor-gate]] appliqué à la directive). + 3. Chaque `downstream_registry[].module` non nul doit être un répertoire de + livrable RÉEL sous `05_deliverables_mvp/` (comme fiscal/ecf vérifie ses + contrats voisins). Un module fantôme (renommé/supprimé) casse la validation. + +Zéro dépendance pip · zéro réseau · aucune écriture VPS (#8) : lecture seule de +fichiers déjà versionnés dans le dépôt. +""" + +from __future__ import annotations + +import os +import re + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_MODULE_DIR = os.path.normpath(os.path.join(_HERE, "..")) # pie/manifest/ +DELIVERABLES_ROOT = os.path.normpath(os.path.join(_HERE, "..", "..", "..")) # 05_deliverables_mvp/ +_REPO_ROOT = os.path.normpath(os.path.join(DELIVERABLES_ROOT, "..")) +_CLAUDE_MD = os.path.join(_REPO_ROOT, "CLAUDE.md") + + +def claude_md_project_codes() -> set[str]: + """Codes Pxx de la ligne `## Projets` de CLAUDE.md (source de vérité #Projets).""" + with open(_CLAUDE_MD, encoding="utf-8") as fh: + text = fh.read() + # Section entre « ## Projets » et le prochain titre « ## ». + m = re.search(r"^##\s+Projets\s*\n(.*?)(?:\n##\s|\Z)", text, re.S | re.M) + section = m.group(1) if m else "" + return set(re.findall(r"\bP0[1-9]\b", section)) + + +def directive_declares(directive_relpath: str, *needles: str) -> dict[str, bool]: + """Vrai/faux, pour chaque `needle`, s'il apparaît verbatim dans la directive PIE.""" + path = os.path.join(_REPO_ROOT, directive_relpath) + try: + with open(path, encoding="utf-8") as fh: + text = fh.read() + except OSError: + return {n: False for n in needles} + return {n: (n in text) for n in needles} + + +def module_exists(rel_module: str) -> bool: + """Le module de livrable `rel_module` (ex. 'faisabilite/bancable') existe-t-il ?""" + return os.path.isdir(os.path.join(DELIVERABLES_ROOT, rel_module)) + + +def module_out_dir() -> str: + """Répertoire de sortie du module (le SEUL endroit où ce worker écrit · #8).""" + return os.path.join(_MODULE_DIR, "out") diff --git a/05_deliverables_mvp/pie/manifest/tests/test_pie_manifest.py b/05_deliverables_mvp/pie/manifest/tests/test_pie_manifest.py new file mode 100644 index 0000000..8e9c647 --- /dev/null +++ b/05_deliverables_mvp/pie/manifest/tests/test_pie_manifest.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Tests du générateur de manifest de dépendances PIE · Annexe 12 · V10.1. + +Stdlib pur (`unittest`) → aucune installation pip requise sur le runner Gitea. +La bibliothèque `jsonschema` sert d'*oracle* quand elle est présente. + +Axes : + 1. BASELINE : le contrat vanille passe schéma + 10 invariants, est déterministe, + et l'artefact `out/` commité == build frais (reproductibilité · #6). + 2. MUTATION : chaque invariant attrape bien la dérive qu'il protège (on casse + le bundle en mémoire et on vérifie que `_validate` remonte une erreur). + 3. ANCRAGES : codes marques ⊆ CLAUDE.md §Projets ; annexe/version verbatim dans + la directive ; modules downstream « gated » = répertoires de livrables réels. +""" + +from __future__ import annotations + +import copy +import json +import os +import subprocess +import sys +import tempfile +import unittest + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_MODULE = os.path.normpath(os.path.join(_HERE, "..")) +_DELIVERABLES = os.path.normpath(os.path.join(_MODULE, "..", "..")) + +sys.path.insert(0, _MODULE) +sys.path.insert(0, os.path.join(_DELIVERABLES, "publiciste")) + +from pielib import builder, deps # noqa: E402 +from lib import validator as maison # type: ignore # noqa: E402 +import pie_manifest_gen as gen # noqa: E402 + +try: + import jsonschema # type: ignore + + _HAS_JSONSCHEMA = True +except Exception: # pragma: no cover + _HAS_JSONSCHEMA = False + + +def _load(path: str) -> dict: + with open(path, encoding="utf-8") as fh: + return json.load(fh) + + +class Baseline(unittest.TestCase): + """Le contrat vanille passe schéma + 10 invariants et est déterministe.""" + + def setUp(self): + self.bundle = gen._build() + + def test_validate_clean(self): + self.assertEqual(gen._validate(self.bundle), []) + + def test_counts_match_directive(self): + c = self.bundle["traceability"]["counts"] + self.assertEqual(c["master_data_groups"], 12) # §16-29 + self.assertEqual(c["sync_rules"], 4) # §82-86 + self.assertEqual(c["workflow_steps"], 10) # §108-133 + self.assertEqual(c["brands"], 9) # §90-102 + + def test_deterministic_rebuild(self): + self.assertEqual(gen._build(), gen._build()) + + def test_schema_oracle(self): + if not _HAS_JSONSCHEMA: + self.skipTest("jsonschema absent") + schema = _load(gen._SCHEMA_PATH) + jsonschema.validate(self.bundle["manifest"], schema) # ne lève pas + + +class Reproducibility(unittest.TestCase): + """L'artefact commité `out/` == build frais (byte-identique).""" + + def test_committed_matches_fresh(self): + out = os.path.join(_MODULE, "out") + if not os.path.isdir(out): + self.skipTest("out/ non commité") + with tempfile.TemporaryDirectory() as tmp: + rc = gen.main(["build", "-o", tmp]) + self.assertEqual(rc, 0) + for name in ("pie_manifest.json", "MANIFEST.json"): + with open(os.path.join(out, name), encoding="utf-8") as a, \ + open(os.path.join(tmp, name), encoding="utf-8") as b: + self.assertEqual(a.read(), b.read(), f"{name} a dérivé de sa source") + + +class Anchors(unittest.TestCase): + """Les ancrages hors-module pointent bien vers du réel (anti-invention #6).""" + + def setUp(self): + self.man = gen._build()["manifest"] + + def test_brand_codes_subset_of_claude_md(self): + codes = {b["code"] for b in self.man["brands"]} + canon = deps.claude_md_project_codes() + self.assertTrue(canon, "aucun code Pxx lu dans CLAUDE.md §Projets") + self.assertTrue(codes <= canon, f"codes hors constitution : {codes - canon}") + + def test_annexe_version_in_directive(self): + decl = deps.directive_declares(self.man_directive(), "Annexe 12", "V10.1") + self.assertTrue(all(decl.values()), f"ancrage directive rompu : {decl}") + + def test_gated_modules_exist_on_fs(self): + for d in self.man["downstream_registry"]: + if d["statut"] == "gated": + self.assertTrue(deps.module_exists(d["module"]), + f"module gated introuvable : {d['module']}") + + def man_directive(self) -> str: + return gen._build()["traceability"]["directive_source"] + + +class Mutations(unittest.TestCase): + """Chaque invariant attrape la dérive qu'il protège.""" + + def setUp(self): + self.bundle = gen._build() + + def _breaks(self, mutate) -> None: + bundle = copy.deepcopy(self.bundle) + mutate(bundle) + self.assertNotEqual(gen._validate(bundle), [], + "la mutation aurait dû être refusée") + + def test_inv2_missing_group(self): + self._breaks(lambda b: b["manifest"]["master_data_groups"].pop()) + + def test_inv2_wrong_group_key(self): + def m(b): + b["manifest"]["master_data_groups"][0]["cle"] = "inventé" + self._breaks(m) + + def test_inv3_unknown_trigger(self): + def m(b): + b["manifest"]["sync_rules"][0]["trigger"] = "couleur" + self._breaks(m) + + def test_inv4_dangling_downstream(self): + def m(b): + b["manifest"]["sync_rules"][0]["downstream"].append("fantome") + self._breaks(m) + + def test_inv4_dangling_trigger_group(self): + def m(b): + b["manifest"]["sync_rules"][0]["trigger_group"] = "fantome" + self._breaks(m) + + def test_inv5_status_incoherent(self): + def m(b): + b["manifest"]["downstream_registry"][0]["statut"] = "gated" + b["manifest"]["downstream_registry"][0]["module"] = None + self._breaks(m) + + def test_inv6_phantom_module(self): + def m(b): + for d in b["manifest"]["downstream_registry"]: + if d["module"]: + d["module"] = "faisabilite/inexistant" + return + self._breaks(m) + + def test_inv7_non_contiguous_workflow(self): + def m(b): + b["manifest"]["workflow"][0]["etape"] = 99 + self._breaks(m) + + def test_inv8_alien_brand_code(self): + def m(b): + b["manifest"]["brands"][0]["code"] = "P99" + self._breaks(m) + + def test_inv9_wrong_annexe(self): + def m(b): + b["manifest"]["annexe"] = 7 # « Annexe 7 » n'est pas dans la directive + self._breaks(m) + + def test_inv10_storage_not_vps(self): + def m(b): + b["manifest"]["storage_layout"]["racine"] = "05_deliverables_mvp/pie/" + self._breaks(m) + + def test_count_incoherent(self): + def m(b): + b["traceability"]["counts"]["brands"] = 999 + self._breaks(m) + + +if __name__ == "__main__": # pragma: no cover + unittest.main() diff --git a/05_deliverables_mvp/qa/acceptance/acceptance_spec.json b/05_deliverables_mvp/qa/acceptance/acceptance_spec.json index 17acc64..1b0078d 100644 --- a/05_deliverables_mvp/qa/acceptance/acceptance_spec.json +++ b/05_deliverables_mvp/qa/acceptance/acceptance_spec.json @@ -150,9 +150,13 @@ "roadmap_line": 76, "statement": "Plateforme démo live · 5 portails · CRM · BIM auto · OTOIA voice", "status": "in_repo", - "evidence_modules": ["qa/regression", "devops/deploy_runbook"], + "evidence_modules": ["qa/regression", "devops/deploy_runbook", "pie/manifest"], "evidence_artifacts": [], "out_of_scope": [ + { + "item": "Project Identity Engine complet (Brand Books/logos Flux, storage /opt/oto/data/pie/, sync ERPNext/Speckle/OTO AI) — seul le P0 (manifest de dépendances faisabilité(SSOT)→downstream) est in_repo ; P1-P4 restent à construire", + "source": "directive PIE (Annexe 12 · V10.1 · 2026-08-03) §160-166 · génération effective côté agents Flux/ERPNext/OTOIA (VPS · CLAUDE.md #8)" + }, { "item": "déploiement production complet + monitoring exécutés sur le VPS (run-book appliqué)", "source": "CLAUDE.md #8 (VPS) — l'exécution du run-book de déploiement revient à l'agent DevOps / ERPNext Backend" diff --git a/05_deliverables_mvp/qa/acceptance/out/MANIFEST.json b/05_deliverables_mvp/qa/acceptance/out/MANIFEST.json index 8e1c6d5..02070f4 100644 --- a/05_deliverables_mvp/qa/acceptance/out/MANIFEST.json +++ b/05_deliverables_mvp/qa/acceptance/out/MANIFEST.json @@ -10,13 +10,13 @@ "deliverables_total": 15, "in_repo": 15, "out_of_scope_full": 0, - "gated_modules": 23, - "cited_modules": 23, - "out_of_scope_items": 13 + "gated_modules": 24, + "cited_modules": 24, + "out_of_scope_items": 14 }, "coverage": { - "ci_modules_count": 23, - "cited_modules_count": 23, + "ci_modules_count": 24, + "cited_modules_count": 24, "bijective": true, "missing_in_matrix": [], "phantom_evidence": [], @@ -136,10 +136,12 @@ "sprint": "S8", "expected": [ "devops/deploy_runbook", + "pie/manifest", "qa/regression" ], "cited": [ "devops/deploy_runbook", + "pie/manifest", "qa/regression" ], "missing": [], diff --git a/05_deliverables_mvp/qa/acceptance/out/acceptance_matrix.json b/05_deliverables_mvp/qa/acceptance/out/acceptance_matrix.json index 69c3798..774c438 100644 --- a/05_deliverables_mvp/qa/acceptance/out/acceptance_matrix.json +++ b/05_deliverables_mvp/qa/acceptance/out/acceptance_matrix.json @@ -277,16 +277,25 @@ "module": "devops/deploy_runbook", "ci_job": "devops-deploy-runbook-tests", "gated": true + }, + { + "module": "pie/manifest", + "ci_job": "pie-manifest-tests", + "gated": true } ], "artifacts": [], "out_of_scope": [ + { + "item": "Project Identity Engine complet (Brand Books/logos Flux, storage /opt/oto/data/pie/, sync ERPNext/Speckle/OTO AI) — seul le P0 (manifest de dépendances faisabilité(SSOT)→downstream) est in_repo ; P1-P4 restent à construire", + "source": "directive PIE (Annexe 12 · V10.1 · 2026-08-03) §160-166 · génération effective côté agents Flux/ERPNext/OTOIA (VPS · CLAUDE.md #8)" + }, { "item": "déploiement production complet + monitoring exécutés sur le VPS (run-book appliqué)", "source": "CLAUDE.md #8 (VPS) — l'exécution du run-book de déploiement revient à l'agent DevOps / ERPNext Backend" } ], - "n_modules": 2, + "n_modules": 3, "all_modules_gated": true, "all_artifacts_exist": true, "has_evidence": true diff --git a/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json b/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json index a9687bf..c83a449 100644 --- a/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json +++ b/05_deliverables_mvp/qa/audit_4big/out/MANIFEST.json @@ -4,7 +4,7 @@ "audit": "OTO QA · Audit 4Big Qualité", "version": "1.0", "verdict": "PASS", - "modules_audited": 23, + "modules_audited": 24, "min_score": 100, "pass_score": 95, "coverage_ok": true, diff --git a/05_deliverables_mvp/qa/audit_4big/out/quality_report.json b/05_deliverables_mvp/qa/audit_4big/out/quality_report.json index e501e2e..a7279f0 100644 --- a/05_deliverables_mvp/qa/audit_4big/out/quality_report.json +++ b/05_deliverables_mvp/qa/audit_4big/out/quality_report.json @@ -5,8 +5,8 @@ "pass_score": 95, "coverage": { "ok": true, - "ci_modules_count": 23, - "registry_modules_count": 23, + "ci_modules_count": 24, + "registry_modules_count": 24, "missing_in_registry": [], "missing_in_ci": [], "not_in_gate": [], @@ -424,6 +424,48 @@ } ] }, + { + "id": "crm-financement-bancaire", + "path": "crm/financement_bancaire", + "sprint": "S4", + "archetype": "generator", + "applicable_weight": 100, + "earned_weight": 100, + "score": 100, + "verdict": "PASS", + "checks": [ + { + "criterion": "DOC", + "weight": 20, + "passed": true, + "evidence": "README.md (6280 octets)" + }, + { + "criterion": "CONTRAT", + "weight": 20, + "passed": true, + "evidence": "financement.schema.json" + }, + { + "criterion": "TESTS", + "weight": 25, + "passed": true, + "evidence": "35 méthodes test_* dans 1 fichier(s) (seuil 8)" + }, + { + "criterion": "CLI", + "weight": 15, + "passed": true, + "evidence": "financement_bancaire_gen.py (argparse + __main__)" + }, + { + "criterion": "HANDOFF", + "weight": 20, + "passed": true, + "evidence": "MANIFEST + 6 artefact(s) JSON valides" + } + ] + }, { "id": "crm-workflow-vente", "path": "crm/workflow_vente", @@ -550,48 +592,6 @@ } ] }, - { - "id": "crm-financement-bancaire", - "path": "crm/financement_bancaire", - "sprint": "S5", - "archetype": "generator", - "applicable_weight": 100, - "earned_weight": 100, - "score": 100, - "verdict": "PASS", - "checks": [ - { - "criterion": "DOC", - "weight": 20, - "passed": true, - "evidence": "README.md (6280 octets)" - }, - { - "criterion": "CONTRAT", - "weight": 20, - "passed": true, - "evidence": "financement.schema.json" - }, - { - "criterion": "TESTS", - "weight": 25, - "passed": true, - "evidence": "35 méthodes test_* dans 1 fichier(s) (seuil 8)" - }, - { - "criterion": "CLI", - "weight": 15, - "passed": true, - "evidence": "financement_bancaire_gen.py (argparse + __main__)" - }, - { - "criterion": "HANDOFF", - "weight": 20, - "passed": true, - "evidence": "MANIFEST + 6 artefact(s) JSON valides" - } - ] - }, { "id": "legal-confotur", "path": "legal/confotur", @@ -886,6 +886,48 @@ } ] }, + { + "id": "pie-manifest", + "path": "pie/manifest", + "sprint": "S8", + "archetype": "generator", + "applicable_weight": 100, + "earned_weight": 100, + "score": 100, + "verdict": "PASS", + "checks": [ + { + "criterion": "DOC", + "weight": 20, + "passed": true, + "evidence": "README.md (4022 octets)" + }, + { + "criterion": "CONTRAT", + "weight": 20, + "passed": true, + "evidence": "pie.schema.json" + }, + { + "criterion": "TESTS", + "weight": 25, + "passed": true, + "evidence": "20 méthodes test_* dans 1 fichier(s) (seuil 8)" + }, + { + "criterion": "CLI", + "weight": 15, + "passed": true, + "evidence": "pie_manifest_gen.py (argparse + __main__)" + }, + { + "criterion": "HANDOFF", + "weight": 20, + "passed": true, + "evidence": "MANIFEST + 1 artefact(s) JSON valides" + } + ] + }, { "id": "qa-acceptance", "path": "qa/acceptance", @@ -972,8 +1014,8 @@ } ], "totals": { - "modules": 23, - "pass": 23, + "modules": 24, + "pass": 24, "fail": 0, "min_score": 100, "max_score": 100, diff --git a/05_deliverables_mvp/qa/audit_4big/quality_spec.json b/05_deliverables_mvp/qa/audit_4big/quality_spec.json index 06fb437..58580bd 100644 --- a/05_deliverables_mvp/qa/audit_4big/quality_spec.json +++ b/05_deliverables_mvp/qa/audit_4big/quality_spec.json @@ -83,7 +83,8 @@ { "id": "qa-regression", "path": "qa/regression", "sprint": "S8", "archetype": "generator", "source": "roadmap Sprint 8 · Regression tests exhaustifs (matrice méta-niveau) · ci.yml qa-regression-tests" }, { "id": "devops-deploy-runbook", "path": "devops/deploy_runbook", "sprint": "S8", "archetype": "generator", "source": "roadmap Sprint 8 · DevOps Deployment production complet (run-book VPS unifié) · ci.yml devops-deploy-runbook-tests" }, { "id": "qa-acceptance", "path": "qa/acceptance", "sprint": "S8", "archetype": "generator", "source": "roadmap Sprint 8 · QA buffer (matrice d'acceptation / traçabilité MVP roadmap → preuve) · ci.yml qa-acceptance-tests" }, - { "id": "mobile-app-config", "path": "mobile/app_config", "sprint": "S5", "archetype": "generator", "source": "roadmap Sprint 5 · Mobile (config app Expo/EAS + navigation par rôle) · ci.yml mobile-app-config-tests" } + { "id": "mobile-app-config", "path": "mobile/app_config", "sprint": "S5", "archetype": "generator", "source": "roadmap Sprint 5 · Mobile (config app Expo/EAS + navigation par rôle) · ci.yml mobile-app-config-tests" }, + { "id": "pie-manifest", "path": "pie/manifest", "sprint": "S8", "archetype": "generator", "source": "directive PIE (Project Identity Engine · Annexe 12 · V10.1 · 2026-08-03) livrée durant le buffer S8 (roadmap L72-76) · manifest de dépendances faisabilité(SSOT)→downstream · ci.yml pie-manifest-tests" } ], "notes": [ "Audit de MÉTA-NIVEAU : chaque note de module est CALCULÉE depuis des faits du dépôt (présence README/schéma, nombre de méthodes test_*, entrypoint CLI, intégrité out/) — jamais saisie à la main (#6).", diff --git a/05_deliverables_mvp/qa/regression/out/MANIFEST.json b/05_deliverables_mvp/qa/regression/out/MANIFEST.json index 8e056bd..dad96d8 100644 --- a/05_deliverables_mvp/qa/regression/out/MANIFEST.json +++ b/05_deliverables_mvp/qa/regression/out/MANIFEST.json @@ -4,8 +4,8 @@ "matrix": "OTO QA · Matrice de régression exhaustive", "version": "1.0", "verdict": "PASS", - "suites": 23, - "test_methods": 604, + "suites": 24, + "test_methods": 624, "coverage_ok": true, "artifacts": [ "regression_plan.json" diff --git a/05_deliverables_mvp/qa/regression/out/regression_plan.json b/05_deliverables_mvp/qa/regression/out/regression_plan.json index c48e99a..f059e64 100644 --- a/05_deliverables_mvp/qa/regression/out/regression_plan.json +++ b/05_deliverables_mvp/qa/regression/out/regression_plan.json @@ -17,8 +17,8 @@ ], "coverage": { "ok": true, - "suites_count": 23, - "gated_in_ci": 24, + "suites_count": 24, + "gated_in_ci": 25, "self_module_excluded": "qa/regression", "self_module_gated": true, "missing_tests_dir": [], @@ -169,6 +169,17 @@ "test_files": 1, "test_methods": 22 }, + { + "id": "pie.manifest", + "path": "pie/manifest", + "jobs": [ + "pie-manifest-tests" + ], + "in_gate": true, + "has_tests_dir": true, + "test_files": 1, + "test_methods": 20 + }, { "id": "publiciste", "path": "publiciste", @@ -282,9 +293,9 @@ ], "under_threshold": [], "totals": { - "suites": 23, - "test_files": 23, - "test_methods": 604, + "suites": 24, + "test_files": 24, + "test_methods": 624, "min_methods": 10, "under_threshold": 0 },