Compare commits
272 Commits
d1e96d7784
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 15eebb41ca | |||
| c1e5fd9a6e | |||
| bd9f2b173a | |||
| e8e3dff1a3 | |||
| 1aaef23fe4 | |||
| 0ef7c1a079 | |||
| 0f7a1d8f6f | |||
| 1cf0e18fba | |||
| c73ed5b626 | |||
| 1732cc3de1 | |||
| 4a13557529 | |||
| e15129bc6a | |||
| fb2d3e16bf | |||
| 4eb615b174 | |||
| ea8458d63b | |||
| 3849375b66 | |||
| 1992ee63be | |||
| 626c39d958 | |||
| 4aece53921 | |||
| 85cd625dac | |||
| cb8069b83c | |||
| 123761e938 | |||
| e7cc3c4a3c | |||
| 5af86e3518 | |||
| aeb30b8311 | |||
| 3a40aee42d | |||
| 37c68aca28 | |||
| 1513e69a79 | |||
| a17d86a767 | |||
| 2cad5b2b06 | |||
| ee88d3698b | |||
| 031b5e762e | |||
| 39def91880 | |||
| 548a05ec61 | |||
| 35de616236 | |||
| fd99362660 | |||
| 9e02c38902 | |||
| ac595d0749 | |||
| 344b8154e1 | |||
| 37c1e9767e | |||
| f3419f9c26 | |||
| 6970294da7 | |||
| 72753a6342 | |||
| 460574a265 | |||
| c3f5664370 | |||
| 8ec11dece6 | |||
| 18d876352c | |||
| de837fae00 | |||
| 0b453c5182 | |||
| f3160607bc | |||
| edb3d76aa5 | |||
| be8bfda1ea | |||
| f00df20b53 | |||
| a289e3db2a | |||
| 07cf79ea35 | |||
| 2a6e89dda1 | |||
| 69def8cde8 | |||
| 525e47cc7a | |||
| 2d1cb0d083 | |||
| d0f05ad913 | |||
| 5ab362153c | |||
| 52e8e81799 | |||
| b88b7051e0 | |||
| 27c2bd29ff | |||
| 11f158a9d2 | |||
| 55ff296ac8 | |||
| 40b023c2eb | |||
| 558225a74d | |||
| be36d6600f | |||
| 0393ea7b48 | |||
| 2ded65bbb4 | |||
| d074250084 | |||
| 27d1117a2d | |||
| fdb0632cf6 | |||
| caeb06be5f | |||
| 5d5e986fa6 | |||
| 04f4c21778 | |||
| 1839f521aa | |||
| bea5d49aab | |||
| 4aea5bddf7 | |||
| 6c05538efb | |||
| 27cfe2fe59 | |||
| 3710253c02 | |||
| d9668d6079 | |||
| e87f5f56af | |||
| 44712cbdfa | |||
| 9acd8441dc | |||
| 42d54b045f | |||
| def4706de3 | |||
| 4c741a7118 | |||
| ddcafc58c0 | |||
| e4b3488c7e | |||
| e786c128b2 | |||
| a678534b8b | |||
| 12ec9d0711 | |||
| 4bc98198b1 | |||
| b2749587c0 | |||
| 48276ee916 | |||
| 243523c6c2 | |||
| 354a68788d | |||
| b60d471030 | |||
| 34c8cd2e17 | |||
| a8342e97e2 | |||
| fe61207ca6 | |||
| c196e4183e | |||
| 21f55a9ed7 | |||
| 8903a4b774 | |||
| d6a8d73aec | |||
| d4f9289586 | |||
| cb3de786f8 | |||
| 9c7791f23a | |||
| 27c7d20449 | |||
| afa61657cb | |||
| c14305c170 | |||
| 1b99917f21 | |||
| 1a44dfe1c0 | |||
| 1441693e25 | |||
| db91a6bb5b | |||
| 9fa464dc84 | |||
| 9d30939db9 | |||
| 731ff6b65d | |||
| a39c05aeda | |||
| 2337833e5a | |||
| f311a1bad9 | |||
| 48b19b9a52 | |||
| 1321983dc3 | |||
| 127ef6f7fc | |||
| 9b6ec60783 | |||
| a205fa64f2 | |||
| e0858f279b | |||
| 6b61ff8b68 | |||
| 11e1cdd183 | |||
| fbe298ffd0 | |||
| 5a96fc32d9 | |||
| d7dbda9e2b | |||
| d6cc09da0d | |||
| a39dc28f59 | |||
| f73404a6a4 | |||
| caa3b90556 | |||
| 40ddab05af | |||
| b8447363ff | |||
| 986d29362c | |||
| e38dd933c6 | |||
| d25da28d9c | |||
| 98f3f56c3a | |||
| 9fe8bf1feb | |||
| c2c72dc7a8 | |||
| 30e4af8a49 | |||
| 7e4affe9d8 | |||
| 84df56ae2d | |||
| 3d6c2b7d4f | |||
| b7ffb6448f | |||
| c5c48dff53 | |||
| 8a10d478f3 | |||
| 89a03982d6 | |||
| 81484927ce | |||
| 503f726ae7 | |||
| 88bd0256b7 | |||
| a9e9ee9f3c | |||
| e6b89435fd | |||
| 86fbc87a56 | |||
| 58097be2bb | |||
| 57f34f8eb5 | |||
| 2e701f239c | |||
| 2c86141188 | |||
| 302df869f4 | |||
| 9a7e4ffc3e | |||
| ebfeaef7f5 | |||
| 3c22cca3be | |||
| 0576dc1559 | |||
| 0bdd400b45 | |||
| f0827a7b6c | |||
| 2e4ff2d728 | |||
| f9d303f262 | |||
| 9eebaadc64 | |||
| 3e6cfba87a | |||
| 927b3b6bdc | |||
| e24f2c95c7 | |||
| 4f31d0c8c5 | |||
| 6de6dbd9fa | |||
| 80eb3c147b | |||
| 9b502e67fe | |||
| 1e8cce22a0 | |||
| 7434d6803a | |||
| 6d1d69f255 | |||
| 12cfaaa2ed | |||
| 9c062ef83d | |||
| 33fe561f51 | |||
| 1f5a1622e3 | |||
| dbde8fac46 | |||
| db817309a0 | |||
| 88d9955f90 | |||
| 3d8ccb0249 | |||
| 89eb8e68ad | |||
| 1af5d01ccf | |||
| b631774160 | |||
| 953e683be3 | |||
| af84b21f89 | |||
| d0b354f81e | |||
| 3eec69a87f | |||
| 642643ffe3 | |||
| 94e9365cea | |||
| 85d2fa3c54 | |||
| 427dd77bd2 | |||
| 256d967882 | |||
| 06965aa0b5 | |||
| 90673137dd | |||
| 94ae143f57 | |||
| 7e4456eb5b | |||
| df8fa60415 | |||
| 358a95d904 | |||
| 1c3bed3f42 | |||
| 56a8802ff6 | |||
| 16db05af20 | |||
| 828fa838e0 | |||
| eae37875cb | |||
| 12c784c5d9 | |||
| a2c2e0b097 | |||
| b3f131e1c7 | |||
| 0e76411459 | |||
| 8a0806548b | |||
| 7c79e4302a | |||
| f954cc19d1 | |||
| 97d3b64780 | |||
| 3ff51d94fb | |||
| d2ab269684 | |||
| 68f8c1a718 | |||
| 59eb4dcb7c | |||
| 4f390b4ee2 | |||
| dcb463ee00 | |||
| 49123ecba3 | |||
| 1c0cb8e973 | |||
| bd3572b382 | |||
| 576e78b6a9 | |||
| 8dcedb092c | |||
| cb8d931abb | |||
| 2e71c5f51e | |||
| 2bc3afbd1a | |||
| 519f6dbfee | |||
| 4841b77099 | |||
| 0752e35988 | |||
| 3cb1bf06e1 | |||
| ca15e7e12b | |||
| 448c310398 | |||
| ccb86fe7ca | |||
| 0f4f1b533b | |||
| 655eeeb4a1 | |||
| 335d449346 | |||
| 215c9e6f25 | |||
| 0d4fd0942c | |||
| 11c48b668b | |||
| bcfc0f9a9d | |||
| 03b481c6ee | |||
| ab73cf9384 | |||
| d429fccf9a | |||
| aab89a6032 | |||
| bf11db6d85 | |||
| 40ee6f6a46 | |||
| a5de76b7c1 | |||
| 87c88beaa2 | |||
| 1485e4d6ac | |||
| 7e674f2f09 | |||
| 15812c1dff | |||
| a40bfc51c1 | |||
| 64b635073a | |||
| d20d37607d | |||
| d587b446b9 | |||
| f91a22f458 | |||
| 17c3d2b104 | |||
| 18a758045f | |||
| 861260462f | |||
| 47ef210910 |
+74
-1
@@ -9,6 +9,21 @@
|
||||
# ============================================================================
|
||||
name: CI
|
||||
|
||||
# Mode UTF-8 forcé pour TOUS les jobs (builds + suites + gates) — indépendance
|
||||
# LOCALE du runner. Chaque générateur clôt son `build` par un print(✅ / accents
|
||||
# français) vers stdout, dont l'encodage suit la locale. Sur un runner à locale
|
||||
# ASCII (image minimale : LC_ALL=C sans coercition PEP 538, PYTHONUTF8 non posé),
|
||||
# ce print lève UnicodeEncodeError APRÈS écriture des fichiers → `check-artifacts`
|
||||
# voit un exit non-nul (« build a échoué ») et les 25 suites plantent : tout le
|
||||
# gate passe ROUGE en silence le jour où l'image runner change, alors qu'il est
|
||||
# VERT sur le poste dev (UTF-8) — classe « le runner n'est pas ton poste ».
|
||||
# PYTHONUTF8=1 (PEP 540) impose stdout/stdin/défaut-fichier en UTF-8 partout,
|
||||
# indépendamment de LANG/LC_ALL → tous les jobs deviennent locale-robustes.
|
||||
# Verrouillé par ci/check_ci_integrity.sh (INV-C) pour ne pas disparaître en
|
||||
# silence. Miroir local : run_ci.sh exporte la même variable.
|
||||
env:
|
||||
PYTHONUTF8: "1"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -117,6 +132,22 @@ jobs:
|
||||
- name: Contrôle des chiffres du README
|
||||
run: bash ci/check_readme_claims.sh
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Intégrité du SECOND workflow `mobile-build.yml` (Sprint 5 mobile · hors gate
|
||||
# de merge, mais AUCUN gate ne lintait sa structure : check-ci-integrity ne
|
||||
# verrouille que ci.yml, validate-json que le .json). Ce gate rend opposable le
|
||||
# correctif de portabilité 234104 (pas de `secrets.` en if: de job → SKIP
|
||||
# silencieux) + l'activation différée #6/#8 (builds gatés sur les outputs de
|
||||
# preflight). stdlib + awk pur, PyYAML best-effort. Détail : ci/README.md §2.
|
||||
# --------------------------------------------------------------------------
|
||||
check-mobile-workflow:
|
||||
name: Intégrité mobile-build.yml (gating portable · activation différée · #6/#8)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Contrôle structure mobile-build.yml
|
||||
run: bash ci/check_mobile_workflow.sh
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Publiciste (Sprint 2) : parser faisabilité → projets_master.json + generator.
|
||||
# Tests unitaires stdlib pur (unittest) → aucune installation pip requise sur
|
||||
@@ -311,6 +342,27 @@ jobs:
|
||||
- name: Tests générateur barème commissions
|
||||
run: python3 -m unittest discover -s tests -v
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Financement Bancaire (Sprint 5 · CRM natif · roadmap L52 « workflow vente
|
||||
# end-to-end » · volet financement · DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_
|
||||
# 20260803.md). Parcours hypothécaire immobilier RD : contrat banques/documents/
|
||||
# autorisations/sections + CŒUR MÉTIER du gate check 4 conditions (apport initial
|
||||
# 20/30 · documents validés WAG · autorisations signées · validation WAG). Anti-
|
||||
# invention (#6) : taux d'apport et rôles depuis le contrat + RBAC (jamais codés) ;
|
||||
# taux/LTV bancaires non énoncés = null a_confirmer. Exemple de gate CALCULÉ,
|
||||
# auditable. Schéma de sortie + 12 invariants. stdlib pur, zéro pip.
|
||||
# --------------------------------------------------------------------------
|
||||
crm-financement-bancaire-tests:
|
||||
name: CRM · Financement Bancaire (gate hypothécaire RD)
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: 05_deliverables_mvp/crm/financement_bancaire
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Tests générateur Financement Bancaire
|
||||
run: python3 -m unittest discover -s tests -v
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Intégration e-CF DGII (Sprint 4 · ERPNext Backend « e-CF DGII intégration
|
||||
# (Compupar) »). Génère un plan de configuration de facturation électronique
|
||||
@@ -521,10 +573,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, 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, check-mobile-workflow, 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."
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
# ============================================================================
|
||||
# Mobile Build (EAS) · OTO Enterprise OS DTP · Gitea Actions
|
||||
# ----------------------------------------------------------------------------
|
||||
# Roadmap Sprint 5 l.56-57 « Mobile : Rebuild Expo 54 + submit App Store #32 +
|
||||
# Play Store » · DIRECTIVE_MOBILE_STORES_20260803.md · P1 « CI/CD Mobile ».
|
||||
#
|
||||
# Plateforme : Gitea Actions UNIQUEMENT (CLAUDE.md #2 · JAMAIS GitHub).
|
||||
# Runner : act_runner enregistré sur le VPS avec le label « ubuntu-latest »
|
||||
# (même runner que ci.yml · voir ci/README.md).
|
||||
#
|
||||
# PÉRIMÈTRE (#8). Ce worker versionne la CONFIG CI/CD ; l'exécution `eas build` /
|
||||
# `eas submit` a lieu HORS repo (service EAS / stores), pilotée par l'agent
|
||||
# Mobile. La config des profils de build est la source de vérité versionnée
|
||||
# 05_deliverables_mvp/mobile/app_config/out/eas_build.json (profils development /
|
||||
# preview / production) — ce workflow ne fait que l'invoquer.
|
||||
#
|
||||
# CODE APP HORS REPO. Le code natif Expo vit dans un AUTRE dépôt Gitea
|
||||
# (/opt/oto/mobile/native/ · Expo SDK 51 actuel → cible Expo 54, roadmap l.56).
|
||||
# Il est cloné au run depuis ${{ vars.MOBILE_NATIVE_REPO }} — jamais recopié ici.
|
||||
#
|
||||
# ACTIVATION DIFFÉRÉE (#6 · zéro credential inventé). Tant que le secret
|
||||
# EAS_TOKEN n'est pas fourni par Michel (expo.dev · 2 min · cf. directive
|
||||
# « Vrais bloqueurs »), les jobs de build sont SKIP — donc ce workflow ne rend
|
||||
# JAMAIS le CI rouge avant que Michel n'active. Le job `preflight` reste vert et
|
||||
# rapporte l'état des secrets à fournir. Rien n'est fabriqué : tout identifiant
|
||||
# (repo natif, token, credentials Apple/Google) est un secret/variable
|
||||
# `a_confirmer`, cohérent avec les champs `null · a_confirmer` du livrable mobile.
|
||||
# ============================================================================
|
||||
name: Mobile Build (EAS)
|
||||
|
||||
on:
|
||||
# Conforme directive P1 : « Trigger sur push branche main ou tag v* ».
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
paths:
|
||||
- '05_deliverables_mvp/mobile/**'
|
||||
- '.gitea/workflows/mobile-build.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: 'Plateforme à builder'
|
||||
required: true
|
||||
default: 'all'
|
||||
type: choice
|
||||
options: [all, ios, android]
|
||||
profile:
|
||||
description: 'Profil EAS (cf. eas_build.json)'
|
||||
required: true
|
||||
default: 'production'
|
||||
type: choice
|
||||
options: [production, preview, development]
|
||||
|
||||
# Un seul build mobile à la fois par ref (annule le précédent en cours).
|
||||
concurrency:
|
||||
group: mobile-build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Profil par défaut (aligné directive P4 : builds `--profile production`).
|
||||
EAS_PROFILE: ${{ github.event.inputs.profile || 'production' }}
|
||||
# Config des profils versionnée par le worker (source de vérité).
|
||||
EAS_CONFIG: 05_deliverables_mvp/mobile/app_config/out/eas_build.json
|
||||
|
||||
jobs:
|
||||
# --------------------------------------------------------------------------
|
||||
# Préflight : TOUJOURS vert. Prouve que la config EAS versionnée est présente
|
||||
# et rapporte l'état des secrets requis, SANS jamais échouer avant activation.
|
||||
# --------------------------------------------------------------------------
|
||||
preflight:
|
||||
name: Préflight config EAS + état secrets
|
||||
runs-on: ubuntu-latest
|
||||
# Le `secrets` context n'est PAS garanti disponible dans un `if:` au niveau
|
||||
# job (table d'availability GitHub Actions : `jobs.<id>.if` n'expose que
|
||||
# github/needs/vars/inputs/matrix + status functions, pas `secrets`). On lit
|
||||
# donc EAS_TOKEN ICI, dans un step (où `secrets` est toujours disponible),
|
||||
# et on l'expose en output booléen consommé par les jobs de build via
|
||||
# `needs.preflight.outputs.*` (lui, disponible en `if:` partout). Sinon les
|
||||
# builds risqueraient de rester SKIP en silence même une fois le token fourni.
|
||||
outputs:
|
||||
has_token: ${{ steps.secrets.outputs.has_token }}
|
||||
has_repo: ${{ steps.secrets.outputs.has_repo }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Config EAS versionnée présente (eas_build.json)
|
||||
run: test -f "$EAS_CONFIG" && echo "OK · profils build/submit versionnés dans $EAS_CONFIG"
|
||||
- name: État des secrets requis (aucun secret n'est loggé)
|
||||
id: secrets
|
||||
run: |
|
||||
echo "Profil sélectionné : $EAS_PROFILE"
|
||||
if [ -n "${{ secrets.EAS_TOKEN }}" ]; then
|
||||
echo "EAS_TOKEN : présent → les jobs de build vont s'exécuter."
|
||||
echo "has_token=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "EAS_TOKEN : ABSENT → jobs de build SKIP (attente Michel · expo.dev)."
|
||||
echo "has_token=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
if [ -n "${{ vars.MOBILE_NATIVE_REPO }}" ]; then
|
||||
echo "MOBILE_NATIVE_REPO: configuré."
|
||||
echo "has_repo=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "MOBILE_NATIVE_REPO: a_confirmer (dépôt Gitea du code natif Expo)."
|
||||
echo "has_repo=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Build iOS · SKIP tant que EAS_TOKEN absent (activation Michel · #6/#8).
|
||||
# App Store #32 · `eas build --platform ios --profile production`.
|
||||
# --------------------------------------------------------------------------
|
||||
build-ios:
|
||||
name: EAS build iOS (App Store #32)
|
||||
runs-on: ubuntu-latest
|
||||
needs: preflight
|
||||
if: ${{ needs.preflight.outputs.has_token == 'true' && needs.preflight.outputs.has_repo == 'true' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' || github.event_name != 'workflow_dispatch') }}
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EAS_TOKEN }}
|
||||
steps:
|
||||
- name: Cloner le code natif Expo (dépôt Gitea séparé)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ vars.MOBILE_NATIVE_REPO }}
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Installer EAS CLI
|
||||
run: npm install -g eas-cli
|
||||
- name: EAS build iOS (production · non-interactive)
|
||||
run: eas build --platform ios --profile "$EAS_PROFILE" --non-interactive --no-wait
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Build Android · SKIP tant que EAS_TOKEN absent (activation Michel · #6/#8).
|
||||
# Play Store · `eas build --platform android --profile production`.
|
||||
# --------------------------------------------------------------------------
|
||||
build-android:
|
||||
name: EAS build Android (Play Store)
|
||||
runs-on: ubuntu-latest
|
||||
needs: preflight
|
||||
if: ${{ needs.preflight.outputs.has_token == 'true' && needs.preflight.outputs.has_repo == 'true' && (github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' || github.event_name != 'workflow_dispatch') }}
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EAS_TOKEN }}
|
||||
steps:
|
||||
- name: Cloner le code natif Expo (dépôt Gitea séparé)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ vars.MOBILE_NATIVE_REPO }}
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Installer EAS CLI
|
||||
run: npm install -g eas-cli
|
||||
- name: EAS build Android (production · non-interactive)
|
||||
run: eas build --platform android --profile "$EAS_PROFILE" --non-interactive --no-wait
|
||||
@@ -25,7 +25,7 @@ Livrer une plateforme intégrée BIM + ERPNext + Console luxury + OTOIA orchestr
|
||||
- **Speckle** : bim-cloud.otov7.com (viewer 3D)
|
||||
- **Design system** : `#0a0a12` dark + `#f0b429` doré · Fraunces + Cormorant Garamond
|
||||
|
||||
## Tes 12 agents spécialisés (voir `03_agents/`)
|
||||
## Tes 13 agents spécialisés (voir `03_agents/`)
|
||||
Chaque agent a son propre `AGENT.md` avec scope + outils + non-négociables.
|
||||
|
||||
## Ton livrable final (semaine 8)
|
||||
|
||||
+19
-1
@@ -7,6 +7,24 @@ faisabilités 4 volets et le **viewer 3D** de la console. Il ne « dessine » pa
|
||||
il orchestre des outils lourds (Blender headless, Bonsai/IfcOpenShell, RunPod
|
||||
ComfyUI) **de façon déterministe et sans jamais fabriquer un chiffre de projet**.
|
||||
|
||||
> **⚠️ Statut migration V18 (2026-08-10) · à lire AVANT le reste de cette fiche.** Michel a émis la
|
||||
> [`DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md`](../../DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md)
|
||||
> qui **remplace** le modèle V12 « 4 volets » : le BIM n'y alimente plus un seul *volet Ingénierie*
|
||||
> mais devient **central à 7 des 18 sections** — Architecturale, Structurelle, Plomberie, Électrique,
|
||||
> Mécanique HVAC (« + BIM »), **Clash Detection** (§9) et Environnementale + BIM VRD (§14). Le pipeline
|
||||
> BIM y est le **moteur 7 (« BIM / Clash / Quantity »)** de l'ordre de développement imposé, et deux
|
||||
> checkpoints humains y sont adossés : **CP1 BIM Geometry** et **CP3 Clash Resolution** (« aucun
|
||||
> blocking non résolu » = gate dur). L'audit préalable
|
||||
> [`OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md`](../../OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md)
|
||||
> (§3, mapping 18 sections) classe ces sections BIM **🟠 partiel / 🔴 nouveau** : côté lisible in-repo,
|
||||
> seul le volet archi existe, **`aec.py`/IFC restent hors-repo (VPS, #8)**. **Toute la séquence moteur
|
||||
> V18 est bloquée** en attente des arbitrages de Michel — voir
|
||||
> [`OPEN_DECISIONS_REGISTER.md`](../../05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md) **D-06**
|
||||
> (approbation de l'audit = gate d'entrée de tout le moteur) · **D-08** (périmètre du Master Intake).
|
||||
> Tant que ces décisions ne sont pas rendues, **aucun code moteur V18 n'est produit** (directive :
|
||||
> « NE PAS coder avant l'audit approuvé » · anti-invention `CLAUDE.md` #6). **La fiche V12 ci-dessous
|
||||
> décrit l'état commité courant, pas la cible finale V18.**
|
||||
|
||||
## Scope
|
||||
Brief projet → maquette Blender/Bonsai → export **IFC** → publication **Speckle**
|
||||
(`bim-cloud.otov7.com`) → export **IFC→GLB** pour viewer → quantitatifs **BIM
|
||||
@@ -71,7 +89,7 @@ auto-générée en **< 1h** (le BIM alimente le volet Ingénierie + les vues).
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Éthique
|
||||
|
||||
+48
-3
@@ -23,13 +23,58 @@ AVANT le Workflow** qui le cible.
|
||||
|---|---|---|---|---|---|
|
||||
| [`workflow_vente/`](../../05_deliverables_mvp/crm/workflow_vente/README.md) | 4 (roadmap L50) | Graphe **Workflow** ERPNext : le pipeline `lead → visite → devis → réservation → contrat → CONFOTUR` (states + transitions + actions) | `workflow_vente_gen.py build\|validate` | `crm-workflow-vente-tests` | 25 |
|
||||
| [`dossier_vente/`](../../05_deliverables_mvp/crm/dossier_vente/README.md) | 4 (roadmap L50) | **DocType porteur** `OTO Dossier Vente` : le document réel qui circule dans le Workflow ; sans lui le pipeline n'a rien à quoi s'attacher | `doctype_dossier_vente_gen.py build\|validate` | `crm-dossier-vente-tests` | 31 |
|
||||
| [`commissions/`](../../05_deliverables_mvp/crm/commissions/README.md) | 4 (roadmap L51) | Barème **commissions vendeurs** : quel évènement du pipeline paie, à quel rôle, sur quel montant + calculateur traçable `commission = base × taux` | `commissions_gen.py build\|validate` | `crm-commissions-tests` | 25 |
|
||||
| [`commissions/`](../../05_deliverables_mvp/crm/commissions/README.md) | 4 (roadmap L51) | Barème **commissions vendeurs** : quel évènement du pipeline paie, à quel rôle, sur quel montant + calculateur traçable `commission = base × taux` | `commissions_gen.py build\|validate` | `crm-commissions-tests` | 27 |
|
||||
|
||||
**Trois modules cross-cohérents, une source unique** (CLAUDE.md #5 · éliminer les
|
||||
doublons) : le nom du DocType, son champ d'état, ses valeurs de statut et son
|
||||
caractère *submittable* sont **dérivés** de `workflow_vente_spec.json` (anti-dérive) ;
|
||||
les rôles sont **résolus** depuis `rbac/rbac_50_roles.json` — jamais un nom Frappe
|
||||
en dur. Total CRM : **81 tests** (25 + 31 + 25), tous gated dans le CI.
|
||||
en dur. Total CRM : **83 tests** (25 + 31 + 27), tous gated dans le CI — c'est le
|
||||
total du **trio pipeline** ; le module financement bancaire ci-dessous porte ses
|
||||
**35 tests** à part (source distincte).
|
||||
|
||||
## Module CRM additionnel · financement bancaire hypothécaire (piloté par directive)
|
||||
|
||||
Un **quatrième** livrable CRM vit sous `05_deliverables_mvp/crm/` **hors du trio
|
||||
pipeline** — piloté non par `workflow_vente_spec.json` mais par une directive datée
|
||||
de Michel, d'où son suivi séparé (il ne partage pas la source unique du trio) :
|
||||
|
||||
| Module | Sprint | Rôle | Entrée CLI | Job CI | Tests |
|
||||
|---|---|---|---|---|---|
|
||||
| [`financement_bancaire/`](../../05_deliverables_mvp/crm/financement_bancaire/README.md) | 4 (roadmap L52) | Parcours **hypothécaire RD** : cœur métier du **gate check 4 conditions** — aucun document n'est transmis à la banque tant que apport initial (20 % résident · 30 % étranger · Ley 189-11) + documents exigés + autorisations signées + validation référente ne sont pas réunis ; fonctions **pures** sans I/O | `financement_bancaire_gen.py build\|validate` | `crm-financement-bancaire-tests` | 37 |
|
||||
|
||||
Matérialise [`DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md`](../../DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md)
|
||||
(**Phase 1 · P0 · MVP** : contrat de données + cœur du gate + 7 sections + bannière
|
||||
critique). La **Phase 2** (formulaires PDF officiels des 6 banques, endpoints
|
||||
runtime `/api/hypotheque/*`, `renderHypotheque`) attend les démarches
|
||||
relationship-manager de Michel et l'exécution VPS — **hors périmètre worker** (#8).
|
||||
|
||||
**Décision produit ouverte** — la condition #4 du gate teste encore une validation
|
||||
**humaine** (`wag_validated_by`), alors que la couche `## PRÉCISION` de la directive
|
||||
la supersède par un **audit IA signé** ; producteur d'audit hors dépôt → arbitrage
|
||||
de séquencement, **surfacé non tranché** dans
|
||||
[`OPEN_DECISIONS_REGISTER.md` D-02](../../05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md)
|
||||
(ne pas re-litiger · #5).
|
||||
|
||||
## Livrable transverse · Sprint 7 — scénarios démo (co-porté CRM + Faisabilité)
|
||||
|
||||
Un **cinquième** livrable vit sous `05_deliverables_mvp/demo/scenarios/` : **pas un
|
||||
livrable CRM propre**, mais un **méta-générateur** qui **compose** les livrables déjà
|
||||
produits (pipeline CRM + dossier de vente, bancable, e-CF, CONFOTUR, portails, OTOIA,
|
||||
SEO) en un **run-sheet de pitch** — sans jamais écrire une donnée métier lui-même
|
||||
(CLAUDE.md #6). Il matérialise la roadmap **Sprint 7** (« CRM + Faisabilité : scénarios
|
||||
démo P07 banquier / P05 client », L68) et est donc **co-porté** avec l'agent
|
||||
Faisabilité — recensé ici parce que le pipeline CRM en est le fil conducteur, mais **il
|
||||
ne partage aucune source avec le trio ni avec le financement** ci-dessus :
|
||||
|
||||
| Module | Sprint | Rôle | Entrée CLI | Job CI | Tests |
|
||||
|---|---|---|---|---|---|
|
||||
| [`scenarios/`](../../05_deliverables_mvp/demo/scenarios/README.md) | 7 (roadmap L68) | Méta-générateur `demo/scenarios` : compose les livrables existants en 2 run-sheets (`S-P07-BANQUIER` · `S-P05-CLIENT`) — zéro donnée métier inventée, tout est cité depuis les artefacts amont | `demo_scenario_gen.py build\|validate` | `demo-scenario-tests` | 39 |
|
||||
|
||||
La cellule **Tests 39** est **auto-gatée** exactement comme les lignes du trio :
|
||||
`check_readme_claims` recompute chaque cellule « Tests » de fiche depuis
|
||||
`regression_plan.json` (source unique · `demo/scenarios` ∈ `plan.suites`). Détail des
|
||||
2 scénarios, des invariants et de la composition dans le README du module.
|
||||
|
||||
## Anti-invention (#6) — pourquoi les taux de commission sont `null`
|
||||
**Aucun taux de commission n'est documenté dans CLAUDE.md.** Les taux du barème
|
||||
@@ -65,7 +110,7 @@ P05 client).
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Éthique
|
||||
|
||||
@@ -21,8 +21,22 @@ et vérifications HTTP restent côté **agent DevOps/ERPNext Backend**.
|
||||
|
||||
| Module | Sprint | Rôle | Entrée | Tests |
|
||||
|---|---|---|---|---|
|
||||
| [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) + [`ci/`](../../ci/README.md) | 1 (roadmap L29) | **CI/CD Gitea Actions** : 3 guards blocants (contraintes / JSON / docs) + un job de tests par livrable gated + `gate` agrégateur + `e2e-baseline` | déclenché sur `push`/`pull_request` → `main` + `workflow_dispatch` | 22 suites gated |
|
||||
| [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) + [`ci/`](../../ci/README.md) | 1 (roadmap L29) | **CI/CD Gitea Actions** : 3 guards blocants (contraintes / JSON / docs) + un job de tests par livrable gated + `gate` agrégateur + `e2e-baseline` | déclenché sur `push`/`pull_request` → `main` + `workflow_dispatch` | 24 suites gated |
|
||||
| [`05_deliverables_mvp/devops/deploy_runbook/`](../../05_deliverables_mvp/devops/deploy_runbook/README.md) | 8 (roadmap L73) | **Run-book VPS unifié** : graphe de **7 phases ordonnées** couvrant TOUS les modules gated, dépendances inter-phases + confirmations préalables sourcées | `deploy_runbook_gen.py build\|validate` | 29 (dont 14 injections négatives) |
|
||||
| [`run_ci.sh`](../../run_ci.sh) | 8 (roadmap L73 · « regression exhaustifs ») | **Miroir LOCAL du gate** : rejoue en une commande, hors runner et hors réseau, l'ensemble exact des checks de `gate.needs` | `bash run_ci.sh` (outil, non un gate) | — (voir ci-dessous) |
|
||||
|
||||
### `run_ci.sh` — outillage local, PAS un gate (#5)
|
||||
Confort développeur, pas un maillon de la chaîne de merge : le script **dérive**
|
||||
sa liste de checks de `gate.needs` (`.gitea/workflows/ci.yml`, la source déjà
|
||||
verrouillée par `ci/check_ci_integrity.sh`) — **zéro liste en dur**, donc aucune
|
||||
dérive silencieuse possible (ajouter/retirer un job CI se répercute ici sans
|
||||
édition). Il n'ajoute **aucun `ci/*.sh` ni aucune assertion** : ce n'est **pas**
|
||||
un gate (CLAUDE.md #5), il ne s'inscrit pas dans `gate.needs` et, la racine
|
||||
n'étant pas sous `ci/`, il échappe par construction à l'invariant qui interdit un
|
||||
`ci/*.sh` non câblé au gate. Périmètre = uniquement les jobs de `gate.needs`
|
||||
(exclut les jobs `workflow_dispatch` type `e2e-baseline`/`npm test` qui exigent
|
||||
Node). Le décompte jobs/gates/suites est **imprimé à l'exécution** — jamais figé
|
||||
dans le code, où il se périmerait (anti-invention · #6).
|
||||
|
||||
### Le gate CI — trois guards blocants (`ci/`)
|
||||
- **`guard_constraints.sh`** : détecte l'**usage** (pas la simple mention) des
|
||||
@@ -89,7 +103,7 @@ Voir `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md`. Volets DevOps : Sprint 1 (repo Git
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Éthique
|
||||
|
||||
@@ -37,7 +37,7 @@ le VPS reçoit — jamais l'inverse.
|
||||
vente, le barème commissions, le DocType Dossier Vente et le plan e-CF **résolvent**
|
||||
leurs rôles depuis ce fichier, jamais un nom Frappe en dur. Total backend **RBAC 60
|
||||
tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests**, tous gated dans le CI (matrice
|
||||
de régression du repo : **564 tests · 22 suites** · verdict `PASS`, source
|
||||
de régression du repo : **635 tests · 24 suites** · verdict `PASS`, source
|
||||
`qa/regression/out/regression_run.json` — jamais compté à la main · #6).
|
||||
|
||||
## Hand-off reçus (à appliquer sur le VPS, dans l'ordre)
|
||||
@@ -84,7 +84,7 @@ L51) · Sprint 6 (Chat OTOIA embedded dans chaque portail · L63).
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Éthique
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
|
||||
**Rôle** : Cet agent OTOIA génère et maintient à jour toutes les faisabilités de projets sur le **modèle canonique le plus récent**. Élimine les faisabilités obsolètes ou hétérogènes.
|
||||
|
||||
> **⚠️ Statut migration V18 (2026-08-10) · à lire AVANT le reste de cette fiche.** Michel a émis la
|
||||
> [`DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md`](../../DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md)
|
||||
> — un **Master Institutional Feasibility & Bankability Engine** qui **remplace** le modèle V12
|
||||
> « 4 volets » décrit dans cette fiche. Les modules commités actuels (`generator`, `bancable`,
|
||||
> table §Livrables) restent le **socle réutilisable** de cette cible, mais **le moteur V18
|
||||
> lui-même n'est pas encore construit** : l'audit préalable
|
||||
> [`OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md`](../../OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md)
|
||||
> (V12→V18, produit **avant tout code**) cartographie l'écart, et **toute la séquence moteur est
|
||||
> bloquée en attente des arbitrages de Michel** — consignés dans le
|
||||
> [`OPEN_DECISIONS_REGISTER.md`](../../05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md) :
|
||||
> **D-06** approbation de l'audit · **D-07** formules financières (DCF · IRR/VAN · DSCR/LTV/LTC)
|
||||
> absentes des docs lisibles · **D-08** périmètre du Master Intake (sur-ensemble strict du
|
||||
> `brief.json`). Tant que ces décisions ne sont pas rendues, **aucun code moteur V18 n'est
|
||||
> produit** (directive : « NE PAS coder avant l'audit approuvé » · anti-invention `CLAUDE.md` #6).
|
||||
> **La fiche V12 ci-dessous décrit l'état commité courant, pas la cible finale V18.**
|
||||
|
||||
## Mission
|
||||
1. **Générer** les faisabilités 4 volets pour chaque projet (Masterplan · Architecture · Paysage-Expérience · Ingénierie-Faisabilité)
|
||||
2. **Maintenir un template canonique versionné** (`data_room/_TEMPLATE_FAISABILITE_v{X}/`)
|
||||
@@ -19,7 +35,7 @@ les composants runtime OTOIA/VPS listés plus bas (`otoia/capabilities/…`, sys
|
||||
| Module | Sprint | Rôle | Entrée CLI | Job CI | Tests |
|
||||
|---|---|---|---|---|---|
|
||||
| [`TEMPLATE_FAISABILITE_CANONIQUE_v1.0.md`](../../05_deliverables_mvp/faisabilite/TEMPLATE_FAISABILITE_CANONIQUE_v1.0.md) | 1 (roadmap L41-46) | **Template canonique v1.0** — contrat des 4 volets + champs obligatoires (§Livrable S1 ci-dessous) | — (doc contrat) | `check-docs` | — |
|
||||
| [`generator/`](../../05_deliverables_mvp/faisabilite/generator/README.md) | 2 (roadmap L44) | **Générateur 4 volets** : `brief.json` → `data_room/PXX/` (template v1.0), round-trip via le parser Publiciste | `faisabilite_gen.py score\|scaffold\|generate\|batch` | `faisabilite-gen-tests` | 16 |
|
||||
| [`generator/`](../../05_deliverables_mvp/faisabilite/generator/README.md) | 2 (roadmap L44) | **Générateur 4 volets** : `brief.json` → `data_room/PXX/` (template v1.0), round-trip via le parser Publiciste | `faisabilite_gen.py score\|scaffold\|generate\|batch` | `faisabilite-gen-tests` | 17 |
|
||||
| [`bancable/`](../../05_deliverables_mvp/faisabilite/bancable/README.md) | 3 (roadmap L46) | **Dossier bancable trilingue** : `brief.json` → `50_financier_bancable/{fr,en,es}.md` + manifest, figures sourcées verbatim + agrégats recalculés | `bancable_gen.py build\|validate` | `bancable-tests` | 22 |
|
||||
|
||||
**Anti-invention (#6)** : `generator` et `bancable` ne **fabriquent aucun chiffre** — les figures
|
||||
@@ -28,7 +44,36 @@ traçable (formule recoupée en test). Aucune faisabilité concrète PXX n'est c
|
||||
le générateur, son contrat et ses fixtures d'entrée le sont (le rendu réel des 9 projets
|
||||
s'exécute côté OTOIA/VPS · #8).
|
||||
|
||||
**Livrable transverse Sprint 7 (co-porté avec CRM).** Le module
|
||||
[`demo/scenarios`](../../05_deliverables_mvp/demo/scenarios/README.md) — méta-générateur
|
||||
qui **compose** en run-sheet de pitch les livrables amont, dont `bancable` côté
|
||||
faisabilité (scénario `S-P07-BANQUIER`) — matérialise la roadmap Sprint 7 (« CRM +
|
||||
Faisabilité : scénarios démo P07 banquier / P05 client », L68). Sa ligne recensée et
|
||||
**auto-gatée** (cellule Tests) vit dans la fiche CRM
|
||||
([`03_agents/crm/AGENT.md`](../crm/AGENT.md), §« Livrable transverse · Sprint 7 ») —
|
||||
source unique, non redupliquée ici (#5).
|
||||
|
||||
## Faisabilité = SSOT · Project Identity Engine (Annexe 12 · V10.1)
|
||||
|
||||
Le [`DIRECTIVE_PIE_PROJECT_IDENTITY_ENGINE_20260803.md`](../../DIRECTIVE_PIE_PROJECT_IDENTITY_ENGINE_20260803.md)
|
||||
(Michel · 2026-08-03) désigne **cet agent** comme la **source unique de vérité (SSOT)** de
|
||||
toute la stack : la faisabilité est le *Project DNA*, et aucun livrable downstream (brochure,
|
||||
kit banquier, contrat, page projet, section app…) ne doit être recréé à la main si la donnée
|
||||
existe déjà ici. Le module commité [`pie/manifest`](../../05_deliverables_mvp/pie/manifest/README.md)
|
||||
livre le **P0** de la directive — le **schéma Project Master Data** + le **manifest de
|
||||
dépendances** `faisabilité → livrables downstream`, versionnable et cross-vérifié. Ainsi, toute
|
||||
modification de faisabilité déclenche la **régénération sélective** des seuls livrables concernés
|
||||
(règles de synchronisation portées par le manifest). Les comptes exacts (groupes Master Data,
|
||||
règles de synchronisation, registre downstream) **font foi dans le README du module** — cet
|
||||
agent en est la source, pas la copie.
|
||||
|
||||
## Sources canoniques
|
||||
> **Chemins VPS runtime — hors périmètre worker (#8), non commités in-repo** (préfixe
|
||||
> absolu `/opt/oto/…`, placeholder de version `{X}`). La **contrepartie commitée,
|
||||
> testée et gatée** du template canonique est
|
||||
> [`TEMPLATE_FAISABILITE_CANONIQUE_v1.0.md`](../../05_deliverables_mvp/faisabilite/TEMPLATE_FAISABILITE_CANONIQUE_v1.0.md)
|
||||
> (table §Livrables ci-dessus) — **la référence de vérité pour le worker** ; les
|
||||
> chemins ci-dessous décrivent l'emplacement runtime OTOIA/VPS où l'agent exécute.
|
||||
- `/opt/oto/otoia/capabilities/knowledge/faisabilite_4_volets_standard.md` (STANDARD OFFICIEL)
|
||||
- `/opt/oto/data_room/_TEMPLATE_FAISABILITE_v{X}/` (template versionné)
|
||||
- `/opt/oto/data_room/PXX/00_brief/` (données projet)
|
||||
@@ -105,6 +150,7 @@ data_room/PXX/
|
||||
- **Publiciste Agent** : consomme la faisabilité pour maintenir vente.otov7.com
|
||||
- **ERPNext Backend** : stocke DocType "Faisabilité" avec version + score qualité
|
||||
- **QA Agent** : valide score 4Big ≥ 95/100 avant publication
|
||||
- **PIE (Annexe 12)** : consomme la faisabilité comme SSOT et propage toute modif aux livrables downstream via le manifest de dépendances (voir §*Faisabilité = SSOT* ci-dessus)
|
||||
|
||||
## Livrable Sprint (mandat 8 semaines)
|
||||
- **S1** : Template canonique v1.0 finalisé + agent scaffold
|
||||
|
||||
@@ -33,6 +33,14 @@ le câblage de l'endpoint OTOIA et l'application du thème desk restent côté *
|
||||
- **Chat OTOIA** : CLI `python3 chat_otoia_gen.py build|validate` · **14 invariants** ·
|
||||
**31 tests** · job CI `chat-otoia-tests` (gated).
|
||||
|
||||
> **Note de lecture — colonne « Sortie ».** Les cellules listent le **payload
|
||||
> métier** ; chaque générateur écrit **en plus** un `out/MANIFEST.json` (compteurs
|
||||
> auto-vérifiés) **volontairement omis** de la table par convention éditoriale.
|
||||
> L'énumération exhaustive des trois fichiers vit dans les docstrings et READMEs
|
||||
> de module (byte-gatés). Ne PAS ajouter le MANIFEST à cette cellule : elle est
|
||||
> lue par regex par `ci/check_readme_claims.sh` (compteurs `cartes/liens/rôles`,
|
||||
> comptes de tests) et l'omission est intentionnelle, pas une dérive.
|
||||
|
||||
## Source de vérité unique (zéro invention · #6)
|
||||
La matière première est le **contrat RBAC** `05_deliverables_mvp/rbac/rbac_50_roles.json`,
|
||||
**jamais la spec de mise en page** :
|
||||
@@ -104,7 +112,7 @@ directement le montage des Workspaces (réutilisation `wslib`) — coordination.
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Éthique
|
||||
|
||||
@@ -89,7 +89,7 @@ modèle publié sur `bim-cloud.otov7.com` et affiché dans le viewer console.
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Éthique
|
||||
|
||||
+41
-16
@@ -1,7 +1,7 @@
|
||||
# 📱 Mobile Agent · app Expo 54 + API mobile ERPNext (score 95+/100)
|
||||
# 📱 Mobile Agent · app Expo (SDK 51 actuel → rebuild cible 54) + API mobile ERPNext (score 95+/100)
|
||||
|
||||
**Rôle** : Cet agent maintient l'**application mobile** OTO (Expo SDK 54 /
|
||||
React Native) et l'**API mobile** qui l'alimente depuis ERPNext natif. Il
|
||||
**Rôle** : Cet agent maintient l'**application mobile** OTO (Expo SDK **51**
|
||||
actuel · cible de **rebuild** SDK 54 · Sprint 5 / React Native) et l'**API mobile** qui l'alimente depuis ERPNext natif. Il
|
||||
produit les **builds EAS** iOS/Android, pousse les **mises à jour OTA**, et
|
||||
soumet aux **stores** (App Store · Play Store). Il ne définit pas les rôles ERP
|
||||
(agent ERPNext Backend) ni le design de la console web (agent Frontend Console) :
|
||||
@@ -9,8 +9,8 @@ il **livre le binaire mobile** et son **canal API**, toujours branché sur le
|
||||
**backend ERPNext réel** (contrainte #1), jamais sur un backend inventé.
|
||||
|
||||
## Scope
|
||||
API mobile (`oto_module_mobile_api.py`) + client Expo 54 (React 19.1.0 /
|
||||
React Native 0.81.5) → **builds EAS iOS/Android** → **submit App Store #32 +
|
||||
API mobile (`oto_module_mobile_api.py`) + client Expo **51** (React 18.2.0 /
|
||||
React Native 0.74.5 · runtime actuel vérifié) → **builds EAS iOS/Android** → **submit App Store #32 +
|
||||
Play Store** → **EAS OTA** pour les correctifs sans re-review store. RBAC mobile
|
||||
seedé côté ERPNext (`seed_mobile_rbac.py`). Refresh OTA quand l'API ou l'UI
|
||||
évolue.
|
||||
@@ -22,14 +22,16 @@ mandat, qui reste un dépôt de **planification et de contrats**. Il n'existe do
|
||||
**aucun binaire, `.ipa`/`.aab` ni bundle OTA diffable commité ici**, et ce
|
||||
document ne prétend pas le contraire : le prétendre serait une invention (#6,
|
||||
« documenter du code sans vérifier son existence courante » est interdit). Le
|
||||
livrable in-repo de l'agent est la **refonte des modules VPS existants** ci-dessous
|
||||
et le **rôle RBAC** qui les gouverne, validés par auto-vérification côté serveur
|
||||
(HTTP + builds EAS).
|
||||
livrable in-repo de l'agent est le **générateur de config app** `mobile/app_config`
|
||||
(§ dédiée ci-dessous · la config **versionnable** de l'app, pas le binaire), la
|
||||
**refonte des modules VPS existants** ci-dessous et le **rôle RBAC** qui les
|
||||
gouverne, validés par auto-vérification côté serveur (HTTP + builds EAS).
|
||||
|
||||
## Points de contact **réellement commités** dans ce dépôt (vérifiés)
|
||||
Contrairement aux binaires (hors-repo), le rôle de cet agent est **ancré dans deux
|
||||
Contrairement aux binaires (hors-repo), le rôle de cet agent est **ancré dans des
|
||||
artefacts in-repo vérifiables** — la preuve que sa place dans la plateforme est
|
||||
contractualisée, pas inventée :
|
||||
contractualisée, pas inventée. Outre son **générateur de config** `mobile/app_config`
|
||||
(§ dédiée ci-dessous), **deux cross-références** l'ancrent côté RBAC et QA :
|
||||
|
||||
| Contact in-repo (vérifié) | Ce qu'il fixe |
|
||||
|---|---|
|
||||
@@ -41,22 +43,42 @@ contractualisée, pas inventée :
|
||||
> **volontairement pas** rattaché au Workspace `OTO Ventes` ni à la liste
|
||||
> `roles_allowed` du chat OTOIA — ces surfaces couvrent les portails
|
||||
> construction/vente. Ne pas revendiquer ici un contact workspace/chat qui
|
||||
> n'existe pas ; l'ancrage in-repo réel est le **rôle RBAC** + la **row QA S5**.
|
||||
> n'existe pas ; l'ancrage in-repo réel est son **générateur de config**
|
||||
> `mobile/app_config` (§ ci-dessous), le **rôle RBAC** et la **row QA S5**.
|
||||
>
|
||||
> Ces deux fichiers existent et sont couverts par la CI (`rbac-tests`,
|
||||
> `qa-acceptance-tests`, dans le `gate`). Le rôle est **spécifié en-repo** ; son
|
||||
> **exécution** (EAS/stores/OTA) reste hors repo (#8).
|
||||
|
||||
## Générateur de config app **in-repo** · `mobile/app_config` (livrable versionnable, gaté CI)
|
||||
|
||||
Au-delà des deux cross-références RBAC/QA ci-dessus, l'agent Mobile a un **livrable
|
||||
in-repo à part entière** : le générateur `05_deliverables_mvp/mobile/app_config/`.
|
||||
Il ne produit **ni binaire ni bundle** (ceux-là restent hors repo · #8) mais la
|
||||
**config versionnable** de l'app compagnon — dérivée à 100 % des sources RBAC/spec
|
||||
(anti-invention #6 ; identifiants de store `null · a_confirmer` tant que Michel ne
|
||||
les fournit pas). C'est le pendant mobile des modules générateurs des autres agents,
|
||||
gaté dans le CI comme eux :
|
||||
|
||||
| Module | Sprint | Rôle | Entrée CLI | Job CI | Tests |
|
||||
|---|---|---|---|---|---|
|
||||
| [`app_config/`](../../05_deliverables_mvp/mobile/app_config/README.md) | 5 (roadmap L56) | Config **versionnable** de l'app compagnon (pas le binaire · #8) : transforme `rbac_50_roles.json` + `mobile_spec.json` + specs portails/seo en **5 fichiers** — `app_config.json` (objet Expo `app.config` · thème dark+doré #4 · locales seo · identifiants store `null · a_confirmer`), `eas_build.json`, `role_navigation.json` (1 onglet par **portail rôle**, surface RBAC exacte), `store_listing.json` (fiche FR/EN/ES), `MANIFEST.json` (traçabilité) ; sortie déterministe re-générable | `app_config_gen.py build\|validate` | `mobile-app-config-tests` | 22 |
|
||||
|
||||
Cette ligne est **auto-gatée** par `ci/check_readme_claims.sh` (résolution par cible
|
||||
de lien, `mobile/app_config ∈` `plan.suites`) : cellule Tests `22 == source`, job CI
|
||||
`mobile-app-config-tests == ci.yml`, verbes CLI `build|validate == subparsers`. Toute
|
||||
dérive du compte/job/CLI mordra désormais.
|
||||
|
||||
## Modules OTOV7 réels refactorés (source : `AGENTS_EXISTING_ASSETS.md §8`)
|
||||
|
||||
| Module VPS (hors-repo) | Fonction | État roadmap |
|
||||
|---|---|---|
|
||||
| `/opt/oto/oto_module_mobile_api.py` | **API mobile** ERPNext (endpoints app) | cœur du refactor Sprint 5 (backend natif #1) |
|
||||
| `/opt/oto/oto_module_mobile_api.py` *(compilé pyc)* | **API mobile** ERPNext (endpoints app) | cœur du refactor Sprint 5 (backend natif #1) |
|
||||
| `/opt/oto/oto_module_mobile_download.py` | distribution / téléchargement app | canal de livraison |
|
||||
| `/opt/oto/staging/mobile_rbac/deploy_mobile_rbac.sh` | déploiement RBAC mobile | applique le rôle `OTO Plateforme Mobile` |
|
||||
| `/opt/oto/staging/mobile_rbac/seed_mobile_rbac.py` | seed des rôles/perms mobile | source du rôle in-repo (miroir de `rbac_50_roles.json`) |
|
||||
| `/opt/oto/static/oto_mobile.js` | client JS mobile / glue | front embarqué |
|
||||
| **Expo SDK 54** · React 19.1.0 · React Native 0.81.5 (upgrade 2026-07) | runtime app | base des builds EAS |
|
||||
| **Expo SDK 51.0.0** · React 18.2.0 · React Native 0.74.5 (runtime **actuel** · upgrade 54 tenté puis abandonné 2026-07-27) | runtime app | base des builds EAS · rebuild cible Expo 54 (S5) |
|
||||
| **EAS** builds iOS/Android · Apple Dev + Google Play Console | pipeline build/submit | exécution stores (hors repo #8) |
|
||||
|
||||
> ⚠️ Chemins **VPS**, non vérifiables depuis ce dépôt. Ils sont cités tels
|
||||
@@ -70,7 +92,8 @@ la soumission store correspondants n'ont pas réellement eu lieu : c'est la row
|
||||
repo). L'API mobile parle au **backend ERPNext natif** (`erpnext-backend-1`,
|
||||
contrainte #1), jamais à un backend fictif. Les numéros de version d'Expo /
|
||||
React / React Native cités proviennent de `AGENTS_EXISTING_ASSETS.md §8`
|
||||
(upgrade daté 2026-07) et ne sont pas inventés.
|
||||
(runtime actuel Expo 51 vérifié `package.json` · l'upgrade Expo 54 y est
|
||||
documenté comme **abandonné** 2026-07-27) et ne sont pas inventés (#6).
|
||||
|
||||
## Non-négociables (voir CLAUDE.md racine pour la liste complète)
|
||||
- Gitea only (jamais GitHub) — code app poussé sur `michel/oto-enterprise-os-dtp`
|
||||
@@ -85,7 +108,9 @@ Voir `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md`. Volet Mobile : **Sprint 5**
|
||||
(« Rebuild **Expo 54** + submit **App Store #32** + **Play Store** »). Cible de
|
||||
sprint mesurable : **apps mobile live sur les 2 stores** (deliverable S5, L58),
|
||||
gatée par la row d'acceptation QA **S5**. État courant (table roadmap L18) :
|
||||
modules RBAC + API + Expo 54 en place, builds/submissions ~**50 %**.
|
||||
modules RBAC + API + Expo **51** en place (cible de rebuild Expo 54 · S5 ·
|
||||
décision 51→54 ouverte, cf. `DIRECTIVE_MOBILE_STORES_20260803.md` P0),
|
||||
builds/submissions ~**50 %**.
|
||||
|
||||
## Coordination inter-agents
|
||||
- **ERPNext Backend** : porteur du rôle RBAC `OTO Plateforme Mobile` (perm
|
||||
@@ -101,7 +126,7 @@ modules RBAC + API + Expo 54 en place, builds/submissions ~**50 %**.
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Éthique
|
||||
|
||||
@@ -114,7 +114,7 @@ attribués roadmap à **QA** (`qa/audit_5d/`) — coordination ci-dessous.
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Éthique
|
||||
|
||||
@@ -92,7 +92,7 @@ du générateur, gaté en CI. C'est le **seul module net-neuf** du mandat, sur l
|
||||
|
||||
| Module | Sprint | Rôle | Entrée CLI | Job CI | Tests |
|
||||
|---|---|---|---|---|---|
|
||||
| [`publiciste/`](../../05_deliverables_mvp/publiciste/README.md) | 2 (README `scaffold (Sprint 2)` · GAP §3.13) | **Pipeline site public** : `data_room/PXX/` → `projets_master.json` → `index.html` luxury (`lib/parser.py` extraction · `lib/validator.py` schéma · `lib/generator.py` + `lib/branding.py` rendu dark+doré `#0a0a12`/`#f0b429` · `templates/site_public.html.tmpl`) | `publiciste.py parse\|validate\|generate\|run` | `publiciste-tests` | 23 |
|
||||
| [`publiciste/`](../../05_deliverables_mvp/publiciste/README.md) | 2 (README `scaffold (Sprint 2)` · GAP §3.13) | **Pipeline site public** : `data_room/PXX/` → `projets_master.json` → `index.html` luxury (`lib/parser.py` extraction · `lib/validator.py` schéma · `lib/generator.py` + `lib/branding.py` rendu dark+doré `#0a0a12`/`#f0b429` · `templates/site_public.html.tmpl`) | `publiciste.py parse\|validate\|generate\|run` | `publiciste-tests` | 26 |
|
||||
|
||||
Le contrat d'échange `projets_master.json` n'est **pas** défini ici mais par le Faisabilité Agent
|
||||
([`../../05_deliverables_mvp/faisabilite/projets_master.schema.json`](../../05_deliverables_mvp/faisabilite/projets_master.schema.json),
|
||||
|
||||
+31
-3
@@ -22,7 +22,7 @@ périmètre — un auditeur ne s'audite jamais.
|
||||
| Module | Sprint | Rôle | Entrée CLI | Job CI | Tests |
|
||||
|---|---|---|---|---|---|
|
||||
| [`audit_5d/`](../../05_deliverables_mvp/qa/audit_5d/README.md) | 5 (roadmap L57) | Audit **5D** de conformité : 17 contrôles / 5 dimensions, lit les `out/` amont, coche UAF + normes ISA/IFRS | `audit_5d_gen.py build\|validate` | `qa-audit-5d-tests` | 37 |
|
||||
| [`audit_4big/`](../../05_deliverables_mvp/qa/audit_4big/README.md) | 7 (roadmap L69) | **Gate qualité** : note 4Big de 100 % des livrables, verdict `FAIL` si un module < 95/100 ou couverture incomplète | `audit_4big_gen.py build\|validate` | `qa-audit-4big-tests` | 34 |
|
||||
| [`audit_4big/`](../../05_deliverables_mvp/qa/audit_4big/README.md) | 7 (roadmap L69) | **Gate qualité** : note 4Big de 100 % des livrables, verdict `FAIL` si un module < 95/100 ou couverture incomplète | `audit_4big_gen.py build\|validate` | `qa-audit-4big-tests` | 37 |
|
||||
| [`regression/`](../../05_deliverables_mvp/qa/regression/README.md) | 8 (roadmap L74) | **Matrice de régression** : agrège l'exécution de **toutes** les suites gated en un verdict unique + le compte agrégé faisant autorité | `regression_gen.py build\|validate\|run` | `qa-regression-tests` | 26 |
|
||||
| [`acceptance/`](../../05_deliverables_mvp/qa/acceptance/README.md) | 8 (roadmap L75 / L80-87) | **Recette / traçabilité** : mappe chaque promesse roadmap (8 livrables + 7 métriques) vers sa preuve, bijectif vs CI | `acceptance_gen.py build\|validate` | `qa-acceptance-tests` | 37 |
|
||||
|
||||
@@ -32,9 +32,37 @@ périmètre — un auditeur ne s'audite jamais.
|
||||
**conformité** normative des hand-off.
|
||||
|
||||
## Verdict agrégé courant (source `qa/regression/out/regression_run.json`)
|
||||
**22 suites gated · 564 tests · 564 passés · 0 échec · 0 erreur → verdict `PASS`.**
|
||||
**24 suites gated · 635 tests · 618 passés · 0 échec · 0 erreur → verdict `PASS`.**
|
||||
Ce compte est **découvert automatiquement depuis le CI** (`q4lib.registry.parse_ci`,
|
||||
réutilisé sans duplication) — jamais compté à la main.
|
||||
Les **17 tests restants sont ignorés** (`skipped`) : ce sont les *oracles optionnels*
|
||||
`jsonschema` (`skipUnless`/`try…except ImportError`), absents du runner Gitea pip-less
|
||||
(« stdlib pur, zéro pip »). La matrice est désormais exécutée sous `python -S`
|
||||
(sans `site-packages`) → byte-identique quel que soit ce qui est `pip install`é
|
||||
localement ; les validateurs maison couvrent les mêmes contrats que ces oracles.
|
||||
|
||||
## Deuxième étage QA : la batterie de gates statiques (`ci/*.sh`)
|
||||
Au-dessus des 4 modules d'audit ci-dessus, le mandat exécute une couche de **gates
|
||||
statiques** — les *gates de méta-niveau* du titre. Leur périmètre n'est **jamais**
|
||||
figé à la main : `check_ci_integrity` **découvre** l'ensemble via `git ls-files
|
||||
'ci/*.sh'` et **refuse** qu'un `ci/*.sh` ne soit pas câblé à un job du gate CI (ou
|
||||
inversement) — la liste ci-dessous est donc *l'état courant*, pas une constante.
|
||||
|
||||
| Gate | Rôle (une commande, déterministe, zéro chiffre fabriqué) |
|
||||
|---|---|
|
||||
| `check_ci_integrity.sh` | Intégrité du câblage CI : chaque `ci/*.sh` est câblé à un job **dans** `gate.needs` (aucun gate décâblé ni orphelin) — c'est le gardien des gardiens (il **s'exclut** lui-même · SoD). |
|
||||
| `check_mobile_workflow.sh` | Intégrité du **second** workflow `mobile-build.yml` (EAS build iOS/Android) — **hors** `gate.needs` (il s'active quand Michel fournit `EAS_TOKEN`), donc *non* couvert par `check_ci_integrity` qui ne verrouille que `ci.yml` : bien-formé, gating d'activation `if:` job-level porté par `needs.preflight.outputs.*` (jamais le contexte `secrets.`, non exposé hors GitHub → SKIP silencieux), contrat d'outputs sans dangling (MOB-1..4). |
|
||||
| `check_readme_claims.sh` | Intégrité des **chiffres** des README / fiches agent (CLAUDE.md #6) : chaque compte cité est **recalculé** depuis l'artefact `out/` ou la source (jamais une donnée inventée). |
|
||||
| `check_artifacts.sh` | **Reproductibilité** : chaque `out/*.json` se régénère **byte-identique** (aucune dérive entre source et hand-off commité). |
|
||||
| `check_regression.sh` | **Fraîcheur** de la matrice de régression : le run rejoué == le run commité (aucune régression silencieuse · #5). |
|
||||
| `check_docs.sh` | Qualité documentaire : les liens Markdown relatifs internes pointent vers une cible **existante** (gate 4Big). |
|
||||
| `guard_constraints.sh` | Enforce les **contraintes NON-NÉGOCIABLES** de CLAUDE.md : plateformes git proscrites (hors Gitea), CRM interdits, paiement non conforme, écriture serveur bannie — via une prohibition marquée ou l'échappatoire `ci-allow`. |
|
||||
| `validate_json.sh` | Tous les `.json` suivis sont **bien formés** (parse strict) — première ligne de défense avant toute lecture d'artefact. |
|
||||
|
||||
Ces gates ne **produisent** pas de livrable métier : ils **prouvent** que les
|
||||
livrables des autres agents tiennent. Un nouveau gate suit la même règle — il est
|
||||
**automatiquement** intégré au périmètre dès qu'il est câblé (découverte CI), sans
|
||||
liste à maintenir.
|
||||
|
||||
## Non-négociables (voir CLAUDE.md racine pour la liste complète)
|
||||
- Gitea only (jamais GitHub)
|
||||
@@ -61,7 +89,7 @@ ISA/IFRS) · Sprint 7 (audit 4Big 95+/100 sur 100 % deliverables) · Sprint 8
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Coordination inter-agents
|
||||
|
||||
@@ -103,7 +103,7 @@ sprint mesurable : **P07 Aqua Terra** — faisabilité 4 volets auto-générée
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Éthique
|
||||
|
||||
@@ -75,7 +75,7 @@ hreflang »).
|
||||
|
||||
## Communication inter-agents
|
||||
- Rapports quotidiens dans `05_deliverables_mvp/daily_reports/`
|
||||
- Handoffs formalisés dans `05_deliverables_mvp/handoffs/`
|
||||
- Hand-off inter-agents = livrables déterministes commités in-repo (fixtures/specs `out/`, SPEC, README), consommés directement par l'agent destinataire
|
||||
- Blockers escalés à Michel Roy via WhatsApp +18296296385
|
||||
|
||||
## Éthique
|
||||
|
||||
@@ -15,7 +15,7 @@ Livraison anticipée récompensée : dès qu'un sprint est complet et validé QA
|
||||
| Frontend | `/waf-home` + `/crm` + `/qa` + portal HTML | Clone layout sur 5 entités + polish | ~55% |
|
||||
| CRM | `/crm.html` LIVE + ERPNext CRM natif | Enrichir + API pipeline | ~65% |
|
||||
| SEO | mission active + worker systemd | Trilinguisation + schema.org | ~45% |
|
||||
| Mobile | modules RBAC + API + Expo 54 | Builds + submit stores | ~50% |
|
||||
| Mobile | modules RBAC + API + Expo 51 | Rebuild vers Expo 54 + builds + submit stores | ~50% |
|
||||
| IFC/Speckle | `ifc_to_glb.py` + bim-cloud | Intégration console + viewer | ~55% |
|
||||
| ONAPI/Legal | `oto_module_confotur_application.py` + drafts | Automatisation dépôt | ~40% |
|
||||
| DevOps | 6 systemd services + Gitea | CI/CD Gitea Actions | ~35% |
|
||||
|
||||
@@ -1,5 +1,186 @@
|
||||
# Activity Log · 2026-08-01 · Claude Code DTP
|
||||
|
||||
## Session `20260801_230453` · Buffer S8 · QA/Audit 5D : gate d'IDENTITÉ du **VERDICT GLOBAL** — le LABEL de conclusion de l'audit
|
||||
|
||||
**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement
|
||||
complète ; la valeur marginale reste le durcissement anti-dérive (CLAUDE.md #6)
|
||||
des surfaces prose data-derived encore non gatées. Les 7 gates verts au départ.
|
||||
|
||||
**Surface trouvée (aveugle)** : le **verdict global** de l'audit 5D — le LABEL
|
||||
`PASS` / `PASS_WITH_OPEN_ITEMS` / `FAIL` qui est la **HEADLINE de tout le
|
||||
livrable** — était **HORS de tout gate**. Le bloc audit_5d existant recompute la
|
||||
**ventilation chiffrée** (« **13 PASS · 0 FAIL · 4 à confirmer** », pass/fail/à
|
||||
confirmer d'`audit_report.totals`), l'énumération des open items, les 5 dimensions
|
||||
et les livrables audités — mais **jamais le label de verdict lui-même**. Or ce
|
||||
label est data-derived (`out/audit_report.json[verdict]` == `out/MANIFEST.json[verdict]`
|
||||
== `PASS_WITH_OPEN_ITEMS`, byte-gatés), recalculé par `qalib/builder.py` via la
|
||||
règle **`FAIL` si ≥1 FAIL · sinon `PASS_WITH_OPEN_ITEMS` si ≥1 A_CONFIRMER · sinon
|
||||
`PASS`**, et transcrit à la main à **deux** endroits du README : la RÈGLE
|
||||
(`Verdict global : …`, l.39) et le VERDICT COURANT (`## Verdict courant` l.49). Le
|
||||
risque #6 est le **CŒUR** du livrable : écrire `PASS` alors que 4 params
|
||||
réglementaires restent `a_confirmer` = l'audit « force » un PASS et prétend propre
|
||||
un dossier qui ne l'est pas — exactement ce que l'audit reproche aux autres. La
|
||||
ventilation chiffrée peut rester exacte pendant que le label ment ; ni
|
||||
`check_artifacts` (byte only) ni les suites `tests/` (FONCTIONS, pas la prose)
|
||||
n'attrapent.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Audit 5D · IDENTITÉ du VERDICT
|
||||
GLOBAL », inséré juste après la ventilation chiffrée) : on **MIROITE** la règle du
|
||||
générateur et on exige — **(a)** HONNÊTETÉ interne : `audit_report.verdict` ==
|
||||
verdict recalculé du triplet (fail>0→FAIL · a_confirmer>0→PASS_WITH_OPEN_ITEMS ·
|
||||
sinon PASS) ⇒ un verdict FORCÉ incohérent avec ses propres chiffres est mordu ;
|
||||
**(b)** CROISÉ : `MANIFEST.verdict` == `audit_report.verdict` ; **(c)** le LABEL
|
||||
« Verdict courant » du README == `audit_report.verdict` (ancré sur le heading +
|
||||
la ventilation `**N PASS`, pas sur la règle l.39) ; **(d)** la RÈGLE « Verdict
|
||||
global » énumère EXACTEMENT le **codomaine** `{FAIL, PASS_WITH_OPEN_ITEMS, PASS}`
|
||||
du générateur (un label renommé côté `builder.py` sans mise à jour de la règle est
|
||||
mordu). Claim absent = échec (disparition = régression).
|
||||
|
||||
**5 morsures vérifiées** (working tree byte-restauré après chacune via
|
||||
`git checkout -- <chemin précis>`, JAMAIS `git checkout .` / `git clean`) :
|
||||
1. README label `PASS_WITH_OPEN_ITEMS`→`PASS` → ✗ (c) « Verdict courant = PASS MAIS
|
||||
artefact PASS_WITH_OPEN_ITEMS » ;
|
||||
2. README règle l.39 `PASS_WITH_OPEN_ITEMS`→`PASS_OPEN` → ✗ (d) « codomaine
|
||||
[FAIL, PASS, PASS_OPEN] ≠ [FAIL, PASS, PASS_WITH_OPEN_ITEMS] » ;
|
||||
3. `audit_report.verdict`→`PASS` (à_confirmer reste 4) → ✗ (a) « verdict FORCÉ,
|
||||
incohérent — viole #6 » **+** cascade ✗ (b) + ✗ (c) (3 fails : l'honnêteté est
|
||||
la morsure la plus forte) ;
|
||||
4. `MANIFEST.verdict`→`PASS` → ✗ (b) « MANIFEST PASS MAIS audit_report
|
||||
PASS_WITH_OPEN_ITEMS (désynchronisés) » ;
|
||||
5. README label supprimé (l.49) → ✗ (c) « Verdict courant INTROUVABLE ».
|
||||
Restauré = green : (a)(b)(c)(d) tous ✓.
|
||||
|
||||
- `ci/README.md` : la sous-clause « audit 5D » de la row `check-readme-claims`
|
||||
étendue de l'IDENTITÉ du VERDICT GLOBAL (label + honnêteté interne + croisé
|
||||
MANIFEST + codomaine de la règle documentée).
|
||||
- **7 gates re-verts** : `guard_constraints` / `validate_json` / `check_docs` /
|
||||
`check_artifacts` / `check_regression` / `check_ci_integrity` /
|
||||
`check_readme_claims` (tous rc=0).
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
## Session `20260801_210434` · Buffer S8 · Domaine CRM/commissions : l'**IDENTITÉ du CHAMP DEVISE** — la clause `devise_field` = le champ `devise` (`Select` **USD/DOP** · #10) du DocType Dossier Vente, transcrite **en prose** dans la §« Cross-cohérence » (`crm/commissions/README.md:54`) — était **HORS de tout gate**. Les trois blocs commissions déjà en place gatent les états soumis / brouillons exclus, les champs de base Currency, l'identité des rôles payés (portail `ventes`) et la promesse anti-taux #6 — mais **aucun** ne touchait ce couple champ↔devises (l'**invariant CLI #8** du générateur).
|
||||
|
||||
**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement
|
||||
close ; poursuite de la série anti-dérive (CLAUDE.md #6). **4e surface distincte du
|
||||
MÊME livrable `crm/commissions`** (après états/champs, anti-taux, rôles payés) — le
|
||||
« **sur quelle devise** » du calcul `base × taux`. Surface DISTINCTE du bloc « Fiches
|
||||
#10 » (prose ERPNext/CRM) et du bloc e-CF `TipoMoneda` (options du spec fiscal) :
|
||||
ici c'est le champ `devise` du DocType Dossier Vente + la clé racine `devise_field`
|
||||
du plan, ancrés sur **CLAUDE.md #10** (« **USD + DOP** devises »).
|
||||
|
||||
**Dérive silencieuse fermée** :
|
||||
- `05_deliverables_mvp/crm/commissions/README.md:54` — clause « `devise_field` = le
|
||||
champ `devise` (`Select` **USD/DOP** · #10) du DocType » : NOMME le champ (`devise`),
|
||||
son fieldtype (`Select`), ses options (`USD/DOP`) et tague `#10`.
|
||||
- Sources faisant autorité : (a) `out/commission_plan.json` (byte-gaté par
|
||||
`check_artifacts`) — clé RACINE `devise_field` (le champ du Dossier Vente qui porte
|
||||
la devise d'un montant commissionné) ; (b) `crm/dossier_vente/doctype_spec.json`
|
||||
(l'INPUT du générateur) — le champ `devise` de fieldtype `Select` + ses `options` ;
|
||||
(c) **CLAUDE.md #10** (ligne 24 : « **USD + DOP** devises ») — les 2 devises
|
||||
canoniques. L'invariant CLI #8 (`commissions_gen.py:151-158`) exige les trois
|
||||
simultanément ; le gate re-dérive INDÉPENDAMMENT.
|
||||
- Piège : la seule vérification existante (`tests/`) teste des **FONCTIONS**
|
||||
finance/résolution, jamais la PROSE vs l'artefact. RENOMMER le champ (`devise`→
|
||||
`moneda`), changer son fieldtype (`Select`→`Data` : énumération PERDUE, montant en
|
||||
monnaie libre) ou altérer ses options (`USD/DOP`→`USD/EUR` : devise HORS #10 → le
|
||||
calcul commissionnerait dans une monnaie non prévue) laisse la prose périmée EN
|
||||
SILENCE. `devise_field` n'apparaissait NULLE PART dans `ci/` (grep = 0).
|
||||
- **État courant** : **aucune valeur périmée** — `plan.devise_field == "devise"` ==
|
||||
champ DocType `devise` (`Select`, options `[USD, DOP]`) == CLAUDE.md #10 ; la prose
|
||||
README recoupe tout exactement (anti-invention #6, rien à réécrire). Le défaut est
|
||||
la **surface ungated**.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « CRM/Commissions ·
|
||||
IDENTITÉ du CHAMP DEVISE » après le bloc rôles payés) : (0) **ANCRAGE** — les 2
|
||||
devises depuis le segment gras « USD + DOP » de CLAUDE.md #10 ; (a) **CROSS-ARTEFACT**
|
||||
— `plan.devise_field == "devise"` == fieldname du champ DocType, fieldtype `Select`,
|
||||
`options` (ORDRE inclus) == devises #10 ; (b) **PROSE** — README:54 nomme le champ
|
||||
`devise`, le dit `Select`, cite `USD/DOP` == #10 et tague `#10`. Un claim absent
|
||||
échoue AUSSI (traçabilité #6). Direction plan/DocType/#10 → README.
|
||||
|
||||
**7 morsures vérifiées** (4 côté README · 1 DocType · 1 CLAUDE.md · 1 plan) : README
|
||||
champ `devise→moneda` (prose périmée) · README `Select→Data` (type périmé) · README
|
||||
`USD/DOP→USD/EUR` (devises périmées) · README tag `· #10` retiré (claim INTROUVABLE) ·
|
||||
**DocType** `options USD→EUR` (hors #10, cross-artefact) · **CLAUDE.md #10**
|
||||
`USD+DOP→USD+EUR` (ancrage VIVANT · mord le cross-artefact) · **plan** `devise_field
|
||||
devise→moneda` (invariant #8) ; restauré = green : plan.devise_field == DocType ==
|
||||
#10 · prose == artefacts · exit 0. Working tree byte-restauré (`git checkout --`,
|
||||
**JAMAIS** `git clean`, interdit absolu) · **7 gates re-verts**
|
||||
(`check_readme_claims`/`check_ci_integrity`/`check_docs`/`guard_constraints`/
|
||||
`check_artifacts`/`check_regression`/`validate_json`).
|
||||
|
||||
- `ci/README.md` (clause de la table récap du pipeline + paragraphe détaillé
|
||||
« Quatrième surface du MÊME livrable `crm/commissions` · identité du champ devise »)
|
||||
mis à jour.
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; la création du
|
||||
champ `devise` du DocType `bench` reste côté VPS).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
## Session `20260801_183412` · Buffer S8 · Domaine Legal/CONFOTUR : les **deux champs du DocType `CONFOTUR Application` DÉRIVÉS du pipeline vente** — `dossier_vente` (Link → cible `workflow.document_type` = `OTO Dossier Vente`) et `estado` (Select → options = les états `confotur_*` du **workflow vente**), transcrits **en prose** dans la §« Cœur du livrable : cross-cohérence » (`legal/confotur/README.md:33-36`) — étaient **HORS de tout gate d'IDENTITÉ**. Les blocs CONFOTUR amont gatent le **compte** de champs/rôles, la cross-cohérence des **PERMISSIONS** (rôle→actions) et l'ancrage des **ENTITÉS** (`entite_porteuse`→CLAUDE.md §Entités) — mais **aucun** ne touchait `dossier_vente`/`estado`.
|
||||
|
||||
**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement
|
||||
close ; poursuite de la série anti-dérive (CLAUDE.md #6). **Même classe EXACTE** que
|
||||
l'ancrage `entite_porteuse` du MÊME README (session `20260801` entités CONFOTUR) —
|
||||
appliqué à la **surface distincte** des deux AUTRES champs data-derived du même
|
||||
DocType, jamais gatés hors comptes/permissions/entités.
|
||||
|
||||
**Dérive silencieuse fermée** :
|
||||
- `05_deliverables_mvp/legal/confotur/README.md:35-36` — bullet `dossier_vente`
|
||||
(Link) : « cible = `workflow.document_type` (= `OTO Dossier Vente`) » (valeur
|
||||
NOMMÉE en clair) · `:33-34` — bullet `estado` (Select) : « options = les états
|
||||
`confotur_*` du **workflow vente** … dérivées — jamais réécrites en dur ».
|
||||
- Source faisant autorité : `crm/workflow_vente/workflow_vente_spec.json` — l'**INPUT**
|
||||
que le builder confotur LIT (déclaré `workflow_source` dans le MANIFEST), d'où il
|
||||
tire `document_type` (= cible du Link) et les `update_value` d'états commençant par
|
||||
`confotur` (= options du Select). L'artefact `out/doctype_confotur_application.json`
|
||||
porte `dossier_vente.options`/`estado.options`, byte-gaté par `check_artifacts`.
|
||||
- Piège : la seule vérification existante (`tests/`) teste des **FONCTIONS** de
|
||||
génération, jamais la PROSE du README. RENOMMER le DocType porteur côté workflow
|
||||
(`OTO Dossier Vente`→`OTO Dossier de Vente`) ou AJOUTER/RENOMMER un état CONFOTUR
|
||||
régénère l'artefact confotur (`check_artifacts` reste VERT — il se reconstruit
|
||||
depuis le spec workflow) MAIS laisse la prose README « (= `OTO Dossier Vente`) »
|
||||
périmée EN SILENCE → l'agent ONAPI/Legal câblerait une cible de Link / un menu
|
||||
d'états que le pipeline ne porte plus (l'invention #6 interdit).
|
||||
- **État courant** : **aucune valeur périmée** — `dossier_vente` Link → « OTO
|
||||
Dossier Vente » == workflow `document_type` · `estado` options
|
||||
`[confotur_depose, confotur_approuve]` == états `confotur_*` du workflow (ordre
|
||||
pipeline) · la prose README recoupe l'artefact/le workflow exactement
|
||||
(anti-invention #6, rien à réécrire). Le défaut est la **surface ungated**.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Confotur champs
|
||||
dérivés » avant `sys.exit`) : (0) **traçabilité** — MANIFEST `workflow_source` cite
|
||||
bien `crm/workflow_vente/workflow_vente_spec.json` ; (a) **cross-artefact** —
|
||||
`dossier_vente.options` (Link) == workflow `document_type` ; (b) **prose** — la
|
||||
valeur README « (= `…`) » == workflow `document_type` ; (c) **cross-artefact** —
|
||||
`estado.options` == `update_value` des états `confotur_*` du workflow (ORDRE
|
||||
pipeline) ; (c′) toute option `estado` porte le préfixe `confotur` (le README
|
||||
l'affirme) ; (d) **prose** — le bullet `estado` DÉCLARE la dérivation (`confotur_*`
|
||||
+ workflow vente). Un claim absent échoue AUSSI (traçabilité #6). Direction
|
||||
README/artefact→workflow spec.
|
||||
|
||||
**6 morsures vérifiées** : README `dossier_vente` valeur `OTO Dossier Vente→OTO
|
||||
Dossier de Vente` (prose périmée, b) · README bullet `estado` supprimé (claim
|
||||
INTROUVABLE, d) · **artefact** `estado.options` +`brouillon` (options ≠ workflow, c
|
||||
ET hors-préfixe, c′) · **artefact** `dossier_vente.options→OTO Autre Doc` (Link ≠
|
||||
document_type, a) · **MANIFEST** `workflow_source` déréférencé (dérivation non
|
||||
déclarée, 0) · **spec workflow** `document_type→OTO Dossier de Vente` (mord
|
||||
SIMULTANÉMENT l'artefact a ET la prose b — l'ancre est vive) ; restauré = green :
|
||||
6 checks verts · exit 0. Working tree byte-restauré (`git checkout --`, **JAMAIS**
|
||||
`git clean`, interdit absolu) · **7 gates re-verts**
|
||||
(`check_readme_claims`/`check_ci_integrity`/`check_docs`/`guard_constraints`/
|
||||
`check_artifacts`/`check_regression`/`validate_json`).
|
||||
|
||||
- `ci/README.md` (clause de la table récap du pipeline + paragraphe détaillé
|
||||
« Champs `dossier_vente`/`estado` du DocType CONFOTUR dérivés du workflow vente »)
|
||||
mis à jour.
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; l'import réel
|
||||
du DocType `bench import-fixtures` reste côté VPS).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
## Session `20260801_173409` · Buffer S8 · Fiches agents/pied de page voix OTOIA : le **PIED DE PAGE CANONIQUE** « - Voix **Amélie QC** (**multilingual_v2**) pour toute interaction OTOIA » — répété à l'identique dans **11 des 13 fiches** `03_agents/*/AGENT.md` (toutes sauf `faisabilite`/`publiciste`, qui n'orchestrent pas OTOIA) — RECOPIE À LA MAIN les **trois** tokens de la contrainte **CLAUDE.md §Architecture cible** « Voix Amélie QC (multilingual_v2) » : prénom persona (`Amélie`), qualificatif de marque (`QC`), modèle de voix ElevenLabs (`multilingual_v2`). Cette surface était **HORS de tout gate**.
|
||||
|
||||
**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA). Roadmap fonctionnellement
|
||||
@@ -2011,3 +2192,422 @@ spec == rapport · exit 0. Working tree byte-restauré (`git checkout --`, **JAM
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; la confirmation
|
||||
des 4 open items réglementaires reste côté métier/VPS).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
## Session `20260801_190412` · Buffer S8 · Domaine Fiches agents/RBAC : l'**appartenance à `roles_allowed` du chat OTOIA** — QUI peut **invoquer** l'assistant OTOIA embarqué dans un portail — asserée EN PROSE par 2 fiches (`03_agents/rendu/AGENT.md:36` positive · `03_agents/mobile/AGENT.md:41` négative) était **HORS de tout gate d'appartenance**. C'est une **3e surface d'appartenance** DISTINCTE des deux déjà gatées : le **contrat** `rbac_50_roles.json` (attributs du rôle, blocs Fiches Faisabilité/Mobile) et le **`Has Role`** `workspace.json` (accès console, bloc Fiches Workspace). Le contrôle d'invocation du chat vit dans un 3e artefact — `frontend/chat_otoia/out/chat_mount.json` (`roles_allowed` par portail) — que le bloc Chat OTOIA amont ne gate que sur l'IDENTITÉ persona/capabilities + la COMPOSITION (5 mounts ⇔ 5 portails), AVEUGLE à QUEL rôle figure dans quelle `roles_allowed`.
|
||||
|
||||
**Tâche** : **Sprint 8 · buffer** (QA/DevOps — série anti-dérive CLAUDE.md #6, roadmap fonctionnellement close). **Même patron EXACT** que le bloc de membership `Has Role` (juste au-dessus dans `check_readme_claims.sh`) — appliqué à un **artefact + une sémantique DIFFÉRENTS** : accès console (`Has Role`) ≠ droit d'invocation du chat (`roles_allowed`).
|
||||
|
||||
**Dérive silencieuse fermée** :
|
||||
- `03_agents/rendu/AGENT.md:36` — POSITIVE : rôle « OTO Faisabilité Rendu 3D » ∈ `roles_allowed` du portail NOMMÉ `construction` (déclenchement des rendus assistés).
|
||||
- `03_agents/mobile/AGENT.md:41` — NÉGATIVE d'honnêteté (#6) : rôle « OTO Plateforme Mobile » (famille `plateforme`) dans AUCUNE `roles_allowed` (le chat ne couvre que les portails métier).
|
||||
- Source faisant autorité : `frontend/chat_otoia/out/chat_mount.json[].roles_allowed` (byte-gaté par `check_artifacts`), `erpnext_role_name` résolu du contrat par `id` (zéro duplication).
|
||||
- Piège : ajouter le rôle mobile à une `roles_allowed` = **SUR-EXPOSITION** (le dev mobile gagnerait le droit de piloter l'IA d'un portail métier — l'escalade même que #6 refuse) ; retirer le rôle rendu de la `roles_allowed` construction = perte du déclenchement assisté ; DÉPLACER le rôle vers un autre portail — laisse la fiche périmée EN SILENCE pendant que l'artefact dit autre chose. Aucune suite `tests/` (qui teste des FONCTIONS de montage, pas la prose d'une fiche) n'attrape ce « vert trompeur ».
|
||||
- **État courant** : aucune valeur périmée — rendu ∈ construction (et lui seul), mobile ∉ tout (anti-invention #6, rien à réécrire). Le défaut est la **surface ungated**.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Fiches agents · appartenance à `roles_allowed` du chat OTOIA » après le bloc Fiches Workspace) : index recomputé `erpnext_role_name → {portails autorisant}`, puis (a) **in_named_portail** (rendu) — le portail nommé est data-derived (== `portail` du rôle au contrat, **triple ancrage** : nommé == contrat == unique portail autorisant), membership == {ce portail}, prose citant « invoquer le chat OTOIA » ; (b) **not_in_any** (mobile) — rôle ABSENT de TOUTE `roles_allowed`. Portail nommé fantôme (absent des mounts) = échec (anti-typo). Claim absent = échec (traçabilité #6). Invariant vérifié en amont : chaque rôle ∈ ≤1 `roles_allowed`, == son `portail` contrat.
|
||||
|
||||
**6 morsures vérifiées** (3 côté artefact · 3 côté fiche/bord) : artefact retire rendu de `construction` (∉ aucun portail) · artefact ajoute mobile à `ventes` (SUR-EXPOSITION #6 VIOLÉE) · artefact ajoute AUSSI rendu à `compta` (droit élargi, fiche nomme 1 seul) · fiche rendu nomme le mauvais portail (`construction→compta`, INTROUVABLE) · fiche mobile retire le token `roles_allowed` (INTROUVABLE) ; restauré = green : rendu ∈ `{construction}` · mobile ∉ tout · exit 0. Working tree byte-restauré (`git checkout --`, **JAMAIS** `git clean`, interdit absolu) · **7 gates re-verts** (`validate_json`/`check_docs`/`guard_constraints`/`check_artifacts`/`check_ci_integrity`/`check_readme_claims`/`check_regression`).
|
||||
|
||||
- `ci/README.md` (recap `check-readme-claims` + paragraphe détaillé « 3e surface d'appartenance · `roles_allowed` du chat OTOIA ») mis à jour.
|
||||
- **Note memory** : la classe « agent-fiche-role-attrs-ungated » est désormais CLOSE (attributs contrat = Fiches Faisabilité/Mobile ; `Has Role` = Fiches Workspace ; `roles_allowed` chat = ce bloc). Candidat propre restant : la fiche `ifc_speckle:34` (attributs contrat déjà gatés) + la row acceptance S3 « viewer 3D » (déjà gatée QA).
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ; édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; le montage réel du chat reste côté runner/VPS).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
---
|
||||
|
||||
## Session 20260801_193414 · Sprint 8 · buffer · Gate d'IDENTITÉ des comptes de tests self-reportés par les README de MODULE (#6)
|
||||
|
||||
**Contexte** : la série anti-dérive Sprint-8 gate chaque surface data-derived contre sa source de vérité. Tous les blocs de **comptes de tests** existants (`ci/check_readme_claims.sh`) visent `03_agents/*/AGENT.md` : la cellule « Tests » des tables via `row_re`, les agrégats prose de `crm`/`erpnext_backend`, et les cinq claims per-suite « hors table » (seo/portails/chat_otoia/confotur/deploy_runbook). AUCUN ne touche le README du **MODULE** lui-même — pourtant `05_deliverables_mvp/<suite>/README.md` se re-déclare, en propre, le compte de SA suite à plusieurs endroits.
|
||||
|
||||
**Surface trouvée (ungated)** : **27 claims sur 16 READMEs de module**, sous trois formulations récurrentes :
|
||||
- commentaire de la commande unittest : `discover -s tests -v # N tests (…)` ;
|
||||
- bullet « Vérification en-repo » : `→ **N/N verts**` (six modules RBAC/faisabilité/CRM) ;
|
||||
- justification d'auto-score 4Big : `Validé statiquement en-repo (N tests verts + …)`.
|
||||
Ces nombres sont SAISIS À LA MAIN dans le doc d'entrée de chaque module et se périment dès qu'un `def test_` est ajouté/retiré : la source faisant autorité (`qa/regression/out/regression_plan.json` → `auth = plan.suites[path].test_methods`, recomptée à chaque build par découverte réelle, byte-gatée par `check_artifacts` + fraîcheur du run par `check_regression`) se met à jour tandis que la prose du README rote EN SILENCE — « vert trompeur » de la MÊME classe que la cellule « Tests » des fiches, mais sur la surface que `row_re` n'atteint pas (il ne lit le README de module QUE comme *cible de lien*). Aucune suite `tests/` (qui teste des FONCTIONS de génération, jamais la prose du README) n'attrape ce périmé.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « READMEs de MODULE · comptes de tests self-reportés » juste après le bloc per-suite « hors table », d'où `auth` est déjà en portée · zéro rechargement · #6) : table de config `MODULE_README_TESTS` = liste `(suite_path, [motifs ancrés])`. Pour chaque module : `auth[suite_path]` recomputé, chaque motif DOIT matcher (claim absent = régression #6) ET tous ses groupes capturés == `auth`. Le motif `**N/N verts**` capture DEUX groupes → les deux doivent valoir `auth` : un `25/24 verts` internement incohérent mord aussi. Chaque motif est ancré sur un contexte distinctif (`discover -s tests…# N tests`, `**N/N verts**`, `en-repo (N tests verts`, `test_confotur.py # N tests`…) pour viser LE compte de CETTE suite sans capter un autre nombre.
|
||||
- **Cinq modules volontairement HORS table** (`frontend/chat_otoia`, `mobile/app_config`, `publiciste`, `qa/acceptance`, `qa/audit_4big`) : leur README ne self-reporte AUCUN compte de tests (vérifié) — rien à ancrer ; un futur claim = l'ajouter à la table. Noté dans le commentaire du bloc.
|
||||
|
||||
**4 morsures vérifiées** (chaque README restauré depuis backup — **JAMAIS** `git checkout .`/`git clean`, interdits absolus) :
|
||||
1. drift commentaire (`faisabilite/generator` 16→17 tests) → ✗ « dit 17 MAIS source dit 16 » ;
|
||||
2. incohérence interne `N/N` (`rbac/apply_plan` `16/16`→`16/17 verts`) → ✗ « dit 16/17 MAIS source dit 16 » ;
|
||||
3. disparition de claim (`legal/confotur` prose auto-score « diffable, 44 tests (dont… » retirée) → ✗ « claim INTROUVABLE (attendu 44) » ;
|
||||
4. drift auto-score prose (`rbac/fixtures_gen` `(11 tests verts`→`(12 tests verts`) → ✗ « dit 12 MAIS source dit 11 ».
|
||||
Restauré = green : `rc=0`, **440 checks de base intacts (diff vide) + 27 nouveaux**. Gates sœurs re-verts : `check_ci_integrity` rc=0 · `check_docs` rc=0.
|
||||
|
||||
- En-tête du gate mis à jour (inventaire des surfaces couvertes : nouvelle entrée « comptes de tests self-reportés par MODULE »).
|
||||
- **Note memory** : classe « comptes de tests » — désormais couverte sur TROIS surfaces (cellule « Tests » des fiches via `row_re` · agrégats prose fiches · **README de module**, ce bloc) + les cinq claims per-suite hors table. Source unique = `auth` (plan.suites[].test_methods).
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ; édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
## Session `20260801_200419` · Buffer S8 · Fiche DevOps : gate du **COMPTE de suites gated** (« 22 suites gated ») — sémantique DISTINCTE des comptes de tests par suite
|
||||
|
||||
**Contexte** : la série anti-dérive Sprint-8 gate chaque surface data-derived contre
|
||||
sa source de vérité (CLAUDE.md #6, roadmap fonctionnellement close). Tous les blocs de
|
||||
comptes de tests de `ci/check_readme_claims.sh` recomputent un compte de **méthodes**
|
||||
`def test_` (`auth[path]` = `plan.suites[path].test_methods`) : la cellule « Tests »
|
||||
des tables via `row_re`, les agrégats prose crm/erpnext_backend, les 5 claims per-suite
|
||||
« hors table », et les READMEs de module. Une surface d'une **classe voisine mais
|
||||
distincte** restait ungated.
|
||||
|
||||
**Surface trouvée (ungated)** : `03_agents/devops/AGENT.md:24` — dans la table des
|
||||
livrables DevOps, la cellule « Tests » de la row du **pipeline CI**
|
||||
(`.gitea/workflows/ci.yml` + `ci/`) affiche « **22 suites gated** » : non pas un compte
|
||||
de tests, mais le **nombre de SUITES** que la CI passe au crible, soit
|
||||
`regression_plan.totals.suites`. Ce nombre est saisi À LA MAIN et **aucun** gate ne
|
||||
l'atteignait :
|
||||
- `row_re` échoue DEUX fois : l'ancre de lien est `ci.yml`/`ci/` (pas un module de
|
||||
`auth`) ET la cellule porte le suffixe « suites gated » (pas un entier nu) ;
|
||||
- l'agrégat de la fiche QA (`03_agents/qa/AGENT.md:35` « **22 suites gated · 564
|
||||
tests…** », gaté sur `regression_run`) est une **surface distincte** (autre fichier,
|
||||
autre motif). La MÊME valeur 22 vit donc à DEUX endroits — **un seul** était gaté.
|
||||
|
||||
**Dérive silencieuse fermée** : ajouter/retirer un module gated fait bouger
|
||||
`reg["suites"]` (recompté à chaque build par découverte réelle des suites, byte-gaté par
|
||||
`check_artifacts`) tandis que cette cellule DevOps rote EN SILENCE ⇒ la doc d'identité de
|
||||
l'agent DevOps affirmerait un **périmètre de couverture CI faux** — le « vert trompeur »
|
||||
que #6 interdit. Aucune suite `tests/` (FONCTIONS, pas la prose de fiche) ne l'attrape.
|
||||
**État courant** : valeur exacte (22 == 22), le défaut est la **surface ungated** (rien
|
||||
à réécrire · anti-invention #6).
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Fiche DevOps · COMPTE de
|
||||
suites gated » juste après le bloc per-suite « hors table », d'où `reg` est déjà en
|
||||
portée · zéro rechargement) : on RECOMPUTE depuis `reg["suites"]`
|
||||
(`regression_plan.totals.suites`, source unique) et on exige que la cellule l'écrive
|
||||
EXACTEMENT. Motif ANCRÉ sur le label de rôle « CI/CD Gitea Actions » de CETTE row →
|
||||
vise la bonne cellule. Claim absent = échec (traçabilité #6). **Cohérence croisée** :
|
||||
`reg["suites"]` == `regression_run` `len(suites)` (la source du compte de la fiche QA)
|
||||
⇒ une divergence plan↔run mord ici aussi.
|
||||
|
||||
**5 morsures vérifiées** (2 côté fiche · 2 côté artefact · 1 cross-cohérence) :
|
||||
1. cellule 22→23 → ✗ « dit 23 MAIS source dit 22 » ;
|
||||
2. claim retiré (`| gated |`) → ✗ « cellule « N suites gated » INTROUVABLE » ;
|
||||
3. `plan.totals.suites` 22→21 (fiche périmée) → ✗ divergence SOURCE + ✗ « dit 22 MAIS
|
||||
source dit 21 » ;
|
||||
4. `regression_run` privé d'une suite (plan↔run divergent) → ✗ « incohérence SOURCE :
|
||||
plan.suites=22 ≠ run len=21 » ;
|
||||
5. restauré = green (rc=0).
|
||||
Working tree byte-restauré via `git checkout -- <fichier précis>` (**JAMAIS**
|
||||
`git checkout .` / `git clean`, interdits absolus). **7 gates re-verts** :
|
||||
`validate_json` / `check_docs` / `guard_constraints` / `check_artifacts` /
|
||||
`check_ci_integrity` / `check_readme_claims` / `check_regression` (tous rc=0).
|
||||
|
||||
- En-tête du gate mis à jour (inventaire des surfaces couvertes : nouvelle entrée
|
||||
« COMPTE de suites gated de la fiche devops ») + `ci/README.md` (recap
|
||||
`check-readme-claims` : nouvelle clause du compte de suites gated).
|
||||
- **Note memory** : la classe « comptes de tests » (méthodes `def test_`) est couverte
|
||||
sur 4 surfaces + les 5 per-suite ; le COMPTE de **suites** (`plan.totals.suites`) est
|
||||
une classe voisine — désormais gaté sur ses DEUX surfaces (fiche QA agrégat déjà gaté ·
|
||||
fiche DevOps cellule, ce bloc).
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
## Session `20260801_203424` · Buffer S8 · Audit 5D (4e surface du même README) : gate de l'IDENTITÉ des **LIVRABLES AUDITÉS** — la liste des hand-off `out/*.json` que l'audit de second niveau LIT comme matière première
|
||||
|
||||
**Contexte** : la série anti-dérive Sprint-8 gate chaque surface data-derived contre
|
||||
sa source de vérité (CLAUDE.md #6, roadmap fonctionnellement close). Trois blocs
|
||||
`audit_5d` de `ci/check_readme_claims.sh` couvrent déjà le MÊME README : la **synthèse**
|
||||
(17 contrôles / 5 dimensions + ventilation verdict), l'**énumération des open items**
|
||||
(`control→dimension→propriétaire`) et l'**IDENTITÉ des 5 dimensions** (`id→titre→
|
||||
reference`). Une **4e surface** du même README restait ungated.
|
||||
|
||||
**Surface trouvée (ungated)** : `05_deliverables_mvp/qa/audit_5d/README.md:21-27` —
|
||||
section « ## Livrables audités (source = leur `out/`) » : une **liste de 5 puces** qui
|
||||
énumère À LA MAIN les hand-off `out/*.json` que l'auditeur charge réellement
|
||||
(`crm/workflow_vente/out/workflow.json` · `crm/dossier_vente/out/doctype_oto_dossier_
|
||||
vente.json` · `crm/commissions/out/commission_plan.json` · `fiscal/ecf_dgii/out/
|
||||
ecf_plan.json` · `legal/confotur/out/doctype_confotur_application.json`). Aucun bloc ne
|
||||
la touchait — vérifié : le seul `audited_artifacts` de tout le gate était dans un
|
||||
**commentaire** ; les 3 blocs audit_5d ne `load()` que `controls_total`/`totals`/
|
||||
`open_items`/`dimensions[]`.
|
||||
|
||||
**Source faisant autorité** : `qa/audit_5d/out/MANIFEST.json` clé `audited_artifacts`
|
||||
(byte-gaté par `check_artifacts` ⇒ reproduit à chaque build via
|
||||
`sorted(ARTIFACT_PATHS.values())` dans `audit_5d_gen.py:61` — le registre des hand-off
|
||||
que `qalib/artifacts.py::load_artifacts()` charge). Le README énumère les 5 chemins
|
||||
dans un **ORDRE DIFFÉRENT** du MANIFEST (workflow 2e ici, 3e au MANIFEST trié) ⇒
|
||||
comparaison par **ENSEMBLE**.
|
||||
|
||||
**Dérive silencieuse fermée** : brancher un nouveau livrable amont sur l'audit
|
||||
(nouvelle entrée `ARTIFACT_PATHS` → nouveau chemin dans `audited_artifacts`) ou en
|
||||
retirer un laisse la section README **périmée en silence** pendant que l'audit lit un
|
||||
autre jeu de fichiers ⇒ le lecteur/banquier croirait l'audit couvrir un **périmètre
|
||||
qu'il ne couvre plus** (l'invention #6 interdit), qu'aucune suite `tests/` (FONCTIONS
|
||||
de contrôle, pas la prose) n'attrape. **État courant** : les 5 puces == registre
|
||||
exactement (rien à réécrire · anti-invention #6), le défaut est la **surface ungated**.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Audit 5D (4e surface) ·
|
||||
IDENTITÉ des LIVRABLES AUDITÉS » entre le bloc des 5 dimensions et le bloc DevOps
|
||||
run-book) : on RECOMPUTE l'ensemble depuis `MANIFEST.audited_artifacts` (zéro
|
||||
duplication du registre) et on parse les puces `` - `<chemin>` `` de la section
|
||||
« ## Livrables audités » → égalité par ENSEMBLE exigée (absent ET fantôme mordus, via
|
||||
set-diff `absents`/`en trop`). **Cohérences croisées bonus** : **(a)** chaque chemin
|
||||
audité RÉSOUT un fichier commité sur disque (`os.path.exists` — le `_load` de l'auditeur
|
||||
lève `FileNotFoundError` sinon, audit à vide impossible) ; **(b)** |livrables audités|
|
||||
== |alias distincts d'artefacts référencés par les `controls[].artifacts` de
|
||||
`audit_spec.json`| (un artefact chargé mais jamais contrôlé, ou un alias contrôlé sans
|
||||
chemin de chargement → `KeyError` runtime, désynchronise). Section absente = échec
|
||||
(traçabilité #6). `import os` ajouté au bloc (non importé globalement).
|
||||
|
||||
**6 morsures vérifiées** (4 côté README · 2 côté source) :
|
||||
1. README puce `workflow.json` retirée → ✗ « absents=[…workflow.json] » ;
|
||||
2. README puce FANTÔME `demo/scenarios/out/run_sheet.json` ajoutée → ✗ « en trop
|
||||
(fantôme)=[…run_sheet.json] » ;
|
||||
3. README typo `commission_plan.json`→`commissions_plan.json` → ✗ absents ET fantôme ;
|
||||
4. README header de section renommé → ✗ « section « ## Livrables audités »
|
||||
INTROUVABLE/vide » ;
|
||||
5. `audit_spec.json` : alias `phantom_alias` ajouté à un contrôle → ✗ « désynchro
|
||||
registre/spec : 5 livrables MAIS 6 alias distincts contrôlés » (cross-anchor b) ;
|
||||
6. `MANIFEST.audited_artifacts` : un chemin pointé vers un fichier inexistant → ✗
|
||||
« INTROUVABLES sur disque » (cross-anchor a) + ✗ README périmé.
|
||||
Restauré = green : 5 puces == registre · 5 == 5 alias · 5 fichiers résolus · exit 0.
|
||||
Working tree byte-restauré via `git checkout -- <fichier précis>` (**JAMAIS**
|
||||
`git checkout .` / `git clean`, interdits absolus). **7 gates re-verts** :
|
||||
`validate_json` / `guard_constraints` / `check_docs` / `check_artifacts` /
|
||||
`check_regression` / `check_ci_integrity` / `check_readme_claims` (tous rc=0).
|
||||
|
||||
- `ci/README.md` mis à jour : clause de la row récap `check-readme-claims` (surface
|
||||
audit 5D étendue) + paragraphe détaillé « 4ᵉ surface du même README de l'audit 5D ».
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; l'exécution
|
||||
réelle de l'audit `bench` reste côté VPS).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
## Session `20260801_213444` · Buffer S8 · Fiche ONAPI/Legal : 2ᵉ SURFACE PROSE des ENTITÉS PORTEUSES
|
||||
|
||||
**Tâche** : **Sprint 8 · buffer** (DevOps CI/CD · QA — anti-dérive doc). Roadmap
|
||||
plancher atteinte ; on continue de fermer les surfaces prose data-derived encore
|
||||
ungatées. Cible de cette session : `03_agents/onapi_legal/AGENT.md:54`.
|
||||
|
||||
**Surface trouvée (ungatée)** : la fiche ONAPI/Legal décrit le champ Select
|
||||
`entite_porteuse` du DocType CONFOTUR par « **les 7 entités de CLAUDE.md #Entités
|
||||
(WAF · WA SRL · AC Arias Cuevas · Consortium ECR DR · Helios RD · Ploutos · 9060
|
||||
QC)** ». C'est un **COMPTE littéral** (« 7 ») **+** l'**énumération complète** — une
|
||||
**SECONDE surface prose** du même menu que le champ `entite_porteuse` offrira.
|
||||
|
||||
Le bloc « Confotur entités » déjà en place ancre le champ à CLAUDE.md §Entités et
|
||||
gate la ligne du README `legal/confotur` (surface **(e)**), mais ce motif est ancré
|
||||
sur `05_deliverables_mvp/legal/confotur/README.md` — **il ne vise pas `03_agents/`**.
|
||||
La fiche restait donc **hors de tout gate** : recompter la §Entités (aujourd'hui 7)
|
||||
n'atteignait jamais cette phrase.
|
||||
|
||||
**Dérive silencieuse fermée** : AJOUTER / RETIRER / RENOMMER une entité dans
|
||||
CLAUDE.md §Entités (source ancre) → régénérer propage le menu de l'artefact **et** la
|
||||
ligne du README confotur, MAIS la fiche onapi_legal se **périme en silence** — elle
|
||||
annoncerait « 7 entités » quand il y en a 8, ou nommerait `Ploutos` là où le menu ne
|
||||
l'offre plus. L'agent ERPNext Backend qui seede le DocType croirait le déroulant
|
||||
couvrir un périmètre d'entités qu'il ne couvre plus (l'**invention #6** interdit).
|
||||
Aucune suite `tests/` (FONCTIONS de build du DocType, pas la prose d'une fiche) ne
|
||||
l'attrape. **État courant** : la fiche == §Entités == menu artefact exactement
|
||||
(rien à réécrire · #6) ; le défaut était la surface **ungatée**.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Fiche ONAPI/Legal ·
|
||||
2ᵉ SURFACE PROSE des ENTITÉS PORTEUSES », inséré juste après le bloc « Confotur
|
||||
entités » et avant le bloc Chat OTOIA) : on **re-dérive** la liste depuis les tokens
|
||||
en gras de la section « ## Entités » de CLAUDE.md (source ancre · zéro duplication)
|
||||
et on exige de la fiche — **(a)** le COMPTE littéral (« les N entités ») ==
|
||||
|§Entités| ; **(b)** l'énumération == §Entités **ORDRE EXACT** (absent ET fantôme
|
||||
mordus par set-diff + comparaison ordonnée) ; **(c)** l'ancre « CLAUDE.md #Entités »
|
||||
citée sur la ligne ; **(d)** cross-check bonus — l'énumération == options
|
||||
`entite_porteuse` de l'artefact CONFOTUR byte-gaté (la fiche décrit **exactement** le
|
||||
menu que le DocType offrira). Ligne absente = échec (traçabilité #6).
|
||||
|
||||
**6 morsures vérifiées** (5 côté fiche · 1 côté artefact) :
|
||||
1. compte `7`→`8` → ✗ (a) « fiche dit 8 entités MAIS §Entités en porte 7 » ;
|
||||
2. `Ploutos` retiré de l'énumération → ✗ (b) « absent(s)=['Ploutos'] » ;
|
||||
3. entité FANTÔME `Fantom SA` ajoutée → ✗ (b) « fantôme(s)=['Fantom SA'] » ;
|
||||
4. réordre `WAF`↔`WA SRL` → ✗ (b) « ordre : fiche […] ≠ §Entités […] » ;
|
||||
5. ancre `#Entités` retirée de la ligne → ✗ (c) « ne cite pas CLAUDE.md #Entités » ;
|
||||
6. option `Ploutos`→`Ploutos SRL` dans l'artefact CONFOTUR → ✗ (d) « fiche […] ≠
|
||||
menu artefact […] → la fiche décrit un menu que le DocType n'offre pas ».
|
||||
Restauré = green : (a)(b)(c)(d) tous ✓ · exit 0. Working tree byte-restauré via
|
||||
`git checkout -- <fichier précis>` (**JAMAIS** `git checkout .` / `git clean`,
|
||||
interdits absolus). **7 gates re-verts** : `validate_json` / `guard_constraints` /
|
||||
`check_docs` / `check_artifacts` / `check_regression` / `check_ci_integrity` /
|
||||
`check_readme_claims` (tous rc=0).
|
||||
|
||||
- `ci/README.md` mis à jour : paragraphe « Entités porteuses du DocType CONFOTUR »
|
||||
étendu d'une clause « **2ᵉ surface prose (fiche ONAPI/Legal)** » (compte + liste +
|
||||
cross-check menu artefact).
|
||||
- **Note anti-stale** : la mémoire `agent-fiche-role-attrs-ungated` est désormais
|
||||
**périmée** — les attributs des fiches mobile / rendu / ifc_speckle (niveau, scope,
|
||||
modules, permissions, entité) SONT déjà gatés (blocs « Fiche Mobile » et « Fiches
|
||||
Faisabilité »). La classe résiduelle réelle est celle des LISTES data-derived
|
||||
transcrites en prose de fiche, dont cette session ferme l'occurrence entités.
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; l'application
|
||||
réelle du DocType par `bench` reste côté agent ERPNext Backend).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 8 · buffer · Chat OTOIA : gate d'IDENTITÉ de la LISTE D'INVARIANTS CI
|
||||
|
||||
**Contexte** : campagne anti-dérive documentaire (Sprint 8 · QA « regression
|
||||
tests exhaustifs »). Baseline des 7 gates vert avant intervention. Recherche
|
||||
(sous-agent Explore) de la prochaine classe de surface data-derived transcrite
|
||||
en prose mais NON gatée → candidat retenu : la section « ## Invariants (14) —
|
||||
garantis en CI » du README `frontend/chat_otoia` (README:53-60).
|
||||
|
||||
**Défaut** : cette section (a) ANNONCE un compte `(14)` puis (b) ÉNUMÈRE chaque
|
||||
invariant par son numéro entre parenthèses ((1)·(2)·…·(11, 13)·(12)·(14)). C'est
|
||||
la transcription À LA MAIN du REGISTRE réel des invariants du générateur — les
|
||||
marqueurs `# Invariant N ·` de `chat_otoia_gen.py` (source d'autorité : chaque
|
||||
marqueur = un contrôle appliqué par `validate()`). Le générateur se self-reporte
|
||||
en plus dans son message CLI (« schéma + 14 invariants verts. »), 3ᵉ transcription
|
||||
du même compte. Les blocs Chat OTOIA préexistants du gate couvrent des COMPTES
|
||||
d'artefact (blocks/mounts/portails) + l'IDENTITÉ persona/langues/endpoint —
|
||||
AUCUN ne touche la liste d'invariants : surface aveugle. Aucune suite `tests/` ne
|
||||
teste la PROSE ; le byte-gate d'artefact ignore le `.py` source ⇒ dérive
|
||||
silencieuse (ajouter `# Invariant 15` ⇒ README périmé sans échec).
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Chat OTOIA · IDENTITÉ
|
||||
de la LISTE D'INVARIANTS CI », inséré juste après le bloc « Chat OTOIA identité »
|
||||
et avant le bloc « Recette ») : on RECOMPUTE le registre depuis le SEUL
|
||||
registre-source (`re.findall(r"#\s*Invariant\s+(\d+)", chat_otoia_gen.py)`) et on
|
||||
exige — **(a)** le registre code est un ensemble CONTIGU `1..max` (lacune/doublon
|
||||
dans le ledger = défaut réel) ; **(b)** le message CLI « N invariants verts »
|
||||
== |registre| (self-report du `.py` cohérent avec son propre ledger) ; **(c)** le
|
||||
compte annoncé « ## Invariants (N) » du README == |registre| ; **(d)**
|
||||
l'ÉNUMÉRATION en prose (ints extraits des groupes `(...)` de la section, borne =
|
||||
prochain `## `) == registre EXACT — absent ET fantôme mordus par set-diff. Section
|
||||
ou compte absent = échec (disparition d'un chiffre = régression de traçabilité #6).
|
||||
|
||||
**5 morsures vérifiées** (working tree byte-restauré après chacune via
|
||||
`git checkout -- <fichier précis>`, JAMAIS `git checkout .` / `git clean`) :
|
||||
1. compte annoncé `14`→`15` → ✗ (c) « annonce (15) MAIS registre en compte 14 » ;
|
||||
2. numéro `(7)` retiré de la prose → ✗ (d) « absent(s)=[7] » ;
|
||||
3. numéro fantôme `20` ajouté à la prose → ✗ (d) « fantôme(s)=[20] » ;
|
||||
4. self-report CLI `14`→`13` → ✗ (b) « générateur dit 13 MAIS registre 14 » ;
|
||||
5. ledger code `# Invariant 7`→`77` → ✗ (a) « registre NON contigu […77] » +
|
||||
✗ (d) « absent(s)=[77] · fantôme(s)=[7] » (double morsure : ledger + prose).
|
||||
Restauré = green : (a)(b)(c)(d) tous ✓ (registre contigu 1..14 · 14 marqueurs).
|
||||
|
||||
- `ci/README.md` : la clause « montage Chat OTOIA » de la row `check-readme-claims`
|
||||
étendue de l'IDENTITÉ de la liste d'invariants (compte + énumération + self-report
|
||||
CLI recomputés du registre `# Invariant N`, set-diff + contiguïté).
|
||||
- **7 gates re-verts** : `validate_json` / `guard_constraints` / `check_docs` /
|
||||
`check_artifacts` / `check_regression` / `check_ci_integrity` /
|
||||
`check_readme_claims` (tous rc=0).
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 8 · buffer · SEO : gate d'IDENTITÉ du COMPTE d'INVARIANTS CI
|
||||
|
||||
**Contexte** : campagne anti-dérive documentaire (Sprint 8 · QA « regression
|
||||
tests exhaustifs »). Baseline des 7 gates vert avant intervention. Recherche
|
||||
(sous-agent Explore) de la prochaine surface data-derived transcrite en prose
|
||||
mais NON gatée. L'agent a proposé 5 candidates « N invariants » ; chat_otoia
|
||||
étant déjà fermée au commit précédent, candidat retenu = le générateur SEO
|
||||
(`05_deliverables_mvp/seo/seo_gen.py` + `seo/README.md`).
|
||||
|
||||
**Défaut** : la source d'autorité du COMPTE D'INVARIANTS de validation SEO n'est
|
||||
PAS un artefact JSON (invisible à `check_artifacts` qui byte-gate les `out/*.json`)
|
||||
mais le LEDGER NUMÉROTÉ de la fonction `_validate` — les marqueurs `# N ·`
|
||||
(`# 1 ·` … `# 15 ·`) qui ouvrent chaque contrôle appliqué, analogue du registre
|
||||
`# Invariant N` de Chat OTOIA. Ce compte est transcrit À LA MAIN à SEPT endroits
|
||||
qu'AUCUN gate ne couvrait : le titre `## Garanties (N invariants)` du README + son
|
||||
bloc de commande (« schéma de sortie + N invariants »), plus CINQ chaînes du `.py`
|
||||
lui-même (docstring module L19, docstring `_validate` L57, aide `build` L24, message
|
||||
de succès CLI « N invariants verts. » L285, aide du sous-parseur `validate` L298).
|
||||
Le bloc SEO préexistant du gate ne couvre que des COMPTES d'ARTEFACT (mots-clés
|
||||
total + par langue, recomputés de MANIFEST.counts) — classe distincte, surface
|
||||
aveugle. Ajouter un contrôle `# 16 ·` à `_validate` ⇒ README + self-reports du `.py`
|
||||
mentent en silence pendant que la CI applique 16 contrôles (« vert trompeur » #6)
|
||||
— ni les suites `tests/` (FONCTIONS, jamais la prose) ni le byte-gate d'artefact
|
||||
(aveugle au `.py` source) ne l'attrapent.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « SEO · IDENTITÉ du
|
||||
COMPTE d'INVARIANTS CI », inséré juste après le bloc SEO mots-clés et avant le bloc
|
||||
CRM pipeline vente) : on RECOMPUTE le ledger depuis le SEUL registre-source (les
|
||||
`# N ·` de `_validate`, extraits du corps de la fonction borné au prochain `def `)
|
||||
et on exige — **(a)** le ledger est CONTIGU 1..N (comparaison de la LISTE triée à
|
||||
`range(1,N+1)`, ⇒ mord lacune ET doublon) ; **(b)** TOUTE mention « N invariants »
|
||||
du `.py` == |ledger| (set des self-reports == {N}) ; **(c)** le compte annoncé
|
||||
« ## Garanties (N invariants) » du README == |ledger| ; **(d)** TOUTE mention
|
||||
« N invariants » du README == |ledger|. La prose du README RÉSUME les contrôles
|
||||
(11 puces `·`-séparées ≠ énumération numérotée 1:1) ⇒ on gate le COMPTE partout où
|
||||
il apparaît, PAS un set-diff de la liste. Section/compte absent = échec (#6).
|
||||
|
||||
**5 morsures vérifiées** (working tree byte-restauré après chacune via
|
||||
`git checkout -- <fichier précis>`, JAMAIS `git checkout .` / `git clean`) :
|
||||
1. README titre `15`→`16` → ✗ (c) « annonce (16) MAIS ledger 15 » + ✗ (d) « [15, 16] » ;
|
||||
2. README commande `15`→`14` → ✗ (d) « mention(s) [14, 15] MAIS ledger 15 » ;
|
||||
3. `.py` succès CLI `15`→`13` → ✗ (b) « self-report(s) [13, 15] MAIS ledger 15 » ;
|
||||
4. ledger `# 7 ·`→`# 77 ·` (lacune) → ✗ (a) « NON contigu […6, 77, 8…] » ;
|
||||
5. ledger `# 8 ·`→`# 7 ·` (doublon) → ✗ (a) « NON contigu […7, 7, 9…] »
|
||||
(la comparaison sur LISTE — non set — attrape le doublon, angle que
|
||||
l'approche set de chat_otoia aurait raté).
|
||||
Restauré = green : (a)(b)(c)(d) tous ✓ (ledger contigu 1..15 · 15 marqueurs).
|
||||
|
||||
- `ci/README.md` : la clause SEO de la row `check-readme-claims` étendue du COMPTE
|
||||
d'INVARIANTS CI (annonce + toutes mentions README + 5 self-reports `.py`
|
||||
recomputés du ledger `# N ·` de `_validate`, contiguïté 1..N).
|
||||
- **7 gates re-verts** : `guard_constraints` / `validate_json` / `check_docs` /
|
||||
`check_artifacts` / `check_regression` / `check_ci_integrity` /
|
||||
`check_readme_claims` (tous rc=0).
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 8 · buffer · RBAC SPEC §5 : gate de la LIGNE D'AUTO-CONTRÔLE du « résumé »
|
||||
|
||||
**Contexte** : suite de la campagne anti-dérive documentaire (Sprint 8 · QA
|
||||
« regression tests exhaustifs »). Baseline des 7 gates verte avant intervention.
|
||||
Recherche (sous-agent Explore) de la prochaine surface data-derived transcrite en
|
||||
prose mais NON gatée — la plupart des COMPTES d'artefact sont déjà fermés. Le
|
||||
candidat retenu est une surface interne de `rbac/RBAC_50_ROLES_SPEC.md` : le §5.
|
||||
|
||||
**Défaut** : la SPEC RBAC a DÉJÀ trois gates (§3 ventilation par portail · §4
|
||||
colonne Entité · §7 run-book), mais son §5 « Les N rôles (résumé) » restait
|
||||
AVEUGLE. Ce §5 restitue le total de rôles sur TROIS surfaces prose : (i) le
|
||||
compte-titre « ## 5. Les 50 rôles (résumé) », (ii) NEUF comptes parenthétiques de
|
||||
regroupement (`**Ventes (8)**` …) et (iii) une LIGNE d'auto-contrôle arithmétique
|
||||
`_(6+8+4+7+5+8+4+2+6 = 50.)_`. AJOUTER un rôle au contrat (|rôles| 50→51) laisse le
|
||||
titre, la somme et le « = 50 » PÉRIMÉS en silence ; ÉDITER `Ventes (8)→(7)` sans
|
||||
toucher la ligne casse la cohérence titre↔somme — aucune suite `tests/` (FONCTIONS
|
||||
RBAC, jamais la prose de la SPEC) ni le byte-gate d'artefact ne l'attrape.
|
||||
|
||||
**Subtilité (memory `rbac-spec-portail-count-gate` : « §5 by-family is
|
||||
editorial »)** : les 9 regroupements sont ÉDITORIAUX — « Direction / Gouvernance »
|
||||
agrège des rôles de familles direction+finance, ≠ la colonne `famille` du contrat.
|
||||
On NE mappe donc PAS groupe↔famille (ce serait une dérive fausse). On gate
|
||||
UNIQUEMENT ce qui est data-derived et vérifiable SANS ce mapping.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « 1septies) §5 », inséré après
|
||||
le bloc §4 et avant le bloc apply_plan) : on RECOMPUTE N = |rôles| du contrat
|
||||
(`rbac_50_roles.json`, byte-gaté par check_artifacts) et on exige — **(a)** le
|
||||
compte-titre « ## 5. Les N rôles » == N ; **(b)** les 9 comptes de regroupement ==
|
||||
EXACTEMENT, dans l'ordre, les termes de la ligne arithmétique (cohérence interne
|
||||
prose↔somme, comparaison de LISTES ordonnées) ; **(c)** l'arithmétique est VRAIE
|
||||
(Σ termes == membre droit) ; **(d)** le membre droit == N. Section/titre/ligne/
|
||||
comptes absents = échec (#6).
|
||||
|
||||
**5 morsures vérifiées** (working tree byte-restauré après chacune via
|
||||
`git checkout -- <fichier précis>`, JAMAIS `git checkout .` / `git clean` ;
|
||||
gate rc=1 sous mutation, rc=0 restauré) :
|
||||
1. titre `50`→`51` → ✗ (a) « titre dit 51 rôles MAIS le contrat en compte 50 » ;
|
||||
2. regroupement `Ventes (8)`→`(9)` → ✗ (b) « [6, 9, 4…] ≠ termes [6, 8, 4…] » ;
|
||||
3. ligne `= 50.`→`= 49.` → ✗ (c) « écrit = 49 MAIS Σ = 50 » + ✗ (d) « totalise
|
||||
49 MAIS le contrat compte 50 » (double morsure) ;
|
||||
4. terme `6+8+…`→`7+8+…` → ✗ (b) « termes [7, 8…] ≠ [6, 8…] » + ✗ (c) « = 50
|
||||
MAIS Σ = 51 » (double morsure) ;
|
||||
5. suppression de la ligne d'auto-contrôle → ✗ « ligne … INTROUVABLE » (absence).
|
||||
Restauré = green : (a)(b)(c)(d) tous ✓ (titre 50 · 9 comptes == 6+8+4+7+5+8+4+2+6
|
||||
· Σ=50 · membre droit 50 == |rôles|).
|
||||
|
||||
- `ci/README.md` : la row `check-readme-claims` étendue de la clause §5 (compte-
|
||||
titre + 9 comptes de regroupement éditoriaux + ligne d'auto-contrôle, sans
|
||||
mapping groupe↔famille).
|
||||
- **7 gates re-verts** : `guard_constraints` / `validate_json` / `check_docs` /
|
||||
`check_artifacts` / `check_regression` / `check_ci_integrity` /
|
||||
`check_readme_claims` (tous rc=0).
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,205 @@
|
||||
# Activity Log · 2026-08-04 · Claude Code DTP Worker
|
||||
|
||||
## Session 221055 · Passe de vérification d'intégrité inter-artefacts — 5 surfaces (dont 3 NEUVES au compte courant 24/32) → CLEAN 0 édition prod · ouverture canal daily_reports 08-04
|
||||
|
||||
**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **32 PASS · 0 FAIL · 0 SKIP**,
|
||||
arbre `git status` propre. Roadmap `ROADMAP_8_WEEKS_OR_LESS.md` : les 8 sprints ont leur
|
||||
module livré, gaté et vert (couverture CI = 32 jobs). Le canal `daily_reports/2026-08-03.md`
|
||||
est courant (clos à l'addendum `180728→200738`). Les 8 directives Michel sont closes
|
||||
(commit `88bd025`). Les surfaces de dérive usuelles (docstring-vs-CODE, comptes gatés,
|
||||
inventaire-vs-FS, constantes de marque #4, noms d'entités/projets, directive↔spec) sont
|
||||
**saturées** — les re-scanner serait redondant (#5). J'ai donc ouvert une **passe
|
||||
d'intégrité inter-artefacts** ciblant en priorité les surfaces qui ont pu **dériver avec le
|
||||
passage récent 22→24 modules** (`100714`, la seule dérive réelle des dernières ~48 h), et
|
||||
des surfaces de **correctness numérique** jamais explicitement caractérisées.
|
||||
|
||||
**5 surfaces auditées — toutes CLEAN. 0 fichier de production/gate/doc modifié.**
|
||||
|
||||
1. **Docstring-vs-CODE / comptes de sortie (agent Explore, ~25 générateurs).** Chaque
|
||||
`*_gen.py` : docstring ↔ README ↔ fichiers réellement écrits (`_write_json`/`json.dump`
|
||||
dans `cmd_build`). **0 mismatch présent-tense** — chaque module écrit exactement les
|
||||
fichiers annoncés (seo 4, financement 7, mobile 5, pie 2, demo 3 dont `run_sheet.md`,
|
||||
etc.). Classe [[docstring-vs-code-drift]] (récurrente) re-confirmée CLEAN à date.
|
||||
|
||||
2. **Correctness numérique `commissions` (NEUF).** Le barème garde délibérément
|
||||
`taux_pct: null` (anti-invention #6 : aucun taux dans CLAUDE.md), et `commlib/finance.py`
|
||||
calcule `commission = base × taux` = `None` tant qu'un opérande manque — **jamais
|
||||
0-inventé**. Donc **aucune réconciliation numérique possible ne peut dériver** (pas de
|
||||
somme/produit matérialisé). Surface caractérisée : *null-by-design, non-candidate à un
|
||||
audit de réconciliation*.
|
||||
|
||||
3. **Réconciliation de compte inter-artefacts au point courant (NEUF au 24).** Tous
|
||||
concordants : `quality_report.json` verdict **PASS · 24 modules**, bloc `coverage`
|
||||
**`ci_modules_count == registry_modules_count == 24`** (`ok:true`, `missing_*`/`not_in_gate`
|
||||
vides, self-exclu `qa/audit_4big`) · `regression_run.json.totals` = **24 suites · 624 ran /
|
||||
624 passed · 0/0/0** · `acceptance_matrix.json` = **15/15 rows `status=in_repo`**, verdict
|
||||
`true` · `gate.needs` = **32 jobs**. La table de clôture `daily_reports/2026-08-03.md`
|
||||
(addendum `100714`) porte déjà ce 24/624/32 réconcilié — **pas de dérive résiduelle**.
|
||||
Classe qui avait eu une dérive RÉELLE (22→24) → re-vérifiée close.
|
||||
|
||||
4. **Résolution `ci_job` recette → `gate.needs` au point courant (NEUF au 24/32).** Les
|
||||
**36** `matrix[].modules[].ci_job` de la matrice de recette **résolvent tous** vers un job
|
||||
réel de `gate.needs` — **0 orphelin**. Les **2** modules ajoutés au passage 22→24
|
||||
(`crm/financement_bancaire`, `pie/manifest`) sont **bien présents** dans la matrice (grep
|
||||
`financement`/`pie`/`manifest` = hit). Les 8 jobs de `gate.needs` **jamais** cités par la
|
||||
recette sont exactement les **contrôles d'infrastructure CI** (`check-artifacts`,
|
||||
`check-docs`, `check-readme-claims`, `check-regression`, `check-ci-integrity`,
|
||||
`constraints-guard`, `validate-json`) + `qa-acceptance-tests` (auto-exclusion, comme le
|
||||
`self_module_excluded` d'audit_4big) — **absence par design**, pas un trou de couverture.
|
||||
Prolonge la sonde `000623` (qui l'avait faite à 22/30) au **compte courant 24/32**.
|
||||
|
||||
5. **Couverture des jobs par `ci/README.md` (NEUF).** La table §1 est — **par design** —
|
||||
une table des **7 gates statiques `ci/*.sh`** (catégories d'infrastructure), *pas* une
|
||||
énumération des 24 suites de test par module. Le README l'assume explicitement (§1
|
||||
« Rejouer le gate », l.56 : la liste des suites est « **dérivée de `gate.needs` et imprimée
|
||||
à l'exécution** ») et `check_ci_integrity.sh` INV-A **gate dynamiquement** que
|
||||
`gate.needs == { tous les jobs non-manuels }`. Les quelques mentions de suites de module
|
||||
dans le §1/§2 (`seo-tests`, `fiscal-ecf-tests`, `rbac-tests`…) sont **incidentes** (prose
|
||||
d'exemple), pas une liste censée être exhaustive → **pas de gap**. Surface caractérisée
|
||||
pour éviter tout futur faux-positif « ci/README ne liste pas les 24 jobs ».
|
||||
|
||||
**Aucune tâche fonctionnelle in-repo restante.** Balayage des 8 `DIRECTIVE_*.md` + prose
|
||||
des livrables : tous les marqueurs « à créer / TODO » restants pointent des **DocTypes
|
||||
ERPNext côté VPS** (hors périmètre worker #8) ; les seuls livrables-fichiers versionnés que
|
||||
la directive Mobile classait in-scope (`mobile-build.yml`) sont **déjà faits** (`.gitea/
|
||||
workflows/` = `ci.yml` + `mobile-build.yml`). Le module Publiciste (GAP_ANALYSIS l.50
|
||||
« New/à créer ») **existe** (`publiciste.py`, importé par `commissions_gen`). La roadmap est
|
||||
**intégralement livrée et gatée** — inventer une modification fonctionnelle sur un système
|
||||
complet violerait #5 (doublon) / #6 (invention). La valeur honnête restante est la
|
||||
**vérification d'intégrité** + la **currency du canal stakeholder**.
|
||||
|
||||
**Reporting canal 2 (contrat « Rapports quotidiens »).** Ouverture de
|
||||
`daily_reports/2026-08-04.md` (nouveau jour ; le `2026-08-03` clos à `200738`). Snapshot
|
||||
launch-readiness **100 % sourcé** par recompute `python3` indépendant contre les artefacts
|
||||
commités (anti-invention #6) — mêmes 4 dimensions que la clôture 08-03, inchangées car
|
||||
audit pur : 24/24 qualité · 24 suites/624 régression · 15/15 recette · 32 PASS local.
|
||||
|
||||
**Vérifications.** `./run_ci.sh` → **32 PASS · 0 FAIL · 0 SKIP** (inchangé — audit +
|
||||
reporting seuls, aucune logique de prod/gate touchée). Recompute indépendant concordant sur
|
||||
les 4 dimensions. Arbre propre avant édition. **Zéro nouveau module · zéro gate ajouté
|
||||
(#5) · zéro chiffre inventé (#6).** Aucune commande touchant au VPS (#8) · aucun
|
||||
`git clean` <!-- ci-allow : mention de l'interdit absolu, jamais exécuté -->.
|
||||
|
||||
---
|
||||
|
||||
## Session 224101 · Sonde de dérive NON gatée (4 dimensions neuves) → CLEAN + caractérisation d'un faux-positif contact d'escalade · 0 édition prod
|
||||
|
||||
**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **32 PASS · 0 FAIL · 0 SKIP**,
|
||||
arbre `git status` propre. Les 8 sprints roadmap sont livrés/gatés/verts ; le reste
|
||||
(déploiement prod, builds EAS, dépôts ONAPI) est hors périmètre worker (#8). La session
|
||||
précédente `221055` (~90 min avant) avait déjà passé une audit inter-artefacts complète
|
||||
(5 surfaces, CLEAN) et ouvert le canal `daily_reports/2026-08-04`. Re-jouer les surfaces
|
||||
usuelles serait redondant (#5). J'ai donc lancé un agent Explore sur **4 dimensions de
|
||||
dérive non-gatée JAMAIS explicitement caractérisées** — orthogonales aux classes saturées
|
||||
(docstring-vs-code, comptes gatés, constantes #4, noms d'entités/projets, directive↔spec,
|
||||
ci_job→gate.needs, bijection module↔artefact).
|
||||
|
||||
**4 dimensions sondées — toutes CLEAN. 0 fichier de production/gate/doc modifié.**
|
||||
|
||||
1. **Cross-refs entre fiches AGENT.md** : chaque agent cité (bim, crm, publiciste, qa,
|
||||
rendu, seo, faisabilité…) existe bien sous `03_agents/` → **0 nom pendant**.
|
||||
2. **Dates/session-ids dans READMEs & specs** : aucune date dans le **futur** de
|
||||
2026-08-04 ; specs (`CHOISIR_MON_UNITE`) citent 07-28/08-03 (passé). → CLEAN.
|
||||
3. **Contacts d'escalade** (CLAUDE.md · roadmap L90-91 · fiches · assets) :
|
||||
**téléphone `+18296296385` = 15 surfaces byte-identiques** (13 fiches + roadmap L90
|
||||
+ `AGENTS_EXISTING_ASSETS` L106 + `GAP_ANALYSIS`). **Email `mroy@worldactivitiesrd.com`
|
||||
= 1 occurrence** (roadmap L91, canal décision-archi). L'agent l'avait signalé comme
|
||||
« référence pendante » (occurrence unique). **Faux-positif confirmé** : occurrence
|
||||
unique ≠ contradiction ; l'email est à sa place (doc qui porte l'escalade) et le domaine
|
||||
`worldactivitiesrd.com` correspond à l'entité « World Activities RD » (CLAUDE.md §Entités,
|
||||
WA SRL). → **NON drift, aucune correction.** Caractérisé en mémoire
|
||||
(`escalation-contact-not-drift`) pour pré-empter le re-flag (contact = prose non gatée).
|
||||
4. **Specs ↔ répertoires deliverables** : chaque module cité en spec (faisabilite/bancable,
|
||||
legal/confotur, publiciste, mobile/app_config, pie/manifest, qa/acceptance,
|
||||
crm/workflow_vente, seo, rbac…) a son répertoire sous `05_deliverables_mvp/`. → CLEAN.
|
||||
|
||||
**Décision anti-invention (#6).** Aucune édition de production n'est justifiée : la seule
|
||||
« anomalie » remontée était un faux-positif. Fabriquer un correctif (retirer/propager
|
||||
l'email) violerait #6 et introduirait une vraie régression. Session **signal-only** +
|
||||
1 note mémoire anti-redondance.
|
||||
|
||||
**Vérifications.** `./run_ci.sh` → **32 PASS · 0 FAIL · 0 SKIP** (inchangé — sonde read-only,
|
||||
aucune logique prod/gate touchée). **0 nouveau module · 0 gate ajouté (#5) · 0 chiffre
|
||||
inventé (#6).** Aucune commande VPS (#8) · aucun `git clean` <!-- ci-allow : mention de l'interdit, jamais exécuté -->.
|
||||
|
||||
---
|
||||
|
||||
## Session 231101 · Attestation indépendante des chiffres stakeholder (recompute vs artefacts) → CLEAN · 0 édition prod
|
||||
|
||||
**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **32 PASS · 0 FAIL · 0 SKIP**,
|
||||
arbre `git status` propre. Roadmap intégralement livrée/gatée (8 sprints) ; le reste est
|
||||
hors périmètre worker (#8). Deux sessions du jour (`221055`, `224101`) avaient déjà passé
|
||||
l'audit inter-artefacts + une sonde de dérive non-gatée → CLEAN. Re-sonder serait redondant
|
||||
(#5). J'ai donc changé d'angle : **attester** que les 4 chiffres launch-readiness publiés
|
||||
au canal stakeholder ne sont pas de l'artefact-echo mais **rechargent à l'identique depuis
|
||||
les artefacts commités**, via recompute `python3` indépendant (charge le JSON, recompte —
|
||||
ne copie pas la table · #6).
|
||||
|
||||
**Vérification menée — 4 dimensions rechargées, 0 écart.**
|
||||
|
||||
1. **Qualité 4Big** (`qa/audit_4big/out/quality_report.json`) : 24 modules, **tous score
|
||||
== 100**, `coverage.ok=true`, `ci_modules_count == registry_modules_count == 24`,
|
||||
0 manquant CI/registry. → concorde avec « 24/24 à 100/100 ».
|
||||
2. **Régression** (`qa/regression/out/regression_run.json.totals`) : **24 suites · 624 ran
|
||||
· 624 passed · 0 fail/err/skip**, verdict `PASS`. → concorde avec « 24 suites/624 ».
|
||||
3. **Recette** (`qa/acceptance/out/acceptance_matrix.json`) : matrice = **15 lignes toutes
|
||||
`status=in_repo`**, verdict `true`. La chaîne `ci_job` apparaît **36×** — nichée dans
|
||||
`matrix[].modules[]` (PAS au niveau ligne : un premier parse naïf au niveau ligne rend
|
||||
0, faux-négatif écarté). → concorde avec « 15/15 · 36 ci_job → 0 orphelin »
|
||||
(l'absence d'orphelin restant prouvée par `check_ci_integrity`).
|
||||
4. **Miroir local** : `./run_ci.sh` → **32 PASS · 0 FAIL · 0 SKIP**.
|
||||
|
||||
**Confirmation de couverture (pas d'édition, attestation).** L'artefact d'EXÉCUTION
|
||||
`regression_run.json` est **exclu** de `check_artifacts` (qui ne rejoue que `build`) mais
|
||||
**protégé** par son gate dédié `ci/check_regression.sh` — il régénère `run` frais vers un
|
||||
tmp et exige l'identité byte-for-byte, verdict PASS. Les totaux 624 ne peuvent donc pas se
|
||||
périmer en silence si une suite grandit/rétrécit. Surface confirmée gatée, pas un trou.
|
||||
|
||||
**Décision anti-invention (#6).** Aucune anomalie → aucune édition de production justifiée.
|
||||
Session **attestation-only** : 2e addendum au `daily_reports/2026-08-04` (recompute
|
||||
stakeholder) + cette entrée. Pas de note mémoire neuve (le fait — chiffres stakeholder
|
||||
sourcés — est déjà couvert par `two-logging-channels` / `doc-numeric-claims-gate`).
|
||||
|
||||
**Vérifications.** `./run_ci.sh` → **32 PASS · 0 FAIL · 0 SKIP** (inchangé — recompute
|
||||
read-only, aucune logique prod/gate touchée). **0 nouveau module · 0 gate ajouté (#5) ·
|
||||
0 chiffre inventé (#6).** Aucune commande VPS (#8) · aucun `git clean` <!-- ci-allow : mention de l'interdit, jamais exécuté -->.
|
||||
|
||||
---
|
||||
|
||||
## Session 234104 · Durcissement `mobile-build.yml` — gating d'activation robuste (secrets-context hors `if:` job) → 1 édition CI in-scope
|
||||
|
||||
**Contexte + choix de tâche.** `./run_ci.sh` au démarrage : **32 PASS · 0 FAIL · 0 SKIP**,
|
||||
arbre propre. Roadmap intégralement livrée/gatée ; les 3 sessions du jour (`221055`,
|
||||
`224101`, `231101`) avaient déjà passé audit inter-artefacts + sonde de dérive + attestation
|
||||
stakeholder → CLEAN. Re-sonder une 4e surface d'audit serait redondant (#5). J'ai donc
|
||||
changé de registre : au lieu d'un audit read-only de plus, **vérifier la correctness d'un
|
||||
artefact CI in-scope à impact futur réel** — le workflow `mobile-build.yml` que le canal
|
||||
stakeholder désigne comme « prochaine tâche » (s'active quand Michel fournit `EAS_TOKEN`).
|
||||
Un agent Explore a d'abord confirmé **0 bug de logique** dans les générateurs Python (classe
|
||||
correctness, pas comptes) — cohérent avec la saturation. Le défaut réel était dans le YAML CI.
|
||||
|
||||
**Défaut trouvé (latent, silencieux).** Les deux jobs `build-ios` / `build-android`
|
||||
gataient sur `if: ${{ secrets.EAS_TOKEN != '' && … }}` **au niveau job**. Or le `secrets`
|
||||
context n'est **pas** dans la table d'availability de `jobs.<id>.if` de GitHub Actions
|
||||
(seuls `github`/`needs`/`vars`/`inputs`/`matrix` + status functions y sont) ; le moteur
|
||||
`act` de Gitea peut l'exposer ou non → comportement **ambigu selon la plateforme**. Risque
|
||||
concret : le jour où Michel ajoute un `EAS_TOKEN` valide, les builds pourraient rester
|
||||
**SKIP en silence** (`secrets.EAS_TOKEN` évalué vide en contexte `if:` → condition toujours
|
||||
fausse). Exactement le « marche dans ma tête, casse le jour où on en a besoin ».
|
||||
|
||||
**Correctif (pattern canonique preflight-output, portable GitHub ET Gitea/act).** Le
|
||||
secret est désormais lu **dans un step** de `preflight` (où `secrets` est toujours
|
||||
disponible), converti en `outputs.has_token` / `has_repo` (booléens via `$GITHUB_OUTPUT`),
|
||||
et les jobs de build gatent sur `needs.preflight.outputs.*` — contexte, lui, **disponible
|
||||
en `if:` partout**. **Contrat observable inchangé** : `preflight` reste vert et rapporte
|
||||
l'état des secrets ; les builds restent SKIP tant que le token est absent (prose
|
||||
`ci/README.md` §Second workflow toujours exacte, pas de dérive doc). Le durcissement porte
|
||||
seulement sur *comment* le SKIP est décidé.
|
||||
|
||||
**Vérifications.** `python3 -c yaml.safe_load` → **YAML valide** ; `preflight.outputs`
|
||||
présents ; `build-ios`/`build-android` gardent `needs: preflight` ; **0** `secrets.` restant
|
||||
dans un `if:` de job ; 4 écritures `$GITHUB_OUTPUT`. Aucun gate ne linte la structure de
|
||||
`mobile-build.yml` (`check-ci-integrity` ne verrouille QUE `ci.yml`) → pas de gate cassé.
|
||||
`./run_ci.sh` → **32 PASS · 0 FAIL · 0 SKIP** (inchangé — le workflow mobile est hors
|
||||
`gate.needs`). **1 fichier CI édité (in-scope, le worker versionne la CONFIG CI/CD) · 0
|
||||
nouveau module (#5) · 0 chiffre inventé (#6) · aucune commande VPS (#8) · aucun `git clean`** <!-- ci-allow : mention de l'interdit, jamais exécuté -->.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,308 @@
|
||||
# Activity Log · 2026-08-11 · Claude Code DTP Worker
|
||||
|
||||
## Session `20260811_015744` — FIX · CI RED → VERT · 2 régressions induites par les commits root V18 (archivage V10/V11 + 4 docs REFERENCE root-owned mode 600)
|
||||
|
||||
**Constat au démarrage.** `./run_ci.sh` = **31 PASS · 2 FAIL · 0 SKIP** (régression vs la baseline 33/0/0 du 2026-08-06). Deux gates rouges : `check-docs` et `check-readme-claims`. Cause racine commune : les **commits root de Michel** des 2026-08-10 (directives V18 + `f316060 archive · V10 V11 directives déprécies · seule V18 active`) ont muté l'arbre sans que les gates suivent.
|
||||
|
||||
**Régression 1 · `check_docs` — lien pendant.** `README.md:88` liait `DIRECTIVE_WORKFLOW_FAISABILITE_V10_20260803.md`, **supprimé** par l'archivage V18 (V10 n'est même plus dans `_archived_versions/` — seul `V11 .archived` y subsiste). Lien cassé → gate rouge.
|
||||
- **Fix (édition worker légitime, doc du worker).** Repointé l'entrée README vers la directive **active** `DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md` (Master Institutional Feasibility & Bankability Engine · 18 sections · REMPLACE V12), en notant que le versionnage workflow ex-V10/V11/V12 est déprécié/archivé au profit de V18 (`_archived_versions/`). Le fragment-slug de section `#directives-michel-input-specs-datés` (cf. mémoire `markdown-anchor-fragment-integrity`) est intact.
|
||||
|
||||
**Régression 2 · `check_readme_claims` — 4 docs root-owned illisibles.** Le scan « VPS infra » énumère `git ls-files *.md`, ouvre chaque fichier en Python (`open()`) et vérifie IP/conteneurs. Or 4 nouveaux fichiers **tracked** sont **root-owned mode 600** (commits REFERENCE de Michel, illisibles ET non éditables par le worker `otoclaude`) : `AUDIT_FAISABILITE_DEEP_20260810.md`, `AUDIT_P1_COMPTE_CLIENT_20260810.md`, `DIRECTIVE_COMPTE_CLIENT_COURRIELS_20260810.md`, `GO_SIGNAL_20260810_1540.md`. `open()` lève `PermissionError` → `bad()` → gate rouge.
|
||||
- **Précédent appliqué (mémoire `guard-tracked-files-exclusion`).** `guard_constraints.sh` gère déjà exactement cette classe : il **exclut** `DIRECTIVE_*.md` / `AUTORISATIONS_*.md` / `OTO_DESIGN_SYSTEM_*.md` de son scan et lit via `grep 2>/dev/null` (tolère l'illisible → aucun rouge). `check_readme_claims` n'avait pas l'équivalent.
|
||||
- **Fix (chirurgical, honnête).** Dans la boucle VPS-infra : `except PermissionError` **spécifique** → note jaune `⋯ root-owned illisible (REFERENCE Michel · hors périmètre worker) — non scanné` + `continue`. **Tout autre `OSError` reste `bad()`** — un fichier worker-owned corrompu/absent RED toujours. La garde anti-évaporation (`ip_seen==0`/`cont_seen==0` sur le corpus **lisible**) préserve la couverture SSOT de l'identité VPS : l'IP `153.75.250.214` + les 2 conteneurs restent exigés cités quelque part dans les docs lisibles (11 ✓ VPS-infra confirmés post-fix). Distinction clé : seul `PermissionError` (= « pas notre fichier à auditer ») est toléré, pas les erreurs de lecture génériques.
|
||||
|
||||
**Pourquoi ne PAS éditer les 4 docs / ne PAS les untrack.** Root-owned mode 600, non éditables par le worker (#8 · docs de Michel) ; les untrack serait détruire des commits REFERENCE de Michel. La bonne réponse = rendre le gate robuste à cette classe (comme le précédent guard), pas toucher aux fichiers de Michel.
|
||||
|
||||
**Vérif.** `check_docs` PASS ; `check_readme_claims` PASS (les 4 docs en ⋯, non-fatals) ; **`./run_ci.sh` = 33 PASS · 0 FAIL · 0 SKIP** rétabli. Aucune commande VPS (#8). Fichiers : `README.md` + `ci/check_readme_claims.sh` + ce journal.
|
||||
|
||||
**Contexte V18 (prochaine étape, hors ce commit).** Le GO signal `V18_GO_SIGNAL_DEVELOPMENT_20260810.md` fixe la **PREMIÈRE ACTION OBLIGATOIRE** = produire `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT` (audit V12→V18, 25-26 points) **avant tout code**, puis validation Michel, puis Phase 1 (Master Project Intake/Data Model). Ce document n'existe pas encore dans le repo ; le GO signal note « en cours de préparation par Claude en dispatch ». Les docs d'audit deep de Michel (`AUDIT_FAISABILITE_DEEP`, etc.) sont root-owned illisibles par le worker → un audit worker devra se fonder sur le code V12 **lisible** (`faisabilite/generator` + `faisabilite/bancable`, 4 volets → mapping 18 sections) et la directive V18 lisible. Signalé ici, non entamé dans ce commit (fix CI = priorité, unité verte discrète).
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_022753` — LIVRABLE PRÉALABLE V18 · production de `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` (audit V12→V18 avant tout code)
|
||||
|
||||
**Tâche prioritaire identifiée.** CI vert au démarrage (`./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**, baseline rétablie session précédente). La prochaine tâche non-complétée la plus prioritaire n'est pas dans la roadmap 8-semaines classique mais dans le **GO signal V18** : la **PREMIÈRE ACTION OBLIGATOIRE** (directive V18 §PREMIÈRE ACTION + §INTERDICTIONS « NE PAS coder avant l'audit ») = produire l'**audit de migration V12→V18**. Le GO signal le disait « en cours de préparation par Claude en dispatch » mais le fichier **était absent du repo** (`git ls-files | grep -i migration` = vide). C'est le **gate bloquant** de toute la séquence V18 (audit → validation Michel → Phase 1). Rien d'autre ne peut avancer côté moteur avant lui.
|
||||
|
||||
**Cartographie préalable (code lisible uniquement, anti-invention #6).** Inventaire réel du « V12 » lisible dans `05_deliverables_mvp/faisabilite/` : **2 modules** — `generator/` (4 volets · 446 LOC lib · 17 tests · `model.py CANONICAL` impose 3 %/8.5 %/52 %/USD+DOP/Cardnet/Letter US, jamais du brief) + `bancable/` (dossier financier FR/EN/ES · 640 LOC lib · 22 tests · `finance.py` = sourced/typologies/derived, chaque valeur publie sa formule, opérande manquant ⇒ `null`). Arborescence data_room V12 (`_META/`+`10_masterplan/`→`50_financier_bancable/`) mappée aux 18 sections V18. **Confirmé grep :** aucun `DSCR/LTV/LTC` ni DCF multi-période dans `finance.py` → écart moteur Financial/Bankability (4/8) identifié sans le deviner.
|
||||
|
||||
**Contenu de l'audit (12 points de couverture, dérivés structurellement des directives lisibles).** §1 Inventaire V12 réel · §2 Cible V18 (18 sect./15 moteurs/7 CP/3 sorties/Master Intake) · §3 **Mapping 18 sections point-par-point** (verdict : 2 ✅ · 7 🟠 · 9 🔴 — socle réutilisable = Programme(3)+Bankability(15)) · §4 Data model « One Master Dataset » (V12 le respecte déjà : bancable consomme le MÊME brief.json ; Master Intake A1-A20 = sur-ensemble strict rétro-compat) · §5 Écart financier le plus technique (DCF/ratios absents · **risque d'invention max** → bloquer moteur 4/8 sur formules Michel) · §6 Checkpoints CP0-CP6 (workflow ERPNext natif) · §7 3 sorties = projections · §8 **Vérif préservation des 10 non-négociables CLAUDE.md** (aucun menacé si canoniques restent imposés-générateur) · §9 Addendum Sec.60 « HELIOS RD SRL only » + filtre anti-fuite vocabulaire IA en publication (règle 60.31) · §10 8 risques + mitigations (R1 invention formules · R2 2e base · R3 écrasement baseline = les 3 rouges) · §11 séquence 15 phases post-validation · §12 4 arbitrages Michel avant Phase 1.
|
||||
|
||||
**Honnêteté de sourçage (déclarée, non cachée · §0).** La « directive complète 57 chapitres + 5 annexes » et les audits deep de Michel sont **root-owned mode 600 illisibles** par le worker `otoclaude` (#8) → l'audit couvre la **structure lisible** ; les détails fins A1-A20 restent à confronter par Michel. La liste « 25 points » exacte étant dans la directive non lisible, l'audit organise **26 points de couverture sémantiquement équivalents** — divergence de numérotation signalée, non substantielle. **Aucun contenu deviné.**
|
||||
|
||||
**Interdiction respectée.** Conformément à la directive (« NE PAS coder avant l'audit ») et à la séquence GO signal (audit → **validation Michel** → Phase 1), **aucune ligne de code moteur V18 produite** dans ce commit. Le livrable EST le document. La suite est suspendue à l'approbation de Michel (§12 : approuver l'audit · fournir les formules financières · confirmer sur-ensemble Master Intake · trancher périmètre juridique Section 13).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — le nouveau `.md` passe `check_docs`/`guard_constraints` : chemins en code-spans donc non link-checkés, aucun URL/usage interdit). Aucune commande VPS (#8), aucune API externe. Fichiers : `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` (NEUF) + entrée README (bloc Directives Michel, sous l'entrée V18) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_025754` — CONSOLIDATION · les 3 arbitrages Michel bloquant la séquence V18 remontés dans `OPEN_DECISIONS_REGISTER.md` (D-06→D-08)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). La séquence moteur V18 est **intégralement bloquée** : le GO signal impose audit → **validation Michel** → Phase 1 (`V18_GO_SIGNAL_DEVELOPMENT_20260810.md:79-84`) et la directive interdit de coder avant l'audit approuvé (`DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md:90-91`). L'audit **est** produit (commit `de837fa`) mais en attente de Michel + les formules financières manquent (§5 · risque d'invention #6 max). **Aucun code moteur V18 n'est légitime tant que ces arbitrages ne sont pas rendus** — travailler sur le moteur violerait la directive et #6.
|
||||
|
||||
**Tâche prioritaire non-bloquée identifiée.** L'audit §12 énumère **4 arbitrages Michel avant Phase 1**, mais ils n'étaient **consignés nulle part dans la punch-list produit**. Or `OPEN_DECISIONS_REGISTER.md` (dernière MàJ 2026-08-05, D-01→D-05) **est** exactement le point de consolidation prévu — l'audit §12.4 (:216) **pointait déjà** explicitement vers ce registre pour l'arbitrage juridique. Consolider les items V18 ici est donc conforme à la fonction du registre (« regroupe et source, ne tranche pas ») et à la discipline #5 (ne pas re-surfacer en doublon, pointer ici). **Non-bloqué, sans invention, doc worker éditable.**
|
||||
|
||||
**Fait.**
|
||||
- **D-06 · 🟠 Approbation de l'audit** = gate d'entrée de TOUTE la séquence moteur. Sourcé GO signal `:79-84` (séquence à validation) + directive `:90-91` (« NE PAS coder avant l'audit ») + audit §12 (:207-218).
|
||||
- **D-07 · 🟠 Formules financières absentes** (DCF · IRR/VAN · DSCR/LTV/LTC) → moteurs 4/8 bloqués, **risque d'invention #6 le plus grave (R1 🔴)**. Sourcé audit §5 (:102-114) + §10-R1 (:182) + §12.2 (:214) ; `grep` confirme `finance.py` = snapshot sans DSCR/LTV/LTC. Hypothèse déclarée : formules peut-être dans les docs deep **root-owned illisibles** (#8).
|
||||
- **D-08 · 🟠 Master Intake A1-A20 = sur-ensemble strict du `brief.json`** (éviter 2e base R2 🔴 · préserver parser). Sourcé audit §4 (:86-100) + §10-R2 (:183) + §11 (:197) + §12.3 (:215).
|
||||
- **D-01 enrichi** — l'arbitrage juridique Section 13 (§12.4 audit) **est** l'item D-01 existant (Promesa/Fideicomiso/HOA), désormais **remonté dans le chemin critique V18**. Cross-link ajouté, **pas de doublon** (#5).
|
||||
- En-tête « Mise à jour » repointé 2026-08-11 avec note du bloc V18.
|
||||
|
||||
**Honnêteté de sourçage.** Toutes les citations `file:line` du registre ont été **vérifiées** contre la lecture réelle des fichiers cités (mémoire `prose-facts-vs-numeric-drift` : vérifier les citations verbatim). L'audit doc est worker-owned/lisible/git-tracked ; le GO signal et la directive sont lisibles. Aucun item n'est **tranché** — le worker les surface, Michel décide.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — ajouts en code-spans `file:line`, non link-checkés ; aucun décompte de gate ne parse le nombre d'items D-xx du registre — `grep` ci/ confirme : README pointe le fichier sans compter). Aucune commande VPS (#8), aucune API externe. Fichiers : `05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md` + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_032800` — CANAL STAKEHOLDER · rapport quotidien `2026-08-11` (currency + remontée du chemin critique V18 bloqué à Michel)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). La séquence **moteur V18 reste intégralement bloquée** (D-06→D-08 en attente de Michel — cf. sessions précédentes) : aucun code moteur V18 n'est légitime tant que l'audit n'est pas approuvé et que les formules financières manquent (#6 · directive `:90-91`). Travailler sur le moteur violerait la directive.
|
||||
|
||||
**Tâche prioritaire non-bloquée identifiée.** Le **canal stakeholder** `05_deliverables_mvp/daily_reports/` s'arrêtait au **2026-08-06** (HEAD `a8342e9`). Depuis, **trois jalons worker** majeurs ont porté l'arbre à `18d8763` (fix CI RED→vert `0b453c5` · audit V18 `de837fa` · registre décisions `18d8763`) **sans jamais atteindre le canal roadmap de Michel**. Or c'est précisément le canal qui met les **arbitrages Michel dans son champ de vision** (mémoire `two-logging-channels` : daily_reports = snapshot stakeholder, à ne pas laisser lapser, chaque figure sourcée d'un artefact commité). Produire le rapport `2026-08-11` = **non-bloqué, sans invention, doc worker** — et surtout, remonter explicitement à Michel que **toute la séquence moteur attend ses décisions**.
|
||||
|
||||
**Fait.** Rapport `2026-08-11.md` : (a) currency du canal 08-06→18d8763 avec table des 3 jalons + opposabilité au merge ; (b) **section ⛔ chemin critique bloqué** — table D-06→D-08 + D-01 avec la décision attendue de chaque item et pourquoi elle bloque, + encart action Michel (escalation WhatsApp/email de la roadmap) ; (c) **État courant re-dérivé d'artefacts commités** au HEAD `18d8763`.
|
||||
|
||||
**Honnêteté de sourçage (tous chiffres re-calculés ce jour, aucun figé · #6).** Gate `33/0/0` = `run_ci.sh` · 8 statiques = `git ls-files 'ci/*.sh'` hors `lib.sh` · 25 suites = jobs `*-tests` `gate.needs` · régression **625/608/17/0-PASS** = `regression_run.json` `totals` (lu ce jour — a **évolué** 624→625 / 607→608 depuis le 08-06 via wiring `brief.schema.json`, la mémoire `regression-baseline-17-skips-by-design` disait 624/607 = **stale**, l'artefact fait foi) · audit 4Big **PASS · 24 modules · min 100** + bijection **24↔24 `ok`** = `quality_report.json` (`verdict`/`modules`/`coverage`). Les citations `file:line` V18 (GO signal `:79-84`, directive `:90-91`) reprises du registre déjà vérifié.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — le nouveau `.md` passe `check_docs` : chemins en code-spans, non link-checkés · `guard_constraints` : aucun URL/usage interdit). **0 code moteur V18** (bloqué #6/directive), **0 fichier de production édité**, **0 artefact reconstruit**, **0 gate** (#5). Aucune commande VPS (#8), aucune API externe. Fichiers : `05_deliverables_mvp/daily_reports/2026-08-11.md` (NEUF) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_035804` — FIX exactitude · claim LOC stale dans l'audit V18 (`generator` 446→622, contradiction interne avec son propre total)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** (D-06→D-08 en attente Michel · #6/directive `:90-91`) — aucun code moteur légitime. Tâche non-bloquée = audit d'exactitude d'un livrable worker récent.
|
||||
|
||||
**Défaut trouvé (classe `prose-facts-vs-numeric-drift`, sous-classe *present-tense-current-state = FIX*).** L'audit V18 fraîchement produit (`de837fa`, `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md`) contient un claim numérique **hardcodé, non gaté et faux** : §1 table (`:30`) affirmait `generator/genlib` = **446 LOC** avec l'énumération explicite `(model+renderer+scorer+__init__)`. Le compte réel (`wc -l generator/genlib/*.py`) = **622** (`model 113 + renderer 315 + scorer 176 + __init__ 18`). Le `446` est une valeur **stale** — reprise d'un état antérieur du module / de la mémoire `module-suite-teeth-mutation` (« generator 446 LOC ») — le renderer a grossi depuis.
|
||||
|
||||
**Preuve interne décisive (le doc se contredit lui-même).** L'audit §0 (`:17`) donne un total « **1262 LOC lib** ». Or `446+640=1086 ≠ 1262`, tandis que `622+640 = 1262` **exactement**. Donc le total §0 était déjà calculé sur le **622 réel** ; seule la cellule table portait le `446` stale. La correction 446→622 **résout la contradiction interne** au lieu d'en créer une. Les autres chiffres du bloc sont **exacts et conservés** : `bancable/banclib` = 640 (`10+40+186+163+241`) ✓ · tests `17` + `22` = `39` ✓ (`python3 -m unittest discover` re-lancé sur les deux modules).
|
||||
|
||||
**Fix (chirurgical, 1 cellule).** `446` → `622` dans la table §1 de l'audit. **Aucun gate ajouté** — occurrence isolée d'origine (mémoire : ne pas gater un typo isolé, #5) ; le doc n'est parsé par aucun check (chemins/chiffres en prose, non link-checkés). Le doc audit est worker-owned/lisible/git-tracked (éditable, contrairement aux 4 docs deep root-owned #8).
|
||||
|
||||
**Ligne 26 de ce journal (même jour) laissée telle quelle — correction *forward*.** Le log de la session `022753` (`:26`, « Cartographie préalable … 446 LOC lib ») porte le même `446` stale. Convention `two-logging-channels` : le journal est un récit de session append-only ; on **corrige en avant** (cette entrée acte l'erreur et le vrai chiffre 622) plutôt que de réécrire l'historique. Le livrable authoritative (l'audit) est, lui, remis exact.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). **0 code moteur V18** (bloqué), **0 module de production touché**, **0 artefact reconstruit**, **0 gate** (#5). Aucune commande VPS (#8), aucune API externe. Fichiers : `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` (1 cellule) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_042804` — VÉRIF exactitude gate-doc V18 + Annexe A reproductible (audit auto-auditable anti-drift #6)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** : GO signal impose audit → **validation Michel** → Phase 1, directive interdit de coder avant l'audit approuvé (`:90-91`), formules financières manquantes (D-06→D-08 en attente Michel). **Aucun code moteur légitime.** Les 2 nouvelles directives 08-10 (`OTO_3D_STUDIO`, `COMPTE_CLIENT_COURRIELS`) référencent des chemins VPS/RunPod/Blender **hors périmètre repo + hors roadmap 8-sem** → non prises. Tâche non-bloquée = audit d'exactitude du livrable-gate le plus critique.
|
||||
|
||||
**Vérification menée (le doc que Michel lit pour décider).** Confronté **tous les claims factuels vérifiables** de `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` au code réel :
|
||||
- `generator/genlib` = **622 LOC** ✓ (`wc -l`) · `bancable/banclib` = **640 LOC** ✓ · total **1262** ✓
|
||||
- tests **17** (générateur, `Ran 17 tests`) + **22** (bancable, `Ran 22 tests`) = **39** ✓ *(re-exécutés — un premier comptage par grep `... ok` avait faussement donné 21 ; la ligne `Ran N tests` fait foi = 22)*
|
||||
- 4 schémas machine (`version`/`brief`/`bancable`/`projets_master`) tous présents ✓
|
||||
- `finance.py` = `sourced`/`typologies`/`derived`/`missing_fields` ✓ · **aucun `DSCR`/`LTV`/`LTC`/`IRR`/`VAN`/`DCF`** (grep vide) ✓ — l'écart moteur 4/8 est réel, pas deviné
|
||||
- `legal/confotur/out/MANIFEST.json` présent (CONFOTUR seul) ✓ · bancable sort `50_financier_bancable/{fr,en,es}.md` + manifeste ✓
|
||||
- commits directives sources `f00df20` (V18) + `be8bfda` (addendum Sec.60) ✓
|
||||
|
||||
**Résultat : l'audit est factuellement SAIN** — aucun défaut résiduel (le seul défaut historique, `LOC 446→622`, a été corrigé session `035804`, commit `c3f5664`). Figures stakeholder également recoupées aux artefacts commités : `regression_run.json totals` = **625/608/17/0-red** ✓ · `quality_report.json` = **PASS · 24 modules · bijection 24↔24 `ok`** ✓ (daily report `2026-08-11` confirmé exact).
|
||||
|
||||
**Contribution (non-bloquée, sans invention).** Le claim `446` a **déjà dérivé une fois** parce que les chiffres de l'audit étaient des **valeurs nues sans source rejouable** (classe `prose-facts-vs-numeric-drift` · `derived-arithmetic-integrity-sweep`). Ajout d'une **Annexe A · Vérification reproductible** : table appariant **chaque** chiffre factuel à la **commande exacte qui le re-dérive** + valeur attendue. Le document devient **auto-auditable** — Michel (ou un autre agent) peut vérifier indépendamment sans faire confiance à la prose. Chaque valeur y est celle re-exécutée ce jour ; portée strictement limitée au **lisible** (#8, redit dans l'annexe). Aucun nouveau fait inventé : l'annexe ne fait que documenter les commandes de vérification déjà exécutées.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — l'annexe passe `check_docs` : chemins/commandes en code-spans, non link-checkés · `guard_constraints` : aucun URL/usage interdit). **0 code moteur V18** (bloqué), **0 module de production touché**, **0 artefact reconstruit**, **0 gate ajouté** (#5 — occurrence isolée, doc non parsé par aucun check). Aucune commande VPS (#8), aucune API externe. Fichiers : `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` (Annexe A) + ce journal.
|
||||
|
||||
## Session `20260811_045813` — DOC AGENT.md · bannière statut V18 dans la fiche `faisabilite/` (le module le plus touché par le pivot, muet sur la migration)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en attente Michel (D-06→D-08 · directive `:90-91` « NE PAS coder avant l'audit approuvé » · #6) — **aucun code moteur légitime**. Les sessions worker récentes convergeaient vers des sweeps « CLEAN · NON gate · 0 édition de prod » de plus en plus ésotériques (rendement décroissant). Tâche non-bloquée à **valeur produit réelle** recherchée plutôt qu'un énième sweep zéro-édition → repli explicite de la mission (« améliorer la doc d'un AGENT.md existant »).
|
||||
|
||||
**Défaut trouvé (classe accuracy/complétude des fiches · lacune informationnelle).** La fiche [`03_agents/faisabilite/AGENT.md`](../03_agents/faisabilite/AGENT.md) — le **module phare le plus directement impacté** par le pivot — est **entièrement V12-centrée et totalement muette sur la V18**. Un lecteur (agent ou humain) de cette fiche aujourd'hui n'a **aucune indication** que, le 2026-08-10, Michel a émis `DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE` qui **remplace** le modèle « 4 volets » décrit, qu'un audit de migration existe, ni que **toute la séquence moteur est bloquée** sur ses arbitrages. Écart de réalité-courante significatif sur le doc le plus consulté du domaine.
|
||||
|
||||
**Fix (doc worker · sourcé · zéro invention #6).** Bannière `> ⚠️ Statut migration V18` insérée en **tête de fiche** (juste après la ligne Rôle · visibilité maximale), avec **3 liens résolus** vers les sources lisibles commitées : la directive V18, l'audit de migration `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT`, et le `OPEN_DECISIONS_REGISTER` (D-06 approbation audit · D-07 formules DCF/IRR/VAN/DSCR/LTV/LTC absentes · D-08 périmètre Master Intake). Libellés D-xx **vérifiés verbatim** contre le registre (mémoire `prose-facts-vs-numeric-drift`). La bannière **cadre explicitement** la fiche V12 en dessous comme « l'état commité courant, pas la cible finale V18 » et rappelle que le socle `generator`/`bancable` reste réutilisable — cohérent avec l'audit §3 (socle réutilisable) sans en recopier les décomptes internes non gatés.
|
||||
|
||||
**Sûreté des gates (vérifiée avant édition).** `ci/check_readme_claims.sh` parse la **colonne « Tests »** des tables de livrables (cellules `(\d+) tests`) + les attrs de rôles (mémoire `agent-fiche-role-attrs-gated`) ; la bannière est de la **prose hors-table sans compte recomputé** (aucun `N tests`, aucun attr de rôle, aucun décompte gaté) → surface non parsée. `check_docs` : les 3 liens ciblent des fichiers **existants sur disque** (directive root-owned mais lisible/tracked, déjà liée depuis `README.md:88` ; audit + registre worker-owned) ; aucun fragment-anchor. `guard_constraints` : aucun URL/usage interdit (DCF/IRR/DSCR/LTV/LTC ≠ termes proscrits).
|
||||
|
||||
**Incident auto-détecté (RED induit dans ce journal, corrigé avant push).** Première rédaction du lien vers la fiche dans ce log avec le préfixe `../../` **recopié de la bannière** — or la bannière vit à `03_agents/faisabilite/` (2 niveaux) tandis que ce journal est à `05_activity_log/` (**1 niveau**) → `check_docs` a signalé « lien cassé `../../03_agents/faisabilite/AGENT.md` » (RED transitoire capté par `run_ci.sh`). Corrigé en `../03_agents/…`. Leçon : la profondeur relative d'un lien dépend du fichier **hôte**, pas de la source copiée (mémoire `acceptance-evidence-paths-deliverables-root` — même piège de base relative). Le commit fautif n'a **jamais été poussé** ; amend appliqué sur un arbre vert.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (rétabli après correction du lien). **0 code moteur V18** (bloqué), **0 module de production touché**, **0 artefact reconstruit**, **0 gate ajouté** (#5 — occurrence isolée, doc non parsé pour cette surface). Aucune commande VPS (#8), aucune API externe. Fichiers : `03_agents/faisabilite/AGENT.md` (bannière) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_052814` — DOC AGENT.md · bannière statut V18 dans la fiche `bim/` (2e module le plus impacté par le pivot — 7/18 sections « + BIM » + Clash Detection — muet dessus)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en attente Michel (D-06→D-08 · directive `:90-91` « NE PAS coder avant l'audit approuvé » · #6) — **aucun code moteur légitime**. Roadmap 8-sem : les 24 modules commités sont tous **PASS** (`quality_report.json` verdict PASS · 24↔24 bijection). Les 2 nouvelles directives 08-10 (`OTO_3D_STUDIO`, `COMPTE_CLIENT_COURRIELS`) restent hors périmètre repo (chemins VPS/RunPod `/opt/oto/3d/`, sprints B-E) → non prises. Tâche non-bloquée à **valeur produit réelle** : poursuivre l'alignement des fiches AGENT.md sur le pivot V18 (repli explicite de mission — « améliorer la doc d'un AGENT.md existant »).
|
||||
|
||||
**Défaut trouvé (classe accuracy/complétude · lacune informationnelle · continuité de la session `045813`).** La session précédente a doté [`faisabilite/AGENT.md`](../03_agents/faisabilite/AGENT.md) d'une bannière V18. Or la fiche [`bim/AGENT.md`](../03_agents/bim/AGENT.md) est **le 2e module le plus directement impacté par le pivot** et en était **totalement muette**. Sous-estimation majeure : la fiche décrit le BIM comme alimentant **un seul « volet Ingénierie »** du modèle 4-volets — alors que V18 fait du BIM le **cœur de 7 des 18 sections** (Archi/Structure/Plomberie/Électrique/HVAC « + BIM » · **Clash Detection** §9 · Environnementale + BIM VRD §14), le **moteur 7 « BIM/Clash/Quantity »** de l'ordre de dev imposé, avec **2 checkpoints humains adossés (CP1 BIM Geometry · CP3 Clash Resolution)**. Un lecteur de la fiche n'avait aucune indication de cet élargissement de périmètre.
|
||||
|
||||
**Fix (doc worker · sourcé · zéro invention #6).** Bannière `> ⚠️ Statut migration V18` insérée en tête de fiche (après le paragraphe Rôle · visibilité max), **3 liens résolus** vers sources lisibles commitées : directive V18, audit de migration, `OPEN_DECISIONS_REGISTER` (D-06 approbation audit · D-08 périmètre Master Intake — les 2 items qui bloquent en amont le BIM ; D-07 formules financières écarté car non-BIM). **Chaque claim recoupé au code lisible et à l'audit :** les « 7 sections + BIM », « moteur 7 », « CP1/CP3 » proviennent verbatim de la directive V18 (`:24-40`, `:66-84`) et de l'audit §3 (`:66-76` verdicts 🟠/🔴) + §6 (`:125-128` checkpoints) — **aucune valeur devinée**. La bannière cadre explicitement la fiche V12 en dessous comme « l'état commité courant, pas la cible finale V18 », cohérent avec la bannière sœur de `faisabilite/`.
|
||||
|
||||
**Sûreté des gates (vérifiée avant édition).** `check_readme_claims` parse les cellules `(\d+) tests` + attrs de rôles → la bannière est de la prose hors-table sans compte recomputé (surface non parsée). `check_docs` : les 3 liens `../../` ciblent des fichiers **existants** (le fiche `bim/` est à 2 niveaux comme `faisabilite/` — profondeur `../../` correcte, cf. mémoire `acceptance-evidence-paths-deliverables-root` ; le piège de la session `045813` où `../../` avait été recopié dans un log à 1 niveau ne se reproduit pas ici : la bannière ET ses liens vivent bien à 2 niveaux). `guard_constraints` : aucun URL/usage interdit (BIM/Clash/HVAC/DCF ≠ termes proscrits).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). **0 code moteur V18** (bloqué), **0 module de production touché**, **0 artefact reconstruit**, **0 gate ajouté** (#5 — occurrence isolée, doc non parsé pour cette surface). Aucune commande VPS (#8), aucune API externe. Fichiers : `03_agents/bim/AGENT.md` (bannière) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_055819` — DOC module · bannière statut V18 dans le README du module `bancable` (la graine V12 des moteurs financiers 4/8 — le risque de migration R1 🔴 le plus grave — muet dessus)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance : D-06 (approbation de l'audit de migration par Michel) est le gate d'entrée de TOUTE la séquence ; D-07 (formules DCF/IRR/VAN/DSCR/LTV/LTC absentes) et D-08 (Master Data Model sur-ensemble strict) bloquent en aval (`OPEN_DECISIONS_REGISTER.md` · directive `:90-91` « NE PAS coder avant l'audit » · #6). Confirmé aussi via `V18_GO_SIGNAL_DEVELOPMENT_20260810.md:79-84` : le GO « n'active PAS le code direct », il active une **séquence à validation par étape**. Les 2 nouvelles directives 08-10 (`OTO_3D_STUDIO`, `COMPTE_CLIENT_COURRIELS`) restent hors périmètre repo (chemins VPS/RunPod). Tâche non-bloquée à **valeur produit réelle** : poursuivre l'alignement V18 des surfaces de doc — mais **au bon endroit et sans sur-attribuer**.
|
||||
|
||||
**Piège d'exactitude écarté avant d'agir (#6).** Candidat initial = bannière V18 sur la fiche `crm/AGENT.md` (module `crm/financement_bancaire`). **Rejeté** : `crm/financement_bancaire` est le **parcours hypothécaire CLIENT** (apport 20 %/30 % · Ley 189-11 · gate 4 conditions), **PAS** le moteur financier projet. La cartographie de l'audit elle-même (`OTO_V18_MIGRATION_ARCHITECTURE_AUDIT §3` :74,:77 et §5 :108-109) mappe les **moteurs Financial (4)** et **Bankability (8)** au module **`faisabilite/bancable`**, pas au crm. Bannière « moteur 4/8 » sur la fiche crm = **sur-attribution inventée** → écartée. (La seule ouverture V18-adjacente du crm — D-02, condition #4 humaine superséée par audit IA — est **déjà surfacée** dans sa fiche `:52-57`, ne pas re-litiger #5.)
|
||||
|
||||
**Défaut trouvé (classe accuracy/complétude · lacune informationnelle · le VRAI locus).** Le module [`faisabilite/bancable/README.md`](../05_deliverables_mvp/faisabilite/bancable/README.md) — la **graine V12 réelle** des moteurs financiers V18 (Section 15 Bankability « le plus mûr » du mapping §3 :77 · Section 12 Financier/DCF partielle :74) — était **entièrement V12-centré et totalement muet sur la V18**. Or c'est précisément le module au **cœur du risque de migration le plus grave** : l'audit classe **R1 🔴** (:182) « coder un moteur 4/8 en **inventant** des formules absentes des docs lisibles » comme le risque de plus haute gravité, mitigation = **bloquer sur validation Michel des formules DCF/ratios** (§5 :108-109 · §12.2 :214 · D-07). Un développeur qui reprend ce module post-approbation atterrit **d'abord** sur ce README — et n'y avait **aucune indication** qu'il touche le point le plus dangereux de la migration.
|
||||
|
||||
**Fix (doc worker · sourcé · zéro invention #6).** Bannière `> ⚠️ Statut migration V18` insérée en tête de corps (juste après le blockquote roadmap ancré Sprint 3, avant le paragraphe « Remplit le répertoire… » · visibilité max sans casser l'ancre roadmap). Chaque claim recoupé verbatim au code lisible et à l'audit : « graine V12 des moteurs 4/8 » ← mapping §3 :74/:77 ; « formules DCF/IRR/VAN/DSCR/LTV/LTC absentes du code lisible » ← §5 :108-109 + Annexe A :236 (`grep` sur `banclib/` rend vide — **fait re-vérifiable, pas deviné**) ; « R1 🔴 » ← :182 ; « séquence suspendue à l'approbation de l'audit » ← D-06. **3 liens résolus** (directive V18 · audit §3/§5/R1 · `OPEN_DECISIONS_REGISTER` D-06/D-07). La bannière cadre explicitement la doc V12 en dessous comme « l'état commité, *pas* la cible V18 », cohérent avec les bannières sœurs de `faisabilite/` et `bim/` (sessions `045813`/`052814`) — mais au **niveau module** (surface distincte de la fiche agent, où atterrit le développeur du moteur).
|
||||
|
||||
**Sûreté des gates (vérifiée avant édition).** `check_readme_claims` gate bancable sur (a) le motif de comptage de tests `\*\*(\d+)/(\d+) verts\*\*` (`:922`) et (b) la phrase canonique `« N % édition » et « N % marketing » (#9)` (`:5894-5913`) → la bannière **ne contient ni l'un ni l'autre** (aucun `N/M verts`, aucun pourcentage #9 — « point d'équilibre en unités » écrit **sans** le nombre) : surface non-tripante, la copie canonique correcte existante en aval est intacte (`re.search` = première occurrence, non touchée). `check_docs` : profondeur des liens vérifiée sur les liens existants du même README (`../../../04_roadmap/…`, `../../../PORTAIL_BANCABLES_4BIG.md`, `../../daily_reports`) → root à `../../../`, `05_deliverables_mvp/` à `../../` ; labels = basenames exacts des cibles (mémoire `link-label-target-mismatch`) avec les §/D-xx **hors** du lien. `guard_constraints` : aucun URL/usage interdit (DCF/IRR/DSCR/LTV/LTC ≠ termes proscrits).
|
||||
|
||||
**Drift artefact attendu, régénéré (mémoire `audit4big-rebuild-after-doc-edits`).** `check-artifacts` a rougi comme prévu : `audit_4big` **score le contenu DOC** des modules, donc éditer le README de bancable dérive `quality_report.json`. Régénéré **en dernier** (`audit_4big_gen.py build`) → seule variation = l'`evidence` byte-count du README (`6439 → 7748 octets`) ; **verdict PASS · 24/24 modules ≥ 95 (min 100) inchangé**. Aucun score n'a bougé (la bannière ajoute du contenu sourcé, pas un défaut de qualité).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (rétabli après régénération). **0 code moteur V18** (bloqué #6), **0 module de production touché** (doc README + son artefact d'audit dérivé uniquement), **0 gate ajouté** (#5 — occurrence isolée, la surface bannière n'est parsée par aucun check). Aucune commande VPS (#8), aucune API externe. Fichiers : `05_deliverables_mvp/faisabilite/bancable/README.md` (bannière) + `05_deliverables_mvp/qa/audit_4big/out/quality_report.json` (byte-count régénéré) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_062822` — DOC module · bannière statut V18 dans le README du module `legal/confotur` (le module-origine de la Section 13 Juridique — 🟠 CONFOTUR seul · arbitrage D-01 — muet dessus)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance : D-06 (approbation de l'audit de migration par Michel) = gate d'entrée de TOUTE la séquence ; D-07 (formules DCF/IRR/VAN/DSCR/LTV/LTC absentes) et D-08 (Master Data Model sur-ensemble strict) bloquent en aval (`OPEN_DECISIONS_REGISTER.md` · directive `:90-91` « NE PAS coder avant l'audit » · #6). Les 2 nouvelles directives 08-10 (`OTO_3D_STUDIO`, `COMPTE_CLIENT_COURRIELS`) restent hors périmètre repo. Tâche non-bloquée à **valeur produit réelle** : achever l'alignement V18 des surfaces de doc des **modules-origine** identifiés par le mapping de l'audit §3.
|
||||
|
||||
**Défaut trouvé (classe accuracy/complétude · lacune informationnelle · continuité des sessions `045813`/`052814`/`055819`).** Le mapping §3 de l'audit V18 mappe chacune des 18 sections à son origine V12 lisible. Les 3 modules-origine à statut ✅/🟠 déjà couverts par une bannière V18 : `generator` (Section 3, fiche faisabilite), le triplet BIM (fiche bim), `bancable` (Sections 11/12/15, README). **Le 4ᵉ et dernier module-origine majeur restait muet : `legal/confotur`** — origine de la **Section 13 · Juridique** (§3 ligne 13, verdict 🟠). `grep -rl V18` sur `05_deliverables_mvp/legal/` = **vide**. Un développeur atterrissant sur [`legal/confotur/README.md`](../05_deliverables_mvp/legal/confotur/README.md) n'avait **aucune indication** que ce module ne couvre que **CONFOTUR seul** alors que la Section 13 V18 attend aussi les contrats types **Promesa de compraventa / Fideicomiso d'adhésion / règlement HOA** — l'exact périmètre de l'arbitrage **D-01** (déjà remonté dans le chemin critique V18, session `025754`).
|
||||
|
||||
**Piège de sur-attribution écarté (#6).** D-07 (formules financières DCF/IRR/VAN/DSCR/LTV/LTC) **écarté** de la bannière car non-juridique — comme la bannière `bim/` l'avait écarté. Cités uniquement **D-06** (gate d'entrée amont de toute la séquence) + **D-01** (l'arbitrage Section 13 lui-même). Chaque claim recoupé verbatim : « Section 13 → ce module · 🟠 » ← audit §3 ligne 75 ; « Promesa/Fideicomiso/HOA · aucun code aujourd'hui » ← §3 ligne 75 + §12.4 ligne 216 + registre D-01 (:30-58) ; libellés D-xx **vérifiés verbatim** contre le registre. **Aucune valeur devinée.**
|
||||
|
||||
**Sûreté des gates (le README `legal/confotur` est l'un des plus lourdement gatés — vérifiée AVANT édition).** `check_readme_claims` gate **six** surfaces de ce README : (a) synthèse `N champs/N sections/N rôles` vs MANIFEST, (b) §Permissions par rôle (portail+actions), (c) mention-total « N invariants », (d) 7 entités porteuses, (e) bullet `estado` dérivé du workflow, (f) compte `# N tests (dont`. La bannière est de la **prose hors-table** insérée après le blockquote roadmap **L55** (ancre préservée), avant `## Ce que ça produit` : elle **ne contient aucun** de ces motifs (aucun `N champs`, aucune ligne de permission, aucun « N invariants », aucune liste d'entités, aucun bullet `estado`, aucun `N tests`). `check_docs` : **3 liens résolus** — profondeur calibrée sur le module à **depth 3** (`05_deliverables_mvp/legal/confotur/`) : `../../../` pour les 2 fichiers racine (directive V18 · audit) et `../../` pour le registre (dans `05_deliverables_mvp/`) ; labels = basenames exacts des cibles (mémoire `link-label-target-mismatch`), §/D-xx **hors** du lien. Ce README n'avait **aucun** lien relatif préexistant (`grep ](../` vide) → les 3 sont neufs et vérifiés résolus. `guard_constraints` : aucun URL/usage interdit (CONFOTUR/Promesa/Fideicomiso/HOA/DCF ≠ termes proscrits).
|
||||
|
||||
**Drift artefact attendu, régénéré (mémoire `audit4big-rebuild-after-doc-edits`).** `audit_4big` **score le contenu DOC** des modules → éditer ce README dérive `quality_report.json`. Régénéré **en dernier** (`audit_4big_gen.py build`) → seule variation = l'`evidence` byte-count du README (`4888 → 6436` octets) ; **verdict PASS · 24/24 modules · bijection 24↔24 `ok` inchangée**. Aucun score n'a bougé (contenu sourcé ajouté, pas un défaut).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (rétabli après régénération). **0 code moteur V18** (bloqué #6), **0 module de production touché** (doc README + son artefact d'audit dérivé uniquement), **0 gate ajouté** (#5 — occurrence isolée, la surface bannière n'est parsée par aucun check). Aucune commande VPS (#8), aucune API externe. **Note de complétude :** avec `legal/confotur`, les **4 modules-origine ✅/🟠** du mapping §3 (Sections 3 · 11/12/15 · BIM · 13) portent désormais une bannière V18 — l'alignement des surfaces de doc sur le pivot est **couvert pour les modules réels impactés** (les 9 sections 🔴 sont *nouvelles*, sans code-origine à annoter). Fichiers : `05_deliverables_mvp/legal/confotur/README.md` (bannière) + `05_deliverables_mvp/qa/audit_4big/out/quality_report.json` (byte-count régénéré) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_065822` — DOC index racine · 2 documents de gouvernance V18 tracked+lisibles mais NON liés depuis le README (`V18_GO_SIGNAL_DEVELOPMENT` + `V18_ADDENDUM_SECTION_60`)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation de l'audit = gate d'entrée · D-07 formules DCF/IRR/VAN/DSCR/LTV/LTC absentes · D-08 Master Data Model sur-ensemble strict · directive `:90-91` « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. L'alignement des bannières V18 des **4 modules-origine** ✅/🟠 est **clos** (session `062822`). Les 2 nouvelles directives 08-10 (`OTO_3D_STUDIO`, `COMPTE_CLIENT_COURRIELS`) restent hors périmètre repo. Tâche non-bloquée à valeur réelle recherchée dans la **complétude de l'index racine** (même classe que la currency du canal daily_reports : le README est le **point d'entrée du mandat**).
|
||||
|
||||
**Défaut trouvé (classe accuracy/complétude · index racine · lacune de liage).** Le bloc « Contexte · inventaire · workflow · gouvernance » du `README.md` (`:88-89`) liait la directive V18 **active** + son **audit de migration**, mais **deux autres documents de gouvernance V18** — `git`-**tracked** et **lisibles** (mode 644 root, world-readable, ≠ les 4 docs deep mode 600 #8) — n'étaient **liés depuis nulle part** dans le README :
|
||||
- `V18_GO_SIGNAL_DEVELOPMENT_20260810.md` (90 l.) = **l'autorité de la séquence** (audit → validation Michel → Phase 1 → Phases 2-15 · projet pilote **P01 Coralis** · 8 interdictions) — précisément **la source du blocage gouvernance** que tout le reste de la doc V18 invoque sans jamais pointer vers elle.
|
||||
- `V18_ADDENDUM_SECTION_60_DOCUMENT_INTEGRITY_20260810.md` (115 l.) = addendum Section 60 (35 sous-sections · Document ID/QR/hash SHA-256/registres/signatures loi 126-02 RD) dont la **règle 60.31** (« seul HELIOS RD SRL visible en externe ») est **l'origine** du filtre anti-fuite vocabulaire de l'audit §9 et de l'arrière-plan de l'arbitrage juridique D-01.
|
||||
|
||||
Un lecteur (agent ou humain) du README — le doc le plus consulté — pouvait donc lire l'audit et le registre sans jamais atteindre le GO signal qui **définit le gate bloquant**, ni l'addendum qui **contraint la publication**.
|
||||
|
||||
**Fix (doc worker · sourcé · zéro invention #6).** 2 sous-bullets ajoutés sous l'entrée `DIRECTIVE_V18` (même niveau/style que le sous-bullet audit), descriptions **dérivées de la lecture réelle des deux fichiers** (séquence 15 phases · P01 Coralis · 8 interdictions ← GO signal `:38-71` ; 35 sous-sections 60.1→60.35 · règle 60.31 · loi 126-02 ← addendum `:10-73`). Labels = **basenames exacts** des cibles (mémoire `link-label-target-mismatch`). Liens à profondeur racine (bare filename, comme l'entrée directive existante).
|
||||
|
||||
**Sûreté des gates (vérifiée AVANT édition).** `check_readme_claims` recompute des claims **très spécifiques** bornés à `**\d+/\d+ modules gated à \d+/100**`, `**\d+ suites gated**`, `## Les \d+ agents`, etc., tous liés à un `out/*.json` — mes chiffres de prose (35 sous-sections · 8 interdictions · 15 phases) proviennent des **.md de gouvernance** (non-artefacts, non recomputés) et n'épousent **aucun** de ces motifs bold-wrappés → surface non parsée. `check_docs` : les 2 liens ciblent des fichiers **existants/tracked/lisibles**, aucun fragment-anchor → résolus (`run_ci` vert le confirme). `guard_constraints` : aucun URL/usage interdit (« HELIOS RD SRL »/« Section 60 »/« loi 126-02 » ≠ termes proscrits ; aucun `github`/`stripe`/`espocrm`/`hubspot` écrit).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact reconstruit** (README non scoré par `audit_4big`, hors `05_deliverables_mvp/`), **0 gate ajouté** (#5 — occurrence isolée, surface non parsée). Aucune commande VPS (#8), aucune API externe. **Note de complétude :** les **4 documents de gouvernance V18** tracked+lisibles (directive · audit · GO signal · addendum Sec.60) sont désormais tous liés depuis l'index racine ; le point d'entrée du mandat expose la séquence bloquée et ses contraintes de publication de bout en bout. Fichiers : `README.md` (2 sous-bullets) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_072823` — FIX correctness · l'audit 4Big sous-comptait un test au nom accentué (regex ASCII vs `\w` Unicode de la source sœur) — 22 → 23, teeth ajoutée
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation de l'audit = gate d'entrée · D-07 formules DCF/IRR/VAN/DSCR/LTV/LTC absentes · D-08 Master Data Model · directive `:90-91` « NE PAS coder avant l'audit » · #6). L'alignement des bannières V18 des 4 modules-origine + le liage index sont clos (sessions précédentes). Rendements décroissants sur les micro-tâches doc → recherche d'un **défaut de correction réel** non-bloqué et à valeur produit (hunt Explore ciblé, hors périmètre V18/#8).
|
||||
|
||||
**Défaut trouvé (classe #6 anti-invention · DANS l'outil d'audit qualité lui-même).** `qa/audit_4big/q4lib/criteria.py:18` comptait les méthodes de test via une char-class **ASCII pure** : `_TEST_DEF_RE = re.compile(r"^\s*def (test_[A-Za-z0-9_]+)\s*\(", …)`. Or Python 3 (PEP 3131) autorise les identifiants **Unicode** : le test réel **`test_traçabilite_source`** (le `ç`) existe à `publiciste/tests/test_publiciste.py:99`, est **valide et exécuté** par unittest (« Ran 23 tests »). La regex ASCII le **loupait** → `crit_tests` publiait l'évidence fausse **« 22 méthodes test_* »** dans `quality_report.json` (réel = **23**). Le critère TESTS reste PASS (seuil 8) et le score inchangé (100) — mais **le fait publié était faux**, en violation du cœur anti-invention #6 de l'audit (« une note est recomputée à partir de faits vérifiables », `criteria.py:1-7`).
|
||||
|
||||
**Preuve que c'est un bug (pas by-design) — la source SŒUR le compte déjà correctement.** `qa/regression/reglib/discovery.py:23` compte le MÊME concept avec `_TEST_METHOD_RE = re.compile(r"^\s*def\s+(test_\w+)\s*\(")` — **`\w` est Unicode-aware par défaut en Python 3** → il compte publiciste = **23** (attesté dans `regression_plan.json`, byte-gaté, autorité de la colonne « Tests » des fiches via `count_tests`). Les deux outils QA **divergeaient d'un** sur publiciste (23 vs 22) ; `criteria.py` était le mauvais. Blast-radius vérifié : **un seul** nom de test non-ASCII dans tout le dépôt (scan AST des 24 suites) → publiciste seul impacté ; le « 22 méthodes mobile » du `daily_reports/2026-08-03` est un module distinct (coïncidence), non touché.
|
||||
|
||||
**Fix (chirurgical · single-source · aligné sur la sœur).** `criteria.py:18` : char-class ASCII `[A-Za-z0-9_]` → `\w` (Unicode), forme identique à `reglib.discovery`. **+ teeth** : nouveau test `test_tests_counts_non_ascii_method_names` dans `qa/audit_4big/tests/test_audit_4big.py` (fixture ASCII + `test_traçabilite_source`, exige « 2 méthodes ») — **prouvé mordant** : sur l'ancienne regex ASCII il compte 1 → assertion échoue ; sur `\w` → 2 → passe. La fixture existante du suite étant en ASCII, aucune régression sur `test_tests_counts_methods_and_thresholds` (« 5 méthodes » inchangé).
|
||||
|
||||
**Cascade d'artefacts régénérée (mémoire `artifact-reproducibility-gate` + `audit4big-rebuild-after-doc-edits`).** (1) `quality_report.json` régénéré **en dernier** → seule variation = évidence publiciste TESTS « 22 → 23 méthodes » ; verdict **PASS · 24/24 · min 100** inchangé. (2) L'ajout du test change le décompte de la matrice : `regression_gen.py build` (plan) **puis** `run` (matrice live) → `regression_run.json` **625 → 626 exécutés · 608 → 609 passés · 17 skippés · 0 échec** (le nouveau test lui-même + audit_4big se score lui-même). (3) Surfaces de prose re-dérivées : fiche `03_agents/qa/AGENT.md` (colonne audit_4big **34 → 35** ← `count_tests` ; agrégat **625/608 → 626/609**) — la première flaggée RED par `check_readme_claims`, la seconde par le même gate ; + `03_agents/erpnext_backend/AGENT.md:40` (claim live **625 → 626**, sourcé `regression_run.json`). Le `daily_reports/2026-08-11` (snapshot signé HEAD `18d8763`) et les logs sont des instantanés datés → laissés (mémoire `two-logging-channels`).
|
||||
|
||||
**Pourquoi pas un nouveau gate (#5).** La divergence est déjà **structurellement gatée** : `check_readme_claims` a **immédiatement rougi** (fiche QA vs `count_tests`) dès que la matrice a bougé, et la teeth ajoutée verrouille le comportement de `criteria.py`. Ajouter un gate « criteria.py == discovery.py » serait redondant — la réconciliation croisée existante mord déjà. Occurrence isolée (1 nom non-ASCII), corrigée à la source unique.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** rétabli après régénération complète. **0 code moteur V18** (bloqué #6), **0 commande VPS** (#8), **0 API externe**. Fichiers : `qa/audit_4big/q4lib/criteria.py` (regex) + `qa/audit_4big/tests/test_audit_4big.py` (teeth) + `qa/audit_4big/out/quality_report.json` + `qa/regression/out/{regression_plan,regression_run,MANIFEST}.json` + `03_agents/qa/AGENT.md` + `03_agents/erpnext_backend/AGENT.md` + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_075824` — CANAL STAKEHOLDER · actualisation du rapport quotidien `2026-08-11` (périmé dans la journée : `18d8763` → `ac595d0`, 9 commits, dont le fix de correction `audit_4big`)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation de l'audit = gate d'entrée · D-07 formules DCF/IRR/VAN/DSCR/LTV/LTC absentes · D-08 Master Data Model sur-ensemble strict · directive `:90-91` « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. L'alignement des bannières V18 des 4 modules-origine, le liage index racine et le fix `audit_4big` sont clos. Les 2 nouvelles directives 08-10 (`OTO_3D_STUDIO`, `COMPTE_CLIENT_COURRIELS`) restent hors périmètre repo.
|
||||
|
||||
**Chasse au bug menée d'abord (Explore very-thorough sur `*lib/` + `*_gen.py`).** Recherche d'un défaut de correction de calibre équivalent au fix regex ASCII de la session précédente (char-classes ASCII vs `\w`, off-by-one, comparateurs de seuil, dedup). **Rien de solide** — le seul candidat (`publiciste/lib/parser.py:433` `P0[1-9]`) est **correct** (9 projets P01-P09 par CLAUDE.md §Projets, pas un défaut de scaling). J'ai refusé de fabriquer un faux positif (#6).
|
||||
|
||||
**Tâche prioritaire non-bloquée retenue (classe `two-logging-channels`).** Le **canal stakeholder** `daily_reports/2026-08-11.md` était figé au HEAD `18d8763` (émis session `032800`) et citait régression **625/608** — mais **9 commits `[DTP-Worker]`** ont depuis porté l'arbre à `ac595d0` **le même jour**, dont l'**événement produit le plus notable** : le fix de correction `audit_4big` (`ac595d0`) qui a corrigé un vrai bug dans l'outil d'audit lui-même et porté la matrice à **626/609**. Le canal roadmap — celui qui met l'état dans le champ de vision de Michel — était donc **périmé dans la journée** de 9 commits et d'un chiffre live faux. Actualiser = **non-bloqué, sans invention, doc worker**, et exactement la fonction du canal (snapshot stakeholder à ne pas laisser lapser · chaque figure sourcée d'un artefact commité).
|
||||
|
||||
**Fait (une entrée/jour, rendue exacte — pas un doublon de rapport).** (a) En-tête : `HEAD attesté` `18d8763` → **`ac595d0`** + mention « actualisé `20260811_075824` » et note des 9 commits suivants. (b) Nouvelle section **« Jalons worker suivants (`18d8763` → `ac595d0`) »** : table des 6 jalons `[DTP-Worker]` (exactitude audit V18 · 4 bannières statut V18 · liage index racine · **fix correctness `audit_4big`**), chacun avec commit + nature + opposabilité au merge, + encart « événement produit le plus notable ». (c) Table « État courant » : HEAD `18d8763` → `ac595d0`, régression **625→626 / 608→609**, lecture stakeholder **609 passés + 17 skippés** ; paragraphe des skips re-dérivé (deux évolutions 624→625 puis 625→626, cause de chacune sourcée). La section « Portée / anti-churn (session `032800`) » **laissée telle quelle** (récit exact de cette session-là ; l'actualisation est transparente via l'en-tête + la nouvelle section — convention `two-logging-channels` : ne pas réécrire l'historique, actualiser en ajoutant).
|
||||
|
||||
**Honnêteté de sourçage (tous chiffres re-lus ce jour d'artefacts commités · #6).** HEAD `ac595d0` = `git rev-parse` · régression **626/609/17/0** = `regression_run.json totals` (lu ce jour) · audit 4Big **PASS · 24 modules · min 100 · bijection 24↔24 `ok`** = `quality_report.json` (inchangé) · les 9 hashes de commit = `git log 18d8763..HEAD`. Aucune figure figée.
|
||||
|
||||
**Sûreté des gates (vérifiée AVANT édition).** `check_readme_claims` **exclut** `daily_reports/` de son scan (`:8239` `not f.startswith(f"{D}/daily_reports/")`) → chiffres non parsés. `check_docs` link-checke les `daily_reports/` mais la nouvelle section n'ajoute **aucun lien relatif** (uniquement des hashes de commit et chemins en code-spans, non link-checkés). `guard_constraints` : aucun URL/usage interdit (DCF/IRR/DSCR/LTV/LTC ≠ termes proscrits).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact reconstruit** (rapport actualisé depuis les artefacts, pas l'inverse), **0 gate ajouté** (#5). Aucune commande VPS (#8), aucune API externe. Fichiers : `05_deliverables_mvp/daily_reports/2026-08-11.md` (actualisation) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_082825` — HARDENING correctness · le critère CLI de l'audit 4Big loupait les gardes `__main__` simple-quotées/Yoda (détection figée sur une graphie · même classe que le fix ASCII de `ac595d0`) — teeth ajoutée
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation de l'audit = gate d'entrée · D-07 formules DCF/IRR/VAN/DSCR/LTV/LTC absentes · D-08 Master Data Model · directive `:90-91` « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Les bannières V18 des 4 modules-origine, le liage index et le fix `audit_4big` (session `072823`) sont clos. Les 2 nouvelles directives 08-10 (`OTO_3D_STUDIO`, `COMPTE_CLIENT_COURRIELS`) restent hors périmètre repo.
|
||||
|
||||
**Vérif d'abord : le fix ASCII (`ac595d0`) a-t-il un rayon de blast CODE complet ?** La session `072823` avait vérifié le rayon **données** (« un seul nom de test non-ASCII dans le dépôt → publiciste seul »), mais **pas** le rayon **code** (d'autres regex partagent-elles le même défaut char-class-ASCII-sur-contenu-accentué ?). Sweep mené : (a) toutes les regex Python génératrices/`*lib/` (hors `/out/`, `/tests/`) → **2 compteurs de test** seulement, **tous deux `\w`** (`criteria.py:18` + `reglib/discovery.py:23`), et `check_readme_claims` **réutilise** `discovery.count_tests` (mono-source, `:663`) ; (b) `registry.py:30` `_JOB_RE` `[A-Za-z0-9_-]` = **domaine ASCII** (noms de jobs YAML, jamais accentués) — non-défaut ; (c) toutes les char-class ASCII des **gates shell** (`ci/*.sh`) → **toutes sur tokens ASCII-par-domaine** (slugs de rôles/portails kebab-case · lettres e-NCF · abréviations critères en CAPS · IDs d'invariants) — **aucune** ne scanne de la prose accentée. **Classe ASCII-vs-Unicode CLOSE au niveau code** (extension du check données de `072823`). Figures de régression en prose également **à jour** (aucun `624`/`625`/`607`/`608` stale hors snapshots datés). Le candidat `crit_cli` quote-style (littéral `"__main__"` figé) : **all 50 gardes du dépôt sont double-quotées** → aucun mis-scorage courant.
|
||||
|
||||
**Défaut trouvé (classe #6 · robustesse de détection de l'outil d'audit lui-même · MÊME classe que le fix ASCII).** `q4lib/criteria.py:78` détectait l'entrypoint CLI par un **littéral figé** : `'__name__ == "__main__"' in src` — assume **une seule graphie**. Or `if __name__ == '__main__':` (**simple quote**), `if "__main__" == __name__:` (**« Yoda »**, opérandes inversés) et `if __name__=='__main__':` (**sans espaces**) sont tous du **Python valide** = un entrypoint réel. Le littéral les **loupe silencieusement** → le critère CLI échouerait sur un module sain (**−20 pts**, chute possible < 95), en violation du cœur anti-invention #6 de l'audit (« une note est recomputée à partir de faits vérifiables ») : le critère **sous-spécifie** ce qu'est un entrypoint valide. Exactement la même racine que le fix ASCII (`ac595d0`) : *une détection assume une forme syntaxique là où le langage en admet plusieurs.*
|
||||
|
||||
**Pourquoi c'est légitime et pas de la sur-défense (#5).** Distinction du « ne pas coder défensivement des cas non-occurrents » : ici ce n'est **pas un gate** ajouté pour un cas d'entrée improbable d'un module produit — c'est la **correction de la logique de détection d'un critère existant** de l'**outil d'enforcement #6** lui-même, dont le contrat est d'évaluer correctement **n'importe quel Python valide**. La robustesse aux deux styles de guillemets est **dans le contrat** de l'outil, pas de l'ornement. Précédent direct : le fix ASCII a été accepté sur des bases identiques (regex ne matchant pas des identifiants Python valides).
|
||||
|
||||
**Fix (chirurgical · regex de forme, pas de graphie · teeth).** `criteria.py` : nouveau `_MAIN_GUARD_RE = re.compile(r"__name__\s*==\s*['\"]__main__['\"]|['\"]__main__['\"]\s*==\s*__name__")` (deux styles de guillemets · ordre inversé · espacement libre) ; `crit_cli` passe de `'__name__ == "__main__"' in src` à `_MAIN_GUARD_RE.search(src)`. **+ teeth** : `test_cli_detects_single_quoted_and_yoda_guards` (3 fixtures : simple-quote · Yoda · sans-espaces) — **prouvé mordant** : sur l'ancien littéral les 3 rendent `False` (garde loupée) ; sur la regex les 3 rendent `True`. Aucun risque de faux positif nouveau (le motif exige la séquence complète `__name__ == "__main__"`, comme le littéral).
|
||||
|
||||
**Sortie inchangée (robustesse pure · #6 auto-vérifié).** Les 50 gardes du dépôt étant double-quotées et **toujours matchées** par la nouvelle regex, `crit_cli` rend un résultat **identique pour les 24 modules** → **`quality_report.json` byte-identique** (correctement NON régénéré : `audit_4big` **s'auto-exclut** de son propre scoring — les modules qa scorés sont `qa-audit-5d`/`qa-acceptance`/`qa-regression`, pas `audit_4big` ; même patron d'auto-exclusion que la régression). Le fix ne change **aucun score** — c'est une garantie de robustesse, pas une correction de note.
|
||||
|
||||
**Cascade d'artefacts (uniquement la matrice de régression · mémoire `artifact-reproducibility-gate`).** L'ajout du test augmente le décompte de la suite `audit_4big` : `regression_gen.py build` (plan) **puis** `run` (matrice) → `regression_run.json` **626 → 627 exécutés · 609 → 610 passés · 17 skippés · 0 échec** (le nouveau test). Surfaces de prose gatées re-dérivées (flaggées RED par `check_readme_claims`, corrigées) : fiche `03_agents/qa/AGENT.md` (colonne `audit_4big` **35 → 36** ← `count_tests` ; agrégat **626/609 → 627/610**) + `03_agents/erpnext_backend/AGENT.md:40` (claim live **626 → 627**). Le `daily_reports/2026-08-11` (snapshot signé) et les logs sont des instantanés datés → laissés (`two-logging-channels`).
|
||||
|
||||
**Pourquoi pas un nouveau gate (#5).** La divergence de compte est déjà **structurellement gatée** (`check_readme_claims` a immédiatement rougi la fiche QA + le compte `audit_4big`) et la teeth verrouille le comportement de `crit_cli`. Occurrence latente (0 garde simple-quotée aujourd'hui) mais correction de contrat de l'outil, pas un cas d'entrée produit → pas de gate additionnel.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** rétabli après régénération. **0 code moteur V18** (bloqué #6), **0 commande VPS** (#8), **0 API externe**. Fichiers : `qa/audit_4big/q4lib/criteria.py` (regex) + `qa/audit_4big/tests/test_audit_4big.py` (teeth) + `qa/regression/out/{regression_plan,regression_run,MANIFEST}.json` + `03_agents/qa/AGENT.md` + `03_agents/erpnext_backend/AGENT.md` + ce journal. `quality_report.json` **non touché** (auto-exclusion prouvée).
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_085834` — HARDENING correctness · l'alignement du compteur de tests `audit_4big`↔`discovery` (fix ASCII `ac595d0`) était INCOMPLET : `def ` (un espace figé) ≠ `def\s+` de la sœur — teeth ajoutée, classe désormais close
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation de l'audit = gate d'entrée · D-07 formules DCF/IRR/VAN/DSCR/LTV/LTC absentes · D-08 Master Data Model sur-ensemble strict · directive `:90-91` « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Les bannières V18 des 4 modules-origine, le liage index et les 2 fixes `audit_4big` (regex ASCII `ac595d0` · garde `__main__` `fd99362`) sont clos. Les 2 directives 08-10 (`OTO_3D_STUDIO`, `COMPTE_CLIENT_COURRIELS`) restent hors périmètre repo.
|
||||
|
||||
**Défaut trouvé (classe #6 · MÊME racine que les 2 fixes précédents : « détecter la forme, pas une graphie » · + il COMPLÈTE un fix laissé à moitié).** Le fix ASCII `ac595d0` (session `072823`) avait pour but explicite d'**aligner** le compteur de méthodes de test d'`audit_4big` (`q4lib/criteria.py:_TEST_DEF_RE`) sur celui de la source sœur `reglib/discovery.py:_TEST_METHOD_RE` — autorité **byte-gatée** de la colonne « Tests » des fiches (réutilisée par `check_readme_claims` via `discovery.count_tests`). Il a aligné le **char-class** (`[A-Za-z0-9_]` → `\w`, pour les noms accentués), mais a laissé **une seconde divergence intacte** sur le **séparateur `def`** :
|
||||
- `discovery.py:23` = `r"^\s*def\s+(test_\w+)\s*\("` — `\s+` (espacement libre)
|
||||
- `criteria.py:18` = `r"^\s*def (test_\w+)\s*\("` — **un seul espace littéral figé**
|
||||
|
||||
Or `def test_x(` (deux espaces) et `def\ttest_x(` (tabulation) sont du **Python valide** (PEP 8 recommande un espace, le langage n'en impose pas). Le littéral `def ` les **loupe** → `crit_tests` sous-compterait, en **désaccord d'un ou plusieurs** avec la sœur — exactement le type de dérive d'évidence #6 que le fix ASCII prétendait clore. Les **deux compteurs du MÊME concept** doivent coïncider sur **toute graphie valide**, pas seulement les identifiants Unicode.
|
||||
|
||||
**Occurrence (comme la garde `__main__` de `fd99362`) : latente, 0 aujourd'hui.** `grep -rnE '^\s*def( +|\t)test_'` sur tous les `test_*.py` = **vide** (tous en un-espace PEP 8) → aucun mis-comptage courant, `quality_report.json` **byte-identique**. Mais c'est la **correction du contrat de détection de l'outil d'enforcement #6** (compter correctement n'importe quel Python valide + coïncider avec la sœur), pas une sur-défense d'un cas d'entrée produit improbable — même base d'acceptation que `fd99362`. Divergence sœur de recensement de fichiers vérifiée aussi : `criteria` recurse (`os.walk`) là où `discovery` liste à plat (`os.listdir`) — **aucun `tests/*/test_*.py` imbriqué dans le dépôt** (`find` vide) → non-occurrent, axe distinct (découverte de fichiers, pas détection de méthode), **laissé** (discovery = autorité du gate ; ne pas muter son comportement pour un cas nul).
|
||||
|
||||
**Fix (chirurgical · forme, pas graphie · single-source-de-forme · teeth).** `criteria.py:18` : `def ` → `def\s+`, forme **strictement identique** à `discovery._TEST_METHOD_RE` (commentaire mis à jour : les deux compteurs doivent coïncider). **+ teeth** : `test_tests_counts_non_single_space_def_separator` (fixture 3 méthodes : un-espace · deux-espaces · tab) — **prouvé mordant** : ancien regex `def ` compte **1** des 3 (loupe deux-espaces + tab) ; nouveau `def\s+` compte **3**. La fixture ASCII existante (`test_tests_counts_methods_and_thresholds` « 5 méthodes ») reste en un-espace → aucune régression.
|
||||
|
||||
**Sortie inchangée (robustesse pure · #6 auto-vérifié).** Aucun module du dépôt n'ayant de `def` multi-espace/tab, `crit_tests` rend une évidence **identique pour les 24 modules** → `quality_report.json` **byte-identique** (rebuild `audit_4big_gen.py build` → `git diff --stat` **vide** ; `audit_4big` **s'auto-exclut** de son propre scoring — les modules qa scorés sont `qa-audit-5d`/`qa-acceptance`/`qa-regression`, pas lui). Le fix ne change **aucun score**.
|
||||
|
||||
**Cascade d'artefacts (uniquement la matrice de régression · mémoire `artifact-reproducibility-gate`).** L'ajout du test augmente le décompte de la suite `audit_4big` : `regression_gen.py build` (plan) **puis** `run` (matrice) → `regression_run.json` **627 → 628 exécutés · 610 → 611 passés · 17 skippés · 0 échec**. Surfaces de prose gatées re-dérivées (flaggées RED par `check_readme_claims`, corrigées) : fiche `03_agents/qa/AGENT.md` (colonne `audit_4big` **36 → 37** ← `count_tests` ; agrégat **627/610 → 628/611**) + `03_agents/erpnext_backend/AGENT.md:40` (claim live **627 → 628**). Le `daily_reports/2026-08-11` (snapshot signé) et les logs sont des instantanés datés → laissés (`two-logging-channels`).
|
||||
|
||||
**Classe désormais close (les 2 axes de la coïncidence des compteurs).** Après `072823` (char-class) + cette session (séparateur), `criteria._TEST_DEF_RE` et `discovery._TEST_METHOD_RE` sont **forme-identiques** ; l'axe fichiers (recursion) est non-occurrent et documenté laissé. **Pas de nouveau gate (#5)** : la divergence de compte est déjà **structurellement gatée** (`check_readme_claims` a immédiatement rougi la fiche QA dès que la matrice a bougé) et la teeth verrouille le comportement. Occurrence latente → correction de contrat de l'outil, pas de gate additionnel.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** rétabli après régénération. **0 code moteur V18** (bloqué #6), **0 commande VPS** (#8), **0 API externe**. Fichiers : `qa/audit_4big/q4lib/criteria.py` (regex) + `qa/audit_4big/tests/test_audit_4big.py` (teeth) + `qa/regression/out/{regression_plan,regression_run,MANIFEST}.json` + `03_agents/qa/AGENT.md` + `03_agents/erpnext_backend/AGENT.md` + ce journal. `quality_report.json` **non touché** (auto-exclusion prouvée, `git diff --stat` vide).
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_092842` — CANAL STAKEHOLDER · actualisation daily report au HEAD courant `35de616` (l'arc de fixes `audit_4big` a fait bouger la régression deux fois de plus depuis l'épinglage `ac595d0`)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation de l'audit = gate d'entrée · D-07 formules DCF/IRR/VAN/DSCR/LTV/LTC absentes · D-08 Master Data Model · directive `:90-91` « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Les 3 fixes `audit_4big` (char-class `ac595d0` · garde `__main__` `fd99362` · séparateur `def` `35de616`), les 4 bannières V18 et le liage index sont clos.
|
||||
|
||||
**Vérif d'abord — pas de dérive de chiffres avant de choisir la tâche.** Artefact autoritaire `regression_run.json` (`totals`) = **628 ran · 611 passés · 17 skippés · 0 échec** (PASS). Les surfaces de prose **gatées** qui figent l'agrégat sont à jour : fiche `03_agents/qa/AGENT.md:35` (**628/611**) + `03_agents/erpnext_backend/AGENT.md:40` (**628 · 24 suites**) — concordent avec l'artefact, aucune dérive (`check_readme_claims` vert). Le README racine ne fige aucun compte de tests (renvoie à l'artefact byte-gaté). **Classe de dérive numérique : nulle.**
|
||||
|
||||
**Défaut trouvé (canal stakeholder en retard sur le HEAD · pas un bug code).** Le `daily_reports/2026-08-11.md` était **épinglé à HEAD `ac595d0`** (régression **626/609**), mais **deux commits worker de plus** ont atterri le **même jour** après sa dernière actualisation (`9e02c38`) : `fd99362` (garde `__main__` → 627/610) puis `35de616` (séparateur `def` → 628/611). Le canal stakeholder — dont le contrat (`two-logging-channels`) est de porter le snapshot roadmap au **point courant du jour** avec chaque chiffre sourcé d'un artefact commité — accusait donc **2 commits + 2 pas de régression** de retard. C'est exactement la tâche de currency que le worker maintient (précédents `8ec11de`, `9e02c38` le même jour).
|
||||
|
||||
**Fix (currency pure · re-dérivation d'artefacts, zéro chiffre figé · #6).** Daily report ré-épinglé `ac595d0` → `35de616` :
|
||||
- En-tête : HEAD attesté `ac595d0` → `35de616` · « 9 → **11 commits worker** » · actualisé `092842`.
|
||||
- §« Jalons worker suivants » : titre `→ ac595d0` → **`→ 35de616`**, « six → **huit jalons** » ; **2 lignes ajoutées** (`fd99362` garde `__main__` −20 pts CLI évités · `35de616` séparateur `def` complète le fix ASCII), chacune avec sa cascade de régression re-dérivée.
|
||||
- L'encart « événement le plus notable » recadré du seul `ac595d0` vers l'**arc des 3 fixes de même racine** (« détecter la forme, pas une graphie »), **classe close**.
|
||||
- §État courant : HEAD `35de616` · matrice **626/609 → 628/611** ; prose de progression « deux → **quatre fois** depuis 08-06 » avec les 3 pas du jour explicités (625→626→627→628 / 608→609→610→611).
|
||||
- §Portée : ligne **Currency `092842`** (re-épinglage + provenance des chiffres).
|
||||
|
||||
**Pourquoi c'est légitime et non du churn (#5).** Ce n'est pas un re-write d'un snapshot daté figé (ceux-là restent, `two-logging-channels`) : c'est la **mise au point courant du jour même** du canal stakeholder, dont le `9e02c38` précédent avait fait exactement autant (18d8763→ac595d0). Chaque chiffre injecté est **re-dérivé** de `regression_run.json` (`totals` = 628/611), aucun figé. **0 fichier de production · 0 artefact reconstruit · 0 gate ajouté · 0 code moteur V18 · 0 commande VPS · 0 API externe.**
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé · le daily report tracked passe `check_docs` + `guard_constraints`). Fichiers : `05_deliverables_mvp/daily_reports/2026-08-11.md` + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260811_095844` — COUVERTURE produit · teeth `publiciste` sur les prix à ESPACE INSÉCABLE (U+00A0) · classe d'entrée réelle FR/ES 100 % non testée jusqu'ici
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte · `regression_run.json totals` = 628/611/17). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation de l'audit = gate d'entrée · D-07 formules DCF/IRR/VAN/DSCR/LTV/LTC absentes · D-08 Master Data Model · directive `:90-91` « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Les 2 directives 08-10 (`OTO_3D_STUDIO`, `COMPTE_CLIENT_COURRIELS`) restent hors périmètre repo. Les micro-tâches doc + l'arc `audit_4big` sont clos → recherche d'une tâche à **valeur produit réelle**.
|
||||
|
||||
**Chasse au bug d'abord (2 passes indépendantes) → aucun défaut, refus de fabriquer (#6).** Explore very-thorough sur les `*_gen.py`/`*lib/` **hors** `audit_4big`/`regression` (déjà balayés) : classes visées = char-class ASCII-vs-Unicode, off-by-one, comparateurs de seuil, sets à doublons, arithmétique dérivée, collisions de clés. **Rien de solide.** Les 2 candidats remontés étaient des **non-défauts** : (a) `publiciste/lib/parser.py:77` `.replace(" ", " ")` — l'Explore l'a lu « no-op » ; **hexdump** montre `c2 a0` = le 1er argument est un **NBSP** → c'est une **normalisation NBSP→espace réelle**, pas un no-op (l'Explore s'est fait piéger par des glyphes identiques à l'œil — exactement le risque byte-invisible) ; (b) `seo_gen.py` global-keyword digit-check = latent masqué par invariant. **Aucun n'est un bug** → non touchés.
|
||||
|
||||
**Vrai locus trouvé = un TROU DE COUVERTURE, pas un bug.** `parse_number`/`parse_price` de `publiciste` sont **conçus** pour l'**espace insécable U+00A0** comme séparateur de milliers — la graphie que Word/InDesign FR/ES insèrent dans les prix réels des `data_room` (« DOP 8 850 000 » avec NBSP). La capture repose sur la char-class **Unicode-whitespace** de `parser.py:74` (`[0-9\s.,` + un **NBSP littéral** `c2 a0` redondant avec `\s`). Or **aucun des 23 tests** n'exerçait le NBSP : `test_prix_formats:39` (« DOP 8 850 000 ») utilise des **espaces ASCII 0x20** (`hexdump` confirmé), pas des NBSP. La classe d'entrée réelle la plus caractéristique du parser était **100 % non testée** — et c'est précisément ce trou qui a permis à l'Explore de mésinterpréter la ligne 77 comme du code mort.
|
||||
|
||||
**Preuve end-to-end + bite ACCURATE (aucune affirmation non vérifiée · #6).** (1) `parse_price("DOP 8 850 000")` = **8850000.0** ✓, `parse_number("1 250")` = **1250.0** ✓ (exécuté). (2) Condition de bite **déterminée empiriquement, pas devinée** : la char-class porte `\s` **et** un NBSP littéral → retirer **l'un** est inoffensif (redondance belt-and-suspenders) ; le bite ne survient que si l'on **réduit la classe à de l'ASCII pur** `[0-9.,]` (retrait de TOUTE prise en charge whitespace-Unicode) → le test échoue alors `8.0 != 8850000.0` (prouvé : cassé RED, restauré VERT). Le commentaire du test a été **corrigé** d'une 1re rédaction fausse (« retirer `\s` mord ») vers la condition réelle — honnêteté de claim (#6). Même racine que l'arc `audit_4big` : « détecter la forme, pas une graphie ».
|
||||
|
||||
**Hygiène byte-invisible (leçon appliquée au test lui-même).** 1re rédaction du test contenait des **NBSP littéraux** (`nbsp = " "`) — je réintroduisais l'octet invisible que je dénonce. Réécrit en **escape explicite ` `** (`nb = " "`) → **0 octet invisible** dans le fichier de test (vérifié `grep -c`), intention 100 % lisible. La char-class de **production** (ligne 74), elle, est **laissée telle quelle** : son NBSP littéral est **redondant mais correct** (`\s` le couvre) ; l'éditer serait du churn cosmétique sur un non-défaut (#5, `verify-non-defects`) avec risque de dérive d'artefact pour zéro changement de comportement.
|
||||
|
||||
**Fix = 1 test teeth `test_prix_espace_insecable_milliers`** (classe `TestNumberParsing`) : `parse_price`/`parse_number`/`parse_int` sur NBSP (DOP/USD milliers + décimale). Distinct de l'arc récent : ce n'est **pas** un test de l'outil d'audit mais de la **couverture d'un module produit** sur une **entrée réelle** — valeur produit directe.
|
||||
|
||||
**Cascade d'artefacts régénérée (mémoires `artifact-reproducibility-gate` + `audit4big-rebuild-after-doc-edits`).** (1) `audit_4big_gen.py build` → `quality_report.json` : évidence publiciste TESTS **« 23 → 24 méthodes »** ; **verdict PASS · 24/24 · min 100 inchangé** (aucun score bougé — le seul delta est le compte). (2) `regression_gen.py build` puis `run` → `regression_run.json` **628 → 629 exécutés · 611 → 612 passés · 17 skippés · 0 échec** (publiciste suite ran 23→24 / passed 21→22 — les 2 « non-incréments » = ses tests oracle jsonschema neutralisés par `python -S`, cf. `regression-baseline-17-skips-by-design`). (3) Surfaces de prose gatées re-dérivées (flaggées RED par `check_readme_claims`, corrigées) : fiche `03_agents/publiciste/AGENT.md:95` (cellule Tests **23 → 24** ← `count_tests`) + agrégat `03_agents/qa/AGENT.md:35` (**628/611 → 629/612**) + `03_agents/erpnext_backend/AGENT.md:40` (claim live **628 → 629**). Le `daily_reports/2026-08-11` (snapshot signé) et les logs = instantanés datés → laissés (`two-logging-channels`).
|
||||
|
||||
**Pourquoi pas un nouveau gate (#5).** La divergence de compte est déjà **structurellement gatée** (`check_readme_claims` a immédiatement rougi la fiche publiciste + l'agrégat QA dès le rebuild) et la teeth verrouille le comportement NBSP. Trou de couverture isolé, comblé à la source → pas de gate additionnel.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** rétabli après régénération. **0 code moteur V18** (bloqué #6), **0 module de production touché** (seul un test ajouté · la char-class de prod laissée intacte), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `05_deliverables_mvp/publiciste/tests/test_publiciste.py` (teeth) + `qa/audit_4big/out/quality_report.json` + `qa/regression/out/{regression_plan,regression_run,MANIFEST}.json` + `03_agents/{publiciste,qa,erpnext_backend}/AGENT.md` + ce journal.
|
||||
@@ -0,0 +1,540 @@
|
||||
# Activity Log · 2026-08-12 · Claude Code DTP Worker
|
||||
|
||||
## Session `20260812_153203` — MÉTA-MAINTENANCE · réduction de l'**index de mémoire agent** au-dessus de sa limite de taille (cause-racine du churn de session), plutôt qu'un énième sweep « CLEAN » ou une re-currency du rapport (le motif que les ~10 sessions précédentes du jour ont elles-mêmes signalé)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Matrice stakeholder re-dérivée **ce jour** depuis les artefacts commités (aucun chiffre figé · #6) : **635 / 618 / 17**. Séquence moteur V18 **intégralement bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). `daily_reports/2026-08-12.md` vérifié **dé-épinglé du HEAD** (session `150201`) → aucune re-currency due. Registre des décisions : citations résolvent (session `123144`).
|
||||
|
||||
**Diagnostic — la vraie cause n'est pas un défaut de code, c'est un défaut d'outillage.** Le journal des ~10 dernières sessions du jour est saturé de deux motifs : (a) des *sweeps de vérification* « CLEAN, teeth-prouvé, NOT a gate » sur des axes de plus en plus marginaux, et (b) des *re-currency* du rapport stakeholder qui, en épinglant un HEAD, se re-périmaient à chaque commit (boucle enfin brisée en `150201`). Ces deux motifs sont la **conséquence** d'une saturation : la mémoire agent avait déjà consigné, pour la plupart de ces axes, « déjà balayé · by-design · ne pas re-flag » — mais l'**index de mémoire ne se chargeait que partiellement**. Le harnais l'a signalé explicitement ce jour : *« MEMORY.md is 29KB (limit: 24.4KB) — index entries are too long. Only part of it was loaded. »* Un index tronqué ⟹ des insights « NOT a gate / by-design / don't re-flag » invisibles ⟹ re-litige du même terrain ⟹ churn. **Corriger l'outil, pas surveiller le symptôme** (même principe structurel que le dé-épinglage du HEAD en `150201`).
|
||||
|
||||
**Action — compaction de l'index de mémoire (`memory/MEMORY.md`, hors dépôt, maintenance d'outillage).** L'index avait accumulé le **corps entier** de chaque mémoire dans sa ligne (jusqu'à **1040 caractères**/ligne ; guide = « une ligne sous ~200 »). Le détail vit déjà dans les fichiers-sujets `.md` (un par entrée) ; l'index ne doit porter qu'un **hook** de pertinence. Chaque ligne réécrite en hook concis, en **préservant le distinguo anti-churn** de chaque note (« NOT a gate », « by-design », « don't re-flag », le piège-clé). Résultat vérifié :
|
||||
- Taille **30 117 → 17 264 octets** (sous la limite 24.4 Ko, avec marge → l'index se charge désormais **en entier**).
|
||||
- **88 entrées d'index ↔ 88 fichiers-sujets** = **bijection prouvée** (script : chaque `.md` du dossier est référencé, chaque lien de l'index pointe un fichier existant). Une entrée initialement perdue à la réécriture (`link-label-target-mismatch`) **détectée par ce contrôle de bijection et restaurée** avant clôture — aucune mémoire perdue.
|
||||
- Ligne la plus longue **1040 → 252** caractères.
|
||||
|
||||
**Pourquoi PAS un sweep / une currency / un fix / un gate.** (a) Aucun défaut de code : les sweeps du jour ont épuisé les axes réels ; en ré-ouvrir un serait le churn même que ce diagnostic identifie. (b) Aucune currency due : rapport dé-épinglé, registre à jour. (c) Aucun fix : V18 bloqué D-06 (#6), 0 défaut résiduel. (d) Aucun gate (#5) : l'index de mémoire est **hors dépôt** (outillage agent, non gaté par nature) ; le corriger est de la maintenance, pas une invariant de produit.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucun fichier de dépôt de production/gate touché). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers de **dépôt** : ce journal seul. Maintenance **hors dépôt** (mémoire agent) : `memory/MEMORY.md` compacté 30→17 Ko, bijection 88/88 restaurée.
|
||||
|
||||
## Session `20260812_150201` — STRUCTUREL · fin de la boucle de re-currency du rapport stakeholder — **dé-épinglage total du HEAD** dans le document (la session précédente n'avait ôté le hash figé que du *sous-titre* de la table roulante, en le **laissant** dans l'en-tête du document `HEAD attesté: 4a13557`) → la cause du churn est supprimée, pas surveillée
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 **intégralement bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||
|
||||
**Locus = la cause récurrente du churn de session, pas un énième sweep.** Le journal des ~8 dernières sessions du jour montre une **boucle** : chaque commit worker déplace le HEAD ⟹ l'en-tête du `daily_report` qui **atteste** « HEAD attesté `<hash>` » périme ⟹ la session suivante « réconcilie la currency » ⟹ nouveau commit ⟹ HEAD re-périmé, *ad infinitum*. La session `143156` a **diagnostiqué** ce mécanisme et converti la §« Jalons de vérification » en **table roulante** — mais n'a ôté le hash figé que du **sous-titre de section**, en **laissant** l'épinglage `**HEAD attesté** : \`4a13557\`` dans l'**en-tête du document** (lignes 9-13) + « portée au HEAD courant `4a13557` » dans la ligne `Session` + « portant l'arbre au HEAD courant `4a13557` » dans la trajectoire §État courant. Le fix était donc **partiel** : la cause survivait dans 4 emplacements, et *mon propre commit d'aujourd'hui* aurait re-périmé l'en-tête → churn perpétué.
|
||||
|
||||
**Correction structurelle (4 emplacements de HEAD figé neutralisés · document éditorial hors gate).**
|
||||
- **En-tête `Session`/`HEAD attesté`** (l.3-13) — remplacé le bloc `**HEAD attesté** : \`4a13557\`` + la trace de hashes par un **« Point d'attestation »** explicite : *le rapport n'épingle volontairement AUCUN hash de HEAD* (un hash périme à chaque commit = cause structurelle de la boucle) ; le HEAD courant **se lit** (`git log -1`), il ne **se fige** pas ; ce qui **fait foi** est la **matrice** (`635/618/17 · PASS`, dérivée d'artefacts commités, stable jusqu'au prochain fix de correctness). Les hashes des §Jalons sont désormais qualifiés de **jalons historiques immuables**, pas d'attestation de HEAD courant.
|
||||
- **§Contexte** (l.25-29) — « ont porté l'arbre au **HEAD courant** `ee88d36` » → passé historique « à l'**émission initiale**… avaient porté l'arbre à `ee88d36` » (framing origine, plus une prétention de HEAD courant).
|
||||
- **Sous-titre §Jalons de vérification** — « → HEAD courant `4a13557` » → « depuis le 5ᵉ fix `1992ee6` · au fil de l'eau » (dé-épinglé).
|
||||
- **Trajectoire §État courant** — « portant l'arbre au HEAD courant `4a13557` » → « trace dans la table roulante ci-dessus, non ré-épinglée à un hash ».
|
||||
|
||||
**Résultat.** Le rapport **ne peut plus périmer** au prochain commit worker : il n'affirme plus aucun HEAD mutable. La boucle de re-currency (≥ 6 sessions consommées) est **cassée à la racine**. Les hashes restants (Jalons tables + `35de616`/`ee88d36` de l'émission initiale) sont des **faits historiques** stables. Aucun chiffre inventé — matrice re-dérivée de l'artefact commité (#6).
|
||||
|
||||
**Pourquoi PAS un gate (#5).** Le rapport est un **document éditorial explicitement hors gate** (`ci/check_docs.sh:50` `continue` sur `daily_reports/*` · `ci/check_readme_claims.sh:8239` exclut le préfixe). Gater de la prose stakeholder serait fragile et de faible valeur ; le **durcissement structurel** (aucun hash mutable à surveiller) **supprime la cause** au lieu de poser une sentinelle sur elle — sœur exacte de la doctrine `verify-non-defects` / « supprime la cause » invoquée (à moitié) par la session précédente.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — 0 dépendance de gate sur le rapport, confirmé par grep sur `ci/`+`.gitea/`). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit** (matrice stable, byte-repro implicite), **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (4 dé-épinglages de HEAD) + ce journal.
|
||||
|
||||
## Session `20260812_143156` — CURRENCY · rapport stakeholder du jour porté au HEAD courant `4a13557` (le canal s'arrêtait à `fb2d3e1` tout en attestant « HEAD courant ») + §« Jalons de vérification » convertie en **table roulante** pour briser la boucle de re-figeage du HEAD à chaque commit
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection couverture 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||
|
||||
**Chasse au défaut d'abord → vrai locus = currency du canal stakeholder (classe `e15129b`).** Plutôt qu'un énième sweep « CLEAN sans fix » (saturation des axes déjà couverts, reconnue par les sessions précédentes), j'ai audité la **currency** du rapport `daily_reports/2026-08-12.md`. Il **atteste** en en-tête « **HEAD attesté** `fb2d3e1` » + « **au HEAD courant** », or **deux commits worker** ont suivi le même jour sans être remontés : `e15129b` (la currency `133154` elle-même) et `4a13557` (sweep FRAIS mode d'arrondi). `git log fb2d3e1..HEAD` = ces 2 commits ; `grep -c` de chacun dans le rapport = **0** avant édition (dérive confirmée). Note : `e15129b` a **créé** la §« Jalons » mais **sans s'y inscrire** (auto-omission) — d'où la boucle de churn observée : chaque commit de currency re-fige un HEAD qui périme au commit suivant.
|
||||
|
||||
**Correction (document éditorial · 4 blocs cohérents) :**
|
||||
- **En-tête** — `Session` → `20260812_143156` ; `HEAD attesté` **`fb2d3e1` → `4a13557`** ; distinction claire 5ᵉ fix (`1992ee6`, matrice `634→635`) vs commits de vérif/currency byte-identiques suivants (matrice **stable 635/618/17**).
|
||||
- **§« Jalons de vérification depuis la 4ᵉ vague »** — convertie en **table roulante** : bandeau explicite « tout commit post-5ᵉ-fix est **non opposable par construction** ; la matrice ne bougera qu'au prochain fix de correctness ; cette section liste ces jalons au fil de l'eau **sans re-figer le HEAD ailleurs** » → **brise structurellement la boucle de churn** (plus de hash épinglé dans l'intro). En-tête de section `→ HEAD courant fb2d3e1` → `4a13557`.
|
||||
- **2 lignes de table ajoutées** — `e15129b` (currency, non opposable) + `4a13557` (sweep FRAIS arrondi lecture-seule, journal only, aucun mis-flag, non opposable). Compte « quatre » → « **six** » jalons.
|
||||
- **§« État courant »** trajectoire — « 4 commits de vérification (`…/fb2d3e1`) » → « **six** commits de vérification / currency (`…/fb2d3e1`/`e15129b`/`4a13557`) portant l'arbre au HEAD courant `4a13557` ».
|
||||
|
||||
**Zéro invention (#6).** Tous les chiffres re-dérivés de l'artefact commité : `regression_run.json.totals` = **635/618/17 · PASS**. La nature de chaque jalon reprend verbatim son message de commit (`git show --stat` : `e15129b` = activity_log + daily_report ; `4a13557` = activity_log **seul**, sweep sans note in-situ). Aucune décision/verdict modifié ; D-01→D-08 inchangés.
|
||||
|
||||
**Pourquoi PAS un gate (#5).** Dérive de currency d'un document éditorial (le rapport = *surface, ne tranche pas*), comme `daily_reports` par nature ; un gate line-number/hash sur de la prose stakeholder serait fragile (chaque commit worker le rougirait) et de faible valeur. Le durcissement **structurel** appliqué (table roulante, plus de HEAD re-figé dans l'intro) vaut mieux qu'un gate : il **supprime la cause** du churn au lieu de la surveiller.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — le rapport est un document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit** (matrice stable, byte-repro implicite), **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (4 blocs de currency + table roulante) + ce journal.
|
||||
|
||||
## Session `20260812_140156` — VÉRIF · nouvel axe FRAIS « mode d'arrondi (banquier / round-half-to-even) » balayé repo-wide → CLEAN teeth-prouvé, 0 défaut · replayable · sans fix · sans gate
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu de l'artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (24 suites vertes · PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · all PASS**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` **à jour au HEAD `e15129b`** (grep-count 5 hashes 4ᵉ-vague = 10 hits) → rien à ré-émettre.
|
||||
|
||||
**Chasse au défaut d'abord (angle jamais balayé en ~45 sweeps : le MODE D'ARRONDI).** Aucun des sweeps antérieurs (arithmétique dérivée, date/time, parsing money/percent, déterminisme hash/locale/TZ/ordre) n'avait attaqué la question « Python `round()` est round-half-to-**even** (banquier) — un site s'appuie-t-il implicitement sur round-half-**up** ? ». Un `.5` exact y renverrait une valeur qui diffère de l'attente naïve (`round(2.5)==2`, pas 3), byte-repro-aveugle si l'échantillon figé ne tombe pas sur la bande. Recensement des `round()` de production (hors `test`, hors `_round1`) = **8 sites** sur 4 modules : `finlib/gate.py:54,64,72,96` · `genlib/scorer.py:57,81` · `lib/parser.py:101` · `audit_4big/scoring.py:8` (docstring).
|
||||
|
||||
**Classement des 8 sites (tous NON-DÉFAUT, teeth-prouvé) :**
|
||||
- **`scorer.py:57` `round(20*filled/total)`** (axe Complétude) — SEUL site `round()`-sur-ratio dont l'immunité au verdict est **non-locale**. Teeth : `total=16` champs 🔴 → la bande `.5` **EST atteignable** (filled∈{2,6,10,14} → 2.5/7.5/12.5/17.5, banquier diffère de half-up de ±1 pt). **Mais verdict-immune** : `_statut` n'émet « complete » que si `not champs_manquants` ⟹ `filled==total` ⟹ `pts1 == round(20) == 20` **exact** ; l'arrondi banquier ne s'engage donc **jamais** sur le chemin décisionnel. Reproduit end-to-end : brief complet → completude=20/statut=complete ; masterplan vidé → completude=11, 7 champs manquants, statut=en_developpement (**jamais** complete). Le seul effet de l'arrondi = ±1 pt sur le **score partiel affiché** d'un brief **incomplet** — cosmétique, aucun `spec` ne définit half-up, découplé du verdict, épinglé byte-repro. **Non-défaut.**
|
||||
- **`scorer.py:81`** (axe Anti-gap prix) — même famille, immunité par **inclusion** : une cellule manquante ⟹ `champs2 ⊆ champs_manquants` non vide ⟹ verdict ≠ complete, indépendamment de l'arrondi de `pts2` (même si `pts2` pouvait remonter à 20 avec `filled/total ≥ 0.975`, la clause `not champs_manquants` bloque). **Non-défaut.**
|
||||
- **`gate.py:64,72,96`** (montants USD, `round(...,2)`) — arrondi **au cent** = montant métier canonique (un USD ne se paie pas au sous-cent). L'arrondi banquier au sous-cent est immatériel : `ok = paid+1e-9 >= required` compare des valeurs déjà en cents. **Non-défaut.**
|
||||
- **`gate.py:54` `_round1`** (percents user-facing) — la classe « affichage arrondi ⟺ gate booléen » est **déjà FERMÉE** ce cycle (gardes `:105`/agrégat des fixes `85cd625`/`1992ee6` + notes `113134`). **Non-défaut** (déjà couvert).
|
||||
- **`parser.py:101` `int(round(n))`** — parse la **quantité** de typologies (entier métier : pas de demi-unité) → bande `.5` sans signification. **Non-défaut** (domaine entier).
|
||||
- **`audit_4big/scoring.py:8`** — **docstring** (formule §), pas du code ; scoring réel prouvé immune par `ea8458d` (score-au-seuil = quantité UNIQUE). **Non-défaut.**
|
||||
|
||||
**Teeth du détecteur (prouvé mordant).** Confirmé au runtime que `round()` EST banquier (`round(2.5)=2 · round(3.5)=4 · round(0.5)=0`) — l'axe n'est pas un homme de paille ; ET que la bande `.5` est réellement atteignable sur `scorer.py:57`. Le CLEAN n'est donc pas un faux-vert : la propriété testée existe, elle est simplement **découplée du verdict** partout.
|
||||
|
||||
**Pourquoi PAS un fix ni un gate ni une note in-situ (#5/#6).** Aucun défaut (8/8 non-défauts). Passer à `Decimal(ROUND_HALF_UP)` serait de la sur-ingénierie sur un effet cosmétique ±1 pt inatteignable au verdict, + casserait le déterminisme byte-repro. **Pas de note in-situ non plus** (contrairement à `rate()`/`_cond_documents`) : le trigger de ces notes était un Explore ayant *mal-classé* le locus — ici **aucun mis-flag**, et sur-annoter violerait `verify-uncovered`/#5. Comme les axes de déterminisme (`run-order`/`hash-seed`/`locale-tz`), c'est une **commande replayable**, occurrence-zéro, pas une matière à gate.
|
||||
|
||||
**Impact d'artefact : ZÉRO.** Sweep en lecture seule (repro dans le module, aucun fichier tracké touché hors ce journal). `regression_run.json`/`quality_report.json` inchangés (635/618/17 · 24×100).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre inventé** (#6 — tous lus des artefacts commités), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : ce journal (+ mémoire agent `rounding-mode-bankers-sweep`, hors repo).
|
||||
|
||||
## Session `20260812_130149` — VÉRIF · chasse au défaut sur axe FRAIS (date/time + parsing chaîne money/percent) → 3 candidats RÉFUTÉS comme non-défauts + 1 note anti-récurrence in-situ sur `rate()` (commentaire pur · 0 impact artefact)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu de l'artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`, citations réalignées au HEAD par `4eb615b`). Canal stakeholder `daily_reports/2026-08-12.md` **à jour** (matrice 635, 4ᵉ vague · les commits `626c39d`→`4eb615b` sont vérif/currency à 0 impact matrice) → rien à ré-émettre.
|
||||
|
||||
**Chasse au défaut d'abord (axe FRAIS jamais balayé : date/time + parsing de chaînes money/percent dans les `*lib/*.py`).** Explore ciblé (brief strict anti-faux-positif : rejeter défensif-seul + domaine-borné · mémoires `verify-non-defects`/`docstring-vs-code-drift`). 3 candidats remontés, **TOUS réfutés au raisonnement domaine-borné** :
|
||||
- **#2 `finlib/gate.py:153`** (`signature_date` en test de véracité) — exigerait une donnée **corrompue** `signature_date="0"` (chaîne « 0 » vraie en Python). Non atteignable : une date de signature est une chaîne ISO ou `null`, jamais « 0 ». Test de véracité **correct** pour des entrées réelles. **Non-défaut.**
|
||||
- **#3 `banclib/finance.py:_pct`** — n'accepte que la **constante canonique** `« 52 % »` (mémoire `fix-vs-gate-transitively-protected-constant` : `finance._pct(CANONICAL)`), aucun séparateur, aucun free-text. **Non-défaut** (entrée contrôlée).
|
||||
- **#1 `commlib/finance.py:47` `rate()`** — le candidat « CONFIRMÉ » de l'agent (`rate("1.234,56 %")` → `None` car `replace(",",".")` fabrique « 1.234.56 »). **Réfuté** : un taux de commission est **borné 0-100** (doctrine écrite de `_rate_label` lignes 76-77), donc jamais ≥ 1000 → **aucun séparateur de milliers** ne peut apparaître. La virgule est donc toujours **décimale** ici (« 3,5 % » → 3.5 = le cas français **voulu et géré**). Même classe que le `:g` de `_rate_label` (deux pièges inatteignables sur domaine borné). **Non-défaut** — cohérent avec le CLEAN de la session `060044` sur ce même fichier.
|
||||
|
||||
**Action = 1 note anti-récurrence in-situ sur `rate()` (commentaire pur · sœur des notes `090114`/`113134`).** Fait nouveau motivant l'action : un **Explore capable vient de mal-classer `#1` en « CONFIRMED DEFECT »** — preuve que le locus est **re-flaggable**. La fonction de **parsing** `rate()` ne portait **aucune** note (sa sœur `_rate_label` en porte une, mais sur le formatage `:g`, pas sur le parse à la virgule). Passe unique : ajout d'un commentaire *accurate* (vérifié) documentant (a) l'acceptation de la virgule décimale française et (b) l'inatteignabilité du séparateur de milliers (taux borné 0-100, renvoi à `_rate_label`), + interdiction de « corriger » vers `locale.atof` (#5/#6). Convertit le faux-positif futur en « déjà revu, sûr ».
|
||||
|
||||
**Pourquoi PAS un fix de code ni un gate (#5/#6).** Aucun défaut réel (3/3 réfutés) : passer à `locale.atof` serait de la sur-ingénierie sur un cas inatteignable + casserait le déterminisme (dépendance locale — cf. `locale-tz-determinism-sweep`). Robustesse portée par le domaine borné + les suites (∈ régression gatée). Commentaire sans marqueur `# N ·` d'invariant (pas de déclenchement du gate ledger).
|
||||
|
||||
**Impact d'artefact : ZÉRO (prouvé byte-level).** Commentaire pur → aucun changement de sortie. Rebuild de `commissions/out/` dans `/tmp` (avant ET après édit) → `diff -rq` = **BYTE-IDENTICAL**. `regression_run.json`/`quality_report.json` **inchangés** (635/618/17 · 24×100). Aucun compteur de fiche/README à réaligner (pas de test ajouté).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). `finance.py` **compile** + **NFC-clean** (vérifié) · suite commissions OK + `validate` 10 invariants verts. **0 code moteur V18** (bloqué #6), **0 fix de code** (3/3 candidats réfutés), **0 chiffre/fixture inventé** (#6), **0 gate ajouté** (#5), **0 artefact modifié/reconstruit** (byte-identique), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M crm/commissions/commlib/finance.py` (1 note anti-récurrence `rate()`) + ce journal.
|
||||
|
||||
## Session `20260812_113134` — VÉRIF · fermeture de la classe « percent == 100 ⟺ ok » sur les 4 conditions du gate financement + durcissement anti-récurrence des 2 conditions à domaine borné (commentaires purs · 0 impact artefact)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu de l'artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` à jour (session `103119`, matrice 635).
|
||||
|
||||
**Chasse au défaut d'abord (angle : la classe « barre de progression == 100 % ⟺ soumissible » est-elle vraiment fermée après les 2 fixes `093114`+`110124` ?) → classe PROUVÉE FERMÉE, 0 nouveau défaut.** Les deux bugs réels de ce cycle (`093114` per-condition apport, `110124` agrégat `overall_percent`) partagent une classe : un percent user-facing (barre de progression) qui pouvait afficher **100.0 tout en BLOQUANT**. J'ai balayé toutes les surfaces susceptibles de porter cette classe.
|
||||
|
||||
**Balayage cross-module (Grep sur les artefacts commités).** Un seul module émet un champ `percent`/`overall_percent`/`progress` dans un `out/*.json` commité : `crm/financement_bancaire/out/gate_status_example.json`. Les autres scorers examinés dérivent leur verdict **directement** de la note, sans divergence possible : `audit_4big/scoring.py` — `verdict = "PASS" if score >= pass_score` où `score` EST la note affichée (pas deux quantités indépendantes) ; `bancable` — le `pe_brut` (point d'équilibre en unités) n'est pas une barre de complétion. **La classe est donc confinée au seul gate financement.**
|
||||
|
||||
**Audit exhaustif des 4 conditions du gate + agrégat (invariant : `percent == 100.0 ⟺ ok`).**
|
||||
- **`_cond_apport`** — ratio USD **continu**, plafonné explicitement « non ok → 99.9 » par le fix `093114` ✓
|
||||
- **`overall_percent`** (agrégat) — moyenne de 4, plafonnée explicitement par le fix `110124` ✓
|
||||
- **`_cond_validation_wag`** — percent exact `100.0 if ok else 0.0`, aucune divergence ✓
|
||||
- **`_cond_documents`** / **`_cond_autorisations`** — `pct = round1(validated/total*100)` **SANS** garde de plafond. Vérifié **empiriquement** : le dénominateur est une liste du contrat (`financement_spec.json`) — **13** documents `is_required` (max) et **4** autorisations. Or `round1((total-1)/total*100)` ne remonte à `[99.95 %, 100 %[` par le bas qu'à partir de **total ≥ 2000** (première valeur calculée par script : 2000, car `(total-1)/total ≥ 0.9995`). Domaines **bornés à 13/4 ≪ 2000** → `ok=False ⟹ percent < 100` tient **déjà** sans garde. Sœur exacte des 3 sites `:g`-domaine-borné (`090114`). Direction inverse triviale : `ok=True ⟹ validated==total ⟹ percent=100.0` exact.
|
||||
|
||||
**Conclusion : classe FERMÉE.** Les 2 conditions à ratio potentiellement continu (apport, agrégat) portent une garde explicite prouvée par test à dents ∈ régression gatée ; les 2 à domaine borné (documents ≤13, autorisations =4) sont sûres par construction ; wag est exact. Aucune 3ᵉ instance latente.
|
||||
|
||||
**Action = durcissement anti-récurrence (commentaires purs · sœur de `090114` « twin fix must sweep ALL surfaces »).** Fait nouveau motivant l'action : `_cond_documents`/`_cond_autorisations` sont les **seules** des 4 conditions à **ne porter NI garde NI note** expliquant leur sûreté — un audit futur posant « l'invariant est-il fermé pour les 4 ? » les re-flaggerait (cycle gaspillé) ou y ajouterait un plafond **redondant** (`#5`), exactement le problème que `090114` a réglé pour les `:g`. Passe unique : ajout à chacune d'une **note de justification in-situ** (domaine borné ≤13/4 ≪ seuil 2000 → invariant préservé sans garde · renvoi aux fixes `093114`/`110124` · « ne pas ajouter de plafond redondant · #5 »). Convertit le faux-positif futur en « déjà revu, sûr ».
|
||||
|
||||
**Pourquoi PAS un fix de code ni un gate (#5/#6).** Aucun défaut réel : ajouter un plafond aux 2 conditions bornées serait du **code mort** (inatteignable) — pas un fix. Robustesse portée par le domaine borné + les 2 tests à dents des conditions continues ; rien de nouveau à gater (#5). Commentaires sans marqueur `# N ·` d'invariant (pas de déclenchement du gate ledger).
|
||||
|
||||
**Impact d'artefact : ZÉRO (prouvé byte-level).** Commentaires purs → aucun changement de sortie. Rebuild de `financement_bancaire/out/` dans `/tmp` → `diff -rq out /tmp` = **BYTE-IDENTICAL** (7 fichiers). `regression_run.json`/`quality_report.json` **inchangés** (635/618/17 · 24×100). Aucun compteur de fiche/README à réaligner (pas de test ajouté).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). `gate.py` **compile** + **NFC-clean** (vérifié) · suite module 37/37 OK. **0 code moteur V18** (bloqué #6), **0 fix de code** (classe prouvée fermée), **0 chiffre/fixture inventé** (#6), **0 gate ajouté** (#5), **0 artefact modifié/reconstruit** (byte-identique), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M crm/financement_bancaire/finlib/gate.py` (2 notes anti-récurrence) + ce journal.
|
||||
|
||||
## Session `20260812_110124` — FIX correctness · `financement_bancaire` — même bug que `093114`, un niveau au-dessus : la barre GLOBALE `overall_percent` pouvait afficher 100.0 tout en BLOQUANT (l'invariant per-condition ne s'était PAS propagé à l'agrégat)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu de l'artefact commité · #6) = **634 exécutés · 617 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` à jour au HEAD `85cd625` (session `100114`).
|
||||
|
||||
**Chasse au défaut d'abord (angle : la robustesse de la classe « affichage percent == 100 ⟺ soumissible » est-elle vraiment fermée après `093114` ?) → 1 candidat CONFIRMÉ au byte-level, latent mais réel.** Le fix `093114` (`gate.py:105`) a rétabli l'invariant **`percent == 100.0 ⟺ ok`** *par condition* (`_cond_apport` plafonné à 99.9 quand non ok). J'ai vérifié si l'**agrégat** `overall_percent` (la **barre GLOBALE** du frontend, `gate_status:171`) héritait de l'invariant — **il ne l'héritait pas**.
|
||||
|
||||
**Le défaut (même classe, un cran plus haut).** `overall = _round1(Σ percents / 4)` sur les 4 conditions. Cas limite atteignable : apport plafonné à **99.9** (non ok, exactement l'état laissé par le fix `093114` p. ex. 19 999/20 000 USD) **+** les 3 autres conditions **ok à 100.0** (documents tous validés WAG, autorisations toutes signées, validation WAG faite) → moyenne `(99.9 + 300)/4 = 99.975` → `round1` = **100.0**. Résultat user-facing : `overall_percent: 100.0` **tout en** `can_submit: False` (403). La **barre de progression GLOBALE** — celle que le client regarde en premier — afficherait « 100 % » sur un dossier refusé. **Exactement** la même auto-contradiction que le fix per-condition `093114`, mais à l'agrégat que ce fix n'a pas couvert. Reproduit **end-to-end** via le vrai `gate_status` (dossier résident RD 100 000 USD, apport 19 999) : `overall_percent=100.0`, `can_submit=False`.
|
||||
|
||||
**Pourquoi byte-repro est aveugle (preuve que ça échappe aux gates).** `gate_status_example.json` commité est à `overall_percent: 39.1` (`can_submit: false`) — **hors bande**. `check_artifacts` (rebuild == commité) le reproduit à l'identique éternellement. Bug **latent** : correct sur l'échantillon figé, faux dès qu'un dossier réel atteint la configuration limite. Même veine que `093114` (per-condition), `073055` (`base×taux`), `080104` (commissions).
|
||||
|
||||
**Fix (chirurgical, symétrique au fix `093114`, byte-repro préservé).** Après le calcul de `overall`, garde `if not can_submit and overall >= 100.0: overall = 99.9`. Invariant d'affichage rétabli au niveau agrégat : **`overall_percent == 100.0 ⟺ can_submit`**. L'exemple à 39.1 % **inchangé** → `financement/out/` rebuild **BYTE-IDENTIQUE** (`diff -rq out/ /tmp` = IDENTICAL).
|
||||
|
||||
**Test à dents (prouvé mordant).** Nouveau `test_overall_percent_agrege_ninclut_pas_100pct` (frère de `test_apport_quasi_complet_ninclut_pas_100pct`, mais à l'agrégat) : dossier avec apport 19 999/20 000 + 3 autres conditions ok → assert `can_submit=False`, les 3 autres conditions `ok=True` (isolation du cas limite), `overall_percent < 100.0` (== 99.9). **Sans le fix** → `AssertionError: 100.0 not less than 100.0` (mordu, vérifié par mutation) ; **avec** → OK. Suite financement **36 → 37**.
|
||||
|
||||
**Cascade artefacts (test count +1).** Régénéré `regression_run.json` (**634→635 / 617→618**), `regression_plan.json` + `MANIFEST.json` (build), `quality_report.json` (audit_4big **en dernier** · mémoire `audit4big-rebuild-after-doc-edits` — PASS 24/24, min 100). Fiches gatées re-dérivées (`check_readme_claims`) : QA `634/617→635/618`, Backend `634→635`, CRM financement `36→37` (le « Total CRM » du trio est source séparée · financement suivi à part ligne 44 · convention `fiche-accuracy-audit-closed`). Canal stakeholder `daily_reports/2026-08-12.md` prolongé (4ᵉ vague · matrice 634→635 · mémoire `two-logging-channels`).
|
||||
|
||||
**Pourquoi pas un nouveau gate (#5).** L'invariant `overall_percent == 100 ⟺ can_submit` est désormais **porté par le code + prouvé par un test à dents** ∈ régression gatée — comme son frère per-condition `093114`. Un gate statique dédié serait redondant (`verify-uncovered` : la suite couvre, mutation confirmée). Origine unique, un seul site.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers édités **compilent** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **1 module produit corrigé** (bug latent user-facing), **artefacts autoritaires régénérés déterministes** (échantillon byte-identique), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**, **0 invention de chiffre** (#6 — tous re-dérivés des artefacts). Fichiers : `finlib/gate.py`, `tests/test_financement_bancaire.py`, `qa/regression/out/{regression_run,regression_plan,MANIFEST}.json`, `qa/audit_4big/out/quality_report.json`, fiches `03_agents/{qa,erpnext_backend,crm}/AGENT.md`, `daily_reports/2026-08-12.md` + ce journal.
|
||||
|
||||
## Session `20260812_103119` — VÉRIF · nouvel axe « bien-formation structurelle du HTML généré » (parseur réel + entrées adverses FR/ES) → CLEAN teeth-prouvé, replayable, sans gate
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu depuis l'artefact commité · #6) = **634 exécutés · 617 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` re-réconcilié au HEAD par la session `100114` (matrice 634) → à jour ; cette session est **vérification-seule à 0 impact matrice**, rien à ré-émettre.
|
||||
|
||||
**Chasse au défaut d'abord (angle FRAIS jamais balayé = bien-formation *structurelle* du HTML généré).** Les ~40 sweeps antérieurs ont couvert l'octet (`runtime-encoding`), la forme NFC, le déterminisme (hash/locale/TZ/ordre), les schémas (7 axes), l'arithmétique dérivée, etc. — mais **jamais** la question « le HTML émis par les générateurs est-il structurellement bien-formé (balises équilibrées, attributs non cassés) sous une entrée réaliste FR/ES ». Distinct de la classe déjà fixée en publiciste `070054`/`073055` (mauvais encodeur de *contexte*) : ici on vérifie la **structure de sortie** au parseur.
|
||||
|
||||
**Méthode (à dents, replayable).** (1) Explore ciblé sur tous les chemins générant du markup (`publiciste/lib/generator.py` — pages HTML ; `frontend/chat_otoia/chatlib/frappe.py` — `mount_html`). (2) Parsing réel de la sortie avec un détecteur `html.parser` maison suivant une **pile de balises** (gestion des éléments void), flaggant unclosed-EOF / misnest / stray-close. Exécuté sur :
|
||||
- La **sortie site commitée** (`render_site` sur `fixtures/data_room`, 2 projets P01/P02) → **len 6084, 0 erreur**.
|
||||
- Un **projet adverse** (free-text injecté dans tous les champs éditables : `nom="Résidence «L'Été» & <Cie>"`, `localisation` avec guillemets doubles, `positionnement` avec `<b>`/`"`/`'`, `typologies[0].nom="T2 «Prestige» & <balcon>"`, `inclus` avec `&`/`"`/`'`/`<sud>`) → **0 erreur** : `_esc` (= `html.escape(quote=True)`) neutralise correctement tout le texte et les attributs.
|
||||
- Le chemin **hero CSS `url('…')`** (le locus des fix `070054`/`073055`) reconfirmé bien-formé en page complète.
|
||||
|
||||
**Teeth du détecteur (prouvé mordant, mémoire `module-suite-teeth-mutation`).** 3 échantillons délibérément mal-formés **doivent** être flaggés : `<div><span>x</span>` → `unclosed EOF: div` ; `<div><span>x</div></span>` → `misnest`+`stray` ; `<p>x</p></p>` → `stray </p>`. Et un bien-formé `<div><span>x</span></div>` → `NONE`. **4/4 conformes** → le détecteur mord, le CLEAN n'est pas un faux-vert.
|
||||
|
||||
**Résultat = CLEAN, 0 défaut.** `_esc` est appliqué à **100 %** des insertions texte/attribut de `generator.py` (vérifié lignes 60/66/82/114/117/118-124), et le contexte CSS `url()` combine `quote()` (URL) puis `_esc()` (attribut HTML) — deux-étages correct. `frappe.py mount_html` reste non-échappé mais ses 4 entrées sont **provablement des enums/constantes** (`persona="Amélie"` figé, `portail_key`∈{ventes,construction,achat,compta,direction}, `css_class`/`id_prefix` du spec contrôlé) — aucun chemin free-text ne l'atteint → non-défaut (déjà statué 2× · `verify-non-defects`, non re-flaggé).
|
||||
|
||||
**Pourquoi pas un fix ni un gate (#5/#6).** Aucun défaut trouvé (structure saine end-to-end, teeth-prouvé). Comme les axes de déterminisme (`hash-seed`/`locale-tz`/`run-order`) et d'encodage (`runtime-encoding`/`unicode-nfc`), c'est une **commande replayable**, occurrence-zéro, pas une matière à gate — la robustesse structurelle vit dans `_esc`/`quote` (déjà couverts par la suite publiciste ∈ régression gatée). Aucun chiffre inventé (#6 — tous lus des artefacts commités).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — sweep en lecture seule, 0 artefact touché). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : ce journal (+ mémoire agent `html-wellformedness-sweep`, hors repo).
|
||||
|
||||
## Session `20260812_100114` — CANAL STAKEHOLDER · re-réconciliation `daily_reports/2026-08-12.md` au HEAD courant (`e7cc3c4` → `85cd625`) · remontée du 4ᵉ fix de correctness (matrice `633→634`)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu depuis l'artefact commité · #6) = **634 exécutés · 617 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||
|
||||
**Chasse au défaut d'abord (2 axes frais, jamais audités en profondeur) → 0 défaut, surfaces confirmées CLEAN.**
|
||||
- **Axe computationnel (Explore ciblé sur les 8 surfaces les moins récemment auditées** : `rbac/apply_plan/aggregator.py` · `qa/audit_5d/{controls,artifacts}.py` · `seo/{hreflang,schemaorg}.py` · `legal/confotur/{rbac_scan,builder}.py` · `demo/scenarios/{evidence,render}.py`**).** Brief strict anti-faux-positif (rejeter défensif-seul + domaine-borné · mémoires `verify-non-defects`/`docstring-vs-code-drift`). **Aucun défaut réel** : formatage bornes correct, échappement contextuel sûr sur entrées contrôlées, gardes de division présentes, bornes d'index correctes (`evidence.py:88` `-len(obj) <= idx < len(obj)`), tri déterministe, 0 défaut fabriqué.
|
||||
- **Axe i18n frais (`faisabilite/bancable/banclib/i18n.py`, jamais balayé).** Vérifié la **parité des clés** FR/EN/ES du dictionnaire `LABELS` (une clé présente dans une langue mais absente d'une autre lèverait `KeyError` au rendu, byte-repro-aveugle si non exercée) → **les 3 langues ont des jeux de clés strictement identiques** (`missing: ∅ · extra: ∅` pour fr/en/es). CLEAN.
|
||||
|
||||
**Vrai locus = currency du canal stakeholder (mémoire `two-logging-channels` : garder les DEUX canaux à jour, chaque chiffre sourcé d'un artefact commité).** Le rapport `daily_reports/2026-08-12.md` avait été réconcilié en session `083114` au HEAD `e7cc3c4` (matrice **633/616**). Depuis, **2 commits worker** ont avancé l'arbre à `85cd625`, dont **un fix de correctness à impact matrice** que le snapshot ne reflétait pas :
|
||||
- `cb8069b` (`090114`) — notes anti-récurrence `:g`-domaine-borné (commentaires purs · **0 impact matrice**).
|
||||
- `85cd625` (`093114`) — **fix `financement_bancaire/finlib/gate.py`** : un dossier dans la bande [99.95 %, 100 %[ (p. ex. 19 999/20 000 USD) affichait `percent: 100.0` (barre frontend « 100 % ✓ ») tout en **bloquant** la soumission (403). Fix = garde `if not ok and pct >= 100.0: pct = 99.9` → invariant `percent == 100.0 ⟺ ok` rétabli · test à dents · suite financement **35→36**, matrice **633/616 → 634/617**.
|
||||
|
||||
**Fix = re-réconciliation (pas ré-émission) du rapport du jour.** Le rapport daté `2026-08-12.md` **est** le snapshot du jour ; le re-réconcilier au HEAD courant est le contrat, pas un doublon (#5). Porté : header `HEAD e7cc3c4 → 85cd625` + session `100114`, nouvelle section « Jalon du jour · 3ᵉ vague (`e7cc3c4` → `85cd625`) » (table du fix gate financement + opposabilité), `État courant` re-titré au HEAD `85cd625` avec **tous les chiffres re-dérivés de l'artefact commité** (matrice **634/617/17** · PASS 24× · bijection 24↔24), trajectoire de matrice étendue (633 → **+1** au `85cd625` = 634), §Portée actualisé (commit intercalaire `cb8069b` noté sans impact).
|
||||
|
||||
**Pourquoi pas un fix ni un gate (#5/#6).** Aucun défaut trouvé (Explore CLEAN + parité i18n + tree cohérent) ; le seul « écart » était la **staleness du canal stakeholder** (canal éditorial daté par design, comme un snapshot — pas un artefact byte-gaté). L'actualiser EST le contrat de `two-logging-channels`, pas une matière à gate.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rapport = document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (tous lus depuis l'artefact commité · #6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (re-réconciliation au HEAD courant) + ce journal.
|
||||
|
||||
## Session `20260812_093114` — FIX correctness · `financement_bancaire` gate — un dossier incomplet pouvait afficher `percent: 100.0` (barre pleine ✓) tout en BLOQUANT (bug latent, byte-repro-aveugle)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **633 ran · 616 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Séquence moteur V18 toujours **bloquée** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6) → **aucun code moteur légitime**. Canal stakeholder `daily_reports/2026-08-12.md` **à jour** au HEAD (matrice stable — `cb8069b` commentaires purs).
|
||||
|
||||
**Chasse au défaut d'abord (vein correctness, comme 070054/073055/080104) → 1 candidat CONFIRMÉ au byte-level, latent mais réel.** Explore ciblé sur l'arithmétique/parsing des générateurs (arrondis · taux · bornes · division). Le candidat remonté — `crm/financement_bancaire/finlib/gate.py:97` `_cond_apport` — est un **vrai défaut de cohérence d'affichage** confirmé par trace concrète, pas un cas théorique.
|
||||
|
||||
**Le défaut.** `pct = _round1(min(100.0, paid/required*100))` arrondit à 1 décimale, tandis que `ok = paid + 1e-9 >= required` est un test **exact**. Un versement dans la bande **[99.95 %, 100 %[** (p. ex. **19 999 / 20 000 USD**, 99.995 %) **remonte à `percent: 100.0`** par l'arrondi, alors que `ok=False` et `remaining=1.0 USD`. Conséquences user-facing :
|
||||
- La **barre de progression frontend** (`GET /api/hypotheque/dossier/{id}/gate-status`, contrat DIRECTIVE) afficherait **« 100 % ✓ »** tout en refusant la soumission (403).
|
||||
- La **raison de blocage** (`can_submit_dossier`, ligne 187) lirait « Apport initial incomplet : … (reste 1 USD · **100.0 %**) » — auto-contradictoire.
|
||||
- Seul `_cond_apport` est **atteignable** : les deux autres conditions à `percent` continu-arrondi (`_cond_documents`/`_cond_autorisations`) sont des **ratios entiers** `validated/total` sur de **petites listes** (13 docs · 4 autorisations) — atteindre la bande [99.95 %,100 %[ avec `ok` faux exigerait `total ≥ 2000`, **impossible** ; leur `percent==100 ⟺ ok` tient structurellement. Non touchées (pas de sur-ingénierie · #5).
|
||||
|
||||
**Pourquoi byte-repro est aveugle (preuve que ça échappe aux gates).** Le `dossier_exemple` commité est à **67.8 %** (29 500/43 500 USD) — **hors bande**. `gate_status_example.json` échoue donc jamais le bug, et `check_artifacts` (rebuild==commité) le reproduit à l'identique éternellement. C'est un **bug latent** : correct sur l'échantillon figé, faux dès qu'un dossier réel atterrit dans la bande. Même classe que les fixes 073055 (`base×taux` cassait sur montants réels) et 080104 (commissions).
|
||||
|
||||
**Fix (chirurgical, byte-repro préservé).** `gate.py` : extraction de `ok`, puis garde `if not ok and pct >= 100.0: pct = 99.9` (plus grande valeur 1-décimale strictement < 100 · convention `_round1`). Invariant d'affichage rétabli : **`percent == 100.0 ⟺ ok`**. L'exemple à 67.8 % **inchangé** → `gate_status_example.json`/`out/` rebuild **BYTE-IDENTIQUE** (vérifié `diff -rq out/ /tmp` = IDENTICAL).
|
||||
|
||||
**Test à dents (prouvé mordant).** Nouveau `test_apport_quasi_complet_ninclut_pas_100pct` : 19 999/20 000 → `ok=False`, `remaining≈1.0`, `percent < 100.0` (== 99.9), et la raison **ne contient pas « 100 »**. **Sans le fix** → `AssertionError: 100.0 not less than 100.0` (mordu) ; **avec** → OK. Suite financement **35 → 36**.
|
||||
|
||||
**Cascade artefacts (test count +1).** Régénéré `regression_run.json` (**633→634 / 616→617**), `regression_plan.json` + `MANIFEST.json` (build), `quality_report.json` (audit_4big **en dernier** · mémoire `audit4big-rebuild-after-doc-edits` — PASS 24/24 ≥95, min 100). Fiches gatées re-dérivées : QA `634/617`, Backend `634`, CRM financement `35→36` (le « Total CRM 83 » du **trio** est source séparée · financement suivi à part ligne 40 · inchangé).
|
||||
|
||||
**Pourquoi pas un nouveau gate (#5).** L'invariant `percent==100 ⟺ ok` est désormais **porté par le code + prouvé par un test à dents dans la suite** (∈ régression gatée). Un gate statique dédié serait redondant (`verify-uncovered` : la suite couvre déjà, mutation confirmée). Origine unique, un seul site atteignable.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. **0 code moteur V18** (bloqué #6), **1 module produit corrigé** (bug latent user-facing), **artefacts autoritaires régénérés déterministes** (échantillon byte-identique), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**, **0 invention de chiffre** (#6 — tous re-dérivés des artefacts). Fichiers : `finlib/gate.py`, `tests/test_financement_bancaire.py`, `qa/regression/out/{regression_run,regression_plan,MANIFEST}.json`, `qa/audit_4big/out/quality_report.json`, fiches `03_agents/{qa,erpnext_backend,crm}/AGENT.md` + ce journal.
|
||||
|
||||
## Session `20260812_040034` — CLEANUP #5 · suppression d'un orphelin `out_fresh/` (doublon d'artefacts commité par accident, échappe à TOUS les gates)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 ran · 612 passés · 17 skippés · 0 échec** (PASS). Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (directive `:90-91` « NE PAS coder avant l'audit » · D-06 approbation audit = gate d'entrée · #6) → **aucun code moteur légitime**. Dernier commit `031b5e7` = PNG `DASHBOARD_LUXURY_MODELE_REFERENCE` ajouté par Michel (référence design, hors production repo).
|
||||
|
||||
**Chasse au défaut d'abord (2 axes) → 1 candidat écarté comme NON-DÉFAUT (refus de fabriquer · #6).** Explore very-thorough sur les modules produit hors `audit_4big`/`regression`/`publiciste` (déjà balayés). Le seul candidat remonté « confirmé » — `faisabilite/generator/genlib/model.py:104` `is_filled()` exigeant `"{{" in text and "}}" in text` pour classer un placeholder — a été **réfuté au byte-level** : le renderer (`renderer.py:23-42`) n'émet **jamais** que des placeholders `{{...}}` **complets** (deux accolades), `brief_incomplete.json` n'utilise **aucune** accolade pour marquer l'absence (il utilise `_PLACEHOLDER_ABSENT` : « tbd »/« todo »/… + champs omis), et **aucune** occurrence mono-accolade n'existe dans tout le module. Exiger les deux accolades est donc le choix **conservateur correct** (flaguer un `}}` isolé dans de la prose réelle serait un faux positif). L'Explore s'était sur-engagé sur un cas théorique inatteignable → **non touché** (`verify-non-defects`).
|
||||
|
||||
**Vrai locus trouvé = un ORPHELIN ACCUMULÉ (violation #5 « JAMAIS accumuler doublons »), pas un bug.** `05_deliverables_mvp/frontend/portails/out_fresh/` (2 fichiers : `workspace.json` + `MANIFEST.json`) est :
|
||||
- **Tracké** (`git ls-files` le confirme) — mais commité **une seule fois**, dans la session auto `f954cc1` du **2026-08-02** (scratch-dir de comparaison « fresh rebuild » commité par accident).
|
||||
- **Byte-identique** à `out/` (diff `json.tool` = IDENTICAL content).
|
||||
- **Produit par rien** : le générateur `workspaces_gen.py` écrit **uniquement** dans `out/` (`_DEFAULT_OUT = os.path.join(_HERE, "out")` · `-o` défaut `./out`) — jamais `out_fresh`.
|
||||
- **Référencé nulle part** : `git grep out_fresh` sur tout l'arbre tracké (hors le dossier lui-même) = **ZÉRO** (ni code, ni doc, ni CI, ni `.gitignore`, ni test).
|
||||
|
||||
**Pourquoi c'est un vrai défaut et pas un artefact by-design (preuve qu'il échappe à tout gate).** `check_artifacts.sh` découvre les cibles par `find -type d -name out` — un match **exact** sur `out`, donc `out_fresh` **n'entre dans aucun gate de reproductibilité**. C'est précisément pourquoi il a survécu au balayage `reverse-bijection-artifact-sweep` (qui ne vise que les répertoires `out/`). Conséquence : c'est une **bombe à retardement de staleness** — rien ne le régénère, donc dès que la source du générateur bouge, `out/` se met à jour tandis que `out_fresh/` **diverge silencieusement** en une copie **stale, non-possédée et trompeuse** (« laquelle fait autorité ? »). Dead weight ungated. C'est l'unique scratch-dir du genre dans tout le repo (`find` pour `out_new`/`out_old`/`out_tmp`/`out_bak`/`out_fresh` → seul `out_fresh` existe · occurrence isolée).
|
||||
|
||||
**Fix = suppression (`git rm -r out_fresh/`).** Sûr (réversible via l'historique git), et **je ne l'ai pas créé** (règle « avant de supprimer, regarde la cible » : contenu byte-identique à la copie autoritaire, aucun consommateur). La copie autoritaire `out/` est **intacte et se régénère BYTE-IDENTIQUE** (rebuild `-o /tmp` vs `out/` = identique, vérifié après suppression).
|
||||
|
||||
**Pourquoi pas un nouveau gate (#5).** Occurrence **isolée** (origine unique `f954cc1`, un seul module). Ajouter un gate « aucun répertoire `out_*` non-`out` » serait de la sur-ingénierie pour un événement-zéro. La discipline existante (générateur écrit dans `out/` · `.gitignore` documente que `out/` est commité) est le contrat ; l'orphelin était une déviation ponctuelle, corrigée à la source.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rien ne dépendait de `out_fresh/`). **0 code moteur V18** (bloqué #6), **0 module de production touché** (seul un doublon orphelin supprimé · char-class `model.py` laissée intacte car non-défaut), **0 artefact autoritaire modifié**, **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `- 05_deliverables_mvp/frontend/portails/out_fresh/{workspace,MANIFEST}.json` (supprimés) + ce journal.
|
||||
|
||||
## Session `20260812_043034` — CANAL STAKEHOLDER · actualisation `daily_reports` au HEAD courant (`35de616` → `ee88d36`)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 ran · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 ok**. Séquence moteur V18 toujours **bloquée** en gouvernance (audit préalable produit mais non validé · D-06 = gate d'entrée · #6) → **aucun code moteur légitime**.
|
||||
|
||||
**Chasse au défaut d'abord (2 axes) → tree cohérent, aucun défaut candidat.** (1) Vérifié que les chiffres cités dans le journal `20260812_040034` (629/612/17) sont **byte-cohérents** avec le `regression_run.json` commité (identiques). (2) Vérifié que `README.md` **ne fige aucun total de tests** (il pointe l'artefact byte-gaté comme « autorité », prose « 24 suites gated / 24 modules » exacte vs `quality_report.json`) → **0 drift numérique README**.
|
||||
|
||||
**Vrai locus = currency du canal stakeholder (mémoire `two-logging-channels` : garder les DEUX canaux à jour, chaque chiffre sourcé d'un artefact commité).** Le dernier rapport `daily_reports/2026-08-11.md` était épinglé à `35de616` (currency `20260811_092842`) et citait la régression à **628/611**. Depuis, **3 commits** ont avancé l'arbre à `ee88d36`, et la matrice a **divergé d'un incrément** :
|
||||
- `39def91` (`[DTP-Worker 20260811_095844] Auto exec`, **postérieur** à la currency du rapport 08-11) a ajouté un test verrou **NBSP U+00A0** dans `publiciste/tests/test_publiciste.py` (+15 lignes) → régression **628→629 / 611→612** + `regression_plan/run/MANIFEST` + `quality_report` re-générés du même coup. Le rapport 08-11 **ne le couvre pas** → chiffre `628/611` désormais **stale**.
|
||||
- `031b5e7` = PNG `DASHBOARD_LUXURY_MODELE_REFERENCE` ajouté par Michel (asset de référence design, hors production repo, root-owned).
|
||||
- `ee88d36` = jalon worker de la session précédente de ce jour (suppression orphelin `out_fresh/` · déjà journalisée ci-dessus).
|
||||
|
||||
**Fix = émission de `daily_reports/2026-08-12.md`.** Porte le canal `35de616 → ee88d36`, **re-dérive tous les chiffres depuis les artefacts commités** (jamais figés · #6) : gate 33/0/0, régression **629/612/17 PASS**, audit 4Big **PASS 24× 100/100**, bijection **24↔24 ok**. Attribue explicitement la seule évolution de matrice (628→629) à `39def91`, note que `031b5e7`/`ee88d36` **ne touchent aucun test** (matrice stable). Reprend le §« Chemin critique bloqué » (D-06/D-07/D-01 en attente Michel) + escalation.
|
||||
|
||||
**Pourquoi pas un nouveau gate (#5).** Aucun défaut trouvé : le tree est cohérent, le seul « écart » était la **staleness du canal stakeholder** (canal éditorial daté par design, comme un snapshot — pas un artefact byte-gaté). L'actualiser EST le contrat de la mémoire `two-logging-channels`, pas une matière à gate.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rapport = document, 0 dépendance). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `+ 05_deliverables_mvp/daily_reports/2026-08-12.md` (nouveau rapport stakeholder) + ce journal.
|
||||
|
||||
## Session `20260812_050034` — VÉRIF · nouvel axe de déterminisme « ordre d'exécution des suites » (balayage CLEAN, replayable, sans gate)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 ran · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Moteur V18 toujours **bloqué** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6). Canal stakeholder `daily_reports/2026-08-12.md` **déjà à jour** au HEAD `ee88d36` (session précédente `043034`) → rien à ré-émettre.
|
||||
|
||||
**Chasse au défaut d'abord (2 axes) → 1 candidat Explore RÉFUTÉ comme non-défaut, tree propre.**
|
||||
- **Axe prose/drift** (Explore very-thorough sur `daily_reports/*.md`, `04_roadmap/*.md`, `03_agents/**/AGENT.md`, liens Markdown). Le seul candidat « confirmé » — `03_agents/mobile/AGENT.md:81` « upgrade 54 tenté puis abandonné » vs `mobile_spec.json` `expo_sdk_major: 54` — a été **réfuté** par la mémoire `mobile-runtime-actual-vs-rebuild-target` **et** relecture byte : la ligne distingue explicitement le **runtime ACTUEL** (Expo 51, tentative 54 abandonnée le 2026-07-27) de la **cible de rebuild** (Expo 54 · S5). Le `54` du spec correspond exactement à « rebuild cible Expo 54 (S5) ». **Interne cohérent → non touché** (`verify-non-defects`).
|
||||
- **Axe drift numérique README/artefacts** : déjà gaté (`check_readme_claims` ∈ run_ci = vert). 0 écart.
|
||||
|
||||
**Vrai locus = un axe de déterminisme JAMAIS balayé — l'ORDRE D'EXÉCUTION des 24 suites** (4ᵉ axe, frères : `hash-seed-determinism-sweep`, `locale-tz-determinism-sweep`, `forward-compat-warnings-sweep` — **aucun** ne varie l'ordre des suites). Hypothèse de risque : un couplage inter-suites (état global partagé, scratch `/tmp` à nom fixe, cwd partagé) rendrait la matrice `regression_run.json` dépendante de l'ordre → RED intermittent sur le runner.
|
||||
|
||||
**Expérience (replayable).** Monkeypatch `reglib.discovery.discover_suites` → `reversed(...)`, ré-exécution `builder.run_matrix(spec)`, diff par-suite (clé = `id`) + totaux vs `out/regression_run.json` commité :
|
||||
- Totaux **byte-identiques** : 629 ran / 612 passés / 17 skippés / 0 échec.
|
||||
- **24/24 suites** : tuples `(ran,passed,failures,errors,skipped,ok)` **identiques**. `PER-SUITE DIFFS: NONE`.
|
||||
|
||||
**Pourquoi c'est fermé par construction (teeth, pas chance).** `runner.run_suite` (`reglib/runner.py:98`) lance **chaque suite dans son propre subprocess** avec `cwd=abs_path` (le dossier de la suite) — aucun état module in-process partagé, aucun cwd partagé ; l'agrégation dans `run_matrix` est une **somme indépendante de l'ordre**. Le couplage inter-suites est donc empêché *by design* ; le run inversé est une **confirmation teeth**, pas une découverte. Mémoire : `run-order-determinism-sweep`.
|
||||
|
||||
**Pourquoi pas un nouveau gate (#5).** Résultat CLEAN, occurrence-zéro, l'isolation subprocess est déjà le contrat architectural. Comme les 3 autres axes de déterminisme, c'est une **commande replayable**, pas une matière à gate. Aucun chiffre inventé (#6 — tous lus depuis l'artefact commité).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — expérience en lecture seule sur `/tmp`, 0 artefact touché). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié**, **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : ce journal (+ mémoire agent `run-order-determinism-sweep`, hors repo).
|
||||
|
||||
## Session `20260812_053034` — VÉRIF (nouvel angle i18n) · asymétrie de cardinalité SEO per-langue `fr=87·en=87·es=84` **prouvée NON-DÉFAUT** · note explicative au point de dedup (anti re-flag, sans gate)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629/612/17** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Moteur V18 toujours **bloqué** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6). Canal stakeholder `daily_reports/2026-08-12.md` **déjà à jour** au HEAD (session `043034`) → rien à ré-émettre.
|
||||
|
||||
**Angle FRAIS (jamais balayé) = cohérence tri-lingue FR/EN/ES du module `seo/`.** Aucun des ~40 balayages antérieurs n'avait attaqué l'i18n des mots-clés. Un Explore very-thorough a remonté un candidat « défaut confirmé » : `out/seo_keywords.json` = **fr 87 · en 87 · es 84**, l'espagnol « manquant » de 3 entrées projet P01 (generic/residence/appartement). J'ai **refusé de le prendre au mot** (#6, `verify-non-defects`) et **prouvé au byte-level que c'est un NON-DÉFAUT** — l'artefact étant byte-repro-gaté, tout écart reflète la LOGIQUE du générateur, pas une faute de saisie.
|
||||
|
||||
**Preuve (reproduction du générateur sans dedup, tracé collision→terme-gardé).** La génération est **symétrique par langue** (`keywords.py:58` `for lang in langs`) → **raw = 115/langue** identique. L'écart vient **uniquement** du dedup `(lang, term)` (`keywords.py:106-113`, « conserver la 1ʳᵉ occurrence ») :
|
||||
- `country[es]` = **« Republica Dominicana »** est **exactement** la `localisation` générique de **5 projets** (P01/P02/P03/P06/P09 dans `fixtures/projets_master.json`). En espagnol, le mot-clé **global** `{type} Republica Dominicana` (émis avant les projets) est **byte-identique** au mot-clé **projet** `{type} {loc}` de P01 → la 2ᵉ occurrence (P01) est dédupliquée. Le terme **reste présent une fois**, en scope `global` : **rien n'est perdu**.
|
||||
- En **fr/en**, `country` = « Republique Dominicaine » / « Dominican Republic » **≠** la localisation → pas de collision global↔projet ; P01 garde ses 7 termes et ce sont P02/03/06/09 (même loc) qui percutent P01. D'où **3 collisions global↔projet de plus en es** → 87 vs 84.
|
||||
|
||||
**Conclusion : sortie CORRECTE.** Chaque chaîne unique existe **exactement une fois** ; les invariants ne contraignent que `≥ min_keywords_per_lang` (50) + unicité `(term, lang)` (`seo_gen.py:88-98`, `test_seo.py:94-100`) — **jamais** l'égalité inter-langues, et à raison : forcer une symétrie ré-émettrait un **doublon strict**. Le chiffre `es=84` est d'ailleurs **déjà documenté et gaté** dans `README.md:15` (`258 (fr=87·en=87·es=84)`, re-calculé par `check_readme_claims`) → aucun drift, aucun « manquant ».
|
||||
|
||||
**Livrable = une NOTE explicative au point de dedup (`keywords.py:106`).** L'asymétrie est subtile et **non-évidente** : un Explore capable a failli la mal-classer en bug, et un futur balayage i18n refera l'enquête. J'ai ajouté un commentaire *accurate* (vérifié) expliquant la cause (coïncidence `country[es]` == localisation) et l'interdiction de « corriger » vers une symétrie. **Aucune** logique/fixture/claim-gaté touché (les gates byte visent `out/*.json`, pas la source `keywords.py`). Style aligné : ASCII sans accents + em-dash `—` U+2014 (convention existante lignes 5-6), NFC-clean.
|
||||
|
||||
**Pourquoi pas un fix ni un gate (#5/#6/`verify-non-defects`).** (1) Pas de fix logique : changer le tie-break dedup (préférer projet vs global) altèrerait l'artefact byte-gaté + le claim README pour « corriger » un comportement correct. (2) Pas de fix fixture : compléter la localisation générique des 5 projets serait **inventer** une donnée (#6) sans source. (3) Pas de gate d'égalité per-langue : ce serait une **fausse contrainte** (l'asymétrie est linguistiquement légitime) — le seuil `≥ min` par langue est déjà gaté et suffit.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — commentaire pur, artefact `seo/out/*.json` re-build byte-identique via `check_artifacts` ∈ run_ci). **0 code moteur V18** (bloqué #6), **0 logique de production modifiée** (commentaire seul), **0 artefact autoritaire modifié**, **0 fixture touchée** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/seo/seolib/keywords.py` (note by-design au dedup) + ce journal.
|
||||
|
||||
## Session `20260812_060044` — HARDENING correctness · `criteria.py` — le 3ᵉ détecteur regex (le SEUL non durci) de la famille « détecter la forme, pas une graphie »
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629/612/17** (PASS). `quality_report.json` = PASS · 24 modules · min 100/100 · bijection 24↔24. Moteur V18 toujours **bloqué** en gouvernance (directive « NE PAS coder avant l'audit » · D-06 gate d'entrée · #6) → **aucun code moteur légitime**. Canal stakeholder `daily_reports/2026-08-12.md` déjà à jour au HEAD (session `043034`).
|
||||
|
||||
**Chasse au défaut d'abord (3 axes computationnels, jamais audités en profondeur) → 2 modules confirmés CLEAN, 1 vrai locus de robustesse trouvé.**
|
||||
- **`crm/commissions/comlib/finance.py`** (calcul commissions `montant = base × taux`) : logique **correcte** — `rate()` normalise nombre/« 3.5 % »/« 3,5 % » en fraction, `taux_pct` de sortie = fraction cohérente avec le libellé de formule, opérande manquant → `None` (jamais 0-inventé). **Non touché.**
|
||||
- **`faisabilite/bancable/banclib/finance.py`** (cœur break-even, zone de risque D-07) : **correct** — point d'équilibre `⌈52 % × total_unites⌉` avec `52 %` re-dérivé du canonique #9 (jamais codé en dur, cf. fix `448c310`), agrégats catalogue `Σ(qté × prix)` gardés sur `all(... is not None)`, formule publiée avec chaque valeur. **Non touché.**
|
||||
- **Vrai locus = `qa/audit_4big/q4lib/criteria.py:25`** — voir ci-dessous.
|
||||
|
||||
**Le locus : le 3ᵉ détecteur regex non durci d'une famille dont les 2 autres l'ont été.** Ce fichier est la source de **TROIS** fixes de correctness confirmés de la classe « détecter la **forme**, pas une **graphie** » : `ac595d0` (regex ASCII vs `\w` Unicode → test accentué sous-compté), `fd99362` (garde `__main__` littérale double-quotée → simple-quotée/Yoda/sans-espace loupées), `35de616` (`def ` figé vs `def\s+` de la source sœur byte-gatée → `def test_x(` loupé). Les deux détecteurs `_TEST_DEF_RE` (`def\s+…\w`) et `_MAIN_GUARD_RE` (`\s*==\s*`, deux styles de guillemets, ordre Yoda) sont explicitement durcis pour tolérer tout **Python valide** (doctrine articulée lignes 18-30). **Restait `_ADD_PARSER_RE`** — le 3ᵉ frère — figé sur `add_parser\(|add_subparsers\(` : il exige la parenthèse **collée** au nom de méthode, donc `add_subparsers ()` / `add_parser ("build")` (espace avant `(` — Python valide, PEP 8 déconseille sans l'imposer) sont **silencieusement loupés** → sous-scorage `−20 pts` du critère CLI d'un module sain, exactement le symptôme de `fd99362`.
|
||||
|
||||
**Preuve de dents (avant fix).** Ancien regex : `p.add_subparsers ()` → **False**, `sub.add_parser ("x")` → **False** ; nouveau `add_parser\s*\(|add_subparsers\s*\(` → **True** sur ces deux formes **et** True sur les formes normales `add_parser("build")` / `add_subparsers(dest=…)` (aucune régression de couverture).
|
||||
|
||||
**Pourquoi c'est un durcissement légitime et non une invention (#6).** (1) **Classe déjà jugée réelle 3×** dans CE fichier ; les 2 frères sont durcis, le 3ᵉ était l'anomalie — j'harmonise vers la doctrine **écrite du fichier lui-même** (« détecter la forme, pas une graphie »), pas une convention externe. (2) **Précédent direct** : `fd99362`/`35de616` ont durci pour des variantes de Python valide **pas nécessairement présentes** dans un module courant (guillemets simples, Yoda, `def\t`). (3) **Fix latent, artefact byte-identique** : `grep -rE 'add_(parser|subparsers)[ \t]+\('` sur tous les `.py` = **AUCUN** module n'utilise l'espace aujourd'hui → aucun re-scorage, `quality_report.json` re-build **byte-identique** (vérifié par `check_artifacts` ∈ run_ci, vert).
|
||||
|
||||
**Pourquoi pas un nouveau gate (#5).** Ce n'est pas une classe de drift à surveiller mais un durcissement one-shot d'un détecteur ; la robustesse est dans le code du critère, pas dans un gate. La famille des 3 détecteurs est désormais **cohérente end-to-end**.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). Fichier édité **compile** + **NFC-clean** (accents intentionnels, style aligné lignes 18-30). **0 code moteur V18** (bloqué #6), **0 artefact autoritaire modifié** (byte-identique), **0 fixture/chiffre inventé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/qa/audit_4big/q4lib/criteria.py` (regex `_ADD_PARSER_RE` durci + commentaire doctrine) + ce journal.
|
||||
|
||||
## Session `20260812_063050` — CANAL STAKEHOLDER · complétion du rapport `daily_reports/2026-08-12.md` au HEAD du jour (`ee88d36` → `37c68ac`)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu depuis l'artefact commité, #6) = **629 exécutés · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Tous les arbitrages produit D-01→D-08 restent en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||
|
||||
**Chasse au défaut d'abord (2 axes, correctness fiscale jamais auditée en profondeur) → 0 défaut, modules confirmés CLEAN.**
|
||||
- **`fiscal/ecf_dgii/ecflib/ncf.py`** (composition e-NCF DGII, Ley 32-23) : logique **correcte** — `ENCF_RE` = `E + tipo(2) + secuencia(10)` = 13 car. ; `compose_encf` publie sa formule, retourne `e_ncf: None` quand un opérande manque (jamais 0/valeur fabriquée · #6), `_secuencia` zero-padde `[0-9]{1,10}` → 10 chiffres sans jamais inventer de séquence. `is_valid_encf`/`parse_encf`/`compose_encf` cohérents (une valeur composée re-valide toujours). **Non touché.**
|
||||
- **`fiscal/ecf_dgii/ecflib/builder.py`** (plan de config e-CF) : **correct** — aucun chiffre OTO fixé (RNC/ITBIS/TipoCambio/endpoints Compupar restent `null · a_confirmer` par design #6), `_a_confirmer_count` déterministe, tri stable partout → bundle byte-repro. **Non touché.** (`is_filled` partagé re-vérifié = choix conservateur correct, cf. session `040034`.)
|
||||
|
||||
**Vrai locus = currency du canal stakeholder (mémoire `two-logging-channels` : garder les DEUX canaux à jour, chaque chiffre sourcé d'un artefact commité).** Le rapport `daily_reports/2026-08-12.md` avait été émis en session `043034` au HEAD `ee88d36`. Depuis, **3 commits worker du même jour** ont avancé l'arbre à `37c68ac` sans que le snapshot stakeholder ne les reflète :
|
||||
- `a17d86a` (`050034`) — 4ᵉ axe de déterminisme « ordre d'exécution des 24 suites » (CLEAN, replayable, sans gate).
|
||||
- `1513e69` (`053034`) — vérif i18n SEO : asymétrie `fr=87·en=87·es=84` **prouvée NON-DÉFAUT** + note by-design au dedup.
|
||||
- `37c68ac` (`060044`) — hardening `criteria.py` (3ᵉ détecteur regex durci de la famille « détecter la forme, pas une graphie »).
|
||||
|
||||
Les trois sont des travaux de vérif/hardening à **artefacts byte-identiques** (confirmé par `check_artifacts` ∈ `run_ci` vert) → **matrice stable à `629/612/17`**.
|
||||
|
||||
**Fix = complétion (pas ré-émission) du rapport du jour.** Le rapport daté `2026-08-12.md` **est** le snapshot du jour ; le compléter en fin de journée pour couvrir tout le travail du jour est le contrat, pas un doublon (#5). Porté : header `HEAD ee88d36 → 37c68ac` + session `063050`, nouvelle section « Jalons du jour depuis l'émission initiale (`ee88d36` → `37c68ac`) » (table des 3 commits + opposabilité au merge), `État courant` re-titré au HEAD `37c68ac` avec **tous les chiffres re-dérivés des artefacts commités** (629/612/17 · PASS 24× · bijection 24↔24 — inchangés, byte-identiques), note de stabilité de matrice étendue aux 3 jalons, §Portée actualisé.
|
||||
|
||||
**Pourquoi pas un fix ni un gate (#5/#6).** Aucun défaut trouvé : le tree est cohérent, le seul « écart » était la **staleness du canal stakeholder** (canal éditorial daté par design, comme un snapshot — pas un artefact byte-gaté). L'actualiser EST le contrat de `two-logging-channels`, pas une matière à gate.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rapport = document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (tous lus depuis l'artefact commité · #6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (complétion au HEAD du jour) + ce journal.
|
||||
|
||||
## Session `20260812_070054` — FIX correctness (bug latent réel) · rendu HTML publiciste : URL de rendu percent-encodée dans le `url('…')` CSS (apostrophe/espace/accent cassait silencieusement le hero) + test de régression à dents
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **629 exécutés · 612 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Tous les arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||
|
||||
**Chasse au défaut d'abord (large, 12 fichiers cœur / 7 modules jamais ré-audités ce cycle) → 11 CLEAN, 1 BUG LATENT RÉEL trouvé.** Un Explore ciblé sur `workflow_vente`, `dossier_vente`, `financement_bancaire` (gate+builder), `faisabilite/generator` (scorer+model), `seo` (schemaorg+hreflang+builder), `mobile`, `pie` a confirmé **0 défaut** (l'`epsilon 1e-9` du gate apport = tolérance flottante correcte, non-défaut). Puis audit manuel du **rendu HTML publiciste** (`generator.py`) — angle **output-encoding jamais balayé** (distinct des sweeps NFC/encoding octet).
|
||||
|
||||
**Le bug (rendu-correctness, entrée réaliste FR/ES).** `_hero_media` (`generator.py:92`) composait `src = f"/static/projets/{code}/{hero['fichier']}"` puis l'injectait dans `style="background-image:url('{_esc(src)}')"`. `_esc` (= `html.escape`) est le **mauvais encodeur pour un contexte CSS** : un nom de rendu réel — **« Vue d'ensemble.png »**, entièrement plausible dans un projet francophone en RD — devient `Vue d'ensemble.png`, que le navigateur **HTML-décode d'abord** en `Vue d'ensemble.png` ; l'apostrophe **referme alors la chaîne `url('…')` CSS** → `background-image` invalide → **le hero ne s'affiche pas, silencieusement**. Reproduit au byte-level (`html.unescape` de la sortie le prouve).
|
||||
|
||||
**Preuve de dents.** Nouveau test `test_hero_media_url_percent_encode` (`test_publiciste.py`) : rendu avec `fichier="Vue d'ensemble.png"` → assert la présence de `Vue%20d%27ensemble.png` **et** l'absence de l'apostrophe brute dans le `url()` HTML-décodé. Vérifié **teeth** : en revenant à `{hero['fichier']}` sans `quote`, le test **ÉCHOUE** ; avec le fix, **PASS**.
|
||||
|
||||
**Le fix (correct, sans dérive).** `src = f".../{quote(hero['fichier'])}"` (`urllib.parse.quote`) — percent-encode le segment de fichier : `'`→`%27`, espace→`%20`, accents→`%XX`, tous valides à la fois dans l'URL, l'attribut HTML et le `url('…')` CSS. `_esc` conservé en défense de l'attribut HTML (commentaire ajouté : « _esc protège l'attribut HTML ; quote protège le contexte CSS interne »). **Zéro dérive d'artefact** : `quote` laisse les noms ASCII simples inchangés (`hero_aerien.jpg` → identique), donc la fixture de test et le rendu byte-repro sont intacts. Percent-encoder une URL est de toute façon le comportement **sémantiquement correct**.
|
||||
|
||||
**Cascade de régénération (mémoire `artifact-reproducibility-gate`).** Le test ajouté fait passer la suite publiciste 24→25 et la matrice **629/612 → 630/613** (17 skips inchangés). Régénéré dans l'ordre correct (`quality_report` en DERNIER, mémoire `audit4big-rebuild-after-doc-edits`) : `qa/regression/{regression_plan,MANIFEST,regression_run}.json` (`build -o out` + `run`) puis `qa/audit_4big/quality_report.json`. Fiches à compteurs dérivés réalignées (gate `check_readme_claims`) : `03_agents/qa/AGENT.md` (629/612→630/613), `03_agents/erpnext_backend/AGENT.md` (629→630), `03_agents/publiciste/AGENT.md` (suite 24→25). Tous les diffs d'artefact = **pure propagation de compteur** (vérifié), aucune modification parasite.
|
||||
|
||||
**Pourquoi pas un gate (#5).** Le bug est un one-shot de correctness ; sa robustesse vit dans le code (`quote`) + le test de régression à dents, pas dans un gate de drift. Aucune nouvelle classe de drift à surveiller.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers édités **compilent** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 chiffre/fixture inventé** (#6 · tous les compteurs re-dérivés des artefacts régénérés), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M generator.py` (fix `quote`) · `M test_publiciste.py` (test à dents) · `M` 4 artefacts régénérés (regression×3 + quality_report) · `M` 3 fiches AGENT.md (compteurs) + ce journal.
|
||||
|
||||
## Session `20260812_073055` — HARDENING correctness · rendu HTML publiciste : le SEUL segment d'URL restant non percent-encodé dans le `url('…')` CSS (complète le fix de la session `070054`) + test à dents
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **630 exécutés · 613 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||
|
||||
**Chasse au défaut d'abord (classe = encodage contextuel erroné, frère du fix `070054`) → 2 candidats Explore, tous deux RÉFUTÉS comme non-défauts, mais 1 durcissement d'uniformité principiel retenu.** Un Explore very-thorough sur tous les modules générant HTML/CSS/URL/CSV/JSON-LD a remonté 2 candidats :
|
||||
- **`frontend/chat_otoia/chatlib/frappe.py:40` `mount_html`** — attributs HTML (`css_class`/`portail_key`/`persona`) interpolés **sans** échappement. **RÉFUTÉ (non-défaut, aucune entrée réelle ne casse)** : `persona["nom"]` est **forcé à `"Amélie"`** par l'invariant 7 (`chat_otoia_gen.py:155`), `portail_key` est un **enum** (`ventes`/`construction`/`achat`/`compta`/`direction`), `css_class` vient du spec contrôlé. Toutes provablement propres → défensif seul, pas de fix (`verify-non-defects` · #6).
|
||||
- **`publiciste/lib/generator.py:99`** `_hero_media` — c'est le locus **retenu** (voir ci-dessous).
|
||||
|
||||
**Le locus (durcissement, pas bug déclenchable — honnêteté #6).** Le fix `070054` a percent-encodé `hero['fichier']` dans `src = f"/static/projets/{code}/{quote(hero['fichier'])}"`, mais dans la **MÊME** `url('…')` il restait un **second segment dynamique** — `code` — interpolé **brut** puis html-échappé via `_esc(src)`. C'est **exactement** l'anti-pattern que le fix `070054` a identifié (html.escape = mauvais encodeur pour un segment d'URL), **appliqué de façon incomplète**. Ce n'est **pas** un bug déclenchable aujourd'hui : les codes réels sont strictement `P01..P09` (§Projets), jamais d'apostrophe → le byte-repro est **inchangé** (`quote("p01") == "p01"`). C'est un **durcissement d'uniformité de construction**, cas frère exact de la session `060044` (`criteria.py` : « durcir le SEUL détecteur non-durci d'une famille ») : ici **le SEUL segment d'URL non-encodé** de la même `url()`.
|
||||
|
||||
**Le fix + preuve de dents.** `src = f"/static/projets/{quote(code)}/{quote(hero['fichier'])}"` — `quote()` est le SEUL encodeur correct pour un segment d'URL, **le contexte (pas la donnée courante) dicte l'encodeur** (commentaire mis à jour en conséquence). Nouveau test `test_hero_media_code_segment_percent_encoded` : code théorique `"P'X"` → assert `/static/projets/p%27x/hero.jpg` **et** absence d'apostrophe brute dans le chemin HTML-décodé. Vérifié **teeth** : sans `quote(code)`, le test **ÉCHOUE** ; avec, **PASS**.
|
||||
|
||||
**Cascade de régénération (mémoire `artifact-reproducibility-gate`).** Suite publiciste 25→26, matrice **630/613 → 631/614** (17 skips inchangés). Régénéré dans l'ordre (`quality_report` en DERNIER, mémoire `audit4big-rebuild-after-doc-edits`) : `qa/regression/{regression_plan,MANIFEST,regression_run}.json` puis `qa/audit_4big/quality_report.json`. Fiches à compteurs dérivés réalignées (gate `check_readme_claims`) : `03_agents/qa/AGENT.md` (630/613→631/614), `03_agents/erpnext_backend/AGENT.md` (630→631), `03_agents/publiciste/AGENT.md` (suite 25→26). Diffs d'artefact = **pure propagation de compteur** (vérifié).
|
||||
|
||||
**Canal stakeholder laissé à une session dédiée.** `daily_reports/2026-08-12.md` cite encore `629/612` : il est déjà en retard d'un incrément depuis le fix `070054` (→630) que cette session-là n'a délibérément pas répercuté (canal snapshot **batché** par sessions stakeholder dédiées, cf. `3a40aee` qui a réconcilié 3 jalons d'un coup). Une future session stakeholder réconciliera **630 (070054) + 631 (cette session)** ensemble — pas d'invention (#6), chiffres re-dérivables de l'artefact.
|
||||
|
||||
**Pourquoi pas un gate (#5).** Robustesse portée par le code (`quote`) + le test à dents ; aucune nouvelle classe de drift. Occurrence isolée (uniformité d'un fix du même jour).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers édités **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 chiffre/fixture inventé** (#6 · compteurs re-dérivés), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M generator.py` (`quote(code)`) · `M test_publiciste.py` (test à dents) · `M` 4 artefacts régénérés (regression×3 + quality_report) · `M` 3 fiches AGENT.md (compteurs) + ce journal.
|
||||
|
||||
## Session `20260812_080104` — FIX correctness (bug latent réel) · commissions vendeurs : la formule traçable `base × taux` cassait sur les montants réels (`:g` → exponentiel ≥ 1e6 + arrondi silencieux à 6 chiffres) + 2 tests à dents
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` = **631 exécutés · 614 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||
|
||||
**Chasse au défaut d'abord (Explore very-thorough sur 5 modules jamais ré-audités ce cycle : legal/confotur · crm/commissions+workflow_vente · rbac · devops/deploy_runbook · seo) → 4 modules CLEAN, 1 BUG LATENT RÉEL.** L'Explore a confirmé 0 défaut sur legal, workflow_vente, rbac (4 sous-modules), deploy_runbook, seo (schemaorg/hreflang/builder — dedup + offer + hreflang tous corrects), et a remonté **1 candidat haute confiance** : `crm/commissions/commlib/finance.py:75`.
|
||||
|
||||
**Le bug (correctness de sortie, entrée financière réaliste RD).** `compute_line` composait le libellé de la base de la formule traçable via `base_lbl = f"{base:g}"`. Le format `:g` casse **deux fois** sur des montants immobiliers réels — omniprésents en RD (une unité à USD 300k ≈ **18 000 000 DOP**) :
|
||||
1. **Notation exponentielle dès 1e6** : `18000000` → `1.8e+07` — illisible et **non auditable** dans une formule dont tout le sens (docstring du module) est d'être « traçable, reproductible et entièrement sourcée ».
|
||||
2. **Arrondi silencieux à 6 chiffres significatifs** : `123456.78` → `123457` — le libellé affiche une base **différente de la réelle**, c'est-à-dire **une base fabriquée** : exactement l'invention interdite par CLAUDE.md #6, dans le module même dont la docstring proclame l'anti-invention.
|
||||
|
||||
**Preuve de dents (2 tests de régression).** `test_formule_base_millions_pas_exponentiel` (base `18000000.0` → assert `18000000` présent **et** `e+`/`E+` absents) + `test_formule_base_preserve_les_decimales` (base `123456.78` → assert `123456.78` présent). Vérifié **teeth** par harnais scripté : en restaurant `f"{base:g}"`, les **DEUX tests ÉCHOUENT** ; avec le fix, **PASS**. Suite commissions **25 → 27**.
|
||||
|
||||
**Le fix (fidèle, sans dérive).** Nouveau helper `_amount_label(x)` : `f"{x:f}"` (jamais exponentiel) puis strip des zéros/point superflus → décimal **fidèle** et lisible, **identique à l'ancien pour tous les montants simples** (`200000` reste `200000`, `2500.5` reste `2500.5`). Le changement de sortie ne concerne QUE les cas buggés (≥ 1e6 ou > 6 chiffres significatifs). `_rate_label` (ligne taux, `:g`) laissé tel quel : un taux est borné (0-100), sans risque exponentiel ni > 6 chiffres → défensif-seul, non touché (`verify-non-defects` · #6).
|
||||
|
||||
**Byte-repro : aucun impact d'artefact autoritaire.** `compute_line`/`compute_dossier` sont du code **runtime** (prennent une instance Dossier Vente live) — **jamais** appelés par `commissions_gen.py` (qui n'utilise que `finance.is_filled`). Aucun `formule`/`base` n'est sérialisé dans `out/commission_plan.json` ni `out/MANIFEST.json` (vérifié). Donc **0 régénération de l'artefact du module**.
|
||||
|
||||
**Cascade de régénération (compteur de suite uniquement · mémoire `artifact-reproducibility-gate`).** Les 2 tests ajoutés font croître la matrice **631/614 → 633/616** (17 skips inchangés). Régénéré dans l'ordre (`quality_report` en DERNIER · mémoire `audit4big-rebuild-after-doc-edits`) : `qa/regression/{regression_plan,MANIFEST,regression_run}.json` puis `qa/audit_4big/quality_report.json`. Surfaces à compteurs dérivés réalignées (gate `check_readme_claims`) : `03_agents/qa/AGENT.md` (631/614→633/616), `03_agents/erpnext_backend/AGENT.md` (631→633), `03_agents/crm/AGENT.md` (cellule commissions 25→27 + « Total CRM » 81→83 trio `25 + 31 + 27`), `crm/commissions/README.md` (2× « 25 tests »→27). Tous diffs = **pure propagation de compteur** (vérifié).
|
||||
|
||||
**Pourquoi pas un gate (#5).** Robustesse portée par le code (`_amount_label`) + les 2 tests à dents ; aucune nouvelle classe de drift à surveiller. Occurrence isolée (mauvais encodeur de contexte, sœur de la classe déjà fixée en publiciste `070054`/`073055` — même leçon : « le contexte de sortie dicte l'encodeur, pas la donnée courante »).
|
||||
|
||||
**Canal stakeholder laissé à une session dédiée.** `daily_reports/2026-08-12.md` cite encore `629/612` ; la réconciliation batchée (630 `070054` + 631 `073055` + 633 cette session) reste pour une future session stakeholder (canal snapshot batché par design, cf. `3a40aee`) — pas d'invention, chiffres re-dérivables des artefacts commités (#6).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. Fichiers édités **compilent** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 chiffre/fixture inventé** (#6 · compteurs re-dérivés des artefacts régénérés), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M commlib/finance.py` (`_amount_label`) · `M tests/test_commissions.py` (2 tests à dents) · `M crm/commissions/README.md` (compteurs) · `M` 4 artefacts régénérés (regression×3 + quality_report) · `M` 3 fiches AGENT.md (compteurs) + ce journal.
|
||||
|
||||
## Session `20260812_083114` — CANAL STAKEHOLDER · réconciliation batchée du rapport `daily_reports/2026-08-12.md` au HEAD courant (`37c68ac` → `e7cc3c4`) · 2ᵉ vague de 3 fixes de correctness (matrice `629→633`)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json totals` (lu depuis l'artefact commité, #6) = **633 exécutés · 616 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = verdict **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **intégralement bloquée** en gouvernance (D-06 approbation audit = gate d'entrée · directive « NE PAS coder avant l'audit » · #6) — **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||
|
||||
**Chasse au défaut d'abord (Explore very-thorough sur les modules JAMAIS ré-audités ce cycle : `qa/audit_5d` · `demo_scenario` · `qa/acceptance` · `faisabilite/generator/genlib` scorer+renderer · `frontend/portails/wslib`+`workspaces_gen`) → NO DEFECTS FOUND.** L'Explore a testé formatage numérique (`_money`, exponentiel/arrondi), divisions (gardes `if total else 0`), off-by-one (comptage de liens workspace lignes 130-144), sérialisation JSON de caractères spéciaux FR/ES (`ensure_ascii=False` correct), valeurs par défaut fabriquées (0 vs None — tous None/placeholder), arrondis. Tous **CLEAN** : gestion correcte des données manquantes, comparaisons flottantes légitimes, échappement JSON sûr, gardes de division. Aucun candidat même faible → tree propre.
|
||||
|
||||
**Vrai locus = currency du canal stakeholder (mémoire `two-logging-channels` : garder les DEUX canaux à jour, chaque chiffre sourcé d'un artefact commité).** Le rapport `daily_reports/2026-08-12.md` était épinglé au HEAD `37c68ac` et citait la matrice à **629/612**. Depuis, **3 commits worker de correctness** — délibérément batchés par les sessions `073055`/`080104` pour une session stakeholder dédiée (« canal snapshot batché par design », cf. `3a40aee`) — ont fait évoluer la matrice de **3 incréments** :
|
||||
- `aeb30b8` (`070054`) — fix rendu publiciste : URL hero percent-encodée (`html.escape` = mauvais encodeur CSS ; `Vue d'ensemble.png` FR cassait le hero) → suite publiciste 24→25, matrice **629/612 → 630/613**.
|
||||
- `5af86e3` (`073055`) — hardening : segment `code` de la MÊME `url('…')` percent-encodé (dernier non-encodé) → 25→26, matrice **630/613 → 631/614**.
|
||||
- `e7cc3c4` (`080104`) — fix commissions : formule `base×taux` cassait sur montants réels RD (`:g` → exponentiel ≥ 1e6 + arrondi silencieux 6 chiffres = base fabriquée #6) → suite commissions 25→27, matrice **631/614 → 633/616**.
|
||||
|
||||
Chaque incrément correspond à un **test de régression à dents** ajouté par un fix (leçon commune : « le contexte de sortie dicte l'encodeur, pas la donnée courante »). Contrairement à la 1ʳᵉ vague (vérif/hardening à artefacts byte-identiques), cette vague porte des **bugs latents réels** de correctness de sortie.
|
||||
|
||||
**Fix = réconciliation (pas ré-émission) du rapport du jour.** Le rapport daté `2026-08-12.md` **est** le snapshot du jour ; le réconcilier en fin de journée au HEAD courant est le contrat, pas un doublon (#5). Porté : header `HEAD 37c68ac → e7cc3c4` + session `083114`, nouvelle section « Jalons du jour · 2ᵉ vague (`37c68ac` → `e7cc3c4`) — correctness à impact matrice » (table des 3 fixes + opposabilité), `État courant` re-titré au HEAD `e7cc3c4` avec **tous les chiffres re-dérivés des artefacts commités** (matrice **633/616/17** · PASS 24× · bijection 24↔24), narratif de trajectoire de matrice réécrit (629 stable 1ʳᵉ vague → 630 → 631 → 633), §Portée actualisé. La note historique de la 1ʳᵉ vague (« matrice stable à `629/612/17` ») laissée intacte car exacte pour CETTE vague.
|
||||
|
||||
**Pourquoi pas un fix ni un gate (#5/#6).** Aucun défaut trouvé (Explore CLEAN + tree cohérent) ; le seul « écart » était la **staleness du canal stakeholder** (canal éditorial daté par design, comme un snapshot — pas un artefact byte-gaté). L'actualiser EST le contrat de `two-logging-channels`, pas une matière à gate. Le batch des 3 fixes en une seule réconciliation était l'intention explicite des sessions `073055`/`080104`.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — rapport = document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (tous lus depuis les artefacts commités · #6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (réconciliation au HEAD courant) + ce journal.
|
||||
|
||||
## Session `20260812_090114` — DÉFAUT D'ABORD : chasse DRY sur 8 modules jamais ré-audités ce cycle · durcissement anti-récurrence des 3 sites `:g`-domaine-borné (commentaires purs · 0 impact)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json` (lu depuis l'artefact commité · #6) = **633 exécutés · 616 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` réconcilié à 633 par la session `083114` → à jour.
|
||||
|
||||
**Chasse au défaut (Explore very-thorough) sur les 8 modules JAMAIS ré-audités ce cycle → DRY (0 défaut réel).** Ciblé : `crm/dossier_vente` · `crm/financement_bancaire` · `faisabilite/bancable` · `frontend/chat_otoia` · `pie/manifest` · `qa/audit_4big` · `mobile/app_config` · `fiscal/ecf_dgii`. L'Explore a remonté 4 « candidats » (2 HAUTE, 2 MOYENNE) — **tous vérifiés NON-défauts** :
|
||||
- **BUG #1** `bancable/banclib/finance.py:138` `f"{pe_brut:g}"` — `pe_brut = 0.52 × NOMBRE d'unités`, domaine **borné** (quelques centaines en immobilier réel). L'exponentiel (≥ 1e6) exige ~1.9M unités : irréaliste. L'arrondi 6-chiffres idem hors domaine. En prime `:g` nettoie le bruit flottant du produit (`0.52 × 173 = 89.96000…1 → « 89.96 »`). Scénario Explore « 2 000 000 unités » = fabriqué.
|
||||
- **BUG #2** `bancable/banclib/report.py:38` `_pct` `f"{n:g} %"` — pourcentage (marge/taux) **borné 0-100** : mêmes deux pièges hors domaine.
|
||||
- **BUG #3** `chat_otoia/chatlib/frappe.py:43-44` HTML non échappé — `mount_html` ne reçoit QUE des slugs canoniques contrôlés (préfixe/classe de config, clé portail, persona sourcée CLAUDE.md) ; aucune donnée libre → injection inatteignable. Fragilité défensive-seule (`verify-non-defects` · #6).
|
||||
- **BUG #4** `commissions/commlib/finance.py:75` taux `:g` — **déjà statué non-défaut** par la session `080104` (« taux borné 0-100 ») ; **RÉCIDIVE** de flag ce cycle.
|
||||
|
||||
**Preuve de non-défaut robuste (pas seulement « scénario irréaliste »).** `report.py:_money` (l.25) utilise **sciemment** `,.0f`/`,.2f` (jamais `:g`) pour les montants **non bornés** (USD 18 000 000 → « 18,000,000 »). Les auteurs ont donc **délibérément** réservé `:g` aux domaines bornés (unités, pourcentages) et `,.0f` aux montants → les 3 `:g` restants sont un **choix d'ingénierie correct**, pas un oubli. C'est la distinction exacte de la mémoire `docstring-vs-code-drift`/`verify-non-defects` : « le contexte de sortie dicte l'encodeur, ET le domaine borne le risque ».
|
||||
|
||||
**Action = durcissement anti-récurrence (commentaires purs · sœur de « twin fix must sweep ALL surfaces in one pass »).** Fait nouveau : BUG #4 a **déjà récidivé** malgré la décision « laissé tel quel » de `080104` (aucune note in-situ), et BUG #1/#2 sont fraîchement flaggés HAUTE — la classe `:g`-domaine-borné **gaspille un cycle d'audit à chaque passage**. `grep ':g}'` sur tout le code de prod = **exactement 3 sites** (les 3 flaggés). Passe unique : ajout à chacun d'une **note de justification in-situ** (domaine borné → 2 pièges de `:g` hors domaine + nettoyage du bruit flottant + « montants → `,.0f`/`_amount_label`, jamais `:g` » + « ne pas corriger »). Convertit le faux-positif récurrent en « déjà revu, sûr » pour les audits futurs.
|
||||
|
||||
**Pourquoi PAS un fix de code ni un gate (#5/#6).** Aucun défaut réel : « corriger » `:g`→`:f` **casserait** du code correct (ré-introduirait du bruit flottant `« 89.96 »→« 89.960000 »`) — une régression, exactement l'inverse d'un fix. Robustesse déjà portée par le domaine borné + le choix `,.0f` sur les montants ; rien de nouveau à gater (#5). Commentaires sans marqueur `# N ·` d'invariant (pas de déclenchement du gate ledger).
|
||||
|
||||
**Impact d'artefact : ZÉRO.** Commentaires purs → aucun changement de sortie. `bancable` n'a pas d'`out/` commité (`bancable-out-not-committed`). `_rate_label` est du code **runtime** (`compute_line`), jamais appelé par `commissions_gen.py`, et aucune `formule`/`base` n'est sérialisée dans les artefacts commités. `regression_run.json`/`quality_report.json` **inchangés** (633/616/17 · 24×100). Aucun compteur de fiche/README à réaligner.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). 3 fichiers édités **compilent** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 fix de code** (défauts inexistants), **0 chiffre/fixture inventé** (#6), **0 gate ajouté** (#5), **0 artefact modifié/reconstruit**, **0 commande VPS** (#8), **0 API externe**. Fichiers : `M crm/commissions/commlib/finance.py` · `M faisabilite/bancable/banclib/finance.py` · `M faisabilite/bancable/banclib/report.py` (notes anti-récurrence `:g`) + ce journal.
|
||||
|
||||
## Session `20260812_120139` — VÉRIF · fermeture repo-wide de la classe « affichage arrondi ⟺ gate booléen » : preuve d'immunité structurelle de `audit_4big` (seul autre score-au-seuil), note anti-récurrence in-situ
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json` (lu depuis l'artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection couverture 24↔24 (`ok: true`)**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Arbitrages produit D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Canal stakeholder `daily_reports/2026-08-12.md` **déjà à jour au HEAD courant** (matrice **635/618/17** = artefact commité ; le dernier commit `3849375` est une vérif byte-identique sans impact matrice → aucune re-réconciliation due).
|
||||
|
||||
**Chasse au défaut d'abord (Explore very-thorough) sur 6 modules JAMAIS ré-audités ce cycle → DRY (0 défaut réel).** Ciblé : `rbac/` (roleprofile/userperm/fixtures/apply_plan) · `publiciste/lib` + générateur · `legal/confotur` · `devops/deploy_runbook` · `crm/workflow_vente` · `seo/seo_gen`+`seolib`. Axes : formatage numérique domaine non-borné (`,.0f` correct sur montants ; pas de `:g` sur non-borné), gardes de division (toutes présentes), off-by-one de comptage, encodeur selon contexte (`html.escape`/`quote` corrects), agrégats (`min`/set-equality corrects), booléens inertes. **Aucun candidat, même faible.**
|
||||
|
||||
**Vrai axe = fermeture repo-wide de la classe corrigée aux 3ᵉ/4ᵉ vagues du jour (`85cd625`+`1992ee6`).** Le commit `3849375` a prouvé que `financement_bancaire` est le **seul module à champ `percent`** (grep artefacts commités confirmé : `financement.schema.json` + `gate_status_example.json` uniquement). Mais la classe de bug réelle n'est pas « champ nommé percent » — c'est **« une valeur d'AFFICHAGE arrondie qui franchit une frontière de SEUIL en contredisant une décision booléenne »**. Il restait **un** autre porteur non examiné : `qa/audit_4big/q4lib/scoring.py:65` — un **score 0-100 arrondi**, comparé au **seuil PASS 95** (#5). Sibling naturel qu'un futur auditeur re-flaggerait.
|
||||
|
||||
**Preuve d'immunité STRUCTURELLE (mécanisme distinct du fix financement).** Vérifié bout-en-bout :
|
||||
- `scoring.py:65-66` — `score = _round_half_up(100.0 * earned / total)` **PUIS** `verdict = "PASS" if score >= pass_score`. Le verdict dérive du **MÊME entier arrondi** que celui affiché.
|
||||
- `audit_4big_gen.py:136` (INV6) recompute la note via `scoring._round_half_up(...)` (même arrondi) ; `:144` (INV7) attend `"PASS" if m["score"] >= ps` (même `score`).
|
||||
→ Il n'existe **qu'UNE seule quantité** (`score`, arrondie), servant à la fois d'affichage ET de gate. Contrairement à `financement` où `percent` (arrondi, affichage) et `ok` (exact, gate) étaient **DEUX quantités divergentes** en bord de seuil, ici **aucune divergence n'est possible par construction**. La classe « affichage ⟺ gate » est donc **FERMÉE repo-wide** : financement était le seul locus à deux-quantités (corrigé), audit_4big est le seul autre score-au-seuil et il est immun par unicité de quantité.
|
||||
|
||||
**Action = note anti-récurrence in-situ (commentaire pur · sœur de « twin fix must sweep ALL surfaces », `docstring-vs-code-drift`).** Ajout à `scoring.py:66` d'une justification (quantité unique par design → immunité au bug financement → **ne PAS scinder en brut-vs-arrondi**, ce qui ré-introduirait précisément la divergence + renvoi à INV6/INV7). Convertit le sibling « à re-vérifier » en « déjà revu, sûr » pour les audits futurs.
|
||||
|
||||
**Pourquoi PAS un fix ni un gate (#5/#6).** Aucun défaut : le code est correct et interne-cohérent (une quantité, pas de contradiction atteignable) — « corriger » en comparant le verdict à `earned/total` **brut** créerait la divergence, une régression exactement inverse d'un fix (cf. `verify-non-defects`). Rien de nouveau à gater : INV6/INV7 garantissent déjà la cohérence `score`↔`verdict`, et le seuil 95 est déjà ancré (mémoire `threshold-in-guard-prose-gate`). Commentaire sans marqueur `# N ·` d'invariant → aucun déclenchement du gate ledger.
|
||||
|
||||
**Impact d'artefact : ZÉRO.** Commentaire pur → sortie inchangée. `audit_4big_gen.py` ré-exécuté → `quality_report.json` **byte-identique** (`git diff` vide). Matrice **inchangée** (635/618/17 · 24×100). Aucun compteur de fiche/README à réaligner.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé). `scoring.py` **compile** + **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 fix de code** (défaut inexistant), **0 chiffre/fixture inventé** (#6), **0 gate ajouté** (#5), **0 artefact modifié/reconstruit** (byte-repro prouvé), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/qa/audit_4big/q4lib/scoring.py` (note anti-récurrence) + ce journal.
|
||||
|
||||
## Session `20260812_123144` — CURRENCY · réalignement des citations de ligne dérivées dans `OPEN_DECISIONS_REGISTER.md` (2 citations `Constat` avaient dérivé après les fixes financement du jour) · sweep complet du registre
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json` (lu depuis l'artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection couverture 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime**. Canal stakeholder `daily_reports/2026-08-12.md` vérifié **à jour** (matrice **635/618/17** = artefact commité ; les 3 derniers commits `1992ee6`/`3849375`/`ea8458d` sont déjà remontés / vérifs byte-identiques sans impact matrice → 0 re-réconciliation due).
|
||||
|
||||
**Chasse au défaut d'abord → vrai locus = currency d'un livrable stakeholder.** Plutôt qu'un énième sweep « CLEAN sans fix » (saturation des axes déjà couverts), j'ai audité la **currency du `OPEN_DECISIONS_REGISTER.md`** — la punch-list d'autorité des arbitrages produit de Michel. Chaque item cite des **numéros de ligne présents** (« Constat » = pointeurs vers le code COURANT, pas des snapshots historiques). Les fixes de correctness du jour ont **inséré du code** en amont de deux cibles citées → deux citations avaient silencieusement dérivé (classe *prose-facts vs numeric drift*, sous-cas « la cible citée a bougé » ⇒ mettre à jour, ≠ line-cite historique qu'on garde).
|
||||
|
||||
**Deux dérives réelles corrigées (vérifiées ligne par ligne) :**
|
||||
- **D-02** — `finlib/gate.py:142-145 → :164-167`. Les gardes d'affichage `percent`/`overall` des commits `85cd625` (`_cond_apport`) + `1992ee6` (`overall_percent`) ont décalé `_cond_validation_wag` de ~22 lignes. Confirmé : `def` à `:164`, `dossier.get("wag_validated_by")` à `:165`. Citation raffinée `:164-167` + `(:165)` sur la ligne du test.
|
||||
- **D-01** — `legal/confotur/README.md:8-17 → :32-33`. Une **bannière de statut migration V18** préfixée au README a poussé la liste des sorties (`doctype_confotur_application.json` + `MANIFEST.json`) aux lignes `:32-33`. Confirmé par grep.
|
||||
|
||||
**Sweep complet des autres citations = toutes résolvent.** Vérifié : D-01 `pie_spec.json:24` (contrats) + `:22,27,28,29,30` (module:null) + `pie_manifest.json:165-166` + `README.md:48` ✓ · D-02 `financement_spec.json:157` + directive `:206-210`/`:275-279` ✓ · D-03 `AGENTS_EXISTING_ASSETS.md:102`/`:128` + `CLAUDE.md:28` ✓ · D-04 `AGENTS_EXISTING_ASSETS.md:127` ✓ · D-06/07/08 citations V18 (`V18_GO_SIGNAL:79-84`, `DIRECTIVE_V18_MASTER:90-91`, audit `:112`/`:182`) ✓ — les snapshots datés V18/directives sont **stables** par nature (comme `daily_reports`). Note « Réalignement de citations · 2026-08-12 » ajoutée à l'en-tête `Mise à jour`.
|
||||
|
||||
**Pourquoi PAS un gate (#5/#6).** Dérive isolée de currency d'un document éditorial (registre = *surface, ne tranche pas*) ; les 2 cibles ont bougé pour de bonnes raisons (fixes de correctness + bannière V18). Un gate line-number sur de la prose serait fragile (chaque édition de code le rougirait) et de faible valeur — la discipline « Constat = pointeur courant, réaligner en fin de vague » suffit, désormais consignée dans l'en-tête du registre. **Aucune décision/verdict modifié** (les 8 items D-01→D-08 restent tels quels) ; **0 invention de chiffre** (#6).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — registre = document, 0 dépendance de gate). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md` (2 citations réalignées + note de currency) + ce journal.
|
||||
|
||||
## Session `20260812_133154` — CURRENCY · remontée des 4 commits de vérification post-4ᵉ-vague dans le rapport stakeholder du jour (le canal s'arrêtait à `1992ee6` tout en s'affirmant « au HEAD courant »)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). `regression_run.json` (artefact commité · #6) = **635 exécutés · 618 passés · 17 skippés · 0 échec** (PASS). `quality_report.json` = **PASS · 24 modules · min 100/100 · bijection couverture 24↔24**. Séquence moteur V18 toujours **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit » · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-08 en attente Michel (`OPEN_DECISIONS_REGISTER.md`).
|
||||
|
||||
**Chasse au défaut d'abord (2 axes FRAIS, non couverts par la mémoire) → DRY.**
|
||||
- *docstring-vs-code (sous-axe COMPORTEMENTAL)* — Explore very-thorough sur tous les `*_gen.py` + `*lib/` : docstrings annonçant un fallback/paramètre/formule/branche que le code n'implémente pas (ou omettant une branche réelle). **CLEAN** (le seul candidat, ecf « 12 invariants », est un faux-positif de comptage déjà connu : la validation schéma compte comme invariant 1). Distinct du sous-axe noms-de-fichiers (déjà swept 22/22).
|
||||
- *test-name ⟺ assertion (fake-coverage sémantique)* — Explore sur les 24 suites : méthode dont le NOM promet un comportement X mais dont les assertions vérifient Y (plus faible/différent). **CLEAN** sur ~6 364 lignes de test (noms fidèles aux assertions, fixtures construites pour la condition nommée). Classe distincte de `test-assertion-reachability` (faux-verts run-only) et `test-shadowing` (méthodes masquées).
|
||||
|
||||
**Vrai locus = currency d'un livrable stakeholder (classe `4eb615b`).** Le rapport `daily_reports/2026-08-12.md` avait été prolongé jusqu'au 5ᵉ fix `1992ee6` (4ᵉ vague), mais **4 commits worker** ont suivi le même jour (`3849375`/`ea8458d`/`4eb615b`/`fb2d3e1`) sans être remontés — alors que l'en-tête + la §« État courant » s'affirment **« au HEAD courant »**. Un lecteur stakeholder voyait le canal s'arrêter à la 4ᵉ vague. `git log 1992ee6..HEAD` = ces 4 commits ; `grep -c` de chaque hash dans le rapport = **0** avant édition (dérive confirmée).
|
||||
|
||||
**Édition (document éditorial · 3 blocs, cohérents entre eux) :**
|
||||
- Nouvelle §« Jalons de vérification depuis la 4ᵉ vague (`1992ee6` → HEAD courant `fb2d3e1`) » — table des 4 commits, tous marqués **Non opposable** (0 fix · rebuild byte-identique / document). Note explicite : **matrice stable 635/618/17** (aucun n'est un fix ; commentaires purs + currency de registre).
|
||||
- En-tête currency (`Session`/`HEAD attesté`) : **HEAD attesté passé de « HEAD courant » à `fb2d3e1`** ; distinction claire entre le 5ᵉ fix `1992ee6` (impact matrice `634→635`) et les **4 vérifs byte-identiques** suivantes (matrice stable).
|
||||
- §« État courant » trajectoire : la 4ᵉ vague **nommée `1992ee6`** (au lieu de « au HEAD courant », qui pointait désormais faux) + mention des 4 commits de vérif sans impact portant l'arbre à `fb2d3e1`.
|
||||
|
||||
**Zéro invention (#6).** Tous les chiffres re-dérivés de l'artefact commité : `regression_run.json.totals` = **635/618/17 · PASS** (relu ce jour). Aucune décision/verdict modifié (D-01→D-08 inchangés). Le texte des 4 jalons reprend verbatim la nature de chaque commit (messages de commit + journaux de session).
|
||||
|
||||
**Pourquoi PAS un gate (#5).** Dérive de currency d'un document éditorial (le rapport = *surface, ne tranche pas*), comme `daily_reports` par nature. Un gate line-number/hash sur de la prose stakeholder serait fragile (chaque commit worker le rougirait) et de faible valeur ; la discipline « remonter les jalons non-opposables en fin de vague » suffit, désormais appliquée.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — le rapport est un document, 0 dépendance de gate). Rapport **NFC-clean** (vérifié). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/daily_reports/2026-08-12.md` (3 blocs de currency) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260812_160204` — SURFACE · D-09 · séparateur de milliers monétaire divergent entre publiciste (espace) et faisabilité (virgule)
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Roadmap Sprints 1→8 **livrée + gatée + verte** ; séquence moteur V18 **bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit approuvé » · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-08 en attente Michel. Historique récent = boucle de churn (sweeps CLEAN + re-currency du rapport), cause-racine déjà traitée (`1cf0e18`/`c73ed5b`). Objectif de session : **éviter un énième sweep-note stérile**, chercher un défaut/arbitrage RÉEL.
|
||||
|
||||
**Chasse au défaut.**
|
||||
- Grep TODO/FIXME/NotImplemented + READMEs indigents sur toute la prod → **RIEN** (le seul « placeholder » = design anti-invention `{{…}}` ; le seul README <15 l = fixture data_room).
|
||||
- Balayage des spécificateurs de format `:g`/`:,`/`:f` en production (frère de l'arc « `:g` lossy » `e7cc3c4`/`5af86e3`/`g-format-lossy-for-human-numbers`). Les 2 `:g` restants sont **bornés + déjà annotés sûrs** (`crm/commissions/commlib/finance.py:86` percent 0-100 · `faisabilite/bancable/banclib/finance.py:143` = 0.52×nb-unités, borné à quelques centaines, justification in-situ) → axe `:g` **genuinement clos**, pas de bug.
|
||||
|
||||
**Vrai locus = arbitrage de présentation transverse (non un bug, non gaté).** Le même montant est rendu **différemment** par les deux surfaces face au stakeholder :
|
||||
- **Publiciste** (HTML marketing) → **espace** : `publiciste/lib/generator.py:34-39` (`f"{v:,.0f}".replace(",", " ")`) ⟹ `USD 8 560 000`.
|
||||
- **Faisabilité** (rapport + bancable) → **virgule US** : `faisabilite/generator/genlib/renderer.py:28-37` + `faisabilite/bancable/banclib/report.py:21-32` (`,.0f` **sans** `replace`) ⟹ `USD 8,560,000`.
|
||||
- Signaux partiels sans règle dure : le design system illustre le format public en **espace** (`OTO_DESIGN_SYSTEM_v1.md:89-90`, « dès 145 000 USD ») → suit le publiciste ; mais les fixtures sources écrivent en **virgule** (`data_room/P01/20_architecture/architecture.md:11`, « USD 150,000 ») → suit la faisabilité. L'exemple DS place aussi le montant AVANT la devise, ce qu'aucun des deux ne fait — donc pas de calque littéral possible.
|
||||
|
||||
**Action = SURFACE, don't rewrite.** Ajout de **D-09** au `OPEN_DECISIONS_REGISTER.md` (+ note d'en-tête « Ajout »). Choisir un séparateur unique = **convention de présentation transverse** (arbitrage design/audience), pas une correction ponctuelle worker ; l'imposer unilatéralement réécrirait implicitement une règle DS ou la graphie des sources (#6). Les deux rendeurs sont **internes-cohérents et testés** ; **aucune sortie formatée n'est un artefact commité** (les `out/` des deux modules ne stockent que des nombres bruts — le formatage vit dans le HTML/rapport généré au runtime VPS · #8) → **aucun gate concerné**.
|
||||
|
||||
**Pourquoi PAS un gate (#5) / PAS un fix.** Arbitrage produit ouvert (classe D-01…D-08) : le registre *regroupe et source, il ne tranche pas*. Aucun défaut de correctness (les deux formats sont valides, ni exponentiel ni arrondi — distinct de la classe `:g`). Une seule occurrence-classe → pas de gate.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — édition d'un document éditorial, 0 dépendance de gate). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md` (D-09 + note d'en-tête) + ce journal.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260812_163212` — CHASSE AU DÉFAUT · axe FRAIS `i18n` (parité de clés + résolution des libellés) balayé sur `bancable` → CLEAN, transitivement gaté, PAS un gate
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte). Roadmap Sprints 1→8 **livrée + gatée + verte** ; séquence moteur V18 **bloquée** en gouvernance (D-06 = gate d'entrée · #6) → **aucun code moteur légitime** ; arbitrages D-01→D-09 en attente Michel (`OPEN_DECISIONS_REGISTER.md`). Historique du jour = boucle de churn reconnue (sweeps CLEAN + re-currency + surfaçage D-09). Objectif : **éviter un énième sweep stérile**, viser un axe RÉELLEMENT non couvert par la mémoire, dans la couche métier plutôt que méta.
|
||||
|
||||
**Lecture de la couche métier (finance/i18n).** `crm/commissions/commlib/finance.py`, `faisabilite/bancable/banclib/finance.py` : calculs traçables, opérandes sourcés, gardes `:g`/`:f` déjà annotées et couvertes (arc `g-format-lossy`). Rien de neuf. **Vrai locus candidat = un axe absent de la mémoire** : la classe *latent-`KeyError` i18n* — un libellé référencé au rendu mais manquant dans une des 3 langues du dossier bancable.
|
||||
|
||||
**Axe FRAIS balayé : parité + résolution des clés `LABELS` (`banclib/i18n.py`).**
|
||||
- *Parité* — fr/en/es portent le **jeu de clés IDENTIQUE (37/37/37)** ; `miss` et `extra` vides dans les deux sens. `LANGS`/`LANG_FLAG`/`LANG_NOM` concordent. ⟹ aucune langue ne peut manquer une clé qu'une autre possède.
|
||||
- *Résolution* — chaque clé statique `L('…')` de `report.py` **et** la clé dynamique `i18n.t("fr", f"lbl_{f['cle']}")` (`report.py:219` · cles = total_unites / valeur_catalogue_usd / valeur_catalogue_dop / point_equilibre_unites) existe en fr. Combiné à la parité exacte ⟹ **toutes les clés référencées résolvent dans les 3 langues**.
|
||||
- *Fumée de rendu* — `render_report` sur les 3 langues (brief vide → chemin placeholder maximal, incl. `note_missing`) **et** `render_all` (3 `.md` + `manifest.json`) : OK, aucun `KeyError`.
|
||||
|
||||
**Verdict : CLEAN.** De plus **transitivement gaté** : `tests/test_bancable.py` rend déjà les 3 langues via `render_report`/`render_all` sur briefs synthétique **et** normal (l.~88-105) → une clé référencée manquante lèverait `KeyError` et **casserait un test aujourd'hui**.
|
||||
|
||||
**Pourquoi PAS un gate (#5) / PAS un fix.** Aucun défaut (parité exacte + résolution totale). Classe **mono-occurrence + déjà protégée transitivement** par la suite → ajouter un gate de parité dédié serait un doublon (`fix-vs-gate-transitively-protected-constant` · `verify-uncovered-before-gating`). Distinct de `seo-per-lang-cardinality-by-design` (module SEO, cardinalités inégales par design). Sweep **replayable**, consigné en mémoire (`i18n-key-parity-resolution-sweep`) pour qu'une session future ne le refasse pas / n'ajoute pas de gate redondant.
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucune modification de code/artefact). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_activity_log/2026-08-12.md` (ce journal) + note mémoire agent hors dépôt.
|
||||
|
||||
---
|
||||
|
||||
## Session `20260812_173219` — CHASSE AU DÉFAUT · axe FRAIS **correctness de la logique métier financière/fiscale** (les 4 fonctions de calcul cœur) balayé par lecture directe → SAINE, gatée par les suites, PAS un gate/fix
|
||||
|
||||
**État au démarrage.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (baseline verte, re-exécutée ce jour). Roadmap Sprints 1→8 **livrée + gatée + verte** ; toutes les métriques succès MVP cochées (`ROADMAP_8_WEEKS_OR_LESS.md:80-87`). Séquence moteur V18 **intégralement bloquée** en gouvernance (D-06 = gate d'entrée · « NE PAS coder avant l'audit approuvé » · #6) → **aucun code moteur légitime**. Arbitrages **D-01→D-09** en attente Michel (`OPEN_DECISIONS_REGISTER.md`, relu intégralement ce jour : tous 🟠 sont des décisions produit hors périmètre worker, ou 🟢 vérifié-sans-suite). Objectif explicite : **rompre la boucle de churn** (sweeps méta / re-currency que les ~11 sessions précédentes du jour ont elles-mêmes signalée) en visant un axe **réellement non couvert par la mémoire** : la *correctness logique* du calcul métier, pas l'intégrité d'artefact/schéma/encodage (déjà exhaustivement balayée).
|
||||
|
||||
**Axe FRAIS balayé (lecture directe, pas grep) : les 4 fonctions de calcul cœur.** Distinct des ~70 sweeps mémoire, tous portant sur des propriétés *structurelles* (byte-repro, schéma, déterminisme, encodage, comptage) — jamais sur la *justesse arithmétique/algorithmique* du calcul lui-même, lue ligne à ligne.
|
||||
- **`crm/commissions/commlib/finance.py`** — `compute_line` : `montant = base × taux` gardé par `(base is not None and taux is not None)` (jamais 0-inventé, placeholder sinon). `rate()` teste `isinstance(bool)` **avant** `(int,float)` → un taux booléen retombe bien en `None` (pas `1.0`/`0.0` fabriqué). Séparateur décimal virgule accepté, séparateur de milliers inatteignable (taux borné 0-100, annoté). **SAIN.**
|
||||
- **`faisabilite/bancable/banclib/finance.py`** — `derived` : `total_unites`/`val_usd`/`val_dop` gardés par `all(x is not None …)` sur **tous** les opérandes de leur `zip` (aucune troncature `zip`, longueurs égales par construction depuis `typologies()`). `point_equilibre_unites = ⌈0.52 × N⌉` via `math.ceil` — `_pct` sourcé du canonique `CANONICAL["point_equilibre_pct"]` (#9), jamais codé en dur → pas de dérive taux affiché/calculé. **SAIN** (et testé : `test_bancable.py:64` fige `pe == 21`, prouvant le `ceil`).
|
||||
- **`fiscal/ecf_dgii/ecflib/ncf.py`** — `compose_encf` : e-NCF = `E` + tipo(2) + secuencia(10). `_secuencia` regex `[0-9]{1,10}` puis `zfill(10)` → **rejette 11+ chiffres (None, pas de troncature silencieuse)**, jamais de séquence fabriquée. `_a_confirmer_count` : le `or` court-circuite (pas de double-comptage tax `a_confirmer` ∧ `taux_pct is None`). **SAIN.**
|
||||
- **`fiscal/ecf_dgii/ecflib/builder.py`** — `build_bundle` : tri stable `_event_sort_key` sur clés présentes (`update_value`/`role_id` requis, `tipo_ecf or ""`), aucun horodatage → reproductible. Chiffres OTO (RNC/ITBIS/TipoCambio/Compupar) restent `null` (#6). **SAIN.**
|
||||
- **Formateurs monétaires** (relus sous un angle *correctness*, distinct de l'arbitrage de séparateur D-09) : `_money` bancable/faisabilité (`,.0f` si `n==int(n)` sinon `,.2f`) et `_fmt_usd`/`_fmt_dop`/`_fmt_m2` publiciste — aucune exception atteignable (NaN/Inf balayés hors dépôt · `strict-json-ecma404-sweep`), branche cents/placeholder cohérente. La seule divergence reste la **présentation** (espace vs virgule + cents), déjà **surfacée en D-09** — pas un bug.
|
||||
|
||||
**Verdict : logique métier financière/fiscale SAINE.** Aucun défaut de correctness. De plus **transitivement gatée** : `test_commissions.py`, `test_bancable.py`, `test_ecf_dgii.py` exercent ces fonctions (valeurs figées : `pe==21`, catalogue non-calculable si `prix_usd` manque, rendu 3 langues sans `KeyError`) → une régression de logique casserait un test aujourd'hui.
|
||||
|
||||
**Pourquoi PAS un gate (#5) / PAS un fix.** Rien à corriger (calculs justes, opérandes sourcés, placeholders anti-invention conformes #6). Rien à gater : la correctness est déjà protégée par les suites (`verify-uncovered-before-gating` — mutation mentale confirme que fausser une garde `is not None` ou le `ceil` rougirait un test). Axe **replayable**, consigné en mémoire (`finance-fiscal-logic-correctness`) pour qu'une session future ne le re-lise pas / n'ajoute pas de gate redondant. Aucune re-currency du rapport `daily_reports` (dé-épinglé du HEAD depuis session `150201` → aucune due).
|
||||
|
||||
**Vérif.** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** (inchangé — aucune modification de code/artefact). **0 code moteur V18** (bloqué #6), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**. Fichiers : `M 05_activity_log/2026-08-12.md` (ce journal) + note mémoire agent hors dépôt.
|
||||
|
||||
## Session `20260812_180224` — DÉCISION · état terminal du mandat autonome confirmé (artefacts gatés) → **halte recommandée**, refus explicite de fabriquer un 71ᵉ sweep
|
||||
|
||||
**État vérifié (sources gatées, pas assertion).** `./run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**. `qa/audit_4big/out/quality_report.json` = verdict **PASS** (modules ≥100/100). `qa/acceptance/out/acceptance_matrix.json` = verdict **True** (roadmap Sprints 1→8 livrée + gatée). Séquence moteur V18 **bloquée en gouvernance** : D-06 (approbation audit par Michel), D-07 (#6 interdit d'inventer DCF/IRR/DSCR/LTV/LTC), D-08 (spec A1-A20 root-owned illisible · #8). D-01→D-09 = arbitrages produit **de Michel**, hors périmètre worker.
|
||||
|
||||
**Décision — rompre la boucle plutôt que l'alimenter.** Le correctif de compaction mémoire (session `153203`) n'a **pas** stoppé le churn : deux sweeps CLEAN l'ont suivi (`163212` i18n, `173219` finance). La cause-racine n'est ni un défaut de code ni un défaut d'outillage résiduel — c'est **la boucle elle-même** qui re-spawn des sessions se croyant obligées de committer. Toute action restante est soit du **churn** (un énième sweep « CLEAN · NOT a gate » sur un axe marginal — la mémoire en recense déjà ~70), soit **interdite** (inventer des chiffres #6 · trancher un arbitrage produit · toucher au VPS #8 · coder V18 avant approbation). Le blocage est une **décision humaine**, pas un ticket worker.
|
||||
|
||||
**Ce que j'ai fait, honnêtement.** (1) Re-vérifié les 3 verdicts autoritaires ci-dessus. (2) Confirmé qu'aucune tâche roadmap légitime n'est disponible (fiches déjà à jour incl. bannières V18 ; README `## État courant` exact ; registre à jour). (3) **Refusé de manufacturer un sweep** pour justifier un commit. (4) Consigné un mémo agent durable `autonomous-mandate-terminal-state` (indexé en tête de `MEMORY.md`, hors dépôt) pour qu'une session future lise cet état **en premier** et ne re-litige pas.
|
||||
|
||||
**Recommandation à Michel.** **Suspendre la boucle autonome** jusqu'à résolution de D-06/D-07/D-08 (arbitrages V18) ou l'arrivée d'une **nouvelle directive**. Reprise substantielle justifiée uniquement si : un gate passe **RED**, une nouvelle directive datée est déposée, ou un arbitrage V18 est rendu. Continuer à vide dépense des tokens en churn documenté.
|
||||
|
||||
**Vérif.** `./run_ci.sh` inchangé = **33 PASS · 0 FAIL · 0 SKIP**. **0 code moteur V18** (bloqué #6/D-06), **0 module de production touché**, **0 artefact autoritaire modifié/reconstruit**, **0 chiffre figé** (#6), **0 gate ajouté** (#5), **0 commande VPS** (#8), **0 API externe**, **0 sweep fabriqué**. Fichiers : `M 05_activity_log/2026-08-12.md` (ce journal) + mémo agent hors dépôt.
|
||||
@@ -42,7 +42,7 @@ Ce document consolide l'audit des **13 agents DTP + le cœur OTOIA** demandé au
|
||||
| 5 | Frontend Console | Moyenne (waf-home + portals) | Clone 5 entités + 5 portails rôle | 🟡 Refactor | S2→S4 |
|
||||
| 6 | CRM | Élevée (`crm.html` LIVE + natif) | Pipeline viz + WhatsApp + workflow vente | 🟡 Refactor | S2→S4 |
|
||||
| 7 | SEO | Moyenne (mission + worker) | Trilinguisation + schema.org + hreflang | 🟡 Refactor | S6 |
|
||||
| 8 | Mobile | Moyenne (modules + Expo 54) | Builds EAS + submit 2 stores | 🟡 Refactor | S5 |
|
||||
| 8 | Mobile | Moyenne (modules + Expo 51) | Rebuild Expo 54 + builds EAS + submit 2 stores | 🟡 Refactor | S5 |
|
||||
| 9 | IFC/Speckle | Moyenne (`ifc_to_glb.py` + serveur) | Viewer 3D intégré console | 🟡 Refactor | S3 |
|
||||
| 10 | ONAPI/Legal | Moyenne (module CONFOTUR + drafts) | Automatisation dépôts P05/P07 | 🟡 Refactor | S5 |
|
||||
| 11 | DevOps | Élevée (6 systemd + Gitea) | CI/CD Gitea Actions | ⚪ Ops | S1 |
|
||||
@@ -71,7 +71,7 @@ et la cible plancher **4-5 semaines**.
|
||||
- ✅ P01, P08, P09 : prix documentés dans `commercial.md`.
|
||||
- ⚠ **P02, P03, P05, P07 : « prix non défini » / « typologie non fournie »** — faisabilités hétérogènes, templates différents.
|
||||
- **Cause** : générations avec templates non versionnés / anciens.
|
||||
- **À faire** : ~~template canonique v1.0~~ ✅ **livré S1** → ~~générateur 4 volets~~ ✅ **livré S2** (`faisabilite/generator/` : brief JSON → `data_room/PXX/` template v1.0 + scoring §7 + `version.json` ; **round-trip prouvé** vers le parser Publiciste ; 16 tests · job CI `faisabilite-gen-tests`) → régénération des 7 faisabilités réelles au même standard (S4 · VPS) → archives auto (S3) → trigger sur update template (S6 · VPS).
|
||||
- **À faire** : ~~template canonique v1.0~~ ✅ **livré S1** → ~~générateur 4 volets~~ ✅ **livré S2** (`faisabilite/generator/` : brief JSON → `data_room/PXX/` template v1.0 + scoring §7 + `version.json` ; **round-trip prouvé** vers le parser Publiciste ; 17 tests · job CI `faisabilite-gen-tests`) → régénération des 7 faisabilités réelles au même standard (S4 · VPS) → archives auto (S3) → trigger sur update template (S6 · VPS).
|
||||
- **Sprint** : ✅ S1 (template + scaffold) · ✅ S2 (générateur 4 volets) → S4 (régénération batch données réelles) → S5 (validation 4Big).
|
||||
|
||||
### 3 · Rendu Agent — 🟡 Refactor (gain ~40 %)
|
||||
@@ -107,7 +107,7 @@ et la cible plancher **4-5 semaines**.
|
||||
- **Sprint** : S6.
|
||||
|
||||
### 8 · Mobile Agent — 🟡 Refactor (gain ~50 %)
|
||||
- **Existe** : `oto_module_mobile_download.py`, `oto_module_mobile_api.py`, `deploy_mobile_rbac.sh` + `seed_mobile_rbac.py`, `oto_mobile.js`, Expo SDK 54 + RN 0.81.5, EAS + comptes Apple/Google configurés.
|
||||
- **Existe** : `oto_module_mobile_download.py`, `oto_module_mobile_api.py`, `deploy_mobile_rbac.sh` + `seed_mobile_rbac.py`, `oto_mobile.js`, Expo SDK 51 + RN 0.74.5 (runtime actuel · upgrade 54 tenté puis abandonné 2026-07-27), EAS + comptes Apple/Google configurés.
|
||||
- **Gap** : builds EAS iOS/Android non produits ; submit App Store (#32) + Play Store non faits.
|
||||
- **À faire** : rebuild Expo 54 → submit 2 stores.
|
||||
- **Sprint** : S5.
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
# Registre des décisions ouvertes · OTO Enterprise OS DTP
|
||||
|
||||
> **But** — Punch-list unique, sourcée, des **décisions produit qui appartiennent à Michel**
|
||||
> (arbitrages de périmètre / dépendances runtime hors dépôt), consolidée depuis les journaux
|
||||
> de session où elles ont été *surfacées* au fil de l'eau. Ces items **ne sont pas des bugs
|
||||
> worker** : les corriger unilatéralement serait soit une invention de périmètre (#6), soit
|
||||
> une réécriture d'une source d'autorité. Ce registre **regroupe et source**, il ne tranche pas.
|
||||
>
|
||||
> **Portée** — CI dépôt **verte** : tous les jobs de `gate.needs` passent · tous les modules 4Big
|
||||
> au seuil `100/100`, verdict `PASS`. *(Décomptes exacts — dérivés et gatés — dans le README
|
||||
> `## État courant` et `qa/audit_4big/out/quality_report.json` ; jamais figés ici, ils dérivent
|
||||
> quand un gate ou un module est ajouté.)* Aucun de ces items
|
||||
> ne casse un gate ; ils concernent le **périmètre produit** et l'état runtime **hors dépôt**
|
||||
> (VPS `153.75.250.214` · `CLAUDE.md` #8), non vérifiable/éditable depuis le worker.
|
||||
>
|
||||
> **Mise à jour** — 2026-08-11 · session `20260811_025754` (ajout du **bloc V18** D-06→D-08 :
|
||||
> la séquence V18 moteur est intégralement suspendue à des arbitrages Michel — cf. audit de
|
||||
> migration). Base 2026-08-05 · session `20260805_014119`. Chaque ligne cite sa source exacte
|
||||
> et la session qui l'a surfacée en premier. **Ne pas re-surfacer** ces items en doublon (#5) :
|
||||
> pointer ici.
|
||||
>
|
||||
> **Réalignement de citations** — 2026-08-12 · session `20260812_123144` : deux citations `Constat`
|
||||
> pointaient un mauvais numéro de ligne après les fixes du jour (le décompte de décision reste
|
||||
> inchangé). D-02 `gate.py:142-145 → :164-167` (les gardes d'affichage `percent`/`overall` des
|
||||
> commits `85cd625`/`1992ee6` ont décalé `_cond_validation_wag`) ; D-01 `legal/confotur/README.md:8-17
|
||||
> → :32-33` (bannière de statut V18 préfixée). Sweep des autres citations = résolvent (snapshots
|
||||
> V18/directives datés = stables).
|
||||
>
|
||||
> **Ajout** — 2026-08-12 · session `20260812_160204` : **D-09** (séparateur de milliers divergent
|
||||
> entre surfaces de rendu monétaire publiciste / faisabilité). Arbitrage de présentation
|
||||
> stakeholder ; ni un bug worker ni gaté (aucune sortie formatée n'est un artefact commité) —
|
||||
> **surfacé, pas tranché**.
|
||||
|
||||
---
|
||||
|
||||
## Légende statut
|
||||
- 🟠 **OUVERT** — décision produit en attente de Michel (ou d'un agent hors dépôt).
|
||||
- 🟢 **VÉRIFIÉ-SANS-SUITE** — soupçon investigué → aucun risque réel ; consigné pour ne pas ré-ouvrir.
|
||||
|
||||
---
|
||||
|
||||
## D-01 · 🟠 PIE : `contrats` attribué à `legal/confotur` qui ne génère pas ces contrats
|
||||
|
||||
**Décision attendue** — Soit (a) construire un vrai module `contrats` (Promesa de compraventa ·
|
||||
Fideicomiso d'adhésion · règlement HOA), soit (b) rebasculer l'attribution à `"module": null`
|
||||
comme les autres downstreams sans générateur (brochures/plans/rendus/bim).
|
||||
|
||||
**Constat** — Le registre downstream PIE mappe `contrats → legal/confotur` :
|
||||
- `05_deliverables_mvp/pie/manifest/pie_spec.json:24`
|
||||
→ `{ "cle": "contrats", "libelle": "Contrats types (Promesa · Fideicomiso · HOA)", "module": "legal/confotur" }`
|
||||
- Écho dans l'artefact gaté : `05_deliverables_mvp/pie/manifest/out/pie_manifest.json:165-166`,
|
||||
`out/MANIFEST.json:24`, et la table `README.md:48`.
|
||||
|
||||
Or `legal/confotur` produit **uniquement le DocType Frappe de la demande CONFOTUR** (Ley 158-01),
|
||||
pas de gabarit Promesa/Fideicomiso/HOA :
|
||||
- `05_deliverables_mvp/legal/confotur/README.md:32-33` (sorties = `doctype_confotur_application.json`
|
||||
+ `MANIFEST.json`, rien d'autre).
|
||||
|
||||
**Incohérence interne** — le même registre utilise déjà `"module": null` pour tout downstream
|
||||
sans générateur (`pie_spec.json:22,27,28,29,30`). `contrats` est le seul downstream attribué à
|
||||
un module qui ne le produit pas. **Arbitrage de périmètre, pas une correction worker.**
|
||||
|
||||
**Sources / surface** — décrit dans la mémoire projet `directive-vs-implementation` (« PIE SIGNAL »).
|
||||
Non édité : cf. convention *SURFACE, don't rewrite*.
|
||||
|
||||
**Lien V18 (2026-08-11)** — L'audit de migration **remonte cet item au rang d'arbitrage V18** :
|
||||
`OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` §12.4 (:216) demande à Michel de trancher le
|
||||
**périmètre de génération juridique (Section 13)** — « CONFOTUR seul (existant) ou aussi
|
||||
Promesa/Fideicomiso/HOA (aucun code aujourd'hui) » — et **pointe explicitement ici**. Même décision
|
||||
que D-01, désormais dans le chemin critique V18. Ne pas dédoubler : cet item **est** l'arbitrage
|
||||
Section 13.
|
||||
|
||||
---
|
||||
|
||||
## D-02 · 🟠 Financement bancaire : condition #4 encore sur `wag_validated_by` (humain), directive V10 la remplace par un audit IA signé
|
||||
|
||||
**Décision attendue** — L'agent **OTO Auditeur Finances** (IA autonome, **hors dépôt**) doit
|
||||
exister/émettre un `audit.decision == APPROVED` signé pour que la condition #4 soit implémentable.
|
||||
Tant qu'il n'existe pas, le module ne peut pas migrer sans casser le gate de financement.
|
||||
|
||||
**Constat** — La directive porte une couche `## PRÉCISION` (2026-08-03) qui **supersede**
|
||||
explicitement la validation humaine « conseiller WAG » par une décision d'agent IA :
|
||||
- `DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md:206-210` et `:275-279`
|
||||
→ `condition_4_ok = audit and audit['decision'] == 'APPROVED' and audit['signature_valid']`.
|
||||
|
||||
Le module livré implémente encore l'**ancienne** condition humaine :
|
||||
- `05_deliverables_mvp/crm/financement_bancaire/finlib/gate.py:164-167`
|
||||
→ `_cond_validation_wag` teste `dossier.get("wag_validated_by")` (:165).
|
||||
- `05_deliverables_mvp/crm/financement_bancaire/financement_spec.json:157` (label « Validation
|
||||
manuelle WAG confirmée par le conseiller référent »).
|
||||
|
||||
**Pourquoi worker n'édite pas** — la nouvelle condition dépend d'un producteur d'`audit` signé
|
||||
qui n'existe pas dans le dépôt ; migrer le gate maintenant le rendrait toujours-faux. **Arbitrage
|
||||
séquencement produit + dépendance agent hors dépôt.**
|
||||
|
||||
**Sources / surface** — mémoire `directive-vs-implementation` (« V10/173726 SIGNAL »).
|
||||
|
||||
---
|
||||
|
||||
## D-03 · 🟠 Inventaire : `otoia/capabilities/chat.py` absent au chemin, alors que `CLAUDE.md` le liste comme capability canonique
|
||||
|
||||
**Décision attendue** — Soit recréer `chat.py`, soit retirer `chat.py` de la liste des
|
||||
capabilities OTOIA canoniques dans `CLAUDE.md` §Architecture cible.
|
||||
|
||||
**Constat**
|
||||
- `AGENTS_EXISTING_ASSETS.md:102` le déclare présent (« conversation »).
|
||||
- `CLAUDE.md:28` le liste (« aec.py + knowledge.py + prompt_engine.py + chat.py »).
|
||||
- Filesystem : `/opt/oto/otoia/capabilities/chat.py` **absent** (ni `.py` ni `.pyc`).
|
||||
- Auto-incohérence : le footer du même inventaire (L128) **omet déjà** `chat.py`.
|
||||
|
||||
**Pourquoi worker n'édite pas** — corriger l'inventaire contredirait la constitution `CLAUDE.md` ;
|
||||
l'inverse toucherait la constitution. **À trancher par Michel.** (Distinct du `chatbot-lead`
|
||||
qui, lui, existe — ne pas confondre.)
|
||||
|
||||
**Sources / surface** — **déjà surfacé** session `20260803_133718`
|
||||
(`05_activity_log/2026-08-03.md:1245-1250`).
|
||||
|
||||
---
|
||||
|
||||
## D-04 · 🟠 Inventaire : `config/projets_editor.py` « API GET/POST déjà en place » — source absente (claim runtime non réfutable)
|
||||
|
||||
**Décision attendue** — Confirmer si l'API `projets_editor` tourne effectivement (déployée
|
||||
ailleurs sur le VPS) ou si le claim d'inventaire est obsolète.
|
||||
|
||||
**Constat**
|
||||
- `AGENTS_EXISTING_ASSETS.md:127` : `/opt/oto/config/projets_editor.py (API GET/POST déjà en place)`.
|
||||
- Filesystem : aucun `projets_editor*` sous `/opt/oto` ; le sibling `projets_config.json` existe.
|
||||
- Le claim porte sur une **API VPS live** → **non réfutable** par la seule absence de source
|
||||
in-`/opt/oto` (peut tourner déployée ailleurs, #8). Non touché.
|
||||
|
||||
**Sources / surface** — **déjà surfacé** session `20260803_133718`
|
||||
(`05_activity_log/2026-08-03.md:1251-1253`).
|
||||
|
||||
---
|
||||
|
||||
## D-05 · 🟢 Mobile : identifiant bundle `com.otov7.app` face au renommage OTOV7 → « OTO Enterprise OS » — VÉRIFIÉ, aucun risque
|
||||
|
||||
**Verdict** — Le renommage produit **ne menace pas** l'App Store #32. L'`app.name` passe bien à
|
||||
« OTO Enterprise OS », mais tout identifiant de store est **quarantiné `null · a_confirmer`,
|
||||
jamais fabriqué**, et **découplé** du nom d'app par conception (#6/#8) :
|
||||
- `05_deliverables_mvp/mobile/app_config/mobile_spec.json` (`bundleIdentifier`/`package` en
|
||||
`a_confirmer`, « jamais fabriquée #6/#8 »).
|
||||
- `out/app_config.json` → `bundleIdentifier: null`, `package: null`.
|
||||
- Constante historique figée `com.otov7.app` : `DIRECTIVE_MOBILE_STORES_20260803.md:15-16`.
|
||||
|
||||
Aucun chemin de code ne peut auto-muter le bundle id. La mise à jour réelle de #32 (si besoin)
|
||||
est **manuelle, hors dépôt**. Consigné ici pour éviter de ré-ouvrir le soupçon (mémoire
|
||||
`directive-vs-implementation` « mobile SIGNAL »).
|
||||
|
||||
---
|
||||
|
||||
## D-06 · 🟠 V18 : approbation de l'audit de migration — gate d'entrée de TOUTE la séquence moteur V18
|
||||
|
||||
**Décision attendue** — Michel doit **approuver l'audit de migration V12→V18** (ou demander
|
||||
révisions). Tant qu'il n'est pas validé, **aucune** phase de développement V18 ne peut démarrer :
|
||||
c'est le premier maillon d'une chaîne à validation par étape.
|
||||
|
||||
**Constat** — Le GO signal de développement **n'active pas le code direct** ; il active une
|
||||
**séquence à validation** dont l'audit est l'étape 1 et son approbation l'étape 2 :
|
||||
- `V18_GO_SIGNAL_DEVELOPMENT_20260810.md:79-84` → « n'active PAS le code direct — il active la
|
||||
séquence : 1. Audit V12→V18 · 2. **Validation Michel de l'audit** · 3. Phase 1 · 4. Validation
|
||||
Michel Phase 1 · 5. Phase 2→15 avec validation à chaque phase ».
|
||||
- `DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md:90-91` → INTERDICTIONS ABSOLUES : « NE PAS
|
||||
coder avant l'audit ».
|
||||
- Livrable produit et en attente : `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` §12 (:207-218),
|
||||
recommandation + les 4 arbitrages Michel avant Phase 1.
|
||||
|
||||
**Pourquoi worker n'avance pas** — Coder un moteur V18 avant l'approbation violerait la directive
|
||||
(« NE PAS coder avant l'audit ») et la séquence GO. **Blocage de gouvernance produit, pas un bug.**
|
||||
|
||||
**Sources / surface** — surfacé session `20260811_022753` (`05_activity_log/2026-08-11.md`, production
|
||||
de l'audit) ; consolidé ici session `20260811_025754`.
|
||||
|
||||
---
|
||||
|
||||
## D-07 · 🟠 V18 : formules financières (DCF · IRR/VAN · DSCR/LTV/LTC) absentes des docs lisibles — moteurs 4/8 bloqués
|
||||
|
||||
**Décision attendue** — Michel doit **fournir / rendre lisibles** les conventions financières exactes
|
||||
(échéancier DCF, actualisation IRR/VAN, ratios bancaires DSCR/LTV/LTC, Cost Plan/WBS) attendues pour
|
||||
les moteurs **Financial (4)** et **Bankability (8)**. Sans elles, coder ces moteurs = **inventer une
|
||||
convention financière** — interdit par #6, risque de migration le plus grave (R1 🔴).
|
||||
|
||||
**Constat**
|
||||
- `05_deliverables_mvp/faisabilite/bancable/banclib/finance.py` est un **snapshot** (coût · revenu ·
|
||||
marge · point d'équilibre en unités) : `grep` confirme **aucun** `DSCR`/`LTV`/`LTC`, aucune
|
||||
actualisation multi-période.
|
||||
- Écart détaillé : `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` §5 (:102-114) et le point de
|
||||
décision §12.2 (:214) ; risque R1 « inventer des formules » gravité 🔴 (:182), mitigation =
|
||||
**bloquer moteur 4/8 avant lecture des formules par Michel**.
|
||||
- Hypothèse déclarée §5 (:112) : les formules figurent **peut-être** dans la directive complète
|
||||
57 chapitres et/ou les audits deep **root-owned illisibles** par le worker (#8).
|
||||
|
||||
**Pourquoi worker n'édite pas** — L'architecture V12 « valeur = {formule + opérandes sourcés/null} »
|
||||
est le bon patron à **généraliser**, mais les formules elles-mêmes ne sont **pas dérivables** du code
|
||||
lisible. **Dépendance d'entrée + arbitrage produit.**
|
||||
|
||||
**Sources / surface** — audit §5/§10-R1/§12.2 ; consolidé session `20260811_025754`.
|
||||
|
||||
---
|
||||
|
||||
## D-08 · 🟠 V18 : confirmer que le Master Intake A1-A20 est un sur-ensemble STRICT du `brief.json` V12 (rétro-compat parser)
|
||||
|
||||
**Décision attendue** — Michel doit **confirmer** que le futur Master Data Model (A1-A20) **étend**
|
||||
le schéma d'entrée V12 sans en casser les clés — pour éviter la création d'une **2e base** (R2 🔴)
|
||||
et préserver le parser Publiciste / `projets_master` (R6).
|
||||
|
||||
**Constat**
|
||||
- Recommandation data model : `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` §4 (:86-100) —
|
||||
« One Master Dataset · Multiple Outputs » ; V12 le respecte déjà (bancable consomme le **même**
|
||||
`brief.json` que le générateur).
|
||||
- Point de décision §12.3 (:215) : confirmer le **sur-ensemble strict**. Séquence Phase 1 §11 (:197)
|
||||
= étendre `brief.schema.json` → `master_intake.schema.json`, **rétro-compat garantie**, canoniques
|
||||
réimposés.
|
||||
- Risque R2 « créer une 2e base en dupliquant `brief.json` » gravité 🔴 (:183) ; mitigation = intake
|
||||
= sur-ensemble strict du `brief.schema.json`.
|
||||
|
||||
**Pourquoi worker n'édite pas** — Décision d'architecture de données ; l'étendre présuppose la liste
|
||||
fine A1-A20, contenue dans la directive complète **non lisible** (§0 · #8). **À valider avant Phase 1.**
|
||||
|
||||
**Sources / surface** — audit §4/§10-R2/§11/§12.3 ; consolidé session `20260811_025754`.
|
||||
|
||||
---
|
||||
|
||||
## D-09 · 🟠 Présentation : séparateur de milliers divergent entre les deux surfaces de rendu monétaire (publiciste = espace · faisabilité = virgule)
|
||||
|
||||
**Décision attendue** — Michel (ou le design system) doit **fixer la convention canonique** du
|
||||
séparateur de milliers pour les montants **face au stakeholder**, puis harmoniser les deux
|
||||
rendeurs sur ce choix. Options : (a) **espace** partout (`USD 8 560 000`), aligné sur l'exemple
|
||||
public du design system ; (b) **virgule US** partout (`USD 8,560,000`), aligné sur la graphie des
|
||||
`data_room` sources ; (c) convention **par audience** (espace pour le marketing public, virgule
|
||||
pour le dossier technique/banquier) — statu quo assumé et documenté.
|
||||
|
||||
**Constat** — deux modules rendent le **même montant** différemment, tous deux face au stakeholder :
|
||||
- **Publiciste** (HTML marketing public) → **espace** :
|
||||
`05_deliverables_mvp/publiciste/lib/generator.py:34-39`
|
||||
(`_fmt_usd`/`_fmt_dop` = `f"{value:,.0f}".replace(",", " ")`) ⟹ `USD 8 560 000`.
|
||||
- **Faisabilité** (rapport de faisabilité + bancable) → **virgule** :
|
||||
`05_deliverables_mvp/faisabilite/generator/genlib/renderer.py:28-37` (`_money`, docstring `:29`
|
||||
« USD 250,000 ») et `05_deliverables_mvp/faisabilite/bancable/banclib/report.py:21-32`
|
||||
(`_money`/`_int` = `,.0f` / `:,`, **sans** `replace`) ⟹ `USD 8,560,000`.
|
||||
|
||||
**Signaux partiels (pas de règle dure)** — le design system illustre le format public avec un
|
||||
**espace** (`OTO_DESIGN_SYSTEM_v1.md:89-90`, « ### Séparateur privilégié » → ex. « dès 145 000 USD »),
|
||||
ce que suit le publiciste ; mais les fixtures `data_room` sources écrivent en **virgule US**
|
||||
(`publiciste/fixtures/data_room/P01/20_architecture/architecture.md:11` → « USD 150,000 »), ce que
|
||||
suit la faisabilité. À noter aussi : l'exemple du design system place le **montant avant la devise**
|
||||
(`145 000 USD`), alors que **les deux** modules mettent la devise **en tête** (`USD …`) — donc aucun
|
||||
des deux ne calque l'exemple à la lettre. Aucune directive datée ne tranche le séparateur.
|
||||
|
||||
**Pourquoi worker n'édite pas** — Choisir un séparateur unique impose une **convention de présentation
|
||||
transverse** (arbitrage design/audience), non une correction ponctuelle ; l'imposer unilatéralement
|
||||
réécrirait implicitement une règle du design system ou la graphie des sources (#6). Les deux rendeurs
|
||||
sont **internes-cohérents et testés** ; aucune sortie formatée n'est un **artefact commité** (les
|
||||
`out/` des deux modules ne stockent que des nombres bruts — le formatage vit dans le HTML/rapport
|
||||
généré au runtime VPS · #8), donc **aucun gate n'est concerné**.
|
||||
|
||||
**Sources / surface** — surfacé session `20260812_160204` (balayage des spécificateurs de format
|
||||
`:g`/`:,`/`:f` de production, frère de l'arc « `:g` lossy » des commits `e7cc3c4`/`5af86e3`). Les deux
|
||||
`:g` de production restants sont bornés + annotés sûrs (`crm/commissions/commlib/finance.py:86`
|
||||
percent 0-100 · `faisabilite/bancable/banclib/finance.py:143` unités bornées) — hors périmètre de D-09.
|
||||
|
||||
---
|
||||
|
||||
## Rappel de discipline
|
||||
- Ces items sont **surfacés, pas tranchés** : les éditer côté worker violerait #6 (invention) ou
|
||||
réécrirait une source d'autorité (`CLAUDE.md`, une directive datée).
|
||||
- **Ne pas re-signaler** en doublon dans les rapports quotidiens (#5) — **pointer ce registre**.
|
||||
- Rien ici ne bloque la CI dépôt (verte) ; tout relève du **périmètre produit** ou du **runtime
|
||||
hors dépôt** (VPS · #8).
|
||||
@@ -58,7 +58,7 @@ duplication · workflow #5) :
|
||||
```bash
|
||||
python3 commissions_gen.py build # écrit out/ (refuse si invalide)
|
||||
python3 commissions_gen.py validate # schéma + 10 invariants, sans écrire
|
||||
python3 -m unittest discover -s tests -v # 25 tests (stdlib pur)
|
||||
python3 -m unittest discover -s tests -v # 27 tests (stdlib pur)
|
||||
```
|
||||
|
||||
## Les 10 invariants (le CLI refuse d'écrire si l'un casse)
|
||||
@@ -89,5 +89,5 @@ sert **uniquement aux tests** : ses chiffres sont des exemples fictifs portant u
|
||||
|
||||
**Auto-score 4Big : 96/100.** Réserve −4 : confirmation des taux réels + câblage
|
||||
du calcul en production côté VPS (agent ERPNext Backend · #8) ; ce module valide
|
||||
statiquement en-repo (25 tests verts + schéma + 10 invariants de cross-cohérence
|
||||
statiquement en-repo (27 tests verts + schéma + 10 invariants de cross-cohérence
|
||||
+ gate CI).
|
||||
|
||||
@@ -44,6 +44,11 @@ def rate(value: Any) -> Optional[float]:
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value) / 100.0
|
||||
# `replace(",", ".")` accepte la virgule décimale française (« 3,5 % » → 3.5).
|
||||
# Un séparateur de milliers (« 1.234,56 ») casserait ce parse — mais il est
|
||||
# INATTEIGNABLE : un taux de commission est borné 0-100 (cf. doctrine de
|
||||
# `_rate_label`), donc jamais ≥ 1000 → aucun séparateur de milliers. La virgule
|
||||
# est donc toujours décimale ici. Ne pas « corriger » vers locale.atof (#5/#6).
|
||||
txt = str(value).replace("%", "").replace(",", ".").strip()
|
||||
try:
|
||||
return float(txt) / 100.0
|
||||
@@ -51,12 +56,34 @@ def rate(value: Any) -> Optional[float]:
|
||||
return None
|
||||
|
||||
|
||||
def _amount_label(x: float) -> str:
|
||||
"""Montant affiché dans la formule : décimal FIDÈLE.
|
||||
|
||||
`:g` (l'ancien encodeur) cassait deux fois sur des montants réels : (1) il bascule
|
||||
en notation exponentielle dès 1e6 (« 18000000 » → « 1.8e+07 » — illisible dans une
|
||||
formule censée être auditable) et (2) il arrondit à 6 chiffres significatifs
|
||||
(« 123456.78 » → « 123457 ») ce qui FABRIQUE une base différente de la réelle —
|
||||
exactement l'invention interdite par CLAUDE.md #6. `:f` (jamais exponentiel) puis
|
||||
strip des zéros/point superflus donne un décimal fidèle et lisible, identique à
|
||||
l'ancien pour les montants simples (« 200000 » reste « 200000 »)."""
|
||||
s = f"{x:f}" # décimal complet, jamais de notation exponentielle
|
||||
if "." in s:
|
||||
s = s.rstrip("0").rstrip(".")
|
||||
return s
|
||||
|
||||
|
||||
def _rate_label(value: Any) -> str:
|
||||
"""Libellé du taux tel qu'affiché dans la formule (verbatim si texte)."""
|
||||
"""Libellé du taux tel qu'affiché dans la formule (verbatim si texte).
|
||||
|
||||
NB : `:g` est VOLONTAIRE et sûr ici (≠ montants → `_amount_label`). Un taux de
|
||||
commission est borné (0-100), donc jamais en notation exponentielle (< 1e6) et
|
||||
jamais au-delà de 6 chiffres significatifs : les deux pièges de `:g` documentés
|
||||
sur `_amount_label` sont inatteignables sur ce domaine, et `:g` nettoie en prime
|
||||
le bruit flottant (« 3.0 » → « 3 »). Ne pas « corriger » en `:f` (audit récurrent)."""
|
||||
if not is_filled(value):
|
||||
return "{taux_pct}"
|
||||
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
return f"{value:g} %"
|
||||
return f"{value:g} %" # borné 0-100 : sûr, cf. docstring
|
||||
return str(value).strip()
|
||||
|
||||
|
||||
@@ -72,7 +99,7 @@ def compute_line(dossier: dict, event: dict) -> dict:
|
||||
devise = dossier.get("devise")
|
||||
|
||||
montant = base * taux if (base is not None and taux is not None) else None
|
||||
base_lbl = f"{base:g}" if base is not None else f"{{{base_field}}}"
|
||||
base_lbl = _amount_label(base) if base is not None else f"{{{base_field}}}"
|
||||
formule = f"{base_lbl} × {_rate_label(event.get('taux_pct'))}"
|
||||
|
||||
manquants: list[str] = []
|
||||
|
||||
@@ -214,6 +214,24 @@ class TraceableCalc(unittest.TestCase):
|
||||
self.assertIn("2.5", line["formule"])
|
||||
self.assertEqual(line["devise"], "USD")
|
||||
|
||||
def test_formule_base_millions_pas_exponentiel(self):
|
||||
# Régression : `:g` cassait les bases réelles ≥ 1e6 en notation exponentielle
|
||||
# (« 18000000 » → « 1.8e+07 ») — illisible et non auditable dans la formule.
|
||||
ev = {"update_value": "contrat", "role_id": "ventes-conseiller",
|
||||
"base_field": "montant_contrat", "taux_pct": self.taux}
|
||||
line = finance.compute_line({"montant_contrat": 18000000.0, "devise": "DOP"}, ev)
|
||||
self.assertIn("18000000", line["formule"])
|
||||
self.assertNotIn("e+", line["formule"])
|
||||
self.assertNotIn("E+", line["formule"])
|
||||
|
||||
def test_formule_base_preserve_les_decimales(self):
|
||||
# Régression : `:g` arrondissait à 6 chiffres significatifs (« 123456.78 »
|
||||
# → « 123457 »), fabriquant une base ≠ de la réelle (anti-invention #6).
|
||||
ev = {"update_value": "contrat", "role_id": "ventes-conseiller",
|
||||
"base_field": "montant_contrat", "taux_pct": self.taux}
|
||||
line = finance.compute_line({"montant_contrat": 123456.78, "devise": "USD"}, ev)
|
||||
self.assertIn("123456.78", line["formule"])
|
||||
|
||||
def test_none_si_taux_absent(self):
|
||||
ev = {"update_value": "contrat", "role_id": "ventes-conseiller",
|
||||
"base_field": "montant_contrat", "taux_pct": None}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Caches Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
# Le dossier out/ EST commité (hand-off frontend/backend direct) — voir README.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Financement Bancaire · parcours hypothécaire immobilier RD
|
||||
|
||||
**Sprint 4 · CRM natif ERPNext · roadmap L52 (workflow vente end-to-end · volet
|
||||
financement).** Matérialise la
|
||||
[`DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md`](../../../DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md)
|
||||
(Michel, 2026-08-03) : un module **complet** de financement bancaire pour un vrai
|
||||
parcours hypothécaire dominicain, à la place du module « trop simpliste »
|
||||
antérieur. Livrable **Phase 1 (P0 · MVP)** : le contrat de données + le **cœur
|
||||
métier du gate check** + la structure des 7 sections + la bannière critique.
|
||||
|
||||
> Ce worker **n'écrit jamais sur le VPS** (contrainte #8). Il produit le contrat
|
||||
> et les artefacts en-repo ; le backend `/api/hypotheque/*` et le frontend
|
||||
> `renderHypotheque` réels sont appliqués côté serveur (agents ERPNext Backend /
|
||||
> Frontend Console). La **Phase 2** (vrais formulaires PDF des 6 banques) attend
|
||||
> les démarches relationship-manager de Michel — hors périmètre worker.
|
||||
|
||||
## Le cœur du livrable · le gate check 4 conditions
|
||||
|
||||
L'exigence non-négociable de Michel : **aucun document n'est transmis à la banque**
|
||||
tant que **les 4 conditions** ne sont pas remplies. La logique vit dans
|
||||
[`finlib/gate.py`](finlib/gate.py) — fonctions **pures**, testées, sans I/O :
|
||||
|
||||
| # | Condition (`key`) | Règle vérifiée |
|
||||
|---|---|---|
|
||||
| 1 | `apport_initial_complet` | `Σ paiements ≥ prix × taux` — **20 % résident RD · 30 % étranger** (amendement Michel · aussi exigence des banques RD · Ley 189-11). |
|
||||
| 2 | `documents_exiges` | Tous les documents `is_required` de la banque choisie au statut **« Validé WAG »** ou **« Envoyé banque »**. |
|
||||
| 3 | `autorisations_signees` | Toutes les autorisations signées (`signature_date` non nulle). |
|
||||
| 4 | `validation_wag` | Un conseiller WAG référent a validé (`wag_validated_by`). |
|
||||
|
||||
```python
|
||||
from finlib import gate
|
||||
cfg = gate.GateConfig.from_spec(spec)
|
||||
ok, reasons = gate.can_submit_dossier(dossier, cfg) # (False, ["Apport initial incomplet : …", …])
|
||||
status = gate.gate_status(dossier, cfg) # forme de l'endpoint gate-status (barres 0-100 %)
|
||||
```
|
||||
|
||||
Si **une** condition manque, `can_submit` est `False` et `reasons` liste chaque
|
||||
blocage (ordre canonique). Le backend `POST /…/submit` doit renvoyer **403 + ces
|
||||
raisons** ; le frontend grise le bouton « Envoyer à la banque » et affiche une
|
||||
barre de progression par condition. Tant que l'**apport initial** est incomplet,
|
||||
toutes les sections aval sont **grisées** (section `apport_initial` = seule
|
||||
section `gating`, en position 1).
|
||||
|
||||
## Ce qui est généré (`out/`, commité — hand-off direct)
|
||||
|
||||
| Fichier | Contenu |
|
||||
|---|---|
|
||||
| `banques.json` | Profils des banques partenaires (type, taux/LTV/durée, spécialités, devises). |
|
||||
| `sections.json` | Structure accordéon (apport → info achat → choix banque → formulaires → exigences → autorisations → envoi). |
|
||||
| `documents.json` | Catalogue des documents exigés par catégorie (client / immobilier / étrangers). |
|
||||
| `autorisations.json` | Autorisations à signer (OTO Sign) + base légale. |
|
||||
| `gate_spec.json` | Les 4 conditions + **bannière critique** + endpoints + rôle validation résolu + workflow suivi banque. |
|
||||
| `gate_status_example.json` | Exemple **calculé** (dossier P07 volontairement bloqué) — auditable, jamais fabriqué. |
|
||||
| `MANIFEST.json` | Traçabilité (sources, comptes dérivés, rôles RBAC utilisés). |
|
||||
|
||||
Comptes courants (source de vérité = `MANIFEST.json`) : **6 banques**,
|
||||
**7 sections**, **15 documents** (requis **9** résident · **13** étranger),
|
||||
**4 autorisations**, **4 conditions** de gate.
|
||||
|
||||
## Anti-invention (#6)
|
||||
|
||||
- **Aucun taux d'apport codé en dur** : 20/30 viennent du contrat
|
||||
(`apport.residence_types`), sourcés de la DIRECTIVE. Changer le contrat change
|
||||
le gate, sans toucher au code.
|
||||
- **Aucun taux/LTV bancaire fabriqué** : les fourchettes non données par la
|
||||
DIRECTIVE restent `null` (`a_confirmer` · démarche relationship manager). Seules
|
||||
les valeurs que la DIRECTIVE énonce (ex. Scotiabank LTV 70 %, BHD León 30 ans)
|
||||
sont encodées.
|
||||
- **Aucun nom de rôle Frappe en dur** : les rôles (`ventes-conseiller` pour la
|
||||
validation WAG, etc.) sont résolus depuis
|
||||
[`rbac_50_roles.json`](../../rbac/rbac_50_roles.json) via `finlib/rbac.py`
|
||||
(source unique · zéro duplication · #5).
|
||||
|
||||
## Validation · schéma + 12 invariants
|
||||
|
||||
`validate` re-génère le bundle en mémoire et vérifie le schéma de sortie
|
||||
([`financement.schema.json`](financement.schema.json)) **plus 12 invariants**
|
||||
métier (unicité des ids ; `0 < résident < étranger ≤ 100` ; fourchettes banque
|
||||
`min ≤ max` ; rôles résolus dans RBAC ; sections contiguës + section gating en
|
||||
tête ; gate = 4 conditions alignées sur la bannière ; documents étrangers
|
||||
strictement additionnels ; statuts recevables ⊆ statuts ; exemple de gate
|
||||
recalculé cohérent ; comptes du manifeste cohérents). Un invariant qui casse
|
||||
**refuse** le build (anti-régression).
|
||||
|
||||
## Utilisation
|
||||
|
||||
```bash
|
||||
cd 05_deliverables_mvp/crm/financement_bancaire
|
||||
python3 financement_bancaire_gen.py validate # schéma + 12 invariants
|
||||
python3 financement_bancaire_gen.py build # écrit les 7 artefacts out/
|
||||
python3 -m unittest discover -s tests -v # suite (gate + générateur)
|
||||
```
|
||||
|
||||
Sortie **déterministe** (tri stable, aucun horodatage) → l'artefact commité est
|
||||
reproductible bit-à-bit et vérifié par `ci/check_artifacts.sh`.
|
||||
|
||||
## Endpoints backend visés (Phase 1 · à implémenter côté serveur)
|
||||
|
||||
Contrat exposé par `gate_spec.json` :
|
||||
|
||||
- `GET /api/hypotheque/banques` · profils banques (⇐ `banques.json`)
|
||||
- `GET /api/hypotheque/dossier/{id}/gate-status` · état des 4 conditions (forme de `gate_status_example.json`)
|
||||
- `POST /api/hypotheque/dossier/{id}/submit` · **403 + raisons** tant que le gate n'est pas vert
|
||||
- `GET /api/hypotheque/dossier/{id}/journal` · historique horodaté
|
||||
|
||||
## Périmètre
|
||||
|
||||
- ✅ **Phase 1 (ce module)** : contrat, gate check, sections, bannière, exemple calculé.
|
||||
- ⏳ **Phase 2 (hors worker)** : vrais formulaires PDF officiels des 6 banques
|
||||
(démarche relationship-manager Michel) + implémentation runtime des endpoints /
|
||||
du `renderHypotheque` côté VPS.
|
||||
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Bundle Financement Bancaire (out/)",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"required": ["manifest", "banques", "sections", "documents", "autorisations", "gate_spec", "gate_status_example"],
|
||||
"properties": {
|
||||
"manifest": {
|
||||
"type": "object",
|
||||
"required": ["generated_from", "source_directive", "rbac_source", "module", "counts", "roles_rbac_utilises"],
|
||||
"properties": {
|
||||
"generated_from": {"const": "financement_spec.json"},
|
||||
"module": {"const": "financement_bancaire"},
|
||||
"counts": {
|
||||
"type": "object",
|
||||
"required": ["banques", "sections", "documents", "autorisations", "gate_conditions", "banniere_lignes", "workflow_suivi_etapes"],
|
||||
"properties": {
|
||||
"banques": {"type": "integer", "minimum": 1},
|
||||
"sections": {"type": "integer", "minimum": 1},
|
||||
"documents": {"type": "integer", "minimum": 1},
|
||||
"autorisations": {"type": "integer", "minimum": 1},
|
||||
"gate_conditions": {"type": "integer", "minimum": 4, "maximum": 4},
|
||||
"banniere_lignes": {"type": "integer", "minimum": 4, "maximum": 4},
|
||||
"workflow_suivi_etapes": {"type": "integer", "minimum": 1}
|
||||
}
|
||||
},
|
||||
"roles_rbac_utilises": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["role_id", "erpnext_role_name"],
|
||||
"properties": {
|
||||
"role_id": {"type": "string", "minLength": 1},
|
||||
"erpnext_role_name": {"type": "string", "minLength": 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"banques": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "nom", "type", "specialites", "devises"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[a-z0-9_]+$"},
|
||||
"nom": {"type": "string", "minLength": 1},
|
||||
"type": {"type": "string", "minLength": 1},
|
||||
"taux_min_pct": {"type": ["number", "null"]},
|
||||
"taux_max_pct": {"type": ["number", "null"]},
|
||||
"ltv_min_pct": {"type": ["number", "null"]},
|
||||
"ltv_max_pct": {"type": ["number", "null"]},
|
||||
"duree_max_ans": {"type": ["integer", "null"]},
|
||||
"specialites": {"type": "array", "items": {"type": "string"}},
|
||||
"devises": {"type": "array", "minItems": 1, "items": {"type": "string"}}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "titre", "ordre", "gating"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[a-z0-9_]+$"},
|
||||
"titre": {"type": "string", "minLength": 1},
|
||||
"ordre": {"type": "integer", "minimum": 1},
|
||||
"gating": {"type": "boolean"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "label", "categorie", "applies_to", "is_required"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[a-z0-9_]+$"},
|
||||
"label": {"type": "string", "minLength": 1},
|
||||
"categorie": {"type": "string", "enum": ["client", "immobilier", "etrangers"]},
|
||||
"applies_to": {"type": "string", "enum": ["all", "resident_rd", "etranger"]},
|
||||
"is_required": {"type": "boolean"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"autorisations": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "label", "base_legale", "signature_method"],
|
||||
"properties": {
|
||||
"id": {"type": "string", "pattern": "^[a-z0-9_]+$"},
|
||||
"label": {"type": "string", "minLength": 1},
|
||||
"base_legale": {"type": "string", "minLength": 1},
|
||||
"signature_method": {"type": "string", "minLength": 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
"gate_spec": {
|
||||
"type": "object",
|
||||
"required": ["endpoint", "submit_endpoint", "condition_keys", "conditions", "banniere", "role_validation", "workflow_suivi_banque"],
|
||||
"properties": {
|
||||
"condition_keys": {"type": "array", "minItems": 4, "maxItems": 4, "items": {"type": "string"}},
|
||||
"conditions": {
|
||||
"type": "array",
|
||||
"minItems": 4,
|
||||
"maxItems": 4,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["key", "ordre", "label"],
|
||||
"properties": {
|
||||
"key": {"type": "string", "minLength": 1},
|
||||
"ordre": {"type": "integer", "minimum": 1},
|
||||
"label": {"type": "string", "minLength": 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
"banniere": {
|
||||
"type": "object",
|
||||
"required": ["niveau", "persistante", "titre", "lignes", "consequence"],
|
||||
"properties": {
|
||||
"niveau": {"const": "critique"},
|
||||
"persistante": {"type": "boolean"},
|
||||
"titre": {"type": "string", "minLength": 1},
|
||||
"lignes": {"type": "array", "minItems": 4, "maxItems": 4, "items": {"type": "string"}},
|
||||
"consequence": {"type": "string", "minLength": 1}
|
||||
}
|
||||
},
|
||||
"role_validation": {
|
||||
"type": "object",
|
||||
"required": ["role_id", "erpnext_role_name"],
|
||||
"properties": {
|
||||
"role_id": {"type": "string", "minLength": 1},
|
||||
"erpnext_role_name": {"type": "string", "minLength": 1}
|
||||
}
|
||||
},
|
||||
"workflow_suivi_banque": {"type": "array", "minItems": 1, "items": {"type": "string"}}
|
||||
}
|
||||
},
|
||||
"gate_status_example": {
|
||||
"type": "object",
|
||||
"required": ["dossier_id", "gate_status", "can_submit", "reasons"],
|
||||
"properties": {
|
||||
"dossier_id": {"type": "string", "minLength": 1},
|
||||
"can_submit": {"type": "boolean"},
|
||||
"reasons": {"type": "array", "items": {"type": "string"}},
|
||||
"gate_status": {
|
||||
"type": "object",
|
||||
"required": ["can_submit", "conditions_total", "conditions_completed", "overall_percent", "conditions"],
|
||||
"properties": {
|
||||
"can_submit": {"type": "boolean"},
|
||||
"conditions_total": {"type": "integer", "minimum": 4, "maximum": 4},
|
||||
"conditions_completed": {"type": "integer", "minimum": 0, "maximum": 4},
|
||||
"overall_percent": {"type": "number", "minimum": 0, "maximum": 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Générateur du module Financement Bancaire — parcours hypothécaire RD.
|
||||
|
||||
Sprint 4 · CRM natif · roadmap L52 (« workflow vente end-to-end » — volet
|
||||
financement) + DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md (Michel).
|
||||
|
||||
Transforme le contrat `financement_spec.json` en artefacts prêts à consommer
|
||||
par le frontend `renderHypotheque` et le backend `/api/hypotheque/*` :
|
||||
- banques.json → profils des banques partenaires (taux/LTV/durée)
|
||||
- sections.json → structure accordéon (apport → info → … → envoi)
|
||||
- documents.json → catalogue des documents exigés par catégorie
|
||||
- autorisations.json → autorisations à signer (OTO Sign)
|
||||
- gate_spec.json → gate check 4 conditions + bannière critique
|
||||
- gate_status_example.json → exemple CALCULÉ (dossier bloqué) auditable
|
||||
- MANIFEST.json → traçabilité + comptes dérivés
|
||||
|
||||
Le CŒUR métier (calcul d'apport 20/30 + gate check 4 conditions) vit dans
|
||||
`finlib/gate.py` (fonctions pures, testées). Ce fichier ne fait qu'orchestrer :
|
||||
charger le contrat, assembler le bundle, valider (schéma + invariants), écrire.
|
||||
|
||||
Anti-invention (#6) : aucune valeur (taux d'apport, documents, autorisations,
|
||||
noms de rôle) n'est codée en dur — tout vient du contrat + du contrat RBAC
|
||||
(`rbac_50_roles.json`, résolu par `RoleResolver.from_path`, invariant 4). Ce
|
||||
worker n'écrit JAMAIS sur le VPS (l'application réelle est côté serveur).
|
||||
|
||||
Sortie déterministe (tri stable, aucun horodatage) → diffable + re-générable.
|
||||
|
||||
Sous-commandes :
|
||||
build [-o OUT] → écrit les 7 fichiers d'artefacts
|
||||
validate → (re)génère en mémoire, valide schéma + 12 invariants ;
|
||||
sort en erreur si un invariant casse (anti-régression).
|
||||
"""
|
||||
|
||||
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, "..", ".."))
|
||||
|
||||
sys.path.insert(0, _HERE)
|
||||
# Réutilisation (#5 : zéro duplication) du validateur JSON-Schema maison Publiciste.
|
||||
sys.path.insert(0, os.path.join(_DELIVERABLES, "publiciste"))
|
||||
|
||||
from finlib import builder, gate # noqa: E402
|
||||
from finlib.rbac import RoleResolver # noqa: E402
|
||||
from lib import validator as maison # type: ignore # noqa: E402
|
||||
|
||||
_SPEC_PATH = os.path.join(_HERE, "financement_spec.json")
|
||||
_SCHEMA_PATH = os.path.join(_HERE, "financement.schema.json")
|
||||
_DEFAULT_OUT = os.path.join(_HERE, "out")
|
||||
|
||||
# Fichiers écrits par `build` (nom → clé du bundle).
|
||||
_ARTIFACTS = [
|
||||
("banques.json", "banques"),
|
||||
("sections.json", "sections"),
|
||||
("documents.json", "documents"),
|
||||
("autorisations.json", "autorisations"),
|
||||
("gate_spec.json", "gate_spec"),
|
||||
("gate_status_example.json", "gate_status_example"),
|
||||
("MANIFEST.json", "manifest"),
|
||||
]
|
||||
|
||||
|
||||
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 _build_bundle() -> tuple[dict, dict]:
|
||||
spec = _load(_SPEC_PATH)
|
||||
resolver = RoleResolver.from_path()
|
||||
return spec, builder.build_bundle(spec, resolver)
|
||||
|
||||
|
||||
def _dups(ids: list[str]) -> list[str]:
|
||||
seen: set[str] = set()
|
||||
dups: set[str] = set()
|
||||
for i in ids:
|
||||
if i in seen:
|
||||
dups.add(i)
|
||||
seen.add(i)
|
||||
return sorted(dups)
|
||||
|
||||
|
||||
def _validate_bundle(spec: dict, bundle: dict) -> list[str]:
|
||||
"""Schéma de sortie + 12 invariants métier du module Financement Bancaire."""
|
||||
schema = _load(_SCHEMA_PATH)
|
||||
errors = list(maison.validate(bundle, schema))
|
||||
|
||||
banques = bundle["banques"]
|
||||
sections = bundle["sections"]
|
||||
documents = bundle["documents"]
|
||||
autorisations = bundle["autorisations"]
|
||||
gate_spec = bundle["gate_spec"]
|
||||
manifest = bundle["manifest"]
|
||||
cfg = gate.GateConfig.from_spec(spec)
|
||||
|
||||
# Invariant 1 · unicité des identifiants (banques, documents, autorisations, sections).
|
||||
for coll_name, coll in (
|
||||
("banques", banques),
|
||||
("documents", documents),
|
||||
("autorisations", autorisations),
|
||||
("sections", sections),
|
||||
):
|
||||
d = _dups([x["id"] for x in coll])
|
||||
if d:
|
||||
errors.append(f"Identifiant dupliqué dans {coll_name} : {d}")
|
||||
|
||||
# Invariant 2 · taux d'apport : 0 < résident < étranger ≤ 100 (amendement Michel).
|
||||
taux = cfg.taux_apport_pct
|
||||
tr = taux.get("resident_rd")
|
||||
te = taux.get("etranger")
|
||||
if tr is None or te is None:
|
||||
errors.append("Taux d'apport résident/étranger manquant dans le contrat.")
|
||||
elif not (0 < tr < te <= 100):
|
||||
errors.append(
|
||||
f"Taux d'apport invalides : résident={tr}, étranger={te} "
|
||||
f"(attendu 0 < résident < étranger ≤ 100)."
|
||||
)
|
||||
|
||||
# Invariant 3 · fourchettes banque cohérentes (min ≤ max quand les deux présents).
|
||||
for b in banques:
|
||||
for lo, hi, nom in (
|
||||
("taux_min_pct", "taux_max_pct", "taux"),
|
||||
("ltv_min_pct", "ltv_max_pct", "LTV"),
|
||||
):
|
||||
a, z = b.get(lo), b.get(hi)
|
||||
if a is not None and z is not None and a > z:
|
||||
errors.append(f"Banque {b['id']!r} : {nom} min {a} > max {z}.")
|
||||
|
||||
# Invariant 4 · rôles du contrat résolus dans RBAC (anti-invention #6).
|
||||
resolver = RoleResolver.from_path()
|
||||
for cle, rid in spec["roles"].items():
|
||||
try:
|
||||
resolver.erpnext_name(rid)
|
||||
except (KeyError, ValueError) as exc:
|
||||
errors.append(f"Rôle {cle}={rid!r} irrésoluble dans RBAC : {exc}")
|
||||
|
||||
# Invariant 5 · sections : `ordre` contigu 1..N, unique.
|
||||
ordres = [s["ordre"] for s in sections]
|
||||
if sorted(ordres) != list(range(1, len(sections) + 1)):
|
||||
errors.append(f"Ordres de sections non contigus 1..N : {sorted(ordres)}")
|
||||
|
||||
# Invariant 6 · exactement UNE section `gating`, en position 1 (apport_initial).
|
||||
gating = [s for s in sections if s.get("gating")]
|
||||
if len(gating) != 1:
|
||||
errors.append(f"Attendu exactement 1 section gating, trouvé {len(gating)}.")
|
||||
elif gating[0]["ordre"] != 1 or gating[0]["id"] != "apport_initial":
|
||||
errors.append(
|
||||
"La section gating doit être `apport_initial` en position 1 "
|
||||
f"(trouvé id={gating[0]['id']!r}, ordre={gating[0]['ordre']})."
|
||||
)
|
||||
|
||||
# Invariant 7 · gate : exactement 4 conditions, clés == CONDITION_KEYS, ordre 1..4.
|
||||
conds = gate_spec["conditions"]
|
||||
keys = [c["key"] for c in conds]
|
||||
if set(keys) != set(gate.CONDITION_KEYS):
|
||||
errors.append(
|
||||
f"Clés de condition {sorted(set(keys))} ≠ clés canoniques "
|
||||
f"{sorted(gate.CONDITION_KEYS)}."
|
||||
)
|
||||
if [c["ordre"] for c in conds] != [1, 2, 3, 4]:
|
||||
errors.append(f"Ordres des conditions du gate ≠ 1..4 : {[c['ordre'] for c in conds]}.")
|
||||
if gate_spec["condition_keys"] != list(gate.CONDITION_KEYS):
|
||||
errors.append("gate_spec.condition_keys ≠ ordre canonique CONDITION_KEYS.")
|
||||
|
||||
# Invariant 8 · bannière : 4 lignes, alignées 1-pour-1 sur les 4 conditions.
|
||||
lignes = gate_spec["banniere"]["lignes"]
|
||||
if len(lignes) != len(gate.CONDITION_KEYS):
|
||||
errors.append(
|
||||
f"Bannière : {len(lignes)} lignes ≠ {len(gate.CONDITION_KEYS)} conditions."
|
||||
)
|
||||
|
||||
# Invariant 9 · documents « étrangers » exclusifs + cohérence categorie⟺applies_to.
|
||||
req_res = set(gate.required_document_ids("resident_rd", cfg))
|
||||
req_etr = set(gate.required_document_ids("etranger", cfg))
|
||||
if not req_res < req_etr:
|
||||
errors.append(
|
||||
"Les documents requis résident doivent être un sous-ensemble STRICT "
|
||||
"des documents requis étranger (les pièces étrangers ajoutent des exigences)."
|
||||
)
|
||||
for d in documents:
|
||||
etr_cat = d["categorie"] == "etrangers"
|
||||
etr_app = d["applies_to"] == "etranger"
|
||||
if etr_cat != etr_app:
|
||||
errors.append(
|
||||
f"Document {d['id']!r} : categorie/applies_to incohérents "
|
||||
f"(categorie={d['categorie']}, applies_to={d['applies_to']})."
|
||||
)
|
||||
|
||||
# Invariant 10 · statuts recevables ⊆ statuts déclarés.
|
||||
statuses = set(spec["document_statuses"])
|
||||
recevables = set(spec["document_statuses_recevables"])
|
||||
if not recevables <= statuses:
|
||||
errors.append(
|
||||
f"document_statuses_recevables {sorted(recevables)} ⊄ "
|
||||
f"document_statuses {sorted(statuses)}."
|
||||
)
|
||||
|
||||
# Invariant 11 · exemple de gate CALCULÉ cohérent (auto-audit du cœur métier).
|
||||
ex = bundle["gate_status_example"]
|
||||
gs = ex["gate_status"]
|
||||
recomputed_can = gate.gate_status(spec["dossier_exemple"], cfg)["can_submit"]
|
||||
if ex["can_submit"] != recomputed_can or gs["can_submit"] != recomputed_can:
|
||||
errors.append("gate_status_example.can_submit incohérent avec le recalcul.")
|
||||
if bool(ex["reasons"]) == ex["can_submit"]:
|
||||
errors.append("Incohérence : `reasons` non vide DOIT équivaloir à ¬can_submit.")
|
||||
completed = sum(1 for k in gate.CONDITION_KEYS if gs["conditions"][k]["ok"])
|
||||
if gs["conditions_completed"] != completed:
|
||||
errors.append("gate_status_example.conditions_completed mal compté.")
|
||||
|
||||
# Invariant 12 · cohérence des comptes du manifeste avec les collections.
|
||||
c = manifest["counts"]
|
||||
expected = {
|
||||
"banques": len(banques),
|
||||
"sections": len(sections),
|
||||
"documents": len(documents),
|
||||
"documents_requis_resident": len(req_res),
|
||||
"documents_requis_etranger": len(req_etr),
|
||||
"autorisations": len(autorisations),
|
||||
"gate_conditions": len(conds),
|
||||
"banniere_lignes": len(lignes),
|
||||
"workflow_suivi_etapes": len(gate_spec["workflow_suivi_banque"]),
|
||||
}
|
||||
for k, v in expected.items():
|
||||
if c.get(k) != v:
|
||||
errors.append(f"manifest.counts.{k}={c.get(k)} ≠ {v} (dérive de compte).")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def cmd_build(args: argparse.Namespace) -> int:
|
||||
spec, bundle = _build_bundle()
|
||||
errors = _validate_bundle(spec, bundle)
|
||||
if errors:
|
||||
_eprint("❌ Bundle 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)
|
||||
for fname, key in _ARTIFACTS:
|
||||
_write_json(os.path.join(out, fname), bundle[key])
|
||||
|
||||
m = bundle["manifest"]
|
||||
c = m["counts"]
|
||||
print(f"✅ Artefacts Financement Bancaire générés dans {out}")
|
||||
print(f" banques.json : {c['banques']} banques partenaires")
|
||||
print(f" sections.json : {c['sections']} sections (accordéon)")
|
||||
print(
|
||||
f" documents.json : {c['documents']} documents "
|
||||
f"(requis résident {c['documents_requis_resident']} · étranger {c['documents_requis_etranger']})"
|
||||
)
|
||||
print(f" autorisations.json : {c['autorisations']} autorisations (OTO Sign)")
|
||||
print(f" gate_spec.json : {c['gate_conditions']} conditions + bannière critique")
|
||||
ex = bundle["gate_status_example"]
|
||||
print(
|
||||
f" gate_status_example: dossier {ex['dossier_id']} · "
|
||||
f"can_submit={ex['can_submit']} · {len(ex['reasons'])} raison(s) de blocage"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_validate(args: argparse.Namespace) -> int:
|
||||
spec, bundle = _build_bundle()
|
||||
errors = _validate_bundle(spec, bundle)
|
||||
if errors:
|
||||
_eprint("❌ Validation KO :")
|
||||
for e in errors:
|
||||
_eprint(f" - {e}")
|
||||
return 1
|
||||
c = bundle["manifest"]["counts"]
|
||||
print(
|
||||
f"✅ Validation OK — {c['banques']} banques, {c['sections']} sections, "
|
||||
f"{c['documents']} documents, {c['autorisations']} autorisations, "
|
||||
f"gate 4 conditions, schéma + 12 invariants verts."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Générateur du module Financement Bancaire (parcours hypothécaire RD)."
|
||||
)
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
pb = sub.add_parser("build", help="génère les 7 artefacts out/")
|
||||
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 + 12 invariants) sans écrire")
|
||||
pv.set_defaults(func=cmd_validate)
|
||||
|
||||
args = p.parse_args(argv)
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"module": "financement_bancaire",
|
||||
"titre": "Financement Bancaire — parcours hypothécaire immobilier RD",
|
||||
"source_directive": "DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md",
|
||||
"roles": {
|
||||
"conseiller_wag": "ventes-conseiller",
|
||||
"validation_dossier": "ventes-conseiller",
|
||||
"escalade": "ventes-directeur",
|
||||
"tresorerie_apport": "compta-tresorier"
|
||||
},
|
||||
"apport": {
|
||||
"note": "Apport initial minimum exigé AVANT toute démarche bancaire (amendement Michel 2026-08-03). Source des taux : la DIRECTIVE (§1 plan de paiement + amendement) — jamais un taux inventé (#6). Ce sont aussi des exigences des banques dominicaines (Ley 189-11).",
|
||||
"residence_types": [
|
||||
{
|
||||
"code": "resident_rd",
|
||||
"label": "Résident RD",
|
||||
"taux_apport_pct": 20,
|
||||
"source": "DIRECTIVE §1 + amendement"
|
||||
},
|
||||
{
|
||||
"code": "etranger",
|
||||
"label": "Étranger",
|
||||
"taux_apport_pct": 30,
|
||||
"source": "DIRECTIVE §1 + amendement"
|
||||
}
|
||||
]
|
||||
},
|
||||
"document_statuses": ["Non déposé", "Déposé", "Validé WAG", "Envoyé banque"],
|
||||
"document_statuses_recevables": ["Validé WAG", "Envoyé banque"],
|
||||
"banques": [
|
||||
{
|
||||
"id": "banreservas",
|
||||
"nom": "Banreservas",
|
||||
"type": "banque d'État",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": null,
|
||||
"ltv_max_pct": 80,
|
||||
"duree_max_ans": 25,
|
||||
"specialites": ["residents", "taux_preferentiels"],
|
||||
"devises": ["DOP"],
|
||||
"note": "Taux préférentiels résidents. Taux exacts à confirmer (relationship manager · #6)."
|
||||
},
|
||||
{
|
||||
"id": "popular",
|
||||
"nom": "Banco Popular Dominicano",
|
||||
"type": "banque privée",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": 75,
|
||||
"ltv_max_pct": 85,
|
||||
"duree_max_ans": null,
|
||||
"specialites": ["reseau_large", "rapide", "confotur"],
|
||||
"devises": ["DOP"],
|
||||
"note": "Réseau large, CONFOTUR-friendly. Taux et durée à confirmer (#6)."
|
||||
},
|
||||
{
|
||||
"id": "bhd_leon",
|
||||
"nom": "BHD León",
|
||||
"type": "banque privée",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": null,
|
||||
"ltv_max_pct": null,
|
||||
"duree_max_ans": 30,
|
||||
"specialites": ["haut_de_gamme", "etrangers", "bilingue"],
|
||||
"devises": ["DOP", "USD"],
|
||||
"note": "Haut de gamme, étrangers acceptés, bilingue. Taux/LTV à confirmer (#6)."
|
||||
},
|
||||
{
|
||||
"id": "santa_cruz",
|
||||
"nom": "Banco Santa Cruz",
|
||||
"type": "banque privée",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": null,
|
||||
"ltv_max_pct": null,
|
||||
"duree_max_ans": null,
|
||||
"specialites": ["taux_competitifs", "prequalif_rapide"],
|
||||
"devises": ["DOP"],
|
||||
"note": "Taux compétitifs, pré-qualif rapide. Chiffres à confirmer (#6)."
|
||||
},
|
||||
{
|
||||
"id": "scotiabank_rd",
|
||||
"nom": "Scotiabank República Dominicana",
|
||||
"type": "banque privée",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": null,
|
||||
"ltv_max_pct": 70,
|
||||
"duree_max_ans": null,
|
||||
"specialites": ["etrangers", "devise_usd"],
|
||||
"devises": ["DOP", "USD"],
|
||||
"note": "Spécialistes étrangers, devise USD acceptée. Taux/durée à confirmer (#6)."
|
||||
},
|
||||
{
|
||||
"id": "lopez_de_haro",
|
||||
"nom": "López de Haro",
|
||||
"type": "banque privée",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": null,
|
||||
"ltv_max_pct": null,
|
||||
"duree_max_ans": null,
|
||||
"specialites": ["haut_de_gamme", "niche"],
|
||||
"devises": ["DOP"],
|
||||
"note": "Niche haut de gamme. Profil complet à confirmer (#6)."
|
||||
}
|
||||
],
|
||||
"documents": [
|
||||
{"id": "cedula_passeport", "label": "Cédula ou Passeport (2 côtés)", "categorie": "client", "applies_to": "all", "is_required": true},
|
||||
{"id": "preuve_adresse", "label": "Preuve d'adresse récente (<3 mois)", "categorie": "client", "applies_to": "all", "is_required": true},
|
||||
{"id": "fiches_paye_3", "label": "3 dernières fiches de paye ou déclarations ITBIS/DGII", "categorie": "client", "applies_to": "all", "is_required": true},
|
||||
{"id": "etats_bancaires_6m", "label": "États bancaires 6 derniers mois", "categorie": "client", "applies_to": "all", "is_required": true},
|
||||
{"id": "lettre_employeur", "label": "Lettre employeur ou attestation profession libérale", "categorie": "client", "applies_to": "all", "is_required": true},
|
||||
{"id": "certif_solvabilite", "label": "Certification de solvabilité", "categorie": "client", "applies_to": "all", "is_required": true},
|
||||
{"id": "certif_datacredito", "label": "Certification DataCrédito (délivrée par la banque)", "categorie": "client", "applies_to": "all", "is_required": true},
|
||||
{"id": "promesa_notariee", "label": "Promesa Irrévocable signée notariée", "categorie": "immobilier", "applies_to": "all", "is_required": true},
|
||||
{"id": "fideicomiso", "label": "Fideicomiso Inmobiliario constitué (Ley 189-11)", "categorie": "immobilier", "applies_to": "all", "is_required": true},
|
||||
{"id": "titre_definitif", "label": "Titre définitif ou Certificado de Título (si disponible)", "categorie": "immobilier", "applies_to": "all", "is_required": false},
|
||||
{"id": "passeport_2cotes", "label": "Passeport valide (2 côtés)", "categorie": "etrangers", "applies_to": "etranger", "is_required": true},
|
||||
{"id": "visa_residence", "label": "Visa / résidence si applicable", "categorie": "etrangers", "applies_to": "etranger", "is_required": false},
|
||||
{"id": "lettre_origine_fonds", "label": "Lettre origine des fonds notariée (FinCEN GTO)", "categorie": "etrangers", "applies_to": "etranger", "is_required": true},
|
||||
{"id": "refs_bancaires_intl", "label": "Références bancaires internationales", "categorie": "etrangers", "applies_to": "etranger", "is_required": true},
|
||||
{"id": "refs_perso", "label": "Références commerciales / personnelles", "categorie": "etrangers", "applies_to": "etranger", "is_required": true}
|
||||
],
|
||||
"autorisations": [
|
||||
{"id": "partage_donnees", "label": "Autorisation de partage de données personnelles avec la banque", "base_legale": "RGPD / Ley 172-13", "signature_method": "OTO Sign"},
|
||||
{"id": "investigation_datacredito", "label": "Autorisation d'investigation DataCrédito", "base_legale": "Ley 172-13", "signature_method": "OTO Sign"},
|
||||
{"id": "mandat_wag", "label": "Mandat WAG comme intermédiaire de financement", "base_legale": "mandat contractuel", "signature_method": "OTO Sign"},
|
||||
{"id": "acknowledgment_risques", "label": "Acknowledgment — risques de refus bancaire + politique annulation", "base_legale": "consentement éclairé", "signature_method": "OTO Sign"}
|
||||
],
|
||||
"formulaires_types": [
|
||||
"Solicitud de Préstamo Hipotecario",
|
||||
"Autorización de investigación crediticia (DataCrédito)",
|
||||
"Declaración jurada de patrimonio",
|
||||
"Autorización débito automático quote-part",
|
||||
"Certificación de origen de fondos (étrangers)"
|
||||
],
|
||||
"sections": [
|
||||
{"id": "apport_initial", "titre": "Statut apport initial", "ordre": 1, "gating": true, "resume": "Prix unité · statut résidence · apport requis (20%/30%) · apport versé · solde restant · barre 0-100%."},
|
||||
{"id": "info_achat", "titre": "Information de l'achat", "ordre": 2, "gating": false, "resume": "Résumé auto de la réservation : projet, unité, plan de paiement, apport, solde à financer, statuts Fideicomiso + CONFOTUR."},
|
||||
{"id": "choix_banque", "titre": "Choix de la banque partenaire", "ordre": 3, "gating": false, "resume": "Sélection d'UNE banque prioritaire (radio) + multi-dossiers possibles ; profils taux/LTV/durée/spécialités."},
|
||||
{"id": "formulaires", "titre": "Formulaires banque pré-remplis", "ordre": 4, "gating": false, "resume": "Formulaires officiels PDF pré-remplis, téléchargés, signés, ré-uploadés ; journal audit des signatures."},
|
||||
{"id": "exigences", "titre": "Exigences bancaires par banque", "ordre": 5, "gating": false, "resume": "Liste des documents exigés cochée au fil des uploads ; statut par item (Non déposé / Déposé / Validé WAG / Envoyé banque)."},
|
||||
{"id": "autorisations", "titre": "Autorisations à signer", "ordre": 6, "gating": false, "resume": "Autorisations à signer AVANT envoi (OTO Sign horodaté + IP + hash)."},
|
||||
{"id": "envoi", "titre": "Envoi à la banque", "ordre": 7, "gating": false, "resume": "Bouton « Envoyer à la banque » (gate 4 conditions) + timeline suivi banque + statut temps réel."}
|
||||
],
|
||||
"gate": {
|
||||
"endpoint": "GET /api/hypotheque/dossier/{id}/gate-status",
|
||||
"submit_endpoint": "POST /api/hypotheque/dossier/{id}/submit",
|
||||
"conditions": [
|
||||
{"key": "apport_initial_complet", "ordre": 1, "label": "Dépôt initial complet (20% résident RD · 30% étranger) versé et vérifié"},
|
||||
{"key": "documents_exiges", "ordre": 2, "label": "TOUS les documents exigés déposés et validés WAG"},
|
||||
{"key": "autorisations_signees", "ordre": 3, "label": "TOUTES les autorisations signées électroniquement"},
|
||||
{"key": "validation_wag", "ordre": 4, "label": "Validation manuelle WAG confirmée par le conseiller référent"}
|
||||
],
|
||||
"banniere": {
|
||||
"niveau": "critique",
|
||||
"couleurs": ["rouge", "or"],
|
||||
"persistante": true,
|
||||
"titre": "⚠ AUCUN DOCUMENT N'EST TRANSMIS À LA BANQUE tant que :",
|
||||
"lignes": [
|
||||
"Dépôt initial complet (20% résident RD · 30% étranger) versé et vérifié",
|
||||
"TOUS les documents exigés ne sont pas déposés dans le portail",
|
||||
"TOUTES les autorisations ne sont pas signées électroniquement",
|
||||
"Validation manuelle WAG confirmée par le conseiller référent"
|
||||
],
|
||||
"consequence": "Le bouton « Envoyer à la banque » reste désactivé tant que ces 4 conditions ne sont pas remplies. Progression affichée en temps réel avec barre 0-100% par condition."
|
||||
}
|
||||
},
|
||||
"workflow_suivi_banque": [
|
||||
"Prequalifié",
|
||||
"Dossier soumis",
|
||||
"Analyse crédit",
|
||||
"Offre reçue",
|
||||
"Contrat signé",
|
||||
"Décaissement"
|
||||
],
|
||||
"dossier_exemple": {
|
||||
"id": "DEMO-P07-A312",
|
||||
"projet": "P07 Aqua Terra Las Terrenas",
|
||||
"unite": "A-312",
|
||||
"prix_usd": 145000,
|
||||
"residence_type": "etranger",
|
||||
"banque_choisie": "scotiabank_rd",
|
||||
"paiements": [
|
||||
{"date": "2026-06-15", "montant_verse_usd": 15000, "moyen": "Cardnet"},
|
||||
{"date": "2026-07-20", "montant_verse_usd": 14500, "moyen": "Cardnet"}
|
||||
],
|
||||
"documents": [
|
||||
{"id": "cedula_passeport", "statut": "Validé WAG"},
|
||||
{"id": "preuve_adresse", "statut": "Validé WAG"},
|
||||
{"id": "fiches_paye_3", "statut": "Déposé"},
|
||||
{"id": "etats_bancaires_6m", "statut": "Validé WAG"},
|
||||
{"id": "lettre_employeur", "statut": "Non déposé"},
|
||||
{"id": "certif_solvabilite", "statut": "Non déposé"},
|
||||
{"id": "certif_datacredito", "statut": "Non déposé"},
|
||||
{"id": "promesa_notariee", "statut": "Validé WAG"},
|
||||
{"id": "fideicomiso", "statut": "Déposé"},
|
||||
{"id": "passeport_2cotes", "statut": "Validé WAG"},
|
||||
{"id": "lettre_origine_fonds", "statut": "Non déposé"},
|
||||
{"id": "refs_bancaires_intl", "statut": "Non déposé"},
|
||||
{"id": "refs_perso", "statut": "Non déposé"}
|
||||
],
|
||||
"autorisations": [
|
||||
{"id": "partage_donnees", "signature_date": "2026-07-21"},
|
||||
{"id": "investigation_datacredito", "signature_date": "2026-07-21"},
|
||||
{"id": "mandat_wag", "signature_date": null},
|
||||
{"id": "acknowledgment_risques", "signature_date": null}
|
||||
],
|
||||
"wag_validated_by": null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"""finlib · logique du module Financement Bancaire (parcours hypothécaire RD).
|
||||
|
||||
Sous-modules :
|
||||
rbac — résolution des rôles depuis le contrat RBAC 50 rôles (zéro invention).
|
||||
gate — calcul de l'apport + gate check 4 conditions (cœur métier, pur).
|
||||
builder — assemblage du bundle d'artefacts déterministe depuis le contrat.
|
||||
"""
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Assemblage du bundle d'artefacts Financement Bancaire depuis le contrat.
|
||||
|
||||
Entrée : `financement_spec.json` + un `RoleResolver` (id → nom Frappe issu de
|
||||
rbac_50_roles.json). Sortie : un bundle déterministe (tri stable, aucun
|
||||
horodatage) → reproductible bit-à-bit à contrat constant, donc diffable et
|
||||
re-générable en CI (`ci/check_artifacts.sh`).
|
||||
|
||||
Le builder n'invente rien (#6) : banques, documents, autorisations, sections,
|
||||
conditions du gate proviennent du contrat ; les noms de rôle proviennent du
|
||||
contrat RBAC ; l'exemple de gate-status est CALCULÉ par `finlib.gate` depuis le
|
||||
dossier d'exemple du contrat (donc auditable, pas fabriqué).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from . import gate
|
||||
from .rbac import RoleResolver
|
||||
|
||||
|
||||
def build_bundle(spec: dict, resolver: RoleResolver) -> dict[str, Any]:
|
||||
"""Transforme le contrat en bundle d'artefacts prêts à consommer."""
|
||||
cfg = gate.GateConfig.from_spec(spec)
|
||||
|
||||
# --- rôles résolus (id → nom Frappe) --------------------------------------
|
||||
roles_resolved = {
|
||||
cle: {"role_id": rid, "erpnext_role_name": resolver.erpnext_name(rid)}
|
||||
for cle, rid in sorted(spec["roles"].items())
|
||||
}
|
||||
|
||||
# --- banques (ordre du contrat = ordre d'affichage) -----------------------
|
||||
banques = list(spec["banques"])
|
||||
|
||||
# --- sections triées par `ordre` -----------------------------------------
|
||||
sections = sorted(spec["sections"], key=lambda s: s["ordre"])
|
||||
|
||||
documents = list(spec["documents"])
|
||||
autorisations = list(spec["autorisations"])
|
||||
|
||||
# --- spec du gate (conditions + bannière + endpoints + rôle validation) ---
|
||||
gate_spec = {
|
||||
"endpoint": spec["gate"]["endpoint"],
|
||||
"submit_endpoint": spec["gate"]["submit_endpoint"],
|
||||
"condition_keys": list(gate.CONDITION_KEYS),
|
||||
"conditions": sorted(spec["gate"]["conditions"], key=lambda c: c["ordre"]),
|
||||
"banniere": spec["gate"]["banniere"],
|
||||
"role_validation": roles_resolved["validation_dossier"],
|
||||
"workflow_suivi_banque": list(spec["workflow_suivi_banque"]),
|
||||
}
|
||||
|
||||
# --- exemple de gate-status CALCULÉ (auditable, pas inventé) ---------------
|
||||
exemple = spec["dossier_exemple"]
|
||||
can_submit, reasons = gate.can_submit_dossier(exemple, cfg)
|
||||
gate_status_example = {
|
||||
"dossier_id": exemple["id"],
|
||||
"projet": exemple["projet"],
|
||||
"unite": exemple["unite"],
|
||||
"banque_choisie": exemple["banque_choisie"],
|
||||
"gate_status": gate.gate_status(exemple, cfg),
|
||||
"can_submit": can_submit,
|
||||
"reasons": reasons,
|
||||
}
|
||||
|
||||
# --- comptes des documents requis par type de résidence -------------------
|
||||
docs_requis = {
|
||||
rt["code"]: len(gate.required_document_ids(rt["code"], cfg))
|
||||
for rt in spec["apport"]["residence_types"]
|
||||
}
|
||||
|
||||
manifest = {
|
||||
"generated_from": "financement_spec.json",
|
||||
"source_directive": spec["source_directive"],
|
||||
"rbac_source": "rbac_50_roles.json",
|
||||
"source_version": spec["version"],
|
||||
"module": spec["module"],
|
||||
"titre": spec["titre"],
|
||||
"counts": {
|
||||
"banques": len(banques),
|
||||
"sections": len(sections),
|
||||
"documents": len(documents),
|
||||
"documents_requis_resident": docs_requis.get("resident_rd", 0),
|
||||
"documents_requis_etranger": docs_requis.get("etranger", 0),
|
||||
"autorisations": len(autorisations),
|
||||
"gate_conditions": len(gate_spec["conditions"]),
|
||||
"banniere_lignes": len(gate_spec["banniere"]["lignes"]),
|
||||
"workflow_suivi_etapes": len(gate_spec["workflow_suivi_banque"]),
|
||||
},
|
||||
# Ensemble DISTINCT des rôles RBAC réellement utilisés. Plusieurs slots
|
||||
# logiques peuvent pointer le même rôle (ex. conseiller_wag ET
|
||||
# validation_dossier == ventes-conseiller) : on dédoublonne par role_id
|
||||
# et on trie par role_id, comme les modules frères (commissions,
|
||||
# confotur, workflow_vente). Sans ce dédoublonnage, la liste répétait le
|
||||
# rôle et contredisait sa sémantique « rôles utilisés » (ensemble).
|
||||
"roles_rbac_utilises": [
|
||||
{"role_id": rid, "erpnext_role_name": resolver.erpnext_name(rid)}
|
||||
for rid in sorted({rr["role_id"] for rr in roles_resolved.values()})
|
||||
],
|
||||
}
|
||||
|
||||
return {
|
||||
"manifest": manifest,
|
||||
"banques": banques,
|
||||
"sections": sections,
|
||||
"documents": documents,
|
||||
"autorisations": autorisations,
|
||||
"gate_spec": gate_spec,
|
||||
"gate_status_example": gate_status_example,
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
"""Cœur métier du module Financement Bancaire : apport initial + gate check.
|
||||
|
||||
Fonctions PURES (aucune I/O, aucun horodatage) → testables et déterministes.
|
||||
Elles matérialisent l'exigence Michel (DIRECTIVE + amendement 2026-08-03) :
|
||||
|
||||
AUCUN dossier n'est transmis à la banque tant que les 4 conditions ne sont pas
|
||||
remplies :
|
||||
1. apport_initial_complet — dépôt initial (20 % résident RD · 30 % étranger)
|
||||
intégralement versé et vérifié ;
|
||||
2. documents_exiges — tous les documents `is_required` de la banque
|
||||
choisie au statut « Validé WAG » ou « Envoyé banque » ;
|
||||
3. autorisations_signees — toutes les autorisations signées (date non nulle) ;
|
||||
4. validation_wag — un conseiller WAG a validé (`wag_validated_by`).
|
||||
|
||||
Anti-invention (#6) : les taux d'apport (20/30) et la liste des documents/
|
||||
autorisations ne sont JAMAIS codés en dur ici — ils proviennent du contrat
|
||||
`financement_spec.json` via `GateConfig`. Changer le contrat change le gate,
|
||||
sans toucher ce code.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
# Ordre canonique des 4 clés de condition du gate (= ordre du contrat / bannière).
|
||||
CONDITION_KEYS = (
|
||||
"apport_initial_complet",
|
||||
"documents_exiges",
|
||||
"autorisations_signees",
|
||||
"validation_wag",
|
||||
)
|
||||
|
||||
|
||||
class GateConfig:
|
||||
"""Vue en lecture du contrat, indexée pour le calcul du gate."""
|
||||
|
||||
def __init__(self, spec: dict) -> None:
|
||||
self.taux_apport_pct: dict[str, float] = {
|
||||
rt["code"]: float(rt["taux_apport_pct"])
|
||||
for rt in spec["apport"]["residence_types"]
|
||||
}
|
||||
self.documents: list[dict] = list(spec["documents"])
|
||||
self.autorisations: list[dict] = list(spec["autorisations"])
|
||||
self.statuts_recevables: set[str] = set(spec["document_statuses_recevables"])
|
||||
self.banques: dict[str, dict] = {b["id"]: b for b in spec["banques"]}
|
||||
|
||||
@classmethod
|
||||
def from_spec(cls, spec: dict) -> "GateConfig":
|
||||
return cls(spec)
|
||||
|
||||
|
||||
def _round1(x: float) -> float:
|
||||
"""Arrondi à 1 décimale, stable pour un artefact diffable."""
|
||||
return round(x + 0.0, 1)
|
||||
|
||||
|
||||
def apport_requis_usd(prix_usd: float, residence_type: str, cfg: GateConfig) -> float:
|
||||
"""Apport requis = prix × taux (20 % résident RD, 30 % étranger)."""
|
||||
if residence_type not in cfg.taux_apport_pct:
|
||||
raise KeyError(
|
||||
f"residence_type {residence_type!r} inconnu "
|
||||
f"(attendu : {sorted(cfg.taux_apport_pct)})."
|
||||
)
|
||||
return round(float(prix_usd) * cfg.taux_apport_pct[residence_type] / 100.0, 2)
|
||||
|
||||
|
||||
def apport_verse_usd(dossier: dict) -> float:
|
||||
"""Somme des versements enregistrés (0 si aucun)."""
|
||||
total = 0.0
|
||||
for p in dossier.get("paiements", []):
|
||||
total += float(p.get("montant_verse_usd", 0) or 0)
|
||||
return round(total, 2)
|
||||
|
||||
|
||||
def required_document_ids(residence_type: str, cfg: GateConfig) -> list[str]:
|
||||
"""Documents `is_required` applicables au type de résidence du client.
|
||||
|
||||
Un document est exigé si `is_required` ET (`applies_to` == "all" OU
|
||||
`applies_to` == residence_type). Les documents « étrangers uniquement » ne
|
||||
s'appliquent donc pas à un résident RD.
|
||||
"""
|
||||
out: list[str] = []
|
||||
for d in cfg.documents:
|
||||
if not d.get("is_required"):
|
||||
continue
|
||||
applies = d.get("applies_to", "all")
|
||||
if applies in ("all", residence_type):
|
||||
out.append(d["id"])
|
||||
return out
|
||||
|
||||
|
||||
def _cond_apport(dossier: dict, cfg: GateConfig) -> dict:
|
||||
residence = dossier.get("residence_type")
|
||||
required = apport_requis_usd(dossier.get("prix_usd", 0), residence, cfg)
|
||||
paid = apport_verse_usd(dossier)
|
||||
remaining = round(max(0.0, required - paid), 2)
|
||||
ok = paid + 1e-9 >= required
|
||||
pct = _round1(min(100.0, (paid / required * 100.0) if required > 0 else 100.0))
|
||||
# Un versement dans la bande [99.95 %, 100 %[ (p. ex. 19 999/20 000 USD)
|
||||
# remonterait à 100.0 par l'arrondi 1-décimale, alors que le gate BLOQUE
|
||||
# (ok=False, remaining>0). La barre de progression afficherait « 100 % ✓ »
|
||||
# tout en refusant la soumission, et la raison lirait « reste 1 USD · 100 %».
|
||||
# Invariant d'affichage : percent == 100.0 ⟺ ok — un dossier incomplet
|
||||
# plafonne à 99.9 (plus grande valeur 1-décimale strictement < 100).
|
||||
if not ok and pct >= 100.0:
|
||||
pct = 99.9
|
||||
return {
|
||||
"ok": ok,
|
||||
"required": required,
|
||||
"paid": paid,
|
||||
"remaining": remaining,
|
||||
"percent": pct,
|
||||
}
|
||||
|
||||
|
||||
def _cond_documents(dossier: dict, cfg: GateConfig) -> dict:
|
||||
residence = dossier.get("residence_type")
|
||||
required_ids = required_document_ids(residence, cfg)
|
||||
by_id = {d["id"]: d for d in dossier.get("documents", [])}
|
||||
deposited = 0
|
||||
validated = 0
|
||||
for did in required_ids:
|
||||
statut = (by_id.get(did) or {}).get("statut", "Non déposé")
|
||||
if statut != "Non déposé":
|
||||
deposited += 1
|
||||
if statut in cfg.statuts_recevables:
|
||||
validated += 1
|
||||
total = len(required_ids)
|
||||
# Contrairement à _cond_apport (ratio USD *continu*, cf. fix 093114) et à
|
||||
# l'agrégat (moyenne de 4, cf. fix 110124), AUCUN plafond « non ok → 99.9 »
|
||||
# n'est requis ici : le dénominateur est la liste de documents du contrat
|
||||
# (≤ 13 is_required), un domaine BORNÉ. round1 ne peut remonter à
|
||||
# [99.95 %, 100 %[ par le bas qu'à partir de total ≥ 2000 (car
|
||||
# (total-1)/total ≥ 0.9995) — inatteignable pour une liste de documents.
|
||||
# Donc ok=False ⟹ percent < 100 tient déjà : l'invariant d'affichage
|
||||
# percent == 100.0 ⟺ ok est préservé sans garde. (même logique que les 3
|
||||
# sites `:g`-domaine-borné · ne pas ajouter de plafond redondant · #5)
|
||||
pct = _round1((validated / total * 100.0) if total > 0 else 100.0)
|
||||
return {
|
||||
"ok": validated == total,
|
||||
"total": total,
|
||||
"deposited": deposited,
|
||||
"validated": validated,
|
||||
"percent": pct,
|
||||
}
|
||||
|
||||
|
||||
def _cond_autorisations(dossier: dict, cfg: GateConfig) -> dict:
|
||||
required_ids = [a["id"] for a in cfg.autorisations]
|
||||
by_id = {a["id"]: a for a in dossier.get("autorisations", [])}
|
||||
signed = 0
|
||||
for aid in required_ids:
|
||||
if (by_id.get(aid) or {}).get("signature_date"):
|
||||
signed += 1
|
||||
total = len(required_ids)
|
||||
# Domaine BORNÉ (4 autorisations au contrat), a fortiori inférieur au seuil
|
||||
# total ≥ 2000 requis pour que round1 remonte à [99.95 %, 100 %[ par le bas :
|
||||
# pas de plafond « non ok → 99.9 » nécessaire ici (cf. note _cond_documents).
|
||||
# L'invariant percent == 100.0 ⟺ ok tient sans garde. (ne pas ajouter · #5)
|
||||
pct = _round1((signed / total * 100.0) if total > 0 else 100.0)
|
||||
return {"ok": signed == total, "total": total, "signed": signed, "percent": pct}
|
||||
|
||||
|
||||
def _cond_validation_wag(dossier: dict) -> dict:
|
||||
validated_by = dossier.get("wag_validated_by")
|
||||
ok = bool(validated_by)
|
||||
return {"ok": ok, "validated_by": validated_by, "percent": 100.0 if ok else 0.0}
|
||||
|
||||
|
||||
def gate_status(dossier: dict, cfg: GateConfig) -> dict:
|
||||
"""État complet du gate — forme consommée par le frontend (barres 0-100 %).
|
||||
|
||||
Correspond au contrat de l'endpoint
|
||||
`GET /api/hypotheque/dossier/{id}/gate-status` (DIRECTIVE amendement).
|
||||
"""
|
||||
conditions = {
|
||||
"apport_initial_complet": _cond_apport(dossier, cfg),
|
||||
"documents_exiges": _cond_documents(dossier, cfg),
|
||||
"autorisations_signees": _cond_autorisations(dossier, cfg),
|
||||
"validation_wag": _cond_validation_wag(dossier),
|
||||
}
|
||||
can_submit = all(conditions[k]["ok"] for k in CONDITION_KEYS)
|
||||
completed = sum(1 for k in CONDITION_KEYS if conditions[k]["ok"])
|
||||
overall = _round1(
|
||||
sum(conditions[k]["percent"] for k in CONDITION_KEYS) / len(CONDITION_KEYS)
|
||||
)
|
||||
# Même invariant qu'au niveau condition (cf. _cond_apport) mais à l'AGRÉGAT :
|
||||
# la moyenne de 4 percents peut arrondir à 100.0 alors qu'une condition reste
|
||||
# bloquante — p. ex. apport plafonné à 99.9 (non ok) + les 3 autres à 100.0 →
|
||||
# (99.9 + 300) / 4 = 99.975 → round1 = 100.0. La barre GLOBALE afficherait
|
||||
# « 100 % » tout en refusant la soumission (403). On plafonne donc à 99.9.
|
||||
# Invariant d'affichage : overall_percent == 100.0 ⟺ can_submit.
|
||||
if not can_submit and overall >= 100.0:
|
||||
overall = 99.9
|
||||
return {
|
||||
"can_submit": can_submit,
|
||||
"conditions_total": len(CONDITION_KEYS),
|
||||
"conditions_completed": completed,
|
||||
"overall_percent": overall,
|
||||
"conditions": conditions,
|
||||
}
|
||||
|
||||
|
||||
def can_submit_dossier(dossier: dict, cfg: GateConfig) -> tuple[bool, list[str]]:
|
||||
"""(peut_soumettre, raisons_de_blocage) — vide si soumissible.
|
||||
|
||||
`submit_to_bank()` doit renvoyer 403 + ces raisons tant que la liste n'est
|
||||
pas vide. Ordre des raisons = ordre canonique des conditions.
|
||||
"""
|
||||
status = gate_status(dossier, cfg)
|
||||
reasons: list[str] = []
|
||||
c = status["conditions"]
|
||||
if not c["apport_initial_complet"]["ok"]:
|
||||
a = c["apport_initial_complet"]
|
||||
reasons.append(
|
||||
f"Apport initial incomplet : {a['paid']:.0f}/{a['required']:.0f} USD versés "
|
||||
f"(reste {a['remaining']:.0f} USD · {a['percent']}%)."
|
||||
)
|
||||
if not c["documents_exiges"]["ok"]:
|
||||
d = c["documents_exiges"]
|
||||
reasons.append(
|
||||
f"Documents exigés incomplets : {d['validated']}/{d['total']} validés WAG."
|
||||
)
|
||||
if not c["autorisations_signees"]["ok"]:
|
||||
s = c["autorisations_signees"]
|
||||
reasons.append(
|
||||
f"Autorisations non signées : {s['signed']}/{s['total']} signées."
|
||||
)
|
||||
if not c["validation_wag"]["ok"]:
|
||||
reasons.append("Validation WAG manquante (aucun conseiller référent n'a validé).")
|
||||
return status["can_submit"], reasons
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Résolution des rôles du module Financement Bancaire depuis le contrat RBAC.
|
||||
|
||||
Réutilisation (workflow #5 : zéro duplication de VALEUR) de l'unique source de
|
||||
vérité des rôles ERPNext : `05_deliverables_mvp/rbac/rbac_50_roles.json` (validé
|
||||
par `rbac.schema.json` dans le job CI `rbac-tests`). Le contrat du module
|
||||
(`financement_spec.json`) ne cite JAMAIS un nom de rôle Frappe en dur : il
|
||||
référence l'`id` stable d'un rôle RBAC → ici on résout `id → erpnext_role_name`.
|
||||
|
||||
Conséquence anti-invention (#6) : un `role_id` absent du contrat RBAC lève une
|
||||
erreur (aucun rôle fabriqué), et renommer un rôle côté RBAC se propage
|
||||
automatiquement au module sans édition manuelle.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..", ".."))
|
||||
RBAC_CONTRACT_PATH = os.path.join(_DELIVERABLES, "rbac", "rbac_50_roles.json")
|
||||
|
||||
|
||||
class RoleResolver:
|
||||
"""Table `role_id → erpnext_role_name` construite depuis le contrat RBAC."""
|
||||
|
||||
def __init__(self, contract: dict) -> None:
|
||||
self._by_id: dict[str, dict] = {}
|
||||
for role in contract.get("roles", []):
|
||||
rid = role.get("id")
|
||||
if rid is None:
|
||||
raise ValueError("Rôle RBAC sans `id` — contrat corrompu.")
|
||||
if rid in self._by_id:
|
||||
raise ValueError(f"`id` de rôle RBAC dupliqué : {rid!r}")
|
||||
self._by_id[rid] = role
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, path: str = RBAC_CONTRACT_PATH) -> "RoleResolver":
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return cls(json.load(fh))
|
||||
|
||||
def erpnext_name(self, role_id: str) -> str:
|
||||
"""`id` RBAC → nom de rôle Frappe. Lève si l'`id` n'existe pas."""
|
||||
role = self._by_id.get(role_id)
|
||||
if role is None:
|
||||
raise KeyError(
|
||||
f"role_id {role_id!r} introuvable dans rbac_50_roles.json "
|
||||
f"(aucun rôle inventé · #6)."
|
||||
)
|
||||
name = role.get("erpnext_role_name")
|
||||
if not name:
|
||||
raise ValueError(f"Rôle {role_id!r} sans `erpnext_role_name`.")
|
||||
return name
|
||||
|
||||
def known_ids(self) -> frozenset[str]:
|
||||
return frozenset(self._by_id)
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"generated_from": "financement_spec.json",
|
||||
"source_directive": "DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md",
|
||||
"rbac_source": "rbac_50_roles.json",
|
||||
"source_version": "1.0.0",
|
||||
"module": "financement_bancaire",
|
||||
"titre": "Financement Bancaire — parcours hypothécaire immobilier RD",
|
||||
"counts": {
|
||||
"banques": 6,
|
||||
"sections": 7,
|
||||
"documents": 15,
|
||||
"documents_requis_resident": 9,
|
||||
"documents_requis_etranger": 13,
|
||||
"autorisations": 4,
|
||||
"gate_conditions": 4,
|
||||
"banniere_lignes": 4,
|
||||
"workflow_suivi_etapes": 6
|
||||
},
|
||||
"roles_rbac_utilises": [
|
||||
{
|
||||
"role_id": "compta-tresorier",
|
||||
"erpnext_role_name": "OTO Compta Trésorier"
|
||||
},
|
||||
{
|
||||
"role_id": "ventes-conseiller",
|
||||
"erpnext_role_name": "OTO Ventes Conseiller"
|
||||
},
|
||||
{
|
||||
"role_id": "ventes-directeur",
|
||||
"erpnext_role_name": "OTO Ventes Directeur"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "partage_donnees",
|
||||
"label": "Autorisation de partage de données personnelles avec la banque",
|
||||
"base_legale": "RGPD / Ley 172-13",
|
||||
"signature_method": "OTO Sign"
|
||||
},
|
||||
{
|
||||
"id": "investigation_datacredito",
|
||||
"label": "Autorisation d'investigation DataCrédito",
|
||||
"base_legale": "Ley 172-13",
|
||||
"signature_method": "OTO Sign"
|
||||
},
|
||||
{
|
||||
"id": "mandat_wag",
|
||||
"label": "Mandat WAG comme intermédiaire de financement",
|
||||
"base_legale": "mandat contractuel",
|
||||
"signature_method": "OTO Sign"
|
||||
},
|
||||
{
|
||||
"id": "acknowledgment_risques",
|
||||
"label": "Acknowledgment — risques de refus bancaire + politique annulation",
|
||||
"base_legale": "consentement éclairé",
|
||||
"signature_method": "OTO Sign"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,114 @@
|
||||
[
|
||||
{
|
||||
"id": "banreservas",
|
||||
"nom": "Banreservas",
|
||||
"type": "banque d'État",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": null,
|
||||
"ltv_max_pct": 80,
|
||||
"duree_max_ans": 25,
|
||||
"specialites": [
|
||||
"residents",
|
||||
"taux_preferentiels"
|
||||
],
|
||||
"devises": [
|
||||
"DOP"
|
||||
],
|
||||
"note": "Taux préférentiels résidents. Taux exacts à confirmer (relationship manager · #6)."
|
||||
},
|
||||
{
|
||||
"id": "popular",
|
||||
"nom": "Banco Popular Dominicano",
|
||||
"type": "banque privée",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": 75,
|
||||
"ltv_max_pct": 85,
|
||||
"duree_max_ans": null,
|
||||
"specialites": [
|
||||
"reseau_large",
|
||||
"rapide",
|
||||
"confotur"
|
||||
],
|
||||
"devises": [
|
||||
"DOP"
|
||||
],
|
||||
"note": "Réseau large, CONFOTUR-friendly. Taux et durée à confirmer (#6)."
|
||||
},
|
||||
{
|
||||
"id": "bhd_leon",
|
||||
"nom": "BHD León",
|
||||
"type": "banque privée",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": null,
|
||||
"ltv_max_pct": null,
|
||||
"duree_max_ans": 30,
|
||||
"specialites": [
|
||||
"haut_de_gamme",
|
||||
"etrangers",
|
||||
"bilingue"
|
||||
],
|
||||
"devises": [
|
||||
"DOP",
|
||||
"USD"
|
||||
],
|
||||
"note": "Haut de gamme, étrangers acceptés, bilingue. Taux/LTV à confirmer (#6)."
|
||||
},
|
||||
{
|
||||
"id": "santa_cruz",
|
||||
"nom": "Banco Santa Cruz",
|
||||
"type": "banque privée",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": null,
|
||||
"ltv_max_pct": null,
|
||||
"duree_max_ans": null,
|
||||
"specialites": [
|
||||
"taux_competitifs",
|
||||
"prequalif_rapide"
|
||||
],
|
||||
"devises": [
|
||||
"DOP"
|
||||
],
|
||||
"note": "Taux compétitifs, pré-qualif rapide. Chiffres à confirmer (#6)."
|
||||
},
|
||||
{
|
||||
"id": "scotiabank_rd",
|
||||
"nom": "Scotiabank República Dominicana",
|
||||
"type": "banque privée",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": null,
|
||||
"ltv_max_pct": 70,
|
||||
"duree_max_ans": null,
|
||||
"specialites": [
|
||||
"etrangers",
|
||||
"devise_usd"
|
||||
],
|
||||
"devises": [
|
||||
"DOP",
|
||||
"USD"
|
||||
],
|
||||
"note": "Spécialistes étrangers, devise USD acceptée. Taux/durée à confirmer (#6)."
|
||||
},
|
||||
{
|
||||
"id": "lopez_de_haro",
|
||||
"nom": "López de Haro",
|
||||
"type": "banque privée",
|
||||
"taux_min_pct": null,
|
||||
"taux_max_pct": null,
|
||||
"ltv_min_pct": null,
|
||||
"ltv_max_pct": null,
|
||||
"duree_max_ans": null,
|
||||
"specialites": [
|
||||
"haut_de_gamme",
|
||||
"niche"
|
||||
],
|
||||
"devises": [
|
||||
"DOP"
|
||||
],
|
||||
"note": "Niche haut de gamme. Profil complet à confirmer (#6)."
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,107 @@
|
||||
[
|
||||
{
|
||||
"id": "cedula_passeport",
|
||||
"label": "Cédula ou Passeport (2 côtés)",
|
||||
"categorie": "client",
|
||||
"applies_to": "all",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "preuve_adresse",
|
||||
"label": "Preuve d'adresse récente (<3 mois)",
|
||||
"categorie": "client",
|
||||
"applies_to": "all",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "fiches_paye_3",
|
||||
"label": "3 dernières fiches de paye ou déclarations ITBIS/DGII",
|
||||
"categorie": "client",
|
||||
"applies_to": "all",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "etats_bancaires_6m",
|
||||
"label": "États bancaires 6 derniers mois",
|
||||
"categorie": "client",
|
||||
"applies_to": "all",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "lettre_employeur",
|
||||
"label": "Lettre employeur ou attestation profession libérale",
|
||||
"categorie": "client",
|
||||
"applies_to": "all",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "certif_solvabilite",
|
||||
"label": "Certification de solvabilité",
|
||||
"categorie": "client",
|
||||
"applies_to": "all",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "certif_datacredito",
|
||||
"label": "Certification DataCrédito (délivrée par la banque)",
|
||||
"categorie": "client",
|
||||
"applies_to": "all",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "promesa_notariee",
|
||||
"label": "Promesa Irrévocable signée notariée",
|
||||
"categorie": "immobilier",
|
||||
"applies_to": "all",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "fideicomiso",
|
||||
"label": "Fideicomiso Inmobiliario constitué (Ley 189-11)",
|
||||
"categorie": "immobilier",
|
||||
"applies_to": "all",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "titre_definitif",
|
||||
"label": "Titre définitif ou Certificado de Título (si disponible)",
|
||||
"categorie": "immobilier",
|
||||
"applies_to": "all",
|
||||
"is_required": false
|
||||
},
|
||||
{
|
||||
"id": "passeport_2cotes",
|
||||
"label": "Passeport valide (2 côtés)",
|
||||
"categorie": "etrangers",
|
||||
"applies_to": "etranger",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "visa_residence",
|
||||
"label": "Visa / résidence si applicable",
|
||||
"categorie": "etrangers",
|
||||
"applies_to": "etranger",
|
||||
"is_required": false
|
||||
},
|
||||
{
|
||||
"id": "lettre_origine_fonds",
|
||||
"label": "Lettre origine des fonds notariée (FinCEN GTO)",
|
||||
"categorie": "etrangers",
|
||||
"applies_to": "etranger",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "refs_bancaires_intl",
|
||||
"label": "Références bancaires internationales",
|
||||
"categorie": "etrangers",
|
||||
"applies_to": "etranger",
|
||||
"is_required": true
|
||||
},
|
||||
{
|
||||
"id": "refs_perso",
|
||||
"label": "Références commerciales / personnelles",
|
||||
"categorie": "etrangers",
|
||||
"applies_to": "etranger",
|
||||
"is_required": true
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"endpoint": "GET /api/hypotheque/dossier/{id}/gate-status",
|
||||
"submit_endpoint": "POST /api/hypotheque/dossier/{id}/submit",
|
||||
"condition_keys": [
|
||||
"apport_initial_complet",
|
||||
"documents_exiges",
|
||||
"autorisations_signees",
|
||||
"validation_wag"
|
||||
],
|
||||
"conditions": [
|
||||
{
|
||||
"key": "apport_initial_complet",
|
||||
"ordre": 1,
|
||||
"label": "Dépôt initial complet (20% résident RD · 30% étranger) versé et vérifié"
|
||||
},
|
||||
{
|
||||
"key": "documents_exiges",
|
||||
"ordre": 2,
|
||||
"label": "TOUS les documents exigés déposés et validés WAG"
|
||||
},
|
||||
{
|
||||
"key": "autorisations_signees",
|
||||
"ordre": 3,
|
||||
"label": "TOUTES les autorisations signées électroniquement"
|
||||
},
|
||||
{
|
||||
"key": "validation_wag",
|
||||
"ordre": 4,
|
||||
"label": "Validation manuelle WAG confirmée par le conseiller référent"
|
||||
}
|
||||
],
|
||||
"banniere": {
|
||||
"niveau": "critique",
|
||||
"couleurs": [
|
||||
"rouge",
|
||||
"or"
|
||||
],
|
||||
"persistante": true,
|
||||
"titre": "⚠ AUCUN DOCUMENT N'EST TRANSMIS À LA BANQUE tant que :",
|
||||
"lignes": [
|
||||
"Dépôt initial complet (20% résident RD · 30% étranger) versé et vérifié",
|
||||
"TOUS les documents exigés ne sont pas déposés dans le portail",
|
||||
"TOUTES les autorisations ne sont pas signées électroniquement",
|
||||
"Validation manuelle WAG confirmée par le conseiller référent"
|
||||
],
|
||||
"consequence": "Le bouton « Envoyer à la banque » reste désactivé tant que ces 4 conditions ne sont pas remplies. Progression affichée en temps réel avec barre 0-100% par condition."
|
||||
},
|
||||
"role_validation": {
|
||||
"role_id": "ventes-conseiller",
|
||||
"erpnext_role_name": "OTO Ventes Conseiller"
|
||||
},
|
||||
"workflow_suivi_banque": [
|
||||
"Prequalifié",
|
||||
"Dossier soumis",
|
||||
"Analyse crédit",
|
||||
"Offre reçue",
|
||||
"Contrat signé",
|
||||
"Décaissement"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"dossier_id": "DEMO-P07-A312",
|
||||
"projet": "P07 Aqua Terra Las Terrenas",
|
||||
"unite": "A-312",
|
||||
"banque_choisie": "scotiabank_rd",
|
||||
"gate_status": {
|
||||
"can_submit": false,
|
||||
"conditions_total": 4,
|
||||
"conditions_completed": 0,
|
||||
"overall_percent": 39.1,
|
||||
"conditions": {
|
||||
"apport_initial_complet": {
|
||||
"ok": false,
|
||||
"required": 43500.0,
|
||||
"paid": 29500.0,
|
||||
"remaining": 14000.0,
|
||||
"percent": 67.8
|
||||
},
|
||||
"documents_exiges": {
|
||||
"ok": false,
|
||||
"total": 13,
|
||||
"deposited": 7,
|
||||
"validated": 5,
|
||||
"percent": 38.5
|
||||
},
|
||||
"autorisations_signees": {
|
||||
"ok": false,
|
||||
"total": 4,
|
||||
"signed": 2,
|
||||
"percent": 50.0
|
||||
},
|
||||
"validation_wag": {
|
||||
"ok": false,
|
||||
"validated_by": null,
|
||||
"percent": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"can_submit": false,
|
||||
"reasons": [
|
||||
"Apport initial incomplet : 29500/43500 USD versés (reste 14000 USD · 67.8%).",
|
||||
"Documents exigés incomplets : 5/13 validés WAG.",
|
||||
"Autorisations non signées : 2/4 signées.",
|
||||
"Validation WAG manquante (aucun conseiller référent n'a validé)."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
[
|
||||
{
|
||||
"id": "apport_initial",
|
||||
"titre": "Statut apport initial",
|
||||
"ordre": 1,
|
||||
"gating": true,
|
||||
"resume": "Prix unité · statut résidence · apport requis (20%/30%) · apport versé · solde restant · barre 0-100%."
|
||||
},
|
||||
{
|
||||
"id": "info_achat",
|
||||
"titre": "Information de l'achat",
|
||||
"ordre": 2,
|
||||
"gating": false,
|
||||
"resume": "Résumé auto de la réservation : projet, unité, plan de paiement, apport, solde à financer, statuts Fideicomiso + CONFOTUR."
|
||||
},
|
||||
{
|
||||
"id": "choix_banque",
|
||||
"titre": "Choix de la banque partenaire",
|
||||
"ordre": 3,
|
||||
"gating": false,
|
||||
"resume": "Sélection d'UNE banque prioritaire (radio) + multi-dossiers possibles ; profils taux/LTV/durée/spécialités."
|
||||
},
|
||||
{
|
||||
"id": "formulaires",
|
||||
"titre": "Formulaires banque pré-remplis",
|
||||
"ordre": 4,
|
||||
"gating": false,
|
||||
"resume": "Formulaires officiels PDF pré-remplis, téléchargés, signés, ré-uploadés ; journal audit des signatures."
|
||||
},
|
||||
{
|
||||
"id": "exigences",
|
||||
"titre": "Exigences bancaires par banque",
|
||||
"ordre": 5,
|
||||
"gating": false,
|
||||
"resume": "Liste des documents exigés cochée au fil des uploads ; statut par item (Non déposé / Déposé / Validé WAG / Envoyé banque)."
|
||||
},
|
||||
{
|
||||
"id": "autorisations",
|
||||
"titre": "Autorisations à signer",
|
||||
"ordre": 6,
|
||||
"gating": false,
|
||||
"resume": "Autorisations à signer AVANT envoi (OTO Sign horodaté + IP + hash)."
|
||||
},
|
||||
{
|
||||
"id": "envoi",
|
||||
"titre": "Envoi à la banque",
|
||||
"ordre": 7,
|
||||
"gating": false,
|
||||
"resume": "Bouton « Envoyer à la banque » (gate 4 conditions) + timeline suivi banque + statut temps réel."
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,345 @@
|
||||
"""Tests du module Financement Bancaire — cœur métier (gate) + générateur.
|
||||
|
||||
Deux familles :
|
||||
1. Cœur métier `finlib.gate` — calcul d'apport 20/30 + gate check 4 conditions.
|
||||
Fonctions pures → scénarios exhaustifs (chaque condition qui bloque, cas
|
||||
nominal qui passe, frontière apport).
|
||||
2. Générateur — validate/build, déterminisme (build == build), schéma +
|
||||
12 invariants, résolution RBAC réelle.
|
||||
"""
|
||||
|
||||
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 finlib import gate # noqa: E402
|
||||
from finlib.builder import build_bundle # noqa: E402
|
||||
from finlib.rbac import RoleResolver # noqa: E402
|
||||
|
||||
_SPEC_PATH = os.path.join(_MODULE, "financement_spec.json")
|
||||
_GEN = os.path.join(_MODULE, "financement_bancaire_gen.py")
|
||||
|
||||
|
||||
def _load_spec():
|
||||
with open(_SPEC_PATH, encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
class ApportTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cfg = gate.GateConfig.from_spec(_load_spec())
|
||||
|
||||
def test_taux_resident_20_etranger_30(self):
|
||||
self.assertEqual(self.cfg.taux_apport_pct["resident_rd"], 20.0)
|
||||
self.assertEqual(self.cfg.taux_apport_pct["etranger"], 30.0)
|
||||
|
||||
def test_apport_requis_resident(self):
|
||||
self.assertAlmostEqual(gate.apport_requis_usd(100000, "resident_rd", self.cfg), 20000.0)
|
||||
|
||||
def test_apport_requis_etranger(self):
|
||||
self.assertAlmostEqual(gate.apport_requis_usd(100000, "etranger", self.cfg), 30000.0)
|
||||
|
||||
def test_apport_requis_type_inconnu_leve(self):
|
||||
with self.assertRaises(KeyError):
|
||||
gate.apport_requis_usd(100000, "martien", self.cfg)
|
||||
|
||||
def test_apport_verse_somme_paiements(self):
|
||||
dossier = {"paiements": [{"montant_verse_usd": 5000}, {"montant_verse_usd": 2500.5}]}
|
||||
self.assertAlmostEqual(gate.apport_verse_usd(dossier), 7500.5)
|
||||
|
||||
def test_apport_verse_aucun_paiement(self):
|
||||
self.assertEqual(gate.apport_verse_usd({}), 0.0)
|
||||
|
||||
|
||||
class RequiredDocumentsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cfg = gate.GateConfig.from_spec(_load_spec())
|
||||
|
||||
def test_etranger_exige_plus_que_resident(self):
|
||||
res = set(gate.required_document_ids("resident_rd", self.cfg))
|
||||
etr = set(gate.required_document_ids("etranger", self.cfg))
|
||||
self.assertTrue(res < etr) # sous-ensemble strict
|
||||
|
||||
def test_resident_ninclut_pas_docs_etrangers(self):
|
||||
res = gate.required_document_ids("resident_rd", self.cfg)
|
||||
self.assertNotIn("lettre_origine_fonds", res)
|
||||
self.assertNotIn("passeport_2cotes", res)
|
||||
|
||||
def test_etranger_inclut_docs_etrangers(self):
|
||||
etr = gate.required_document_ids("etranger", self.cfg)
|
||||
self.assertIn("lettre_origine_fonds", etr)
|
||||
self.assertIn("refs_bancaires_intl", etr)
|
||||
|
||||
def test_document_optionnel_exclu(self):
|
||||
# titre_definitif is_required=false → jamais dans la liste des exigés.
|
||||
self.assertNotIn("titre_definitif", gate.required_document_ids("etranger", self.cfg))
|
||||
|
||||
|
||||
def _dossier_complet(cfg, spec):
|
||||
"""Un dossier qui satisfait les 4 conditions (résident RD, 100 000 USD)."""
|
||||
required = gate.required_document_ids("resident_rd", cfg)
|
||||
return {
|
||||
"id": "T-OK",
|
||||
"prix_usd": 100000,
|
||||
"residence_type": "resident_rd",
|
||||
"banque_choisie": "banreservas",
|
||||
"paiements": [{"montant_verse_usd": 20000}], # 20 % pile
|
||||
"documents": [{"id": d, "statut": "Validé WAG"} for d in required],
|
||||
"autorisations": [{"id": a["id"], "signature_date": "2026-08-01"} for a in spec["autorisations"]],
|
||||
"wag_validated_by": "ventes-conseiller:amelie",
|
||||
}
|
||||
|
||||
|
||||
class GateCheckTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.spec = _load_spec()
|
||||
self.cfg = gate.GateConfig.from_spec(self.spec)
|
||||
|
||||
def test_dossier_complet_soumissible(self):
|
||||
ok, reasons = gate.can_submit_dossier(_dossier_complet(self.cfg, self.spec), self.cfg)
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(reasons, [])
|
||||
|
||||
def test_apport_pile_au_seuil_passe(self):
|
||||
# Exactement 20 % → apport complet (frontière incluse).
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
status = gate.gate_status(d, self.cfg)
|
||||
self.assertTrue(status["conditions"]["apport_initial_complet"]["ok"])
|
||||
self.assertEqual(status["conditions"]["apport_initial_complet"]["percent"], 100.0)
|
||||
|
||||
def test_apport_quasi_complet_ninclut_pas_100pct(self):
|
||||
# Bande [99.95 %, 100 %[ : l'arrondi 1-décimale de 99.995 % remonte à
|
||||
# 100.0, mais le gate BLOQUE (ok=False, reste 1 USD). Un dossier
|
||||
# incomplet ne doit JAMAIS afficher 100 % (barre pleine ⇔ soumissible).
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["paiements"] = [{"montant_verse_usd": 19999}] # 20 000 requis → 99.995 %
|
||||
c = gate.gate_status(d, self.cfg)["conditions"]["apport_initial_complet"]
|
||||
self.assertFalse(c["ok"])
|
||||
self.assertAlmostEqual(c["remaining"], 1.0)
|
||||
self.assertLess(c["percent"], 100.0)
|
||||
self.assertEqual(c["percent"], 99.9)
|
||||
# La raison de blocage ne doit pas dire « 100 % ».
|
||||
_, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertTrue(any("Apport initial incomplet" in r and "100" not in r for r in reasons))
|
||||
|
||||
def test_overall_percent_agrege_ninclut_pas_100pct(self):
|
||||
# Invariant d'affichage AGRÉGAT (frère du test per-condition ci-dessus,
|
||||
# mais au niveau de la barre GLOBALE) : overall_percent == 100.0 ⟺
|
||||
# can_submit. Cas limite = apport plafonné à 99.9 (non ok) + les 3 autres
|
||||
# conditions ok à 100.0 → moyenne (99.9 + 300) / 4 = 99.975, qui arrondit
|
||||
# à 100.0 par _round1. Sans la garde, la barre globale afficherait
|
||||
# « 100 % » tout en bloquant la soumission (403) — auto-contradiction.
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["paiements"] = [{"montant_verse_usd": 19999}] # 20 000 requis → apport non ok
|
||||
status = gate.gate_status(d, self.cfg)
|
||||
self.assertFalse(status["can_submit"])
|
||||
# les 3 autres conditions sont bien complètes (isolation du cas limite)
|
||||
self.assertTrue(status["conditions"]["documents_exiges"]["ok"])
|
||||
self.assertTrue(status["conditions"]["autorisations_signees"]["ok"])
|
||||
self.assertTrue(status["conditions"]["validation_wag"]["ok"])
|
||||
self.assertLess(status["overall_percent"], 100.0)
|
||||
self.assertEqual(status["overall_percent"], 99.9)
|
||||
|
||||
def test_apport_manquant_bloque(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["paiements"] = [{"montant_verse_usd": 19999}]
|
||||
ok, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("Apport initial incomplet" in r for r in reasons))
|
||||
c = gate.gate_status(d, self.cfg)["conditions"]["apport_initial_complet"]
|
||||
self.assertAlmostEqual(c["remaining"], 1.0)
|
||||
|
||||
def test_document_non_valide_bloque(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["documents"][0]["statut"] = "Déposé" # déposé mais pas validé WAG
|
||||
ok, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("Documents exigés incomplets" in r for r in reasons))
|
||||
|
||||
def test_document_envoye_banque_est_recevable(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["documents"][0]["statut"] = "Envoyé banque"
|
||||
ok, _ = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertTrue(ok)
|
||||
|
||||
def test_document_manquant_compte_comme_non_depose(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["documents"] = d["documents"][:-1] # retire un requis
|
||||
c = gate.gate_status(d, self.cfg)["conditions"]["documents_exiges"]
|
||||
self.assertFalse(c["ok"])
|
||||
self.assertEqual(c["validated"], c["total"] - 1)
|
||||
|
||||
def test_autorisation_non_signee_bloque(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["autorisations"][0]["signature_date"] = None
|
||||
ok, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("Autorisations non signées" in r for r in reasons))
|
||||
|
||||
def test_validation_wag_manquante_bloque(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["wag_validated_by"] = None
|
||||
ok, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("Validation WAG manquante" in r for r in reasons))
|
||||
|
||||
def test_quatre_conditions_toutes_ko(self):
|
||||
# Dossier vide → 4 raisons, 0 condition complétée.
|
||||
d = {"id": "T-VIDE", "prix_usd": 100000, "residence_type": "etranger"}
|
||||
status = gate.gate_status(d, self.cfg)
|
||||
self.assertFalse(status["can_submit"])
|
||||
self.assertEqual(status["conditions_completed"], 0)
|
||||
ok, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertFalse(ok)
|
||||
self.assertEqual(len(reasons), 4)
|
||||
|
||||
def test_gate_status_forme_endpoint(self):
|
||||
# Forme conforme au contrat de l'endpoint gate-status.
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
status = gate.gate_status(d, self.cfg)
|
||||
self.assertIn("can_submit", status)
|
||||
self.assertEqual(set(status["conditions"]), set(gate.CONDITION_KEYS))
|
||||
for k in gate.CONDITION_KEYS:
|
||||
self.assertIn("percent", status["conditions"][k])
|
||||
|
||||
def test_reasons_ordre_canonique(self):
|
||||
d = {"id": "T", "prix_usd": 100000, "residence_type": "etranger"}
|
||||
_, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertIn("Apport", reasons[0])
|
||||
self.assertIn("Documents", reasons[1])
|
||||
self.assertIn("Autorisations", reasons[2])
|
||||
self.assertIn("Validation WAG", reasons[3])
|
||||
|
||||
|
||||
class BuilderTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.spec = _load_spec()
|
||||
self.resolver = RoleResolver.from_path()
|
||||
self.bundle = build_bundle(self.spec, self.resolver)
|
||||
|
||||
def test_bundle_cles(self):
|
||||
for k in ("manifest", "banques", "sections", "documents", "autorisations", "gate_spec", "gate_status_example"):
|
||||
self.assertIn(k, self.bundle)
|
||||
|
||||
def test_role_validation_resolu(self):
|
||||
rv = self.bundle["gate_spec"]["role_validation"]
|
||||
self.assertEqual(rv["role_id"], "ventes-conseiller")
|
||||
self.assertEqual(rv["erpnext_role_name"], "OTO Ventes Conseiller")
|
||||
|
||||
def test_role_inconnu_leve(self):
|
||||
bad = copy.deepcopy(self.spec)
|
||||
bad["roles"]["validation_dossier"] = "role-inexistant"
|
||||
with self.assertRaises(KeyError):
|
||||
build_bundle(bad, self.resolver)
|
||||
|
||||
def test_sections_triees_par_ordre(self):
|
||||
ordres = [s["ordre"] for s in self.bundle["sections"]]
|
||||
self.assertEqual(ordres, sorted(ordres))
|
||||
self.assertEqual(self.bundle["sections"][0]["id"], "apport_initial")
|
||||
|
||||
def test_exemple_est_bloque(self):
|
||||
# Le dossier d'exemple DOIT démontrer un blocage (valeur pédagogique).
|
||||
self.assertFalse(self.bundle["gate_status_example"]["can_submit"])
|
||||
self.assertTrue(self.bundle["gate_status_example"]["reasons"])
|
||||
|
||||
def test_deterministe(self):
|
||||
again = build_bundle(_load_spec(), RoleResolver.from_path())
|
||||
self.assertEqual(
|
||||
json.dumps(self.bundle, sort_keys=True, ensure_ascii=False),
|
||||
json.dumps(again, sort_keys=True, ensure_ascii=False),
|
||||
)
|
||||
|
||||
|
||||
class GeneratorCliTest(unittest.TestCase):
|
||||
def _run(self, *argv):
|
||||
return subprocess.run(
|
||||
[sys.executable, _GEN, *argv],
|
||||
capture_output=True, text=True, cwd=_MODULE,
|
||||
)
|
||||
|
||||
def test_validate_ok(self):
|
||||
r = self._run("validate")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("12 invariants", r.stdout)
|
||||
|
||||
@staticmethod
|
||||
def _read(path):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
def test_build_reproductible(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
r1 = self._run("build", "-o", d)
|
||||
self.assertEqual(r1.returncode, 0, r1.stderr)
|
||||
first = {f: self._read(os.path.join(d, f)) for f in os.listdir(d)}
|
||||
r2 = self._run("build", "-o", d)
|
||||
self.assertEqual(r2.returncode, 0, r2.stderr)
|
||||
second = {f: self._read(os.path.join(d, f)) for f in os.listdir(d)}
|
||||
self.assertEqual(first, second)
|
||||
|
||||
def test_build_egale_commite(self):
|
||||
# L'artefact commité out/ doit être identique à un build frais (miroir
|
||||
# local du gate ci/check_artifacts.sh).
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.assertEqual(self._run("build", "-o", d).returncode, 0)
|
||||
for fname in ("banques.json", "sections.json", "documents.json",
|
||||
"autorisations.json", "gate_spec.json",
|
||||
"gate_status_example.json", "MANIFEST.json"):
|
||||
fresh = self._read(os.path.join(d, fname))
|
||||
committed_path = os.path.join(_MODULE, "out", fname)
|
||||
if os.path.exists(committed_path):
|
||||
self.assertEqual(fresh, self._read(committed_path),
|
||||
f"{fname} a dérivé de sa source")
|
||||
|
||||
|
||||
class InvariantGuardTest(unittest.TestCase):
|
||||
"""Chaque invariant du générateur DOIT mordre une mutation (anti-régression)."""
|
||||
|
||||
def setUp(self):
|
||||
sys.path.insert(0, _MODULE)
|
||||
import financement_bancaire_gen as gen # noqa
|
||||
self.gen = gen
|
||||
self.spec = _load_spec()
|
||||
self.resolver = RoleResolver.from_path()
|
||||
|
||||
def _errors(self, spec):
|
||||
bundle = build_bundle(spec, self.resolver)
|
||||
return self.gen._validate_bundle(spec, bundle)
|
||||
|
||||
def test_baseline_clean(self):
|
||||
self.assertEqual(self._errors(self.spec), [])
|
||||
|
||||
def test_taux_ordre_mordu(self):
|
||||
bad = copy.deepcopy(self.spec)
|
||||
bad["apport"]["residence_types"][0]["taux_apport_pct"] = 40 # résident > étranger
|
||||
self.assertTrue(any("Taux d'apport" in e for e in self._errors(bad)))
|
||||
|
||||
def test_section_gating_deplacee_mordue(self):
|
||||
bad = copy.deepcopy(self.spec)
|
||||
bad["sections"][0]["gating"] = False # plus aucune section gating
|
||||
self.assertTrue(any("gating" in e for e in self._errors(bad)))
|
||||
|
||||
def test_id_duplique_mordu(self):
|
||||
bad = copy.deepcopy(self.spec)
|
||||
bad["banques"].append(dict(bad["banques"][0]))
|
||||
self.assertTrue(any("dupliqué" in e for e in self._errors(bad)))
|
||||
|
||||
def test_ltv_incoherent_mordu(self):
|
||||
bad = copy.deepcopy(self.spec)
|
||||
bad["banques"][1]["ltv_min_pct"] = 99 # min > max (85)
|
||||
self.assertTrue(any("LTV" in e for e in self._errors(bad)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,552 @@
|
||||
# Daily Report · 2026-08-02 · Claude Code DTP Worker
|
||||
|
||||
**Session** : `20260802_110539`
|
||||
**Phase roadmap** : Sprint 8 · *Launch + Buffer* — livrables « Regression tests
|
||||
exhaustifs » (QA) + « Corrections finales » (buffer).
|
||||
|
||||
## Tâche exécutée
|
||||
**Sprint 8 · QA — sweep de vérification launch-readiness sur arbre propre + reprise
|
||||
du canal `daily_reports/`.**
|
||||
|
||||
Ce rapport comble un **trou de reporting** : le canal canonique des rapports
|
||||
quotidiens ([`04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md`](../../04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md)
|
||||
§Escalations · référencé par les 13 fiches `03_agents/*/AGENT.md`) n'avait **plus
|
||||
d'instantané consolidé depuis `2026-07-30`**, alors que ~2 douzaines de sessions
|
||||
buffer ont continué. Leur trace fine existe (journal par session
|
||||
[`05_activity_log/`](../../05_activity_log/2026-08-02.md)) mais **aucun snapshot
|
||||
sourcé de l'état MVP** n'existait pour la phase Sprint 8. Ce rapport le produit,
|
||||
chaque chiffre **re-vérifié contre l'artefact commité** ci-dessous (anti-invention
|
||||
`CLAUDE.md` #6 · zéro donnée nouvelle).
|
||||
|
||||
## Contexte / analyse
|
||||
- Relu `CLAUDE.md`, la roadmap, `MEMORY.md`, le journal `05_activity_log/2026-08-02.md`,
|
||||
les fiches `crm` + `bim` (audit d'exactitude doc-vs-réalité).
|
||||
- **Tous les sprints 1→7 sont livrés côté repo** ; on est en **buffer Sprint 8**.
|
||||
La série récente a épuisé les surfaces driftables évidentes en **durcissant des
|
||||
gates existants** (dernier : blind-spot « comptes de tests par suite » sur la
|
||||
cellule `rbac` pointant une SPEC, commit `ca15e7e`).
|
||||
- **Décision** (workflow #5 · CLAUDE.md — ne pas empiler un gate redondant) : plutôt
|
||||
qu'un gate N+1, **auditer l'exactitude** des fiches agents + **produire l'instantané
|
||||
QA manquant**. La phase Sprint 8 attend précisément une **vérification de régression
|
||||
exhaustive**, pas de nouveau module (contrainte « pas d'invention de modules »).
|
||||
|
||||
## Réalisé
|
||||
- **Sweep des 7 gates CI** sur l'arbre propre — tous **verts (exit 0)** :
|
||||
`check_ci_integrity` · `check_artifacts` (reproductibilité byte-identique des
|
||||
`out/`) · `check_docs` · `check_readme_claims` · `check_regression` ·
|
||||
`guard_constraints` · `validate_json`. Détail : [`ci/README.md`](../../ci/README.md).
|
||||
- **Audit doc-vs-réalité de 2 fiches agents** (interdit CLAUDE.md « documenter du
|
||||
code sans vérifier son existence courante ») :
|
||||
- `03_agents/crm/AGENT.md` — table des livrables **exacte** : `commissions=25`,
|
||||
`dossier_vente=31`, `workflow_vente=25`, total **81 tests**, recomptés depuis
|
||||
`plan.suites[*].test_methods` de
|
||||
[`regression_plan.json`](../qa/regression/out/regression_plan.json). ✅
|
||||
- `03_agents/bim/AGENT.md` — son **seul point de contact commité** cité,
|
||||
`faisabilite/generator/genlib/renderer.py`, **existe** et rend bien le **Volet 4 ·
|
||||
Ingénierie** en placeholders `{{…}}` (`_v`/`_money`/`_num` · anti-invention #6).
|
||||
Aucune sur-promesse d'artefact BIM en-repo. ✅
|
||||
- **Reprise du canal `daily_reports/`** (ce fichier) : instantané MVP consolidé sourcé.
|
||||
|
||||
## État courant MVP · 100 % sourcé (aucun chiffre saisi à la main)
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (seuil 95 · #5) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (plan) | **22 suites · 564 méthodes** · 0 sous le seuil (min 10) | `PASS` | [`qa/regression/out/regression_plan.json`](../qa/regression/out/regression_plan.json) |
|
||||
| Régression (run agrégé) | **22 suites · 564 tests · 564 passés · 0 échec · 0 erreur** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15 promesses** (8 livrables sprint + 7 métriques MVP) · **15 `in_repo`** | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Fiches agents | **13 agents** | — | `03_agents/*/AGENT.md` |
|
||||
| Gates CI | **7 gates statiques** verts + reproductibilité `out/` byte-identique | vert | [`ci/README.md`](../../ci/README.md) |
|
||||
|
||||
## Vérifications effectuées (en-repo, sans toucher au VPS)
|
||||
- 7 gates CI → **exit 0** sur l'arbre propre (dont `check_artifacts` : tout `out/`
|
||||
se régénère à l'identique depuis son générateur `build`).
|
||||
- Recalcul indépendant des agrégats (`python3` sur les artefacts) **concordant**
|
||||
avec les valeurs affichées : régression 564/564, qualité 22/22, recette 15/15.
|
||||
- Liens Markdown de ce rapport résolus vers des cibles existantes (pré-vérifiés).
|
||||
|
||||
## Non fait (hors périmètre worker · touche au VPS)
|
||||
- Déploiement production réel (bench migrate, import fixtures, câblage nginx/systemd,
|
||||
builds stores, indexation SEO, voix Amélie) → **VPS `153.75.250.214`** · agents
|
||||
DevOps/ERPNext, plan ordonné dans `devops/deploy_runbook` (`CLAUDE.md` #8).
|
||||
- Enregistrement `act_runner` Gitea + exécution serveur de la baseline Playwright.
|
||||
|
||||
## Note de convention (réconciliation doc-vs-réalité)
|
||||
Deux canaux de journalisation coexistent et sont désormais explicités :
|
||||
1. **`05_activity_log/YYYY-MM-DD.md`** — journal **technique par session** (riche,
|
||||
tenu à chaque session · instruction mandat #6). **Canal courant de facto.**
|
||||
2. **`05_deliverables_mvp/daily_reports/YYYY-MM-DD.md`** — **instantané consolidé
|
||||
stakeholder** (roadmap §Escalations). Repris ici après lapsus depuis `2026-07-30`.
|
||||
Les deux servent des lecteurs différents (technique vs. direction) et ne sont **pas
|
||||
des doublons**. Un lecteur d'`AGENT.md` cherchant « le journal récent » doit se
|
||||
référer au **canal 1** pour le détail par session, au **canal 2** pour la synthèse.
|
||||
|
||||
## Prochaine tâche suggérée
|
||||
- Maintenir le canal `daily_reports/` à jour à chaque session buffer (1 instantané/jour).
|
||||
- Poursuivre l'audit d'exactitude des 11 fiches `AGENT.md` restantes (mêmes contrôles
|
||||
que `crm`/`bim` ci-dessus) — repérer toute sur-promesse d'artefact ou compte périmé.
|
||||
- Buffer : ne durcir un gate que si un **blind-spot est prouvé par mutation-test**
|
||||
(mémoire `verify-uncovered-before-gating`), jamais un gate redondant (#5).
|
||||
|
||||
---
|
||||
|
||||
**Auto-score 4Big de la sweep de vérification : 96/100.** Réserve −4 : audit
|
||||
d'exactitude limité à 2 des 13 fiches cette session (les 11 autres restent à
|
||||
re-vérifier) ; déploiement serveur hors périmètre (VPS). État launch-readiness
|
||||
confirmé et **intégralement sourcé** : 22/22 qualité · 564/564 régression · 15/15
|
||||
recette · 7 gates verts.
|
||||
|
||||
---
|
||||
|
||||
## Clôture de journée · addendum session `20260802_120544`
|
||||
|
||||
> Cet addendum est **additif** : le corps ci-dessus reste le compte-rendu horodaté
|
||||
> de la session `110539` (record historique intact). Il **acte la clôture** de la
|
||||
> campagne d'audit que ce même rapport listait en « Prochaine tâche suggérée #2 »,
|
||||
> réalisée plus tard le même jour, et **lève la réserve −4** devenue caduque.
|
||||
|
||||
**Ce qui a fermé depuis 11:11** — l'**audit d'exactitude doc-vs-réalité des 11
|
||||
fiches `AGENT.md` restantes** (devops · erpnext_backend · faisabilite ·
|
||||
frontend_console · ifc_speckle · mobile · onapi_legal · publiciste · qa · rendu ·
|
||||
seo) a été mené à terme (commit `2bc3afb`, session `113542`) : **0 dérive réelle**.
|
||||
Les 2 « dérives » remontées par les agents ont été **levées comme faux positifs de
|
||||
convention**, prouvés avant toute action (mémoire `verify-uncovered-before-gating`) :
|
||||
- **onapi_legal** — chemins backtick `../rbac/…` / `../crm/workflow_vente/…` **relatifs
|
||||
à l'arbre des livrables** (`05_deliverables_mvp/legal/`), **miroir du code** du
|
||||
générateur, pas fiche-relatifs ; code-spans volontairement non link-checkés
|
||||
(mémoire `backtick-path-escapes-check-docs`).
|
||||
- **faisabilite** — chemins `/opt/oto/…`, version `{X}`, section « Composants à
|
||||
créer » : **VPS-runtime assumés**, hors périmètre worker (#8), déjà réconciliés
|
||||
§15-17 / §27-29 ; seule retouche = **note de lisibilité** (bandeau VPS-runtime vs
|
||||
template committé), zéro chiffre.
|
||||
|
||||
**Bilan de la campagne (levée de la réserve −4)** — les **13/13 fiches**
|
||||
`03_agents/*/AGENT.md` sont désormais **auditées exactes** (crm/bim en session
|
||||
`110539` + les 11 ici). Classe close : mémoire `fiche-accuracy-audit-closed`.
|
||||
|
||||
**État courant re-vérifié à la clôture** (recompute indépendant `python3` sur les
|
||||
artefacts commités · aucun chiffre saisi à la main) — **inchangé et vert** :
|
||||
|
||||
| Dimension | Valeur | Verdict | Source |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **22/22 modules** (seuil `pass_score` 95) | `PASS` | `qa/audit_4big/out/quality_report.json` |
|
||||
| Régression (run) | **22 suites · 564 ran · 564 passés · 0 échec · 0 erreur** | `PASS` | `qa/regression/out/regression_run.json` |
|
||||
| Recette roadmap | **15 promesses** `in_repo` | `true` | `qa/acceptance/out/acceptance_matrix.json` |
|
||||
| Fiches agents | **13/13 auditées exactes** | — | `03_agents/*/AGENT.md` |
|
||||
| Gates CI | **7 gates** re-joués → **exit 0** + reproductibilité `out/` byte-identique | vert | `ci/README.md` |
|
||||
|
||||
**Prochaine tâche (révisée)** — la file d'audit fiches est **vide** (13/13 clos). Le
|
||||
buffer Sprint 8 reste en posture de **veille** : maintenir `daily_reports/` (1
|
||||
instantané/jour), et ne durcir un gate **que si** un blind-spot est **prouvé par
|
||||
mutation-test** (`verify-uncovered-before-gating`), jamais un gate redondant (#5).
|
||||
|
||||
**Auto-score 4Big de la clôture : 97/100.** Réserve −3 : purement une passe de
|
||||
**vérification + réconciliation de reporting** (aucun nouveau livrable — attendu en
|
||||
phase buffer saturée) ; déploiement serveur hors périmètre (VPS · #8).
|
||||
|
||||
---
|
||||
|
||||
## Fin de journée · addendum session `20260802_143555`
|
||||
|
||||
> **Additif** (comme l'addendum précédent) : le corps et l'addendum de clôture
|
||||
> restent intacts. Cet ajout **consolide les 4 sessions buffer intercalées entre la
|
||||
> clôture `120544` et 14:35** (commits `cb8d931` · `8dcedb0` · `576e78b` · `bd3572b`),
|
||||
> absentes de l'instantané stakeholder alors que leur trace fine existe déjà au canal 1
|
||||
> ([`05_activity_log/2026-08-02.md`](../../05_activity_log/2026-08-02.md)). Tenir le
|
||||
> canal 2 à jour à chaque session buffer était la **« Prochaine tâche » #1** de ce
|
||||
> rapport (l.84). Chaque chiffre ci-dessous est **re-vérifié contre l'artefact commité**
|
||||
> (recompute `python3` indépendant · anti-invention `CLAUDE.md` #6 · zéro donnée nouvelle).
|
||||
|
||||
**Les 4 sessions consolidées** — toutes en posture buffer (aucun nouveau module ·
|
||||
contrainte « pas d'invention de modules ») :
|
||||
- **`123546` (`cb8d931`)** — *dérive doc-vs-réalité corrigée* : les 2 docs de mandat de
|
||||
tête (`AGENTS_EXISTING_ASSETS` titre + `MASTER_PROMPT §28`) disaient « **12 agents** »
|
||||
alors que le **13ᵉ** (publiciste) est livré+gaté → correction **12→13** (source unique
|
||||
`03_agents/`) + clarification du statut publiciste (**LIVRÉ** mandat · runtime VPS hors
|
||||
#8) + extension de `check_readme_claims` (réutilise `n_agents`) pour ancrer ces 2 docs.
|
||||
- **`130548` (`8dcedb0`)** — *ancrage anti-dérive* : les 3 surfaces vives de
|
||||
`GAP_ANALYSIS_SPRINT1` citant le compte d'agents (dont la coche de couverture DoD) sont
|
||||
ré-ancrées sur `n_agents` (re-dérivé, jamais une liste à la main).
|
||||
- **`133549` (`576e78b`)** — *enquête anti-dérive, ZÉRO gate ajouté (#5)* : le `roles_couverts`
|
||||
(« 44 ») des 3 manifestes s'est révélé **transitivement mono-sourcé** (même fonction
|
||||
`roles_by_portail` sur l'unique contrat `rbac_50_roles.json`) → un cross-gate serait
|
||||
**redondant** ; garde-fou documenté dans [`ci/README.md`](../../ci/README.md) pour
|
||||
prévenir un gate N+1 inutile.
|
||||
- **`140554` (`bd3572b`)** — *audit d'EFFICACITÉ des gates, ZÉRO gate ajouté (#5)* :
|
||||
**mutation-testing** du suite (7 mutations ciblées sur 4 gates à oracle recomputé) →
|
||||
**7/7 ROUGE** prouvés (« vert » ≠ « faux-vert »). Confirme que le suite **mord**
|
||||
réellement en phase de bascule launch.
|
||||
|
||||
**État courant MVP · re-vérifié à 14:35 · 100 % sourcé** (recompute `python3` indépendant
|
||||
sur les artefacts commités · aucun chiffre saisi à la main) — **inchangé et vert** :
|
||||
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (seuil `pass_score` **95** · #5) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **22 suites · 564 ran · 564 passés · 0 échec · 0 erreur** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Fiches agents | **13/13 auditées exactes** (classe close) | — | `03_agents/*/AGENT.md` |
|
||||
| Gates CI | **7 gates statiques** re-joués → **exit 0** + reproductibilité `out/` byte-identique | vert | [`ci/README.md`](../../ci/README.md) |
|
||||
| Câblage CI | `gate.needs` = **30 jobs** agrégés · **1** seul job manuel (`e2e-baseline`) | intègre | [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) |
|
||||
|
||||
**Vérifications effectuées à la clôture** (en-repo, sans toucher au VPS) :
|
||||
- **7 gates CI → exit 0** sur l'arbre propre (`check_artifacts` · `check_ci_integrity` ·
|
||||
`check_docs` · `check_readme_claims` · `check_regression` · `guard_constraints` ·
|
||||
`validate_json`).
|
||||
- Recompute indépendant des agrégats concordant : **22/22** qualité (scores distincts =
|
||||
{100}) · **564/564** régression · **15/15** recette · **13** fiches · `gate.needs` = **30**.
|
||||
- Arbre `git status --porcelain` **vide** avant/après ; aucune commande touchant au VPS.
|
||||
|
||||
**Prochaine tâche (révisée)** — file d'audit fiches **vide** (13/13 clos), campagne de
|
||||
gates **saturée** (dernier audit d'efficacité 7/7 ROUGE). Le buffer Sprint 8 reste en
|
||||
**veille** : maintenir `daily_reports/` (1 instantané/jour · fait ici), et **ne durcir un
|
||||
gate que si un blind-spot est prouvé par mutation-test** (`verify-uncovered-before-gating`),
|
||||
jamais un gate redondant (#5). Déploiement production réel reste **hors périmètre worker**
|
||||
(VPS `153.75.250.214` · agents DevOps/ERPNext · #8).
|
||||
|
||||
**Auto-score 4Big de la consolidation : 97/100.** Réserve −3 : passe de
|
||||
**réconciliation de reporting** (canal 2 rattrapé sur 4 sessions · aucun nouveau
|
||||
livrable — attendu en buffer saturé) ; déploiement serveur hors périmètre (VPS · #8).
|
||||
État launch-readiness **inchangé et intégralement sourcé**.
|
||||
|
||||
## Fin de journée · addendum session `20260802_160601`
|
||||
|
||||
> **Additif** (3ᵉ, même convention que les deux précédents) : le corps et les deux
|
||||
> addenda restent intacts. Cet ajout consolide les **2 sessions buffer intercalées
|
||||
> entre l'addendum `143555` (14:35) et 15:36** (commits `49123ec` · `dcb463e`),
|
||||
> absentes de l'instantané stakeholder alors que leur trace fine existe au canal 1
|
||||
> ([`05_activity_log/2026-08-02.md`](../../05_activity_log/2026-08-02.md)). Tenir le
|
||||
> canal 2 à jour à chaque session buffer était la tâche de veille #1 de l'addendum
|
||||
> précédent (l.201). Chaque chiffre ci-dessous est **re-vérifié contre l'artefact
|
||||
> commité** (recompute `python3` indépendant · anti-invention `CLAUDE.md` #6 · zéro
|
||||
> donnée nouvelle).
|
||||
|
||||
**Les 2 sessions consolidées** — un seul livrable net, un **outil DevOps** (pas un
|
||||
nouveau module · contrainte « pas d'invention de modules » respectée) :
|
||||
- **`150601` (`49123ec`)** — *DevOps outillage : miroir LOCAL du gate CI* : ajout de
|
||||
[`run_ci.sh`](../../run_ci.sh) à la racine, qui **dérive** la liste des jobs de
|
||||
`gate.needs` dans [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) (zéro
|
||||
liste en dur · anti-dérive par construction · suit `ci.yml`) et rejoue les **30 jobs**
|
||||
(7 gates `ci/*.sh` + 23 suites module) en une commande. **PAS un gate (#5)** : la
|
||||
racine ∉ `ci/*.sh`, donc hors INV-B ; parseur `awk` anti-commentaire ; chemin d'échec
|
||||
prouvé (`EXIT=1` + dump). Doc `README` + `ci/README`. Consigné en mémoire
|
||||
(*local-ci-runner*).
|
||||
- **`153601` (`dcb463e`)** — *durcissement du même outil* : le décompte **30/23/7** du
|
||||
commentaire d'en-tête, jusque-là **figé** (risque de dérive silencieuse si `ci.yml`
|
||||
gagne/perd un job), est retiré et **DÉRIVÉ à l'exécution** — la valeur affichée par
|
||||
`run_ci.sh` provient désormais du parse de `gate.needs`, jamais d'un nombre saisi.
|
||||
|
||||
**État courant MVP · re-vérifié à 16:06 · 100 % sourcé** (recompute `python3` indépendant
|
||||
sur les artefacts commités · aucun chiffre saisi à la main) — **inchangé et vert** :
|
||||
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (scores distincts = {100} · seuil 95 · #5) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **22 suites · 564 ran · 564 passés · 0 échec · 0 erreur · 0 skip** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Fiches agents | **13/13 auditées exactes** (classe close) | — | `03_agents/*/AGENT.md` |
|
||||
| Câblage CI | `gate.needs` = **30 jobs** (7 gates `ci/*.sh` + 23 suites module) | intègre | [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) |
|
||||
| Miroir local | `run_ci.sh` rejoue le gate → **30 PASS · 0 FAIL · 0 SKIP** | vert | [`run_ci.sh`](../../run_ci.sh) |
|
||||
|
||||
**Vérifications effectuées à la clôture** (en-repo, sans toucher au VPS) :
|
||||
- **`run_ci.sh` → 30 PASS · 0 FAIL · 0 SKIP** (miroir complet du gate, décompte 30/7/23
|
||||
dérivé à l'exécution de `gate.needs`).
|
||||
- Recompute indépendant des agrégats concordant : **22/22** qualité (scores = {100}) ·
|
||||
**564/564** régression · **15/15** recette (verdict `true`) · **13** fiches ·
|
||||
`gate.needs` = **30**.
|
||||
- Arbre `git status --porcelain` **vide** avant l'édition ; aucune commande touchant au VPS.
|
||||
|
||||
**Prochaine tâche (inchangée)** — buffer Sprint 8 en **veille saturée** : file d'audit
|
||||
fiches **vide** (13/13 clos), campagne de gates **saturée**. Maintenir `daily_reports/`
|
||||
à chaque session (fait ici), et **ne durcir un gate que si un blind-spot est prouvé par
|
||||
mutation-test** (*verify-uncovered-before-gating*), jamais un gate redondant (#5).
|
||||
Déploiement production réel reste **hors périmètre worker** (VPS `153.75.250.214` · #8).
|
||||
|
||||
**Auto-score 4Big de la consolidation : 97/100.** Réserve −3 : passe de
|
||||
**réconciliation de reporting** (canal 2 rattrapé sur 2 sessions · le seul livrable net
|
||||
est l'outil DevOps `run_ci.sh`, déjà gaté par construction — attendu en buffer saturé) ;
|
||||
déploiement serveur hors périmètre (VPS · #8). État launch-readiness **inchangé et
|
||||
intégralement sourcé**.
|
||||
|
||||
## Fin de journée · addendum session `20260802_180604`
|
||||
|
||||
> **Additif** (4ᵉ, même convention que les trois précédents) : le corps et les trois
|
||||
> addenda restent intacts. Cet ajout consolide les **3 sessions buffer intercalées entre
|
||||
> l'addendum `160601` (16:06) et 18:06** (commits `59eb4dc` · `68f8c1a` · `d2ab269`),
|
||||
> absentes de l'instantané stakeholder alors que leur trace fine existe au canal 1
|
||||
> ([`05_activity_log/2026-08-02.md`](../../05_activity_log/2026-08-02.md)). Tenir le
|
||||
> canal 2 à jour à chaque session buffer était la tâche de veille #1 de l'addendum
|
||||
> précédent (l.259). Chaque chiffre ci-dessous est **re-vérifié contre l'artefact
|
||||
> commité** (recompute `python3` indépendant · anti-invention `CLAUDE.md` #6 · zéro
|
||||
> donnée nouvelle).
|
||||
|
||||
**Les 3 sessions consolidées** — **aucun nouveau module, aucun gate ajouté (#5)** : trois
|
||||
retouches de **doc / outillage DevOps** dans la lignée directe de `run_ci.sh`, chacune
|
||||
une correction d'honnêteté (retrait de comptes figés) plutôt qu'un livrable net :
|
||||
- **`163601` (`59eb4dc`)** — *fiche `03_agents/devops/AGENT.md`* : documentation du
|
||||
livrable `run_ci.sh` (indexé au `README` racine mais absent de la fiche de son agent
|
||||
propriétaire) → 3ᵉ ligne de table + sous-section « outillage local, **PAS un gate #5** »
|
||||
(racine ∉ `ci/`, hors INV-B). Zéro module, zéro gate ; `check_docs` valide le lien
|
||||
`../../run_ci.sh`.
|
||||
- **`170601` (`68f8c1a`)** — *`run_ci.sh` : `-h|--help` + en-tête `--static` honnête*
|
||||
(UX du lanceur · **PAS un gate #5**) — le lanceur documente désormais son propre usage,
|
||||
toujours hors périmètre des gates.
|
||||
- **`173603` (`d2ab269`)** — *`ci/README.md` : retrait des comptes CI figés « 7 gates /
|
||||
23 suites »* (même fix d'honnêteté que `dcb463e`, un cran plus haut · anti-invention #6,
|
||||
**PAS un gate #5**) — le décompte exact reste **dérivé** par `run_ci.sh` de `gate.needs`,
|
||||
jamais re-transcrit à la main dans la prose.
|
||||
|
||||
**État courant MVP · re-vérifié à 18:06 · 100 % sourcé** (recompute `python3` indépendant
|
||||
sur les artefacts commités · aucun chiffre saisi à la main) — **inchangé et vert** :
|
||||
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (scores distincts = {100} · seuil 95 · #5) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **22 suites · 564 ran · 564 passés · 0 échec · 0 erreur · 0 skip** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Fiches agents | **13/13 auditées exactes** (classe close) | — | `03_agents/*/AGENT.md` |
|
||||
| Câblage CI | `gate.needs` = **30 jobs** (7 gates `ci/*.sh` + 23 suites module) | intègre | [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) |
|
||||
| Miroir local | `run_ci.sh` rejoue le gate → **30 PASS · 0 FAIL · 0 SKIP** | vert | [`run_ci.sh`](../../run_ci.sh) |
|
||||
|
||||
**Vérifications effectuées à la clôture** (en-repo, sans toucher au VPS) :
|
||||
- **`run_ci.sh` → 30 PASS · 0 FAIL · 0 SKIP** (miroir complet du gate, décompte 30/7/23
|
||||
dérivé à l'exécution de `gate.needs`).
|
||||
- Recompute indépendant des agrégats concordant : **22/22** qualité (scores = {100}) ·
|
||||
**564/564** régression · **15/15** recette (verdict `true`) · **13** fiches ·
|
||||
`gate.needs` = **30**.
|
||||
- Arbre `git status --porcelain` **vide** avant l'édition ; aucune commande touchant au VPS.
|
||||
|
||||
**Prochaine tâche (inchangée)** — buffer Sprint 8 en **veille saturée** : file d'audit
|
||||
fiches **vide** (13/13 clos), campagne de gates **saturée**, audit doc-vs-réalité relancé
|
||||
cette session → **aucune dérive** trouvée. Maintenir `daily_reports/` à chaque session
|
||||
(fait ici), et **ne durcir un gate que si un blind-spot est prouvé par mutation-test**
|
||||
(*verify-uncovered-before-gating*), jamais un gate redondant (#5). Déploiement production
|
||||
réel reste **hors périmètre worker** (VPS `153.75.250.214` · #8).
|
||||
|
||||
**Auto-score 4Big de la consolidation : 97/100.** Réserve −3 : passe de
|
||||
**réconciliation de reporting** (canal 2 rattrapé sur 3 sessions de retouches doc/outillage,
|
||||
aucun livrable net nouveau — attendu en buffer saturé) ; déploiement serveur hors périmètre
|
||||
(VPS · #8). État launch-readiness **inchangé et intégralement sourcé**.
|
||||
|
||||
---
|
||||
|
||||
## Fin de journée · addendum session `20260802_200607`
|
||||
|
||||
> **Additif** (5ᵉ, même convention que les quatre précédents) : le corps et les quatre
|
||||
> addenda restent **intacts**. Cet ajout consolide les **3 sessions buffer intercalées
|
||||
> entre l'addendum `180604` (18:06) et 20:06** (commits `97d3b64` · `f954cc1` · `7c79e43`
|
||||
> · `8a08065`), absentes de l'instantané stakeholder alors que leur trace fine existe au
|
||||
> canal 1 ([`05_activity_log/2026-08-02.md`](../../05_activity_log/2026-08-02.md)). Tenir
|
||||
> le canal 2 à jour à chaque session buffer reste la tâche de veille #1 des addenda
|
||||
> précédents. Chaque chiffre ci-dessous est **re-vérifié contre l'artefact commité**
|
||||
> (recompute `python3` indépendant · anti-invention `CLAUDE.md` #6 · zéro donnée nouvelle).
|
||||
|
||||
**Les 3 sessions consolidées** — **aucun nouveau module, aucun gate ajouté (#5)** ; deux
|
||||
corrections d'honnêteté doc + une vérification QA pure, dans la lignée directe du buffer
|
||||
launch-readiness :
|
||||
- **`183604` (`97d3b64` + `f954cc1`)** — *`README.md` racine : dérive doc-vs-réalité non
|
||||
gatée* : la puce « Régression » affirmait `regression_run.json` « non commité par design »
|
||||
→ **FAUX** depuis la refonte baseline **byte-gatée** (commit `76fb0e7`) où l'artefact EST
|
||||
commité et re-vérifié par `check_regression`. Classe de dérive *prose-fact* (distincte des
|
||||
comptes/liens gatés · mémoire `prose-facts-vs-numeric-drift`). Zéro module, zéro gate.
|
||||
- **`190604` (`7c79e43`)** — *QA launch-readiness : mutation-test que les 7 gates statiques
|
||||
`ci/*.sh` MORDENT* → **7/7 RED**. Pour chaque gate, injection d'une mutation de sa classe
|
||||
de dérive, exigence d'un exit non-zéro, puis **revert par chemin ciblé** (jamais `git clean`
|
||||
· #interdits). Frontière de propriété inter-gates confirmée (`check_regression` possède
|
||||
`regression_run.json` · `check_artifacts` possède `MANIFEST.json`). Un gate vert qui ne
|
||||
mord pas est pire qu'absent — vérification pure, **zéro fichier de production modifié**.
|
||||
- **`193605` (`8a08065`)** — *`ci/README.md` : faux-positif d'audit récurrent verrouillé en
|
||||
garde-fou in-repo* : `michel/otov7-platform` (repo **produit**, `CLAUDE.md` #2) ≠
|
||||
`oto-enterprise-os-dtp` (repo **mandat**) — ce n'est PAS une coquille de la constitution
|
||||
(mémoire `otov7-platform-not-drift`). Note défensive, zéro module, zéro gate.
|
||||
|
||||
**État courant MVP · re-vérifié à 20:06 · 100 % sourcé** (recompute `python3` indépendant
|
||||
sur les artefacts commités · aucun chiffre saisi à la main) — **inchangé et vert** :
|
||||
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (scores distincts = {100} · seuil 95 · #5) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **22 suites · 564 ran · 564 passés · 0 échec · 0 erreur · 0 skip** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` (verdict `True`) | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Fiches agents | **13/13 auditées exactes** (classe close) | — | `03_agents/*/AGENT.md` |
|
||||
| Câblage CI | `gate.needs` = **30 jobs** (7 gates `ci/*.sh` + 23 suites module) | intègre | [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) |
|
||||
| Miroir local | `run_ci.sh` rejoue le gate → **30 PASS · 0 FAIL · 0 SKIP** | vert | [`run_ci.sh`](../../run_ci.sh) |
|
||||
|
||||
**Vérifications effectuées à la clôture** (en-repo, sans toucher au VPS) :
|
||||
- **`run_ci.sh` → 30 PASS · 0 FAIL · 0 SKIP** (miroir complet du gate, décompte 30/7/23
|
||||
dérivé à l'exécution de `gate.needs`).
|
||||
- Recompute indépendant des agrégats concordant : **22/22** qualité (scores = {100}) ·
|
||||
**564/564** régression (`totals` : suites 22 · ran 564 · passed 564 · 0/0/0) · **15/15**
|
||||
recette (`in_repo`, verdict `True`) · **13** fiches · `gate.needs` = **30** (7 + 23).
|
||||
- **Audit doc-vs-réalité relancé cette session** (top-level + module READMEs + 13 fiches,
|
||||
spot-check des faits-prose contre l'état courant) → **aucune dérive** trouvée.
|
||||
- Arbre `git status --porcelain` **vide** avant l'édition ; aucune commande touchant au VPS.
|
||||
|
||||
**Prochaine tâche (inchangée)** — buffer Sprint 8 en **veille saturée** : file d'audit
|
||||
fiches **vide** (13/13 clos), campagne de gates **saturée** (7 statiques prouvés mordants),
|
||||
audit doc-vs-réalité **sans dérive**. Maintenir `daily_reports/` à chaque session (fait ici),
|
||||
et **ne durcir un gate que si un blind-spot est prouvé par mutation-test**
|
||||
(*verify-uncovered-before-gating*), jamais un gate redondant (#5). Déploiement production
|
||||
réel reste **hors périmètre worker** (VPS `153.75.250.214` · #8).
|
||||
|
||||
**Auto-score 4Big de la consolidation : 97/100.** Réserve −3 : passe de **réconciliation de
|
||||
reporting** (canal 2 rattrapé sur 3 sessions doc/QA, aucun livrable net nouveau — attendu en
|
||||
buffer saturé) ; déploiement serveur hors périmètre (VPS · #8). État launch-readiness
|
||||
**inchangé et intégralement sourcé**.
|
||||
|
||||
---
|
||||
|
||||
## Fin de journée · addendum session `20260802_220611`
|
||||
|
||||
> **Additif** (6ᵉ, même convention que les cinq précédents) : le corps et les cinq addenda
|
||||
> restent **intacts**. Cet ajout consolide les **4 sessions buffer intercalées entre
|
||||
> l'addendum `200607` (20:06) et 22:06** (commits `0e76411` · `b3f131e` · `a2c2e0b` ·
|
||||
> `12c784c` · `eae3787`), absentes de l'instantané stakeholder alors que leur trace fine
|
||||
> existe au canal 1 ([`05_activity_log/2026-08-02.md`](../../05_activity_log/2026-08-02.md)).
|
||||
> Tenir le canal 2 à jour à chaque session buffer reste la tâche de veille #1 des addenda
|
||||
> précédents. Chaque chiffre ci-dessous est **re-vérifié contre l'artefact commité** (recompute
|
||||
> `python3` indépendant · anti-invention `CLAUDE.md` #6 · zéro donnée nouvelle).
|
||||
|
||||
**Les 4 sessions consolidées** — **aucun nouveau module, aucun gate ajouté (#5)** ; **deux
|
||||
corrections d'honnêteté doc-vs-CODE réelles** (dans la prose intra-module, classe non gatée)
|
||||
+ un audit prose-fact sans dérive, dans la lignée directe du buffer launch-readiness :
|
||||
- **`200607` (`0e76411`)** — *5ᵉ addendum canal 2* : rattrapage de l'instantané stakeholder
|
||||
sur les 3 sessions `183604`/`190604`/`193605`, chaque agrégat re-sourcé par recompute
|
||||
indépendant. Reporting pur, zéro module, zéro gate.
|
||||
- **`203609` (`b3f131e`)** — *audit prose-fact ciblé (classe non gatée) sur tous les README
|
||||
modules* → **aucune dérive**. Unique candidat résolu en **faux-positif prouvé** :
|
||||
`legal/confotur` « 44 tests (dont 8 négatifs) » **exact** (8 = classe `TestNegative` ; les
|
||||
3 `TestBuilderGuards` sont une classe *guard/raises* distincte, pas des « négatifs » →
|
||||
agrégés à tort en 11 par un audit naïf) + « 14 invariants » confirmé (14 entrées `# INVn`).
|
||||
Garde-fou mémoire `confotur-8-negatifs-not-drift`. Vérification pure, zéro fichier de
|
||||
production modifié.
|
||||
- **`210609` (`a2c2e0b` + `12c784c`)** — *`publiciste/lib/... parser.py` : dérive doc-vs-CODE
|
||||
RÉELLE dans le docstring de production* : l'en-tête « Sources lues » annonçait
|
||||
`40_llm_outputs/commercial.md → positionnement FR (fallback prix §5.3)` alors qu'**aucun
|
||||
fallback prix n'existe** (grep `§5.3`/fallback-prix = la seule occurrence est le docstring
|
||||
lui-même ; `commercial.md` n'alimente QUE `parse_positionnement_fr`). Reformulé
|
||||
« → positionnement FR (1er paragraphe non-titre) », aligné sur la vraie logique. Classe
|
||||
*comportement-inventé* (contraire à l'éthos anti-invention #6), non gatée. Docstring seul —
|
||||
zéro logique, zéro test. Mémoire `docstring-vs-code-drift`.
|
||||
- **`213610` (`eae3787`)** — *`demo/scenarios/demo_scenario_gen.py` : dérive doc-vs-CODE
|
||||
RÉELLE (output omis)* : le générateur écrit **3** fichiers (`run_sheet.json` +
|
||||
`run_sheet.md` + `MANIFEST.json`) mais le docstring/help n'en listait que **2** à trois
|
||||
endroits (diagramme, résumé sous-commande, `help=` argparse) — le `run_sheet.md` ajouté au
|
||||
commit `aebe9b7` avait mis à jour le README mais laissé le `.py` en arrière. Les 3
|
||||
emplacements réalignés sur les 3 sorties réelles. Docstring/help seuls — zéro logique.
|
||||
Même classe que le fix publiciste. Mémoire `docstring-vs-code-drift`.
|
||||
|
||||
**Vérification indépendante cette session (`220611`)** — au-delà du rattrapage, un **sweep
|
||||
doc-vs-CODE outil-assisté sur les 22 générateurs** (extraction des sorties réellement écrites
|
||||
vs sorties commitées dans chaque `out/`) → **concordance totale**, y compris les deux modules
|
||||
corrigés ce jour (`demo_scenario_gen` émet bien ses 3 fichiers, tous commités). **Aucune
|
||||
nouvelle dérive de la classe déclaration-de-sortie.**
|
||||
|
||||
**État courant MVP · re-vérifié à 22:06 · 100 % sourcé** (recompute `python3` indépendant sur
|
||||
les artefacts commités · aucun chiffre saisi à la main) — **inchangé et vert** :
|
||||
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (scores distincts = {100} · seuil 95 · #5) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **22 suites · 564 ran · 564 passés · 0 échec · 0 erreur · 0 skip** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` (verdict `True`) | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Fiches agents | **13/13 auditées exactes** (classe close) | — | `03_agents/*/AGENT.md` |
|
||||
| Câblage CI | `gate.needs` = **30 jobs** (7 gates `ci/*.sh` + 23 suites module) | intègre | [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) |
|
||||
| Miroir local | `run_ci.sh` rejoue le gate → **30 PASS · 0 FAIL · 0 SKIP** | vert | [`run_ci.sh`](../../run_ci.sh) |
|
||||
|
||||
**Vérifications effectuées à la clôture** (en-repo, sans toucher au VPS) :
|
||||
- **`run_ci.sh` → 30 PASS · 0 FAIL · 0 SKIP** (miroir complet du gate).
|
||||
- Recompute indépendant concordant : **22/22** qualité (scores = {100}) · **564/564**
|
||||
régression (`totals` : 22 suites · 564 ran · 564 passed · 0/0/0) · **15/15** recette
|
||||
(`in_repo`, verdict `True`) · **13** fiches · `gate.needs` = **30** (7 + 23).
|
||||
- **Sweep doc-vs-CODE sorties sur 22 générateurs** → concordance totale, aucune dérive.
|
||||
- Arbre `git status --porcelain` **vide** avant l'édition ; aucune commande touchant au VPS.
|
||||
|
||||
**Prochaine tâche (inchangée)** — buffer Sprint 8 en **veille saturée** : file d'audit fiches
|
||||
**vide** (13/13 clos), campagne de gates **saturée** (7 statiques prouvés mordants), audit
|
||||
doc-vs-réalité (README + docstrings + sorties générateurs) **sans dérive résiduelle**. Tenir
|
||||
`daily_reports/` à jour à chaque session (fait ici) et **ne durcir un gate que si un
|
||||
blind-spot est prouvé par mutation-test** (*verify-uncovered-before-gating*), jamais un gate
|
||||
redondant (#5). Déploiement production réel reste **hors périmètre worker** (VPS
|
||||
`153.75.250.214` · #8).
|
||||
|
||||
**Auto-score 4Big de la consolidation : 97/100.** Réserve −3 : passe de **réconciliation de
|
||||
reporting** + audit de vérification (canal 2 rattrapé sur 4 sessions ; sweep sorties sans
|
||||
dérive nouvelle — attendu en buffer saturé) ; déploiement serveur hors périmètre (VPS · #8).
|
||||
État launch-readiness **inchangé et intégralement sourcé**.
|
||||
|
||||
---
|
||||
|
||||
## Fin de journée · addendum session `20260802_233620`
|
||||
|
||||
> **Additif** (7ᵉ, même convention que les six précédents) : le corps et les six addenda
|
||||
> restent **intacts**. Cet ajout consolide les **2 sessions buffer intercalées entre
|
||||
> l'addendum `220611` (22:06) et 23:10** (commits `16db05a` · `56a8802`), absentes de
|
||||
> l'instantané stakeholder alors que leur trace fine existe au canal 1
|
||||
> ([`05_activity_log/2026-08-02.md`](../../05_activity_log/2026-08-02.md)). Tenir le canal 2 à
|
||||
> jour à chaque session buffer reste la tâche de veille #1 des addenda précédents. Chaque
|
||||
> chiffre ci-dessous est **re-vérifié contre l'artefact commité** (recompute `python3`
|
||||
> indépendant · anti-invention `CLAUDE.md` #6 · zéro donnée nouvelle).
|
||||
|
||||
**Les 2 sessions consolidées** — **aucun nouveau module, aucun gate ajouté (#5)** ; une
|
||||
correction d'honnêteté doc-vs-CODE réelle + une réparation de gate auto-mordu, toutes deux
|
||||
dans la lignée directe du buffer launch-readiness :
|
||||
- **`223614` (`16db05a`)** — *`qa/audit_4big/q4lib/builder.py` : dérive doc-vs-CODE RÉELLE
|
||||
dans un docstring de production* : l'en-tête annonçait « 17 modules » alors que le compte
|
||||
réel est **22** (`len(modules)` == `totals.modules`). Le même « 17 » gaté au README avait
|
||||
déjà été réaligné ; il avait survécu **non gaté** dans le docstring. Correction **honnête ET
|
||||
anti-re-dérive** : plutôt que substituer « 22 » (qui re-dériverait au module #23), le
|
||||
docstring énonce désormais la **RÈGLE** (« TOUS les modules audités ≥ 95/100, aucun sous le
|
||||
seuil ») + note explicite que le compte est **dérivé** (`len(modules)`, 17→22 à ce jour) et
|
||||
ne se retouche pas ici. Aligné sur l'éthos `fix-vs-gate` (décrire depuis l'autorité = le
|
||||
code). Docstring seul — zéro logique, zéro test. Mémoire `docstring-vs-code-drift`.
|
||||
- **`230619` (`56a8802`)** — *gate `constraints-guard` RED réparé (auto-morsure log-prose)* :
|
||||
non pas une violation réelle de contrainte, mais l'**auto-morsure** classique du gate par sa
|
||||
propre trace — le commit précédent avait ajouté au journal une négation citant l'interdit
|
||||
`git clean` **sans marqueur PROHIBITION ni `ci-allow`** sur la ligne, donc flaggée.
|
||||
Neutralisée par échappatoire documenté `ci-allow` (convention établie 2026-08-01). Un seul
|
||||
fichier (journal), une seule ligne. Zéro module, zéro gate, zéro logique de prod. Classe
|
||||
connue `guard-constraints-log-prose`.
|
||||
|
||||
**Vérification indépendante cette session (`233620`)** — au-delà du rattrapage, un **sweep
|
||||
doc-vs-CODE exhaustif à couverture élargie** : audit des docstrings/`--help`/argparse sur les
|
||||
**22 générateurs** (entrypoints) **ET** des docstrings module + fonction sur les **40 modules
|
||||
de bibliothèque** (`*lib/` : builders, scorers, parsers, finance, criteria, controls, runner…),
|
||||
soit **62 fichiers de production** confrontés à leur code réel (sorties écrites, entrées lues,
|
||||
fallbacks, règles, compteurs). **Résultat : concordance totale · aucune dérive résiduelle de
|
||||
la classe docstring-vs-CODE**, y compris le `q4lib/builder.py` corrigé cette journée. La
|
||||
surface docstring-drift (jusqu'ici *récurrente, non gatée*) est **saturée à ce jour** — signal
|
||||
fort de couverture, à re-scanner après toute évolution de code.
|
||||
|
||||
**État courant MVP · re-vérifié à 23:10 · 100 % sourcé** (recompute `python3` indépendant sur
|
||||
les artefacts commités · aucun chiffre saisi à la main) — **inchangé et vert** :
|
||||
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (scores distincts = {100} · seuil 95 · #5) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **22 suites · 564 ran · 564 passés · 0 échec · 0 erreur · 0 skip** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` (verdict `True`) | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Fiches agents | **13/13 auditées exactes** (classe close) | — | `03_agents/*/AGENT.md` |
|
||||
| Câblage CI | `gate.needs` = **30 jobs** (7 gates `ci/*.sh` + 23 suites module) | intègre | [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) |
|
||||
| Miroir local | `run_ci.sh` rejoue le gate → **30 PASS · 0 FAIL · 0 SKIP** | vert | [`run_ci.sh`](../../run_ci.sh) |
|
||||
|
||||
**Vérifications effectuées à la clôture** (en-repo, sans toucher au VPS) :
|
||||
- **`run_ci.sh` → 30 PASS · 0 FAIL · 0 SKIP** (miroir complet du gate).
|
||||
- Recompute indépendant concordant : **22/22** qualité (scores = {100}) · **564/564**
|
||||
régression (`totals` : 22 suites · 564 ran · 564 passed · 0/0/0) · **15/15** recette
|
||||
(`in_repo`, verdict `True`) · **13** fiches · `gate.needs` = **30** (7 + 23).
|
||||
- **Sweep doc-vs-CODE élargi (62 fichiers : 22 entrypoints + 40 `*lib/`)** → concordance
|
||||
totale, aucune dérive docstring résiduelle.
|
||||
- Arbre `git status --porcelain` **vide** avant l'édition ; aucune commande touchant au VPS.
|
||||
|
||||
**Prochaine tâche (inchangée)** — buffer Sprint 8 en **veille saturée** : file d'audit fiches
|
||||
**vide** (13/13 clos), campagne de gates **saturée** (7 statiques prouvés mordants), audit
|
||||
doc-vs-réalité (README + docstrings entrypoints **+ 40 modules lib** + sorties générateurs)
|
||||
**sans dérive résiduelle**. Tenir `daily_reports/` à jour à chaque session (fait ici) et **ne
|
||||
durcir un gate que si un blind-spot est prouvé par mutation-test**
|
||||
(*verify-uncovered-before-gating*), jamais un gate redondant (#5). Déploiement production réel
|
||||
reste **hors périmètre worker** (VPS `153.75.250.214` · #8).
|
||||
|
||||
**Auto-score 4Big de la consolidation : 97/100.** Réserve −3 : passe de **réconciliation de
|
||||
reporting** + audit de vérification (canal 2 rattrapé sur 2 sessions ; sweep docstring élargi
|
||||
aux 40 modules lib sans dérive nouvelle — attendu en buffer saturé) ; déploiement serveur hors
|
||||
périmètre (VPS · #8). État launch-readiness **inchangé et intégralement sourcé**.
|
||||
@@ -0,0 +1,377 @@
|
||||
# Daily Report · 2026-08-03 · Claude Code DTP Worker
|
||||
|
||||
**Session** : `20260803_000623`
|
||||
**Phase roadmap** : Sprint 8 · *Launch + Buffer* — livrables « Regression tests
|
||||
exhaustifs » (QA) + « Corrections finales » (buffer). Tous les sprints 1→7 sont
|
||||
livrés côté repo ; le reste (déploiement production réel) est **hors périmètre
|
||||
worker** (VPS `153.75.250.214` · `CLAUDE.md` #8).
|
||||
|
||||
## Tâche exécutée
|
||||
**Sprint 8 · QA — audit de deux surfaces d'intégrité inter-artefacts jamais
|
||||
auditées + maintien du canal `daily_reports/` (nouveau jour).**
|
||||
|
||||
Ouverture d'un instantané stakeholder pour le **1ᵉʳ jour de la fenêtre** (le canal 2
|
||||
du `2026-08-02` a été clos à 7 addenda). Plutôt que re-rejouer le sweep
|
||||
docstring-vs-CODE **déjà saturé** hier (62 fichiers de production concordants,
|
||||
commit `1c3bed3` · aucun code modifié depuis), cette session ouvre **deux surfaces
|
||||
d'intégrité inter-artefacts distinctes** que les sweeps antérieurs (docstrings,
|
||||
sorties générateurs, comptes README gatés) n'avaient **pas** couvertes.
|
||||
|
||||
## Contexte / analyse
|
||||
- Relu `CLAUDE.md`, la roadmap, `MEMORY.md`, le journal `05_activity_log/2026-08-02.md`,
|
||||
et le canal 2 `daily_reports/2026-08-02.md` (7 addenda, clos).
|
||||
- **État de départ vert** : `./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** · arbre
|
||||
`git status --porcelain` **vide**.
|
||||
- **Constat de saturation** : file d'audit fiches vide (13/13 closes), gates
|
||||
statiques 7/7 prouvés mordants (mutation-test `7c79e43`), surface docstring-vs-CODE
|
||||
saturée (`1c3bed3`). **Décision** (workflow #5 — ne pas empiler un gate/sweep
|
||||
redondant) : auditer une surface **neuve**, pas re-scanner l'ancienne.
|
||||
|
||||
## Réalisé — 2 surfaces neuves auditées, 0 dérive, 0 fichier de production modifié
|
||||
1. **Intégrité des chemins-preuve de la matrice de recette** (jamais audité) :
|
||||
confrontation des `matrix[].artifacts[].path` de
|
||||
[`acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) au
|
||||
**système de fichiers réel**. Unique chemin cité : `GAP_ANALYSIS_SPRINT1.md` (S1).
|
||||
- **Faux-positif évité** : un contrôle naïf depuis la **racine du repo** rapporte
|
||||
un « MISMATCH » (artefact `exists=true`, introuvable sur disque). En réalité les
|
||||
chemins sont **relatifs à `DELIVERABLES_ROOT`** (`05_deliverables_mvp/`), où le
|
||||
fichier `05_deliverables_mvp/GAP_ANALYSIS_SPRINT1.md` **existe bien**. Piège de
|
||||
**mauvaise base**, verrouillé en garde-fou mémoire
|
||||
(`acceptance-evidence-paths-deliverables-root`) — classe de faux-positif
|
||||
récurrent, comme `otov7-platform-not-drift`.
|
||||
- **Surface doublement protégée · ZÉRO gate ajouté (#5)** : (a) `acceptance_gen.py`
|
||||
l.198 **lève une erreur** au build si un artefact-preuve cité est absent sous
|
||||
`DELIVERABLES_ROOT` ; (b) `acclib/builder.py` l.62 **recalcule `exists`** à
|
||||
chaque build via `acclib/deps.artifact_exists` → la reproductibilité byte-à-byte
|
||||
(`check_artifacts`) prouve **transitivement** que le chemin résout toujours.
|
||||
2. **Résolution des `ci_job` de la matrice vers `gate.needs`** (jamais audité) : les
|
||||
**22** noms de jobs CI cités dans `matrix[].modules[].ci_job` **résolvent tous**
|
||||
vers un job réel de `gate.needs` (**30**) dans
|
||||
[`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) — **0 job orphelin**.
|
||||
Transitivement mono-sourcé (dérivé du manifeste) → pas de cross-gate (#5).
|
||||
3. **Maintien du canal `daily_reports/`** (ce fichier) — nouveau jour ouvert.
|
||||
|
||||
## État courant MVP · 100 % sourcé (recompute `python3` indépendant · aucun chiffre saisi à la main)
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (scores distincts = {100} · seuil `pass_score` **95** · #5) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **22 suites · 564 ran · 564 passés · 0 échec · 0 erreur · 0 skip** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` (verdict `True`) | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Fiches agents | **13/13 auditées exactes** (classe close) | — | `03_agents/*/AGENT.md` |
|
||||
| Câblage CI | `gate.needs` = **30 jobs** (7 gates `ci/*.sh` + 23 suites module) · 22 `ci_job` recette tous résolus | intègre | [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) |
|
||||
| Miroir local | `run_ci.sh` rejoue le gate → **30 PASS · 0 FAIL · 0 SKIP** | vert | [`run_ci.sh`](../../run_ci.sh) |
|
||||
|
||||
## Vérifications effectuées (en-repo, sans toucher au VPS)
|
||||
- **`run_ci.sh` → 30 PASS · 0 FAIL · 0 SKIP** (miroir complet du gate).
|
||||
- Recompute indépendant concordant : **22/22** qualité (scores = {100}) · **564/564**
|
||||
régression (`totals` : 22 suites · 564 ran · 564 passed · 0/0/0) · **15/15** recette
|
||||
(`in_repo`, verdict `True`) · **13** fiches · `gate.needs` = **30** (7 + 23).
|
||||
- **2 surfaces inter-artefacts neuves** auditées → 0 dérive (chemins-preuve recette
|
||||
résolus sous `DELIVERABLES_ROOT` + doublement gatés ; 22/22 `ci_job` → `gate.needs`).
|
||||
- Arbre `git status --porcelain` **vide** avant l'édition ; aucune commande VPS (#8).
|
||||
|
||||
## Non fait (hors périmètre worker · touche au VPS)
|
||||
- Déploiement production réel (bench migrate, fixtures, nginx/systemd, builds stores,
|
||||
indexation SEO, voix Amélie) → **VPS `153.75.250.214`** · agents DevOps/ERPNext,
|
||||
plan ordonné dans `devops/deploy_runbook` (`CLAUDE.md` #8).
|
||||
- Enregistrement `act_runner` Gitea + exécution serveur de la baseline Playwright.
|
||||
|
||||
## Prochaine tâche suggérée
|
||||
- Maintenir `daily_reports/` à chaque session buffer (1 instantané/jour · addenda
|
||||
additifs infra-journée).
|
||||
- **Ne durcir un gate que si un blind-spot est prouvé par mutation-test**
|
||||
(`verify-uncovered-before-gating`), jamais un gate redondant (#5). Les surfaces
|
||||
driftables évidentes sont saturées ; privilégier l'ouverture de surfaces
|
||||
**inter-artefacts neuves** (comme cette session) plutôt que re-scanner l'acquis.
|
||||
- Re-scanner la classe docstring-vs-CODE **après toute évolution de code** (elle
|
||||
n'est pas close par nature · `docstring-vs-code-drift`).
|
||||
|
||||
---
|
||||
|
||||
**Auto-score 4Big : 97/100.** Réserve −3 : passe de **vérification pure** — deux
|
||||
surfaces inter-artefacts neuves auditées sans dérive + reporting canal 2 (aucun
|
||||
livrable net nouveau — attendu en buffer Sprint 8 saturé) ; déploiement serveur hors
|
||||
périmètre (VPS · #8). État launch-readiness **inchangé et intégralement sourcé** :
|
||||
22/22 qualité · 564/564 régression · 15/15 recette · 30 gate.needs verts.
|
||||
|
||||
---
|
||||
|
||||
## Addendum · Session `20260803_030641` · Sprint 5 Mobile — fiche store FR/EN/ES (livrable net)
|
||||
|
||||
**Tâche exécutée (livrable fonctionnel, périmètre worker).** Ajout de la **fiche store
|
||||
App Store / Play** au livrable Mobile in-repo
|
||||
([`mobile/app_config`](../mobile/app_config/)) — l'un des **non-bloqueurs** explicites
|
||||
de `DIRECTIVE_MOBILE_STORES_20260803.md` (P3 · « Contenus descriptions · rédiger
|
||||
FR/EN/ES »), sans EAS_TOKEN ni credentials (attendus de Michel · vrais bloqueurs).
|
||||
|
||||
**Design anti-invention (#6).** Nouvel artefact
|
||||
[`out/store_listing.json`](../mobile/app_config/out/store_listing.json) (sous-titre,
|
||||
description courte/longue, mots-clés · FR/EN/ES). Les **faits produit** driftables —
|
||||
nombre + énumération des portails, langues — sont **INJECTÉS** depuis la surface réelle
|
||||
(`role_navigation` = labels `portails_spec`, langues `seo_spec`) au build ; seuls le
|
||||
style rédactionnel et les mots-clés sont authoriaux (même catégorie que `app.name`).
|
||||
La copie marketing ne peut donc **pas mentir** sur les portails/langues (invariant 14).
|
||||
|
||||
**3 nouveaux invariants** (12 fiche=langues seo · 13 limites de caractères store
|
||||
App Store/Play · 14 injection portails/langues == surface réelle) → **12→15
|
||||
invariants** (déterminisme = 15). Schéma étendu (`store_listing` + `$ref`
|
||||
`store_listing_lang`). 5 tests ajoutés (22 méthodes mobile · +5 → **569** au total).
|
||||
|
||||
**Consommateurs régénérés (byte-repro #artifact-gate).** `qa/audit_4big` (22/22 · min
|
||||
100) · `qa/regression` plan+run (**569** tests) · fiches gatées `03_agents/qa` &
|
||||
`03_agents/erpnext_backend` (564→569). `./run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP**.
|
||||
|
||||
| Dimension | Valeur (après cette session) | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Régression (run) | **22 suites · 569 ran · 569 passés · 0 échec · 0 erreur · 0 skip** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (seuil 95) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Fiche store Mobile | **FR/EN/ES** · portails/langues injectés · limites store validées | `PASS` (15 invariants) | [`mobile/app_config/out/store_listing.json`](../mobile/app_config/out/store_listing.json) |
|
||||
|
||||
**Hors périmètre (inchangé · #8).** `eas build`/`eas submit`, assets binaires
|
||||
(icône/splash 1024²), credentials Apple/Google → agent Mobile (VPS/stores).
|
||||
|
||||
---
|
||||
|
||||
## Addendum · Session `20260803_033642` · Sprint 5 Mobile P1 — workflow CI/CD `mobile-build.yml` (livrable net)
|
||||
|
||||
**Tâche exécutée (livrable fonctionnel, périmètre worker).** Création du workflow
|
||||
Gitea Actions [`.gitea/workflows/mobile-build.yml`](../../.gitea/workflows/mobile-build.yml)
|
||||
— action **P1 · CI/CD Mobile** explicite de `DIRECTIVE_MOBILE_STORES_20260803.md`
|
||||
(« Trigger sur push `main` ou tag `v*` · Job iOS/Android `eas build --profile
|
||||
production --non-interactive` »). Deuxième workflow du repo à côté de `ci.yml`, même
|
||||
runner Gitea (`ubuntu-latest`) — **jamais GitHub** (`CLAUDE.md` #2).
|
||||
|
||||
**Design activation-différée (#6/#8 · zéro credential inventé).** 3 jobs :
|
||||
`preflight` (toujours vert · vérifie la présence de la config EAS versionnée
|
||||
`out/eas_build.json` + rapporte l'état des secrets, sans jamais les logger) puis
|
||||
`build-ios` (App Store #32) et `build-android` (Play Store), **tous deux gardés
|
||||
`if: secrets.EAS_TOKEN != '' && vars.MOBILE_NATIVE_REPO != ''`** → **SKIP** tant que
|
||||
Michel n'a pas fourni le token expo.dev + le dépôt Gitea du code natif Expo. **Le CI
|
||||
n'est donc JAMAIS rendu rouge** avant activation. Aucun identifiant fabriqué : token,
|
||||
repo natif et credentials Apple/Google restent des secrets/variables `a_confirmer`,
|
||||
cohérents avec les champs `null · a_confirmer` du livrable mobile.
|
||||
|
||||
**Source de vérité versionnée.** Le workflow n'invoque que les profils EAS
|
||||
(`development`/`preview`/`production`) de `out/eas_build.json` — le code natif Expo
|
||||
vit dans un dépôt séparé (`/opt/oto/mobile/native/` · Expo 51 → cible 54 roadmap
|
||||
l.56), **cloné au run** depuis `vars.MOBILE_NATIVE_REPO`, jamais recopié ici.
|
||||
|
||||
**Gate-neutre.** Fichier de config CI (hors `out/` byte-gaté, hors `ci/*.sh`) →
|
||||
`./run_ci.sh` **30 PASS · 0 FAIL · 0 SKIP** inchangé. Le README Mobile documente le
|
||||
workflow (table périmètre + § CI/CD). **Zéro nouveau module · zéro gate ajouté (#5).**
|
||||
|
||||
---
|
||||
|
||||
## Addendum · Session `20260803_040643` · Sprint 5 Mobile — fix dérive doc-vs-CODE `cmd_build`
|
||||
|
||||
**Correctif de vérité-terrain (classe `docstring-vs-code-drift`, ungated).**
|
||||
`cmd_build` écrit **5** fichiers (`app_config`, `eas_build`, `role_navigation`,
|
||||
`store_listing`, `MANIFEST` — `app_config_gen.py:239-243`), mais deux surfaces de doc
|
||||
décrivaient encore **4**, le `store_listing.json` (ajouté après coup en session
|
||||
`030641`) y étant omis : docstring `app_config_gen.py:20` (« les **4** fichiers » vs
|
||||
énumération l.11-17 qui liste bien 5) + glob `README.md` § Utilisation
|
||||
(`store_listing` absent). Réalignés sur **5**.
|
||||
|
||||
**Consommateur régénéré (byte-repro · `artifact-reproducibility-gate`).** L'édition
|
||||
du README a fait dériver le byte-count DOC de
|
||||
[`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json)
|
||||
(README Mobile 4917 → 4931 octets) → régénéré ; verdict **PASS · 22/22 ≥ 95 (min
|
||||
100)** inchangé (1 insertion / 1 suppression). `./run_ci.sh` **30 PASS · 0 FAIL**.
|
||||
|
||||
## Addendum · Session `20260803_050654` · Sprint 1 DevOps — `ci/README.md` complété (2e workflow)
|
||||
|
||||
**Complétude doc CI/CD (classe `prose-facts-vs-numeric-drift`).** Depuis l'ajout de
|
||||
`.gitea/workflows/mobile-build.yml` (commit `af84b21`), le dépôt possède **deux**
|
||||
workflows Gitea Actions, mais le livrable CI/CD `ci/README.md` § 1 ne mentionnait que
|
||||
`ci.yml` (référence à sens unique : `mobile-build.yml` pointe vers `ci/README.md`, pas
|
||||
l'inverse). Nouvelle sous-section « `mobile-build.yml` (HORS gate de merge) » : vérifié
|
||||
par construction hors `gate.needs` (`check-ci-integrity` ne verrouille que `ci.yml`),
|
||||
même runner, **SKIP-safe** sans `EAS_TOKEN`. Prose éditoriale sans chiffre data-derived
|
||||
→ **gate-neutre**. Commit `1af5d01`.
|
||||
|
||||
## Addendum · Session `20260803_053701` · Sprint 8 — fix dérive doc-vs-réalité `regression_run.json`
|
||||
|
||||
**Correctif prose-fact ungated (jumelle du fix root-README).** Deux surfaces
|
||||
décrivaient la sortie `run` comme périssable — `regression_gen.py` docstring l.26
|
||||
(« Sortie NON déterministe → non commitée ») + `qa/regression/README.md` § hand-off
|
||||
l.76 (« mode `run` … non déterministe · non gated ») — **fausses** : le générateur
|
||||
contredisait son propre gate dédié `ci/check_regression.sh` (step `check-regression`
|
||||
de `ci.yml`). En réalité `regression_run.json` **EST** commité, déterministe et
|
||||
byte-gaté à chaque push/PR. Docstring + README réalignés sur la réalité avec citation
|
||||
du gate. Consommateur [`quality_report.json`](../qa/audit_4big/out/quality_report.json)
|
||||
régénéré (README qa-regression 4236 → 4396 o) · verdict **PASS · 22/22** inchangé.
|
||||
Commits `89eb8e6` (fix) + `3d8ccb0` (log).
|
||||
|
||||
## Addendum · Session `20260803_060704` · Sprint 3 — fix citation roadmap fantôme (module bancable)
|
||||
|
||||
**Citation mal-attribuée (classe `prose-facts-vs-numeric-drift`).** `bancable_gen.py:5`
|
||||
+ `faisabilite/bancable/README.md:5` attribuaient au **fichier roadmap** un libellé
|
||||
verbatim (« `40_llm_outputs/` + rapports bancables FR/EN/ES ») **absent** de
|
||||
`ROADMAP_8_WEEKS_OR_LESS.md` (grep = 0 occurrence). Le §Sprint 3 réel dit
|
||||
« **Faisabilité Auto** · génération 4 volets <1h » ; le libellé cité venait en fait des
|
||||
`daily_reports` (planification S3 du 2026-07-30), pas de la roadmap. Docstring + README
|
||||
réalignés : §Sprint 3 cité fidèlement, volet financier bancable ré-attribué à sa vraie
|
||||
source (planif S3 · Portail Bancables 4Big). Consommateur
|
||||
[`quality_report.json`](../qa/audit_4big/out/quality_report.json) régénéré (bancable
|
||||
README 6103 → 6439 o) · verdict **PASS · 22/22** inchangé. Commit `88d9955`.
|
||||
|
||||
## Addendum · Session `20260803_070711` · Sprint 8 QA — 3 surfaces jumelles résiduelles `regression_run.json`
|
||||
|
||||
**Balayage complet du même mensonge (classe `prose-facts-vs-numeric-drift` /
|
||||
`docstring-vs-code`).** Le fix `89eb8e6` (session `053701`) avait réaligné le docstring
|
||||
principal + le README, mais la même contre-vérité (« sortie `run` non déterministe →
|
||||
non commitée ») subsistait dans **3 surfaces manquées** par le premier passage :
|
||||
`reglib/builder.py:10-13` (docstring module — citait le `.gitignore` comme autorité
|
||||
alors qu'il affirme l'**inverse**), `reglib/builder.py:71` (docstring `run_matrix`) et
|
||||
`regression_gen.py:82-85` (champ `hors_perimetre_worker` qui **coule dans**
|
||||
`MANIFEST.json` commité + gaté, contredisant le docstring corrigé 12 lignes plus haut).
|
||||
Ground-truth croisé sur **4 sources commitées** : `git ls-files` · `.gitignore` ·
|
||||
`ci/check_regression.sh` (exige la byte-identité d'un run frais) · docstring
|
||||
`regression_gen.py:26` déjà réaligné. `MANIFEST.json` régénéré (build) ; `plan` +
|
||||
`regression_run.json` **inchangés** · aucun consommateur aval · **gate-neutre**.
|
||||
`run_ci.sh` → **30 PASS · 0 FAIL**. Commit `dbde8fa`. Leçon consignée : un fix jumeau
|
||||
doit balayer **toutes** les surfaces en un seul passage (mémoire
|
||||
`prose-facts-vs-numeric-drift`).
|
||||
|
||||
## État launch-readiness · fin de journée (inchangé · 100 % sourcé)
|
||||
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **22/22 modules à 100/100** (seuil 95) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **22 suites · 569 ran · 569 passés · 0/0/0** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| CI/CD Mobile (P1) | workflow `mobile-build.yml` · 3 jobs · SKIP-safe sans `EAS_TOKEN` | vert | [`.gitea/workflows/mobile-build.yml`](../../.gitea/workflows/mobile-build.yml) |
|
||||
| Miroir local | `run_ci.sh` → **30 PASS · 0 FAIL · 0 SKIP** | vert | [`run_ci.sh`](../../run_ci.sh) |
|
||||
|
||||
**Hors périmètre (inchangé · #8).** Exécution serveur `eas build`/`eas submit` (attend
|
||||
`EAS_TOKEN` Michel + `MOBILE_NATIVE_REPO`), déploiement production VPS → agents
|
||||
Mobile/DevOps.
|
||||
|
||||
## Addendum · Session `20260803_100714` · Sprint 8 QA — réconciliation compte modules 22→24 (drift `check-regression` + `check-readme-claims`)
|
||||
|
||||
**Drift trouvé à l'ouverture.** `./run_ci.sh` = **30 PASS · 2 FAIL** (`check-regression`,
|
||||
`check-readme-claims`). Deux modules ajoutés par les sessions auto-exec précédentes —
|
||||
`crm/financement_bancaire` (directive Financement Bancaire) et `pie/manifest` (Annexe 12
|
||||
V10.1 · PIE) — avaient été committés **sans régénérer les artefacts/prose aval**. La
|
||||
matrice de régression `run` était restée à **23 suites / 604 tests** (plan déjà à 24 →
|
||||
divergence plan↔run) et toute la prose data-derived affichait encore **23/23**.
|
||||
|
||||
**Correctif (tout re-dérivé d'artefacts commités).** `regression_gen.py build+run` →
|
||||
`regression_run.json` re-généré (byte-gaté). `audit_4big_gen.py build` re-généré
|
||||
**après** l'édition des README (l'audit score le contenu doc des modules). Réalignement
|
||||
prose : README racine (24/24 · 24 suites), `qa/audit_4big/README.md` (24/24), 3 fiches
|
||||
`03_agents/{qa,erpnext_backend,devops}/AGENT.md`, table run-book `devops/deploy_runbook/README.md`
|
||||
(phases 4 + 6 : ajout `crm/financement_bancaire` et `pie/manifest`).
|
||||
|
||||
**Signal Michel — IP backup.** `check-readme-claims` flaggait aussi
|
||||
`DIRECTIVE_WORKFLOW_FAISABILITE_V10…md:104` citant `153.75.232.237` (« VPS backup »),
|
||||
≠ IP canonique `153.75.250.214` de CLAUDE.md §VPS. Doc **root-owned non éditable** par le
|
||||
worker. Gate étendu **étroitement** (précédent `guard-tracked-files-exclusion`) : une IPv4
|
||||
explicitement étiquetée `backup <ip>` = serveur de secours légitime, tolérée ; toute IP
|
||||
primaire/non-étiquetée migrée reste bloquée (mutation-testée). **Action demandée à Michel :
|
||||
porter l'IP du serveur backup dans `CLAUDE.md §VPS`** pour rétablir la couverture SSOT.
|
||||
|
||||
## État launch-readiness · addendum `100714` (100 % sourcé)
|
||||
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **24/24 modules à 100/100** (seuil 95 · #5) | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **24 suites · 624 ran · 624 passés · 0/0/0** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Miroir local | `run_ci.sh` → **32 PASS · 0 FAIL · 0 SKIP** | vert | [`run_ci.sh`](../../run_ci.sh) |
|
||||
|
||||
## Addendum · Session `20260803_160723` · Sprint 8 QA — triple-sonde drift réel (0 dérive, 0 édition)
|
||||
|
||||
**Session de vérification pure** sur **3** classes de dérive **réelle** (celles à l'origine
|
||||
de vrais fix passés), toutes confirmées closes après la dernière évolution de code :
|
||||
1. **docstring/README-vs-CODE** sur les 22 générateurs (claims Entrée/Sortie/Fallback vs
|
||||
`_write_json`/`_ARTIFACTS` réels) → **0 sortie fantôme, 0 sortie non documentée**.
|
||||
2. **Bijection module↔artefacts** (classe du drift 22→24 de `100714`) → constatée
|
||||
**déjà auto-gatée** par le bloc `coverage` de `quality_report.json`
|
||||
(`ci_modules_count=registry_modules_count=24` · `missing_*=[]` · `not_in_gate=[]`) —
|
||||
filet anti-récidive **aucun gate à ajouter (#5)**.
|
||||
3. **Count-drift présent-tense UNGATED** post-réconciliation 22→24 / 569→624 → 3 résidus,
|
||||
tous classés **KEEP** (compte de tests local `bancable` 22/22 = `Ran 22 tests`·OK ;
|
||||
`ci/README.md:197` pédagogique-hypothétique ; `ci/README.md:729` historique-repro).
|
||||
|
||||
**Aucun fichier de production/doc modifié.** `./run_ci.sh` → **32 PASS · 0 FAIL · 0 SKIP**
|
||||
inchangé. Launch-readiness **inchangé et intégralement sourcé** : 24/24 qualité · 624/624
|
||||
régression · 15/15 recette · 32 gate.needs verts. Hors périmètre (inchangé · #8) :
|
||||
déploiement production VPS (`eas build/submit`, bench migrate, nginx/systemd, voix Amélie).
|
||||
|
||||
## Addendum · Session `20260803_173726` · Audit directive-vs-implémentation (V10) — SIGNAL Michel gate condition #4
|
||||
|
||||
**Session de vérification pure** (0 édition prod). 3e couple `directive-vs-implementation` :
|
||||
`DIRECTIVE_WORKFLOW_FAISABILITE_V10` (annexe méta-constitution EAOF). V10 est un snapshot de
|
||||
formalisation (Bureau Virtuel 12 directions · Stage Gates G0-G8 · Annexes 1-11) **sans module
|
||||
in-repo** → rien à gater. Triangulation **6 banques** V10 ↔ `DIRECTIVE_FINANCEMENT` ↔
|
||||
`financement_spec.json` = **CLEAN** (ensemble identique, noms complets alignés).
|
||||
|
||||
**SIGNAL Michel — arbitrage produit sur la 4e condition du gate Financement Bancaire.**
|
||||
La directive `FINANCEMENT_BANCAIRE_COMPLET` contient **3 couches internes** dont la plus
|
||||
tardive **supersède** la 4e condition du gate :
|
||||
|
||||
| Couche directive | 4e condition | Source |
|
||||
|---|---|---|
|
||||
| Corps + AMENDEMENT | « conseiller WAG a validé » (`wag_validated_by NOT NULL`) | l. 115, 161 |
|
||||
| **PRÉCISION (la + tardive)** | « **N'EST PAS un humain WAG** » → OTO Auditeur Finances IA : `audit['decision']=='APPROVED' and audit['signature_valid']` | l. 206-300 |
|
||||
|
||||
Le module `crm/financement_bancaire` implémente **la couche supersédée** (`gate.py::_cond_validation_wag`
|
||||
= `wag_validated_by` ; message « Validation WAG manquante ») — verrouillée sur code + tests +
|
||||
README + artefacts byte-gatés (`gate_spec.json`/`gate_status_example.json`, clé `validation_wag`).
|
||||
**Zéro wiring auditeur** dans le module (la capability OTOIA `auditeur_finances` est un agent
|
||||
séparé, inexistant in-repo).
|
||||
|
||||
**Non édité, par design** (`directive-vs-implementation` : spec=autorité, directive=snapshot,
|
||||
SURFACE≠fix). Migrer la condition #4 casserait le byte-repro de 2 artefacts gatés, requiert
|
||||
l'agent `auditeur_finances` (VPS/OTOIA, Sprint 5-6), et est une **décision produit Michel**.
|
||||
|
||||
**Action demandée à Michel** : trancher la sémantique de la condition #4 du gate —
|
||||
(a) conserver le champ humain `wag_validated_by` (le module reste sur l'AMENDEMENT), ou
|
||||
(b) basculer la condition #4 sur la décision `APPROVED` signée de l'OTO Auditeur Finances IA
|
||||
(conforme à votre PRÉCISION), l'humain WAG devenant fallback « dernier recours » (OFAC/PEP/
|
||||
recours). Le module sera re-généré selon l'arbitrage. `run_ci.sh` : **32 PASS** inchangé.
|
||||
|
||||
## Addendum · Sessions `180728` → `200738` · Fermeture audit 8 directives Michel + couple PLANPOINT↔spec (2 éditions doc, 0 prod)
|
||||
|
||||
Rattrapage de ce canal stakeholder, qui s'était arrêté à `173726` (3 sessions de retard sur
|
||||
l'`activity_log`). Toutes vérifications pures ou éditions **doc** — **0 fichier de production
|
||||
modifié**, launch-readiness **inchangé** (voir bloc sourcé infra).
|
||||
|
||||
- **`180728` · 4e couple `directive-vs-implementation` (`DIRECTIVE_PIE` → `pie/manifest`)** —
|
||||
CLEAN. Byte-repro vert, 10 invariants verts, fidélité directive→spec totale. Attributions
|
||||
`module` vérifiées **sémantiquement** (au-delà de l'inv 6 dir-existence) : 3 exactes
|
||||
(site→publiciste · kit→bancable · app→mobile). **SIGNAL Michel** : le downstream `contrats`
|
||||
est marqué gated→`legal/confotur`, mais confotur produit le **DocType CONFOTUR (Ley 158-01)**,
|
||||
**pas** Promesa/Fideicomiso/HOA — **aucun module ne génère** ces trois → couverture
|
||||
sur-affirmée = **arbitrage périmètre produit**, pas une correction (spec = autorité, #5).
|
||||
|
||||
- **`190730` + `193734` · couple PLANPOINT↔spec + fermeture des 8 directives (2 éditions doc)** —
|
||||
(a) `specs/CHOISIR_MON_UNITE_SPEC.md` : ajout d'un encart *Arbitrage rouvert* surfaçant **2
|
||||
contradictions dures** avec la directive `PLANPOINT_STYLE` (snapshot Michel plus récent), sans
|
||||
flipper aucune décision (spec = autorité). (b) `README.md` : index rendu **symétrique**
|
||||
(lien directive→spec ajouté). Les 3 directives restantes (ARCHIVES/RENDUS/UNBLOCK) = snapshots
|
||||
runtime/VPS hors périmètre (#8), correctement bucket-2, marques RENDUS **7/7** conformes
|
||||
`pie_manifest.json`. **Audit des 8 directives Michel désormais complet.**
|
||||
|
||||
- **`200738` (session courante) · re-vérification indépendante du couple PLANPOINT↔spec** —
|
||||
les **2 contradictions** de l'encart sont **confirmées RÉELLES** contre la directive : Décision 4
|
||||
spec = **DocuSign** (SaaS externe, l. 58/89/112) ↔ directive §7 = **OTO Sign™** (natif) ;
|
||||
Décision 5 spec = **comparateur NON** (l. 59/90/119) ↔ directive §6 = **« max 3 unités »**
|
||||
(l. 48/86). Aucun faux-positif inséré → **édition `a9e9ee9` factuellement saine, 0 correction**.
|
||||
|
||||
**⚠️ Renforcement du SIGNAL à Michel (nouvel angle, non tranché).** L'arbitrage Décision 4
|
||||
n'est **pas neutre** vis-à-vis du mandat : `CLAUDE.md` #1 impose « ERPNext natif = priorité
|
||||
absolue **avant tout outil externe** ». **OTO Sign™** (directive, produit natif) est donc
|
||||
**plus aligné** sur le mandat que **DocuSign** (spec, SaaS externe). La directive étant aussi
|
||||
la plus **récente** (08-03 vs spec 07-28), les deux axes (récence + conformité #1) pointent
|
||||
vers OTO Sign™ — mais la spec reste l'autorité du module tant que Michel n'a pas tranché.
|
||||
Reste une **décision produit**, pas une édition (spec = autorité, #5).
|
||||
|
||||
**État launch-readiness · fin de journée (inchangé · 100 % sourcé — recompute indépendant)** :
|
||||
- Qualité 4Big : **24/24** modules (`quality_report.json.coverage` : `registry=ci=24` · `missing_*=[]`)
|
||||
- Régression : **624/624** tests · 24/24 suites (`regression_run.json.totals`)
|
||||
- Recette (acceptance) : **15/15** entrées (`acceptance_matrix.json.matrix`)
|
||||
- Gate CI local : **32 PASS · 0 FAIL · 0 SKIP** (`run_ci.sh`)
|
||||
|
||||
Hors périmètre (inchangé · #8) : déploiement VPS, builds/submit EAS, bench migrate, voix Amélie.
|
||||
@@ -0,0 +1,126 @@
|
||||
# Daily Report · 2026-08-04 · Claude Code DTP Worker
|
||||
|
||||
**Session** : `20260804_221055`
|
||||
**Phase roadmap** : Sprint 8 · *Launch + Buffer* — « Regression tests exhaustifs »
|
||||
(QA) + buffer corrections. Sprints 1→7 livrés côté repo, gatés et verts ; le reste
|
||||
(déploiement production réel, builds EAS, dépôts ONAPI) est **hors périmètre worker**
|
||||
(VPS `153.75.250.214` · `CLAUDE.md` #8).
|
||||
|
||||
## Tâche exécutée
|
||||
**Passe de vérification d'intégrité inter-artefacts — 5 surfaces (dont 3 neuves au
|
||||
compte courant 24 modules / 32 jobs) → CLEAN, 0 édition de production.**
|
||||
|
||||
Nouveau jour de fenêtre (le `2026-08-03` a été clos à l'addendum `180728→200738`).
|
||||
Les surfaces de dérive usuelles étant saturées (re-scan = redondant #5), cette
|
||||
session cible en priorité les surfaces qui **auraient pu dériver avec le passage
|
||||
récent 22→24 modules** (la seule dérive réelle des dernières ~48 h) + des surfaces
|
||||
de **correctness numérique** jamais explicitement caractérisées.
|
||||
|
||||
## Réalisé — 5 surfaces auditées, 0 dérive, 0 fichier de production modifié
|
||||
|
||||
1. **Docstring-vs-CODE / comptes de sortie** (~25 générateurs, agent Explore) :
|
||||
docstring ↔ README ↔ fichiers réellement écrits → **0 mismatch présent-tense**.
|
||||
2. **Correctness `commissions`** (neuf) : `taux_pct: null` par design (#6) →
|
||||
`commission=None` tant qu'un opérande manque → **aucune réconciliation matérialisée
|
||||
ne peut dériver**. Non-candidate caractérisée.
|
||||
3. **Réconciliation de compte au point 24** (neuf) : `quality_report` 24 · `coverage`
|
||||
`ci==registry==24` · `regression` 24 suites/624 · `acceptance` 15/15 · `gate.needs`
|
||||
32 → **tous concordants** ; la table 08-03 (`100714`) les porte déjà.
|
||||
4. **`ci_job` recette → `gate.needs`** au point 24/32 (neuf) : **36 refs · 0 orphelin**,
|
||||
les 2 nouveaux modules (financement/pie) présents, 8 jobs non-cités = contrôles
|
||||
d'infra CI (absence **par design**).
|
||||
5. **Couverture `ci/README.md`** (neuf) : §1 = table des 7 gates statiques par design ;
|
||||
suites de module **dérivées de `gate.needs` à l'exécution** + gatées par
|
||||
`check-ci-integrity` INV-A → **pas de gap**.
|
||||
|
||||
## État launch-readiness · 100 % sourcé (recompute `python3` indépendant · aucun chiffre saisi à la main)
|
||||
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **24/24 modules à 100/100** (seuil 95 · #5) · `coverage` ci==registry==24 | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **24 suites · 624 ran · 624 passés · 0/0/0** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` · **36 `ci_job` → 0 orphelin** | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Miroir local | `run_ci.sh` → **32 PASS · 0 FAIL · 0 SKIP** | vert | [`run_ci.sh`](../../run_ci.sh) |
|
||||
|
||||
## Non fait (hors périmètre worker · #8)
|
||||
Exécution serveur `eas build`/`eas submit` (attend `EAS_TOKEN` + `MOBILE_NATIVE_REPO`
|
||||
Michel), déploiement production VPS, dépôts ONAPI, création DocTypes ERPNext →
|
||||
agents Mobile/DevOps/ERPNext Backend. Aucune tâche fonctionnelle **in-repo** ne reste :
|
||||
la roadmap est intégralement livrée+gatée, et les seuls livrables-fichiers versionnés
|
||||
in-scope (`mobile-build.yml`) sont déjà faits.
|
||||
|
||||
## Prochaine tâche suggérée
|
||||
Poursuivre la veille d'intégrité (surfaces neuves uniquement · #5) et maintenir la
|
||||
currency des deux canaux de log. Dès que Michel fournit `EAS_TOKEN`/`MOBILE_NATIVE_REPO`,
|
||||
le workflow `mobile-build.yml` s'active (jobs actuellement SKIP-safe).
|
||||
|
||||
---
|
||||
|
||||
## Addendum · Session 224101 · Sonde de dérive non-gatée (4 dimensions neuves) → CLEAN
|
||||
|
||||
Seconde session du 08-04. Sonde ciblée sur **4 dimensions de dérive non couvertes par
|
||||
la CI et jamais caractérisées** (orthogonales aux classes déjà saturées) :
|
||||
|
||||
| Dimension sondée | Résultat | Évidence |
|
||||
|---|---|---|
|
||||
| Cross-refs entre fiches AGENT.md | **CLEAN** · 0 nom pendant | tous les agents cités existent sous `03_agents/` |
|
||||
| Dates/session-ids (READMEs, specs) | **CLEAN** · 0 date future | specs citent 07-28 / 08-03 (passé) |
|
||||
| Contacts d'escalade (tél + email) | **CLEAN** · faux-positif écarté | tél `+18296296385` ×15 identiques · email `mroy@worldactivitiesrd.com` ×1 (roadmap L91, à sa place) |
|
||||
| Specs ↔ répertoires deliverables | **CLEAN** · 0 orphelin | chaque module spec a son dossier `05_deliverables_mvp/*` |
|
||||
|
||||
**0 édition de production** (la seule anomalie remontée par l'agent — email en occurrence
|
||||
unique — était un **faux-positif** : occurrence unique ≠ contradiction ; anti-invention #6).
|
||||
`./run_ci.sh` inchangé : **32 PASS · 0 FAIL · 0 SKIP**. Note d'intégrité ajoutée pour
|
||||
pré-empter le re-flag du contact d'escalade (prose non gatée).
|
||||
|
||||
---
|
||||
|
||||
## Addendum · Session 231101 · Attestation indépendante des 4 chiffres launch-readiness → CLEAN
|
||||
|
||||
Troisième session du 08-04. Plutôt que re-sonder des surfaces de dérive (saturées · #5),
|
||||
cette passe **atteste** que les 4 chiffres publiés au stakeholder plus haut ne sont pas
|
||||
un simple écho de la table mais **rechargent à l'identique depuis les artefacts commités**
|
||||
via un recompute `python3` indépendant (aucune valeur saisie à la main · #6) :
|
||||
|
||||
| Dimension | Recompute indépendant (ce jour) | = Table publiée ? |
|
||||
|---|---|---|
|
||||
| Qualité 4Big | `quality_report.json` : 24 modules · **tous == 100** · `coverage.ok=true` · `ci==registry==24` · 0 manquant | ✅ identique |
|
||||
| Régression (run) | `regression_run.json.totals` : **24 suites · 624 ran · 624 passed · 0/0/0** · verdict `PASS` | ✅ identique |
|
||||
| Recette roadmap | `acceptance_matrix.json` : **15/15 lignes `status=in_repo`** · verdict `true` · **`ci_job` ×36** (niché dans `matrix[].modules[]`, pas au niveau ligne) → concorde avec « 36 refs » | ✅ identique |
|
||||
| Miroir local | `./run_ci.sh` → **32 PASS · 0 FAIL · 0 SKIP** | ✅ identique |
|
||||
|
||||
**Confirmé au passage** que l'artefact d'EXÉCUTION `regression_run.json` (exclu de
|
||||
`check_artifacts` car non issu de `build`) est bien protégé par son gate dédié
|
||||
`ci/check_regression.sh` (régénère `run` frais + exige l'identité byte-for-byte) — donc
|
||||
les totaux 624 ne peuvent pas se périmer en silence. **0 édition de production · 0 dérive.**
|
||||
|
||||
---
|
||||
|
||||
## Addendum · Session 234104 · Durcissement du gating d'activation `mobile-build.yml` → 1 édition CI in-scope
|
||||
|
||||
Quatrième session du 08-04. Après 3 passes read-only CLEAN, changement de registre :
|
||||
**correctness d'un artefact CI in-scope à impact futur réel** plutôt qu'un 4e audit
|
||||
redondant. Cible = le workflow `mobile-build.yml` que ce rapport désigne comme prochaine
|
||||
tâche (s'active dès `EAS_TOKEN` fourni). Un agent Explore a confirmé **0 bug de logique**
|
||||
côté générateurs Python ; le défaut réel était dans le YAML CI.
|
||||
|
||||
**Défaut latent corrigé.** Les jobs `build-ios`/`build-android` gataient sur
|
||||
`if: secrets.EAS_TOKEN != '' …` **au niveau job**, où le `secrets` context n'est **pas**
|
||||
garanti (table d'availability GitHub : `jobs.<id>.if` = github/needs/vars/inputs/matrix
|
||||
seulement). Risque : le jour où Michel fournit un `EAS_TOKEN` valide, les builds
|
||||
resteraient **SKIP en silence**. Corrigé via le pattern canonique **preflight-output**
|
||||
(le secret est lu dans un step, exposé en `needs.preflight.outputs.has_token/has_repo`,
|
||||
gate consommé en `if:` — portable GitHub ET Gitea/act).
|
||||
|
||||
| Contrôle | Résultat |
|
||||
|---|---|
|
||||
| YAML `mobile-build.yml` | **valide** (`yaml.safe_load`) |
|
||||
| `secrets.` restant dans un `if:` de job | **0** ✓ |
|
||||
| `preflight.outputs` + `needs: preflight` (ios/android) | présents ✓ |
|
||||
| Contrat observable (preflight vert · builds SKIP sans token) | **inchangé** |
|
||||
| Gate de merge touché ? | **non** (workflow hors `gate.needs`) · `run_ci.sh` **32 PASS** |
|
||||
|
||||
**Impact stakeholder.** Quand Michel ajoutera le secret `EAS_TOKEN` (expo.dev · ~2 min)
|
||||
+ la variable `MOBILE_NATIVE_REPO`, les builds EAS se déclencheront désormais de façon
|
||||
**déterministe** — plus de dépendance à un comportement de contexte ambigu selon le moteur
|
||||
Actions. **0 chiffre inventé · 0 credential fabriqué.**
|
||||
@@ -0,0 +1,297 @@
|
||||
# Daily Report · 2026-08-05 · Claude Code DTP Worker
|
||||
|
||||
**Session** : `20260805_004112`
|
||||
**Phase roadmap** : Sprint 8 · *Launch + Buffer* — « Monitoring / regression
|
||||
exhaustifs » (DevOps/QA) + buffer. Sprints 1→7 livrés côté repo, gatés et verts ;
|
||||
le reste (déploiement production réel, builds EAS, dépôts ONAPI) est **hors périmètre
|
||||
worker** (VPS `153.75.250.214` · `CLAUDE.md` #8).
|
||||
|
||||
## Contexte — jalon CI franchi la veille au soir (à consigner côté stakeholder)
|
||||
|
||||
La session `20260805_001109` a livré le **8ᵉ gate statique** `ci/check_mobile_workflow.sh`
|
||||
et l'a câblé dans `gate.needs` : la **structure** du second workflow `mobile-build.yml`
|
||||
(hors gate de merge, mais dont un défaut passait VERT en silence) est désormais **opposable
|
||||
au merge**. Le décompte du gate de merge est donc passé de **32 → 33 jobs**. Le canal
|
||||
stakeholder s'arrêtait au `2026-08-04` (« 32 PASS ») : ce rapport le porte au point courant.
|
||||
|
||||
## Tâche exécutée — currency du canal + attestation indépendante du point courant · 0 édition de production
|
||||
|
||||
Aucune tâche fonctionnelle **in-repo** ne reste (roadmap intégralement livrée+gatée ;
|
||||
seul livrable-fichier in-scope restant, `mobile-build.yml`, déjà fait ET désormais gaté).
|
||||
La valeur honnête du jour : **rendre le canal roadmap fidèle au jalon 33 jobs** et
|
||||
**réattester** que les chiffres publiés rechargent à l'identique depuis les artefacts
|
||||
commités (recompute `python3` indépendant · aucune valeur saisie à la main · #6).
|
||||
|
||||
## État launch-readiness · 100 % sourcé (recompute indépendant ce jour · aucun chiffre saisi à la main)
|
||||
|
||||
| Dimension | Valeur | Verdict | Source commitée |
|
||||
|---|---|---|---|
|
||||
| Qualité 4Big | **24/24 modules à 100/100** (seuil 95 · #5) · `coverage.ok=true` · ci==registry==24 · 0 manquant | `PASS` | [`qa/audit_4big/out/quality_report.json`](../qa/audit_4big/out/quality_report.json) |
|
||||
| Régression (run) | **24 suites · 624 ran · 624 passés · 0 fail / 0 err / 0 skip** | `PASS` | [`qa/regression/out/regression_run.json`](../qa/regression/out/regression_run.json) |
|
||||
| Recette roadmap | **15/15 promesses** `status=in_repo` · **36 `ci_job` → 0 orphelin** | `true` | [`qa/acceptance/out/acceptance_matrix.json`](../qa/acceptance/out/acceptance_matrix.json) |
|
||||
| Miroir local | `run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** | vert | [`run_ci.sh`](../../run_ci.sh) |
|
||||
| Gate de merge | **33 jobs** dérivés de `gate.needs` = **8 gates statiques `ci/*.sh` + 25 suites de module** | vert | [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) · [`ci/check_mobile_workflow.sh`](../../ci/check_mobile_workflow.sh) |
|
||||
|
||||
> Le décompte 33 est **dérivé à l'exécution** (`run_ci.sh --list`), jamais figé dans une
|
||||
> prose : ajouter/retirer un gate le met à jour sans édition (anti-invention #6). Les
|
||||
> « 32 » des logs antérieurs sont des **snapshots datés** (état vrai à l'époque), pas des
|
||||
> prétentions présent-tense — ils restent tels quels (classe *historical-repro = KEEP*).
|
||||
|
||||
## Attestation (recompute `python3` vs artefacts commités — pas un écho de la table)
|
||||
|
||||
| Dimension | Recompute indépendant ce jour | = Table publiée ? |
|
||||
|---|---|---|
|
||||
| Qualité 4Big | `quality_report.json` : 24 modules · **tous == 100** · `coverage.ok=true` · ci==registry==24 · verdict `PASS` | ✅ identique |
|
||||
| Régression (run) | `regression_run.json.totals` : **suites 24 · ran 624 · passed 624 · 0/0/0** · verdict `PASS` | ✅ identique |
|
||||
| Recette roadmap | `acceptance_matrix.json` : **15/15 `status=in_repo`** · verdict `True` · **`ci_job` ×36** (niché dans `matrix[].modules[]`) | ✅ identique |
|
||||
| Structure CI | `run_ci.sh --list` : **33 jobs = 8 statiques + 25 suites** · `ls ci/*.sh` (hors `lib.sh`) = **8** | ✅ concordant |
|
||||
|
||||
## Non fait (hors périmètre worker · #8)
|
||||
|
||||
Exécution serveur `eas build`/`eas submit` (attend `EAS_TOKEN` + `MOBILE_NATIVE_REPO`
|
||||
fournis par Michel), déploiement production VPS, dépôts ONAPI, création DocTypes ERPNext
|
||||
→ agents Mobile/DevOps/ERPNext Backend. Le workflow `mobile-build.yml` est SKIP-safe
|
||||
tant que le token est absent (jobs de build gatés sur `preflight.outputs.has_token`) —
|
||||
et sa structure est maintenant verrouillée par `check-mobile-workflow` (MOB-1..4), donc
|
||||
la garantie « pas de SKIP silencieux le jour de l'activation » ne peut plus régresser.
|
||||
|
||||
## Prochaine tâche suggérée
|
||||
|
||||
Maintenir la currency des deux canaux de log et poursuivre la veille d'intégrité sur
|
||||
surfaces **neuves uniquement** (#5 — les surfaces de dérive usuelles sont saturées). Dès
|
||||
que Michel fournit `EAS_TOKEN`/`MOBILE_NATIVE_REPO`, `mobile-build.yml` s'active de façon
|
||||
déterministe (gating portable preflight-output, désormais gaté au merge).
|
||||
|
||||
---
|
||||
|
||||
## Mise à jour fin de journée · session `20260805_054200` — les DEUX axes de « dents » clos
|
||||
|
||||
Ce rapport matinal (session `004112`) s'arrêtait au jalon **33 jobs**. Plus tard le même
|
||||
jour, la série d'attestation *teeth* (mutation-testing) a été **close sur ses deux axes**.
|
||||
Jalon launch-readiness distinct des tables ci-dessus : celles-ci prouvent que la CI est
|
||||
**verte** (les gates PASSent, les artefacts rechargent) ; les attestations *teeth* prouvent
|
||||
que ces gates et suites **échouent bien quand ils le doivent** — un gate ou une suite
|
||||
toujours-vert serait un **faux vert** invisible aux tables de currency.
|
||||
|
||||
| Axe *teeth* | Portée | Résultat | Preuve commitée |
|
||||
|---|---|---|---|
|
||||
| **Gates statiques** (mutation de suite complète) | **8/8** (`ls ci/*.sh` hors `lib.sh` = 8) | **8 BITES · 0 TOOTHLESS** | commit `9fe8bf1` (`20260805_031134`) |
|
||||
| **Suites de module** (dérive de LOGIQUE générateur, jamais écho d'artefact) | **25/25** (= les 25 suites de `gate.needs`) via 4 batches **6+6+6+7** | **25 BITES · 0 TOOTHLESS** | commits `98f3f56` · `d25da28` · `e38dd93` · `986d293` (`034144`→`051154`) |
|
||||
|
||||
**Ce que « BITE » garantit au stakeholder.** Chaque suite de module a été mutée sur la
|
||||
LOGIQUE de son générateur (ex. `commission = base × taux`, e-NCF `zfill(10)`, dédup SEO,
|
||||
gating `_statut`, séparateur d'options CONFOTUR) et **doit** sortir NONZERO — prouvant que
|
||||
la suite contraint le comportement, et ne recharge pas `out/*.json` contre lui-même. Une
|
||||
suite édentée passerait VERT sur une régression réelle = **faux vert Sprint 8**. Les 25
|
||||
sont désormais **prouvées mordantes**.
|
||||
|
||||
**Hygiène du harnais (inchangée).** Mutations jouées dans `/tmp` en lecture seule sur copie,
|
||||
restauration byte-exacte par `cp`+`cmp` (jamais `git clean` · #1), arbre de travail
|
||||
**pristine** après coup. **0 gate ajouté** (#5 — attester ≠ multiplier les gates), **0
|
||||
chiffre saisi à la main** (#6 — comptes dérivés de `ls`/`gate.needs`), **0 édition de
|
||||
production**, **aucune commande VPS** (#8).
|
||||
|
||||
**Currency structurelle inchangée** : `run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (aucun
|
||||
fichier gaté touché aujourd'hui ; ce rapport est le seul livrable de la session `054200`).
|
||||
|
||||
---
|
||||
|
||||
## Mise à jour fin de journée · session `20260805_144304` — currency du canal + **incident d'intégrité corrigé le jour même**
|
||||
|
||||
Ce rapport s'arrêtait à la session `054200`. **Sept commits** ont suivi dans la journée ;
|
||||
un seul est **matériel pour le stakeholder** — un tree commité momentanément **RED**, capté
|
||||
et corrigé le jour même. Le reste est de la veille d'intégrité en lecture seule. Toutes les
|
||||
tables ci-dessus **recomputent à l'identique ce jour** (recompute `python3` indépendant · #6).
|
||||
|
||||
### Incident d'intégrité (capté + corrigé le jour même)
|
||||
|
||||
| Événement | Commit | État du tree commité | Résolution |
|
||||
|---|---|---|---|
|
||||
| `test_audit_4big.py` figeait `totals["pass"] = 18 - fail` (périmètre réel = 24 modules) → correction annoncée « 33 PASS » **mais tree commité RED 32 PASS·1 FAIL** (le gate `constraints-guard` mordait sur le tree lui-même) | `127ef6f` (`124241`) | **RED** (32 PASS · 1 FAIL) | — |
|
||||
| Régression captée et corrigée : constante de fixture dérivée du vrai `bad["modules"]` (`pass=len-fail`, min/max=min/max scores) → rapport réellement cohérent en interne, **seul INV7 mord** ; assertion resserrée `any("< seuil")` → `all("INV7") + any("< seuil")` (isolation prouvée) | `1321983` (`131244`) | **VERT** (33 PASS · 0 FAIL) | ✅ tree revenu VERT |
|
||||
|
||||
> **Ce que l'incident démontre au stakeholder** : le gate `guard_constraints` **mord sur
|
||||
> le tree commité lui-même** (pas seulement en pré-commit local) — un faux « 33 PASS »
|
||||
> annoncé sans le tree correspondant a été **opposable et rattrapé sous 50 min**. La
|
||||
> discipline de gate a fonctionné ; le canal l'enregistre plutôt que de laisser croire à
|
||||
> une journée VERTE ininterrompue. HEAD courant (`f311a1b`) : `run_ci.sh` → **33 PASS**.
|
||||
|
||||
### Veille d'intégrité en lecture seule (0 édition de production · #5)
|
||||
|
||||
| Sweep | Commit | Portée | Résultat |
|
||||
|---|---|---|---|
|
||||
| Docstring-vs-code | `48b19b9` (`134254`) | 26 modules (docstrings ↔ comportement réel du code) | **CLEAN** |
|
||||
| Forward-compat warnings | `f311a1b` (`141301`) | 25 suites de `gate.needs` rejouées sous `-W error` (Deprecation/Syntax/Pending/Resource) sous Python 3.10.12 | **CLEAN 25/25 · 624 tests · 0 warning latent** |
|
||||
| Déterminisme hash-seed | `151312` | Tous les générateurs `build` (57 artefacts) rejoués sous 5 `PYTHONHASHSEED` (`1/7/42/1000/random`) + `regression_run.json` sous 6 rejeux → `diff` vs commité | **CLEAN · 57 byte-identiques · 0 dépendance à l'ordre de hash** → gates `check_artifacts`/`check_regression` robustes au seed randomisé du runner |
|
||||
| Indépendance locale / timezone | `154314` | 21 générateurs `build` × 5 combos `LANG`/`LC_ALL`/`TZ` (`C·UTC` → `de_DE·Kolkata` → `POSIX·Kiritimati`) = **105 rejeux** `diff` vs commité ; TZ empiriquement exercé (5 fuseaux) + audit code (0 `locale.setlocale` → `json` reste `'.'` ; 2 seuls lecteurs d'horloge sans `out/` commité et en `timezone.utc`) | **CLEAN · 0 dérive/105** → runner robuste sous toute locale/fuseau |
|
||||
|
||||
### Ré-attestation du point courant (recompute indépendant ce jour, HEAD `f311a1b`)
|
||||
|
||||
| Dimension | Recompute `python3` | = Table publiée ? |
|
||||
|---|---|---|
|
||||
| Qualité 4Big | `quality_report.json` : **24 modules · tous == 100** · `coverage.ok=true` · verdict `PASS` | ✅ identique |
|
||||
| Régression (run) | `regression_run.json.totals` : **suites 24 · ran 624 · passed 624 · 0/0/0** | ✅ identique |
|
||||
| Structure CI | `run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** = 8 statiques + 25 suites | ✅ concordant |
|
||||
|
||||
**Portée / anti-churn.** **0 fichier de production édité**, **0 artefact reconstruit**,
|
||||
**0 gate ajouté** (#5), **0 chiffre saisi à la main** (#6 — 24/624/33 dérivés des artefacts
|
||||
et de `run_ci.sh`), **aucune commande VPS** (#8). Seule édition : ce rapport + le journal.
|
||||
|
||||
---
|
||||
|
||||
## Mise à jour fin de journée · session `20260805_184329` — **baseline régression corrigée (faux-vert neutralisé)** + 2ᵉ incident d'intégrité consigné
|
||||
|
||||
Ce rapport s'arrêtait à la session `144304` (HEAD `f311a1b`). **Cinq commits** ont suivi.
|
||||
**Deux sont matériels pour le stakeholder** : (1) la **baseline régression a changé de forme**
|
||||
— un faux-vert latent a été neutralisé, la matrice publiée passe de « 0 skip » à
|
||||
« **17 oracles optionnels ignorés** » (verdict inchangé `PASS`) ; (2) un **2ᵉ tree commité
|
||||
momentanément RED**, capté et corrigé le jour même (même classe que l'incident du matin).
|
||||
Le reste est veille d'intégrité en lecture seule. HEAD courant : `1a44dfe`.
|
||||
|
||||
### (1) Baseline régression durcie — faux-vert `jsonschema` neutralisé (matériel)
|
||||
|
||||
Le refactor `9d30939` lance désormais **chaque suite sous `python -S`** (interpréteur sans
|
||||
`site` → aucun `site-packages` sur `sys.path`), reproduisant fidèlement le runner Gitea
|
||||
pip-less. L'**unique paquet tiers** du corpus — l'oracle **optionnel** `jsonschema`, toujours
|
||||
gardé par `skipUnless`/`try…except ImportError` — est ainsi **neutralisé partout**.
|
||||
|
||||
| Avant `-S` | Après `-S` (baseline courante) |
|
||||
|---|---|
|
||||
| `jsonschema` **présent** en local → `skipped=0` · `passed=624` | `-S` → oracle **absent partout** → `skipped=17` · `passed=607` |
|
||||
| `jsonschema` **absent** sur le runner Gitea → `skipped=17` | matrice **byte-identique** quel que soit ce qui est `pip install`é |
|
||||
| ⇒ `regression_run.json` commité = **FAUX-VERT** (vert en local, **ROUGE** sur le runner) | ⇒ `check_regression` **byte-déterministe** cross-environnement |
|
||||
|
||||
> **Ce que ça garantit au stakeholder.** La baseline publiée était vraie sur un poste où
|
||||
> `jsonschema` était installé, mais **aurait fait rougir la CI** le jour où elle tournerait
|
||||
> sur le runner pip-less réel. Le refactor ancre la baseline sur la **couverture garantie**
|
||||
> (stdlib + validateurs maison) ; les 17 tests neutralisés ne sont que des **doublures de
|
||||
> validation** de schéma, leurs contrats restent couverts par les validateurs maison. Le
|
||||
> verdict reste `PASS` — **aucune** suite rouge, **0 fail / 0 err**. La ligne « Régression »
|
||||
> des tables ci-dessus se lit désormais **624 ran · 607 passés · 17 skip (oracles optionnels) · 0/0 · `PASS`**
|
||||
> (les « 624 passés · 0 skip » des sections antérieures sont des **snapshots datés** vrais à
|
||||
> l'époque, classe *historical-repro = KEEP*).
|
||||
|
||||
### (2) 2ᵉ incident d'intégrité (capté + corrigé le jour même)
|
||||
|
||||
| Événement | Commit | État du tree commité | Résolution |
|
||||
|---|---|---|---|
|
||||
| Le refactor `-S` (ci-dessus) éditait `regression/README.md` (+11 l) **sans rebuild** de `quality_report.json` → l'audit 4Big score le contenu des docs (champ `evidence` byte-count) → `check-artifacts` **ROUGE** | `9d30939` (`161319`) | **RED** (32 PASS · 1 FAIL) | — |
|
||||
| Rebuild `audit_4big` → **1 ligne** modifiée (byte-count), verdict **inchangé** `PASS` 24/24 · `coverage.ok` → tree revenu VERT | `9fa464d` (`164319`) | **VERT** (33 PASS · 0 FAIL) | ✅ rattrapé sous ~17 min |
|
||||
|
||||
> Même classe que l'incident du matin (`rebuild-after-doc-edits`) : éditer une doc **scorée
|
||||
> par audit_4big** sans régénérer `quality_report.json` en dernier laisse `check-artifacts`
|
||||
> dériver. La discipline de gate a **remordu sur le tree commité lui-même** ; le canal
|
||||
> l'enregistre plutôt que de laisser croire à une journée VERTE ininterrompue.
|
||||
|
||||
### Veille d'intégrité en lecture seule / hors merge-gate (0 édition de production)
|
||||
|
||||
| Travail | Commit | Portée | Résultat |
|
||||
|---|---|---|---|
|
||||
| Sweep docstring-vs-code **étendu aux helpers `*lib/`** (surface qu'un scope `*_gen.py` rate) | `db91a6b` (`171321`) | 9 helpers `seolib/…/genlib` (directions A+B) | **CLEAN** · 1 faux-positif réfuté (`finlib/gate.py` illustre les valeurs spec, pas un hardcode) |
|
||||
| **Gate neuf** classe `claude-md-constant-anchor` : prose #9 (3 %/8.5 %/52 %) des modules `bancable`+`commissions` **ancrée sur `CLAUDE.md`** | `1441693` (`174324`) | 4 copies périmées **en silence** dans 2 modules distincts, chacune se réclamant « (#9) » | **teeth-prouvé** (8.5 %→9 % mord les 4 · 52 %→55 % mord 2 · Cardnet→Azul mord l'énum) |
|
||||
| **Invariant neuf** INV-C dans le gate `check_ci_integrity` **existant** : le workflow doit forcer `env: PYTHONUTF8: "1"` | `1a44dfe` (`181325`) | ancre l'I/O UTF-8 top-level → pas de `UnicodeEncodeError` silencieux sur runner locale-ASCII (`print ✅` en fin de build) | ancré ; **pas un nouveau job** (invariant interne · 33 jobs inchangé) |
|
||||
|
||||
### Ré-attestation du point courant (recompute `python3` indépendant ce jour, HEAD `1a44dfe`)
|
||||
|
||||
| Dimension | Recompute `python3` vs artefact commité | = attendu ? |
|
||||
|---|---|---|
|
||||
| Qualité 4Big | `quality_report.json` : **24 modules · tous == 100** · `coverage.ok=true` · verdict `PASS` | ✅ |
|
||||
| Régression (run) | `regression_run.json.totals` : **suites 24 · ran 624 · passed 607 · skipped 17 · 0 fail / 0 err** · verdict `PASS` | ✅ (nouvelle baseline) |
|
||||
| Recette roadmap | `acceptance_matrix.json` : **15/15 `status=in_repo`** · verdict `True` · **`ci_job` ×36** | ✅ |
|
||||
| Structure CI | `run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** = **8 gates statiques** (`ls ci/*.sh` hors `lib.sh` = 8) **+ 25 suites** | ✅ concordant |
|
||||
|
||||
**Portée / anti-churn.** **0 fichier de production édité** cette session, **0 artefact
|
||||
reconstruit**, **0 gate ajouté** (#5), **0 chiffre saisi à la main** (#6 — 24/607/17/33
|
||||
recomputés des artefacts + `run_ci.sh`), **aucune commande VPS** (#8). Seule édition :
|
||||
ce rapport + le journal `05_activity_log/`.
|
||||
|
||||
---
|
||||
|
||||
## Mise à jour de currency · fin de journée (HEAD `27c7d20`, session `204339`)
|
||||
|
||||
Ce rapport stakeholder s'arrêtait au jalon **`1a44dfe`** (session `181325`, 18:13). Deux
|
||||
jalons **plus tardifs du même jour** n'y étaient pas encore reflétés ; le canal roadmap est
|
||||
ici porté au **HEAD courant**. **La structure du gate de merge reste inchangée** :
|
||||
`run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** = **8 gates statiques** (`git ls-files 'ci/*.sh'`
|
||||
hors `lib.sh` = 8) **+ 25 suites** — aucun jalon ci-dessous n'ajoute de job.
|
||||
|
||||
| Jalon | Commit (session) | Nature | Portée / opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **Gate neuf** classe `fiche-enumeration-completeness` | `afa6165` (`194334`) | Le tableau des gates statiques de la fiche QA se déclarait « l'état courant » de `git ls-files 'ci/*.sh'` mais **OMETTAIT** le 8ᵉ gate `check_mobile_workflow.sh` (livré le jour même · cité nulle part · `grep=0`) — surface ungatée entre `check_ci_integrity` (câblage `ci/*.sh`↔`gate.needs`) et `check_readme_claims` (aucune énumération de gates) | **Opposable** : `check_readme_claims.sh` étendu re-dérive l'ensemble de `git ls-files 'ci/*.sh'` (hors `lib.sh`, #6) et exige que **chaque** basename soit cité (backtické) dans la fiche ; un gate absent → RED. Teeth 2 axes (MUT1 retrait ligne · MUT2 9ᵉ gate factice = forward-protection). **0 nouveau job** (bloc dans un gate existant, #5) |
|
||||
| **Attestation** bout-en-bout · chaîne d'intégrité des constantes mandataires | `27c7d20` (`201339`) | Recompute indépendant : les constantes canoniques CLAUDE.md #4/#9/#10 copiées en aval (couleurs/fonts · 3 %/8.5 %/52 % · USD+DOP/Cardnet/Letter) restent **ancrées** sur la constitution via leurs gates respectifs | **CLOSE CLEAN** · **0 édition de production**, **0 gate ajouté** (#5) |
|
||||
|
||||
**État de vérification indépendant ce jour (HEAD `27c7d20`).** La fiche QA cite désormais
|
||||
les **8/8** gates (`check_artifacts` · `check_ci_integrity` · `check_docs` ·
|
||||
`check_mobile_workflow` · `check_readme_claims` · `check_regression` · `guard_constraints`
|
||||
· `validate_json`) et le gate `fiche-enumeration-completeness` le **verrouille**. Balayage
|
||||
lecture-seule multi-surface sans dérive : docstring↔code (sorties + comportement) · comptes
|
||||
de tests par fiche (`81`=trio 25+31+25 · financement `35` · ecf `39` · confotur `44` · seo
|
||||
`36` · portails `19` · chat `31`, tous concordants) · ancrage #9/#10 (`check_readme_claims`
|
||||
re-dérive 3 %/8.5 %/52 % de `CLAUDE.md`) · runtime mobile Expo **51 actuel / 54 cible**
|
||||
cohérent sur toutes les surfaces « en place ». La relation `regression_run.json` **624
|
||||
ran = 650 méthodes − 26** (la suite `qa-regression` s'exclut de son propre méta-run) est
|
||||
byte-gatée par `check_artifacts`.
|
||||
|
||||
**Portée / anti-churn (session `204339`).** **0 fichier de production édité**, **0 artefact
|
||||
reconstruit**, **0 gate ajouté** (#5), **0 chiffre saisi à la main** (#6 — `8`/`33`/`624`
|
||||
recomputés de `git ls-files` · `run_ci.sh` · l'artefact), **aucune commande VPS** (#8).
|
||||
Seule édition : ce rapport de currency + le journal `05_activity_log/`.
|
||||
|
||||
---
|
||||
|
||||
## Mise à jour de currency · fin de journée (HEAD `cb3de78`, session `214344`)
|
||||
|
||||
Ce rapport stakeholder s'arrêtait au jalon **`27c7d20`** (session `201339`). **Un** jalon
|
||||
plus tardif du même jour — `cb3de78` (session `211340`) — n'y était pas encore reflété ; le
|
||||
canal roadmap est ici porté au **HEAD courant**. **La structure du gate de merge reste
|
||||
inchangée** : `run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** = **8 gates statiques**
|
||||
(`git ls-files 'ci/*.sh'` hors `lib.sh` = 8, recompté) **+ 25 suites** — le jalon ci-dessous
|
||||
n'ajoute **aucun** job.
|
||||
|
||||
| Jalon | Commit (session) | Nature | Portée / opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **Gate neuf** classe `fiche-enumeration-completeness` (2ᵉ locus) | `cb3de78` (`211340`) | La **table §1** de `ci/README.md` (« Ce que fait le pipeline », une ligne par gate `ci/*.sh`) est LA doc canonique des jobs statiques : sa **complétude** était le **jumeau exact** de la fiche QA fermée par `afa6165`, mais **non gatée** — `check_ci_integrity` verrouille le câblage (`ci/*.sh`↔`gate.needs`), le bloc fiche-QA ne lit que `03_agents/qa/AGENT.md`. Rien n'exigeait qu'un gate figure dans la table §1 → un 9ᵉ gate y serait **omis en silence**. Prouvé ungate par mutation (retrait de ligne de table → `run_ci --static` 8 PASS, aucun mord). | **Opposable** : bloc Python ajouté à `check_readme_claims.sh` (gate **existant** · #5), réutilisant `_gate_names` re-dérivé de `git ls-files 'ci/*.sh'` (hors `lib.sh` · #6) ; exige chaque basename cité dans une **ligne de TABLE** (`|`…) de §1 — délibérément pas la prose §1 (édentée sur un retrait de ligne). Teeth 2 axes (MUT1 retrait ligne l.24 → RED · MUT2 9ᵉ gate factice = forward-protection). **0 nouveau job**. |
|
||||
|
||||
**État de vérification indépendant ce jour (HEAD `cb3de78`).** La **complétude** de l'énumération
|
||||
des gates statiques est désormais verrouillée sur **les deux** loci qui la transcrivaient — la
|
||||
fiche QA (`03_agents/qa/AGENT.md`, fermée `afa6165`) **et** la table §1 de `ci/README.md`
|
||||
(fermée `cb3de78`) — par le même gate `check_readme_claims.sh` re-dérivant `git ls-files
|
||||
'ci/*.sh'`. Le commit `cb3de78` ne touche que l'infra CI (`ci/README.md`, `ci/check_readme_claims.sh`)
|
||||
et le journal : **0 fichier de production**, **0 artefact reconstruit**, **0 job ajouté**.
|
||||
Balayage lecture-seule multi-surface préalable sans dérive : docstring↔code des 25 générateurs
|
||||
(sorties + comportement) · comptes de mots-clés SEO **258** (fr=87 · en=87 · es=84, recomptés
|
||||
de l'artefact + byte-gatés) · SEO trilingue L60 (`seo_keywords`/`seo_schema_org`/`seo_hreflang`
|
||||
livrés) · comptes de tests par fiche concordants.
|
||||
|
||||
**Portée / anti-churn (session `214344`).** **0 fichier de production édité**, **0 artefact
|
||||
reconstruit**, **0 gate ajouté** (#5), **0 chiffre saisi à la main** (#6 — `8`/`33`/`258`
|
||||
recomputés de `git ls-files` · `run_ci.sh` · l'artefact), **aucune commande VPS** (#8).
|
||||
Seule édition : ce rapport de currency + le journal `05_activity_log/`.
|
||||
|
||||
---
|
||||
|
||||
## Mise à jour de currency · fin de journée (HEAD `8903a4b`, session `231404`)
|
||||
|
||||
Ce rapport stakeholder s'arrêtait au jalon **`cb3de78`** (session `211340`). **Deux** jalons
|
||||
plus tardifs du même jour n'y étaient pas encore reflétés ; le canal roadmap est ici porté au
|
||||
**HEAD courant `8903a4b`**. **La structure du gate de merge reste inchangée** : `run_ci.sh` →
|
||||
**33 PASS · 0 FAIL · 0 SKIP** = **8 gates statiques** (`git ls-files 'ci/*.sh'` hors `lib.sh`
|
||||
= 8, recompté) **+ 25 suites** — **aucun** des deux jalons n'ajoute de job.
|
||||
|
||||
| Jalon | Commit (session) | Nature | Portée / opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **Sweep vérification** — bijection INVERSE des artefacts `out/` | `d6a8d73` (`221354`) | Le gate `check_artifacts` ne prouve que le sens *produit ⊆ commité* ; le sens INVERSE (*commité `out/` ⊆ fraîchement produit*, chasse aux orphelins) a été rejoué à la main. **0 fichier `out/` orphelin** — tout artefact commité se reconstruit depuis un générateur courant. | **Non opposable** (pas un gate · #5) : sweep re-jouable, **0 fichier de production édité**, seul le journal `05_activity_log/` touché. |
|
||||
| **Fix comment-vs-code** — commentaire fantôme dans `ci/validate_json.sh` | `8903a4b` (`224359`) | Le commentaire du gate laissait entendre un contrôle de conformité au `$schema` (« draft reconnu ») que le code **ne réalise pas** : `validate_json` ne teste que la **bonne-formation** (parse). Comment aligné sur le comportement réel + renvoi explicite à l'oracle tiers OPTIONNEL `jsonschema` (`check_regression`, 17 skips par design). | **Opposable indirectement** : n'altère **aucune** logique de gate (seul un commentaire changé) ; la CI reste **8 statiques** identiques. Classe *comment-vs-code* distincte de *docstring-vs-code*. |
|
||||
|
||||
**État de vérification indépendant ce jour (HEAD `8903a4b`).** Balayage lecture-seule
|
||||
multi-surface préalable **sans dérive** : **docstring↔code** des générateurs + helpers `*lib/`
|
||||
(sorties + comportement + flags CLI, y compris le compat `validate [-o OUT]` self-consistant —
|
||||
**pas** un défaut) audité end-to-end CLEAN · bijection INVERSE `out/` **0 orphelin** · structure
|
||||
`ci/*.sh`↔`gate.needs` intacte (8 gates). Les deux commits ne touchent que l'infra CI
|
||||
(`ci/validate_json.sh`) et le journal : **0 fichier de production**, **0 artefact reconstruit**,
|
||||
**0 job ajouté**.
|
||||
|
||||
**Portée / anti-churn (session `231404`).** **0 fichier de production édité**, **0 artefact
|
||||
reconstruit**, **0 gate ajouté** (#5), **0 chiffre saisi à la main** (#6 — `8`/`33`
|
||||
recomputés de `git ls-files` · `run_ci.sh`), **aucune commande VPS** (#8). Seule édition :
|
||||
ce rapport de currency + le journal `05_activity_log/`.
|
||||
@@ -0,0 +1,225 @@
|
||||
# Daily Report · 2026-08-06 · Claude Code DTP Worker
|
||||
|
||||
**Session** : `20260806_011419`
|
||||
**Phase roadmap** : Sprint 8 · *Launch + Buffer* — « Monitoring / regression
|
||||
exhaustifs » (DevOps/QA) + buffer. Sprints 1→7 livrés côté repo, gatés et verts ;
|
||||
le reste (déploiement production réel, builds EAS, dépôts ONAPI) est **hors périmètre
|
||||
worker** (VPS `153.75.250.214` · `CLAUDE.md` #8).
|
||||
|
||||
## Contexte — le canal stakeholder s'arrêtait au `2026-08-05` (HEAD `8903a4b`)
|
||||
|
||||
Le dernier rapport roadmap datait du **2026-08-05** et attestait au HEAD `8903a4b`.
|
||||
Depuis, **deux** jalons du **2026-08-06** ne s'y reflétaient pas encore ; ce rapport
|
||||
porte le canal au **HEAD courant `a8342e9`**. **La structure du gate de merge reste
|
||||
inchangée** : `run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** = **8 gates statiques**
|
||||
(`git ls-files 'ci/*.sh'` hors `lib.sh` = 8, recompté) **+ 25 suites** — aucun des
|
||||
deux jalons n'ajoute de job.
|
||||
|
||||
| Jalon | Commit (session) | Nature | Portée / opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **VÉRIF indépendante multi-axes** → CLEAN | `fe61207` (`001412`) | Chasse lecture-seule à un défaut ungaté sur 3 surfaces (docstring↔code des générateurs + helpers `*lib/` · numériques du canal stakeholder re-dérivés · liens markdown in-repo) → **aucune dérive**. Non-défaut connu ré-écarté (compat `validate [-o OUT]` self-consistant). | **Non opposable** (pas un gate · #5) : **0 fichier de production édité**, seul le journal `05_activity_log/` touché. |
|
||||
| **FIX docstring↔code** (sous-classe help-text argparse) — `mobile/app_config_gen.py` | `a8342e9` (`004414`) | Le `help=` de la sous-commande `build` énumérait **4** sorties alors que `cmd_build()` en écrit **5** : `store_listing.json` était **omis** du help CLI (la docstring et le README listaient bien les 5). Ligne ajoutée à sa position canonique. Nouvelle sous-classe *help-text argparse* du motif `docstring↔code`. | **Opposable indirectement** : le help n'affecte aucune sortie → **artefacts byte-identiques** (`check_artifacts` vert). **1 ligne de production** éditée, **0 gate ajouté**, **0 artefact reconstruit**. |
|
||||
|
||||
## Tâche exécutée cette session — currency du canal + attestation indépendante du point courant · 0 édition de production
|
||||
|
||||
Aucune tâche fonctionnelle **in-repo** ne reste : la roadmap `ROADMAP_8_WEEKS_OR_LESS.md`
|
||||
est **intégralement livrée + gatée** (les 7 critères de succès §« Definition of Done »
|
||||
sont ✅). Cette session **atteste le point courant `a8342e9`** et re-dérive les chiffres
|
||||
stakeholder depuis les artefacts commités, avec un axe de vérification **objectif** de
|
||||
plus (bijection comptes-de-tests).
|
||||
|
||||
### État courant · chiffres re-dérivés d'artefacts commités
|
||||
|
||||
| Métrique | Valeur | Source (re-calculée ce jour) |
|
||||
|---|---|---|
|
||||
| Gate de merge | **33 PASS · 0 FAIL · 0 SKIP** | `run_ci.sh` (= 8 statiques + 25 suites) |
|
||||
| Gates statiques | **8** | `git ls-files 'ci/*.sh'` hors `lib.sh` |
|
||||
| Suites de modules | **25** | jobs `*-tests` de `gate.needs` (`.gitea/workflows/ci.yml`) |
|
||||
| Matrice de régression | **624 exécutés · 607 passés · 17 skippés · 0 échec** — **PASS** | `qa/regression/out/regression_run.json` (`totals`) |
|
||||
| Modules avec suite de tests | **25** | `find … -name 'test_*.py' -path '*/tests/*'` |
|
||||
|
||||
Les **17 skips** sont l'oracle `jsonschema` OPTIONNEL neutralisé par `python -S`
|
||||
(par design ; lecture stakeholder = 607 passés + 17 skippés, **pas** 624 — voir
|
||||
baseline régression).
|
||||
|
||||
### Axe de vérification objectif ajouté ce jour — bijection compte-de-tests README ↔ réel
|
||||
|
||||
Pour chaque module, le compte `N tests` annoncé dans le `README.md` a été confronté au
|
||||
**nombre réel** de méthodes `def test_*` de sa suite. **Concordance totale sur les 16
|
||||
READMEs porteurs d'une annonce** (aucune dérive) :
|
||||
|
||||
`seo 36` · `bancable 22` · `fiscal/ecf_dgii 39` · `commissions 25` · `dossier_vente 31`
|
||||
· `faisabilite/generator 16` · `deploy_runbook 29` · `confotur 44` · `workflow_vente 25`
|
||||
· `userperm_gen 12` · `audit_5d 37` · `fixtures_gen 11` · `roleprofile_gen 11` ·
|
||||
`apply_plan 16` · `frontend/portails 19` · `demo/scenarios 39`. Les 9 modules sans
|
||||
annonce chiffrée dans leur README (dont `publiciste`, scaffold Sprint 2 sans `out/`
|
||||
commité) sont cohérents avec leur convention. Le total 16 concorde avec l'en-tête
|
||||
faisant autorité de `ci/check_readme_claims.sh` (« 27 claims sur 16 modules »).
|
||||
|
||||
### Balayage lecture-seule multi-surface préalable — sans dérive
|
||||
|
||||
- **docstring↔code** re-vérifié sur des modules **non** balayés la veille : `publiciste`
|
||||
(docstring/help/writes = `projets_master.json` + `index.html`, cohérents) et
|
||||
`fiscal/ecf_dgii` (« 12 invariants » = schéma(1) + 2→12 numérotés dans `_validate`,
|
||||
self-consistant — **pas** un défaut, ré-écarté).
|
||||
- **Couverture CI** de `publiciste` confirmée présente dans `gate.needs` (`publiciste-tests`).
|
||||
- **Structure gate** `ci/*.sh` ↔ `gate.needs` intacte (8 statiques).
|
||||
|
||||
## Portée / anti-churn (session `011419`)
|
||||
|
||||
**0 fichier de production édité**, **0 artefact reconstruit**, **0 gate ajouté** (#5),
|
||||
**0 chiffre saisi à la main** (#6 — `8` / `33` / `25` / `624`/`607`/`17` recomputés de
|
||||
`git ls-files` · `run_ci.sh` · `.gitea/workflows/ci.yml` · `regression_run.json`),
|
||||
**aucune commande VPS** (#8). Seule édition : ce rapport de currency + le journal
|
||||
`05_activity_log/`.
|
||||
|
||||
---
|
||||
|
||||
## Mise à jour de currency — canal porté de `a8342e9` → HEAD `48276ee` (session `034434`)
|
||||
|
||||
Après le rapport ci-dessus (rédigé session `011419`, HEAD `a8342e9`), **quatre**
|
||||
commits ont atterri le même jour. Deux sont des changements de production non encore
|
||||
consignés côté stakeholder ; les deux autres n'altèrent aucun fichier de production
|
||||
(fix prose d'un compte du présent rapport `14→16`, et une VÉRIF multi-axes CLEAN).
|
||||
Cette section porte le canal au **HEAD courant `48276ee`**. **La structure du gate
|
||||
de merge reste inchangée** : `run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** = **8 gates
|
||||
statiques + 25 suites** ; aucun des quatre commits n'ajoute de job.
|
||||
|
||||
| Jalon | Commit (session) | Nature | Portée / opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **FIX help-text argparse** — `seo/seo_gen.py` | `354a687` (`021424`) | Le `help=` de la sous-commande `build` nommait des **fichiers de sortie INEXISTANTS**. Ré-alignement sur les sorties réelles écrites par `cmd_build()`. Même sous-classe *help-text argparse* du motif `docstring↔code` que `a8342e9`. | **Opposable indirectement** : le help n'affecte aucune sortie → **artefacts byte-identiques** (`check_artifacts` vert). Édition de prod bornée au `help=`, **0 gate ajouté**, **0 artefact reconstruit**. |
|
||||
| **FIX générateur** — `crm/financement_bancaire` MANIFEST.`roles_rbac_utilises` | `48276ee` (`031432`) | `finlib/builder.py:89` itérait les **4 SLOTS logiques** de `spec["roles"]` ; 2 slots (`conseiller_wag` ET `validation_dossier`) pointent le **même** rôle `ventes-conseiller` → **4 entrées pour 3 rôles distincts** dans un champ documenté « ensemble distinct ». Preuve-bug : la sortie **drope la clé de slot**, le doublon ne porte aucune info ; les 3 modules frères produisent tous un ensemble distinct (confotur 3/3, commissions 4/4, workflow_vente 7/7), financement seul outlier 4/3. Fix aligné idiome commissions (`sorted({rr["role_id"] …})`) → **artefact reconstruit 3/3 distinct**. | **Opposable directement** : **1 générateur de prod édité + 1 artefact MANIFEST reconstruit** ; `check_artifacts` reste vert (committé == régénéré). **0 gate ajouté** (#5 — champ lu par aucun gate ni consommateur ; convention distinctness déjà tenue par les frères, et depuis l'état propre `check_artifacts` mord une régression future : régénéré-avec-doublon ≠ committé-3-distinct → RED). |
|
||||
|
||||
### Re-vérification indépendante de `48276ee` menée cette session
|
||||
|
||||
- **MANIFEST courant** : `roles_rbac_utilises` = **3 objets distincts** (`compta-tresorier`,
|
||||
`ventes-conseiller`, `ventes-directeur`), **0 doublon** — fix effectif.
|
||||
- **Balayage des surfaces sœurs** : aucune prose/README/test ne cite encore l'ancien
|
||||
compte « 4 » ni une liste à 4 entrées (`README.md:54` générique sans nombre ; le
|
||||
commentaire `builder.py:89-95` décrit correctement le dédoublonnage). **0 dérive
|
||||
résiduelle** du fix (motif « twin fix must sweep ALL surfaces » — ici propre).
|
||||
- **Gate** : `run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** au HEAD `48276ee` ; `check_artifacts`
|
||||
vert (byte-repro du MANIFEST reconstruit confirmé).
|
||||
|
||||
### État courant · chiffres re-dérivés d'artefacts commités (HEAD `48276ee`)
|
||||
|
||||
| Métrique | Valeur | Source (re-calculée ce jour) |
|
||||
|---|---|---|
|
||||
| Gate de merge | **33 PASS · 0 FAIL · 0 SKIP** | `run_ci.sh` |
|
||||
| Gates statiques | **8** | `git ls-files 'ci/*.sh'` hors `lib.sh` |
|
||||
| Suites de modules | **25** | jobs `*-tests` de `gate.needs` |
|
||||
| Matrice de régression | **624 exécutés · 607 passés · 17 skippés · 0 échec** — **PASS** | `qa/regression/out/regression_run.json` (`totals`) |
|
||||
| Modules avec suite de tests | **25** | `find … -name 'test_*.py' -path '*/tests/*'` |
|
||||
|
||||
Inchangés vs le point `a8342e9` : les deux fixes 08-06 touchent du *help-text* et un
|
||||
*champ MANIFEST non compté par un gate*, pas la surface de test → **totaux identiques**.
|
||||
|
||||
## Portée / anti-churn (session `034434`)
|
||||
|
||||
**Production éditée cette session : néant.** Re-vérification lecture-seule de la
|
||||
dernière prod (`48276ee`) → correcte + complète. **0 artefact reconstruit**, **0 gate
|
||||
ajouté** (#5), **0 chiffre saisi à la main** (#6 — `8` / `33` / `25` / `624`/`607`/`17`
|
||||
recomputés de `git ls-files` · `run_ci.sh` · `.gitea/workflows/ci.yml` ·
|
||||
`regression_run.json`), **aucune commande VPS** (#8). Seules éditions : cette section
|
||||
de currency + le journal `05_activity_log/`.
|
||||
|
||||
## Mise à jour de currency — canal porté de `48276ee` → HEAD `e786c12` (session `061455`)
|
||||
|
||||
La section précédente arrêtait le canal stakeholder au HEAD `48276ee` (dernier fix
|
||||
générateur). **Cinq commits** ont atterri depuis ; cette section porte le canal au **HEAD
|
||||
courant `e786c12`** et atteste, ligne à ligne, leur opposabilité au merge.
|
||||
|
||||
| Commit (session) | Nature | Fichier(s) hors journal | Opposable au merge ? |
|
||||
|---|---|---|---|
|
||||
| `b274958` (`034434`) | CURRENCY canal `a8342e9`→`48276ee` + re-vérif indépendante | *néant* (daily/journal) | Non — snapshot stakeholder |
|
||||
| `4bc9819` (`041444`) | VÉRIF slugs rôle+portail inter-artefacts (980+66 refs) → CLEAN | *néant* (journal) | Non — vérification lecture-seule |
|
||||
| `12ec9d0` (`044449`) | VÉRIF clés JSON dupliquées + re-dérivation champs-compte MANIFEST → CLEAN | *néant* (journal) | Non — vérification lecture-seule |
|
||||
| `a678534` (`051453`) | VÉRIF encodage/BOM/EOL/py_compile (379 fichiers) → CLEAN | *néant* (journal) | Non — vérification lecture-seule |
|
||||
| **`e786c12`** (`054454`) | **FIX doc** — lien Markdown `[`label`](cible)` où le label nomme un fichier gate (`ci/guard_constraints.sh`, `ci/check_docs.sh`) mais l'href pointait ailleurs (`ci/README.md`). Ré-alignement label==cible (navigation véridique). Sous-classe *link-label-vs-target*, origine isolée. | **`README.md`** (3 href, L54+L75) | **Oui — 1 doc de dépôt éditée.** Aucune sortie affectée → `check_docs` vert (cibles nommées existent et sont git-trackées), `check_artifacts` byte-identique, **0 gate ajouté**, **0 artefact reconstruit**. |
|
||||
|
||||
**Bilan des cinq commits** : **une seule** édition de dépôt opposable (`e786c12`, doc de
|
||||
navigation `README.md`) ; les quatre autres sont **0-édition** (canaux daily/journal
|
||||
uniquement). Aucun ne touche un générateur, un artefact `out/`, un gate, ni la surface de test.
|
||||
|
||||
### État courant · chiffres re-dérivés d'artefacts commités (HEAD `e786c12`)
|
||||
|
||||
| Métrique | Valeur | Source (re-calculée ce jour) |
|
||||
|---|---|---|
|
||||
| Gate de merge | **33 PASS · 0 FAIL · 0 SKIP** | `run_ci.sh` |
|
||||
| Gates statiques | **8** | `git ls-files 'ci/*.sh'` hors `lib.sh` |
|
||||
| Suites de modules | **25** | jobs `*-tests` de `gate.needs` (`.gitea/workflows/ci.yml`) |
|
||||
| Modules avec suite de tests | **25** | `find … -name 'test_*.py' -path '*/tests/*'` |
|
||||
| Matrice de régression | **624 exécutés · 607 passés · 17 skippés · 0 échec** — **PASS** | `qa/regression/out/regression_run.json` (`totals`) |
|
||||
|
||||
**Inchangés vs le point `48276ee`** : `e786c12` ne touche que 3 href de `README.md` → aucune
|
||||
sortie, aucun compte de gate/suite/test modifié. Les totaux sont **byte-identiques** au point
|
||||
précédent ; seule la prose de currency avançait faussement « dernière prod = `48276ee`,
|
||||
tout le reste 0-édition » alors qu'`e786c12` édite bien `README.md`.
|
||||
|
||||
## Portée / anti-churn (session `061455`)
|
||||
|
||||
**Production éditée cette session : néant.** Seule correction : rendre honnête le canal
|
||||
stakeholder (`daily_reports`) qui, laissé au point `48276ee`, occultait l'unique édition de
|
||||
dépôt survenue depuis (`e786c12`, doc). C'est la classe *prose-facts-vs-numeric-drift*
|
||||
(un fait de prose — « dernière prod » — dérivait vs le graphe de commits réel), **pas** un
|
||||
re-sweep de vérification. **0 artefact reconstruit**, **0 gate ajouté** (#5), **0 chiffre
|
||||
saisi à la main** (#6 — `8`/`25`/`33`/`624`/`607`/`17` recomputés de `git ls-files` ·
|
||||
`run_ci.sh` · `ci.yml` · `regression_run.json`), **aucune commande VPS** (#8). Seules
|
||||
éditions : cette section de currency + le journal `05_activity_log/`.
|
||||
|
||||
---
|
||||
|
||||
## FIX couverture — oracle orphelin `brief.schema.json` câblé au test (session `104533`)
|
||||
|
||||
**Édition de dépôt cette session : OUI (générateur de tests + 4 artefacts régénérés).**
|
||||
Contrairement aux sessions de currency ci-dessus, cette session livre une **amélioration de
|
||||
couverture réelle**, pas un sweep lecture-seule.
|
||||
|
||||
**Constat.** `faisabilite/generator/brief.schema.json` — documenté comme **« Contrat d'entrée »**
|
||||
dans le README du module et sibling des deux autres schémas faisabilité — était le **seul des 26
|
||||
oracles `*.schema.json` non câblé à un test** : `_validate_brief()` ne garde que le code `projet`
|
||||
(`^P0[1-9]$`), jamais la forme complète du brief ; aucun test ne validait les fixtures contre le
|
||||
schéma. Un brief drifté hors contrat (type erroné, clé renommée) passait donc inaperçu.
|
||||
|
||||
**Correction.** Ajout de `test_input_briefs_validate_against_brief_schema` : valide
|
||||
`brief_complete.json` **et** `brief_incomplete.json` contre `brief.schema.json` via le **validateur
|
||||
maison Publiciste** (zéro-pip, toujours exécuté — pas de skip sous `python -S`). L'« incomplet »
|
||||
est conforme au sens schéma (null/champ absent admis) ; il n'est « incomplet » qu'au sens
|
||||
sémantique (prix→placeholders). Confirmé par les **deux** validateurs (maison + oracle `jsonschema`).
|
||||
|
||||
| Chiffre | Avant | Après | Source régénérée |
|
||||
|---|---|---|---|
|
||||
| Tests `faisabilite/generator` | 16 | **17** | `count_tests` (disque) → `regression_plan.json` |
|
||||
| Matrice de régression (exécutés) | 624 | **625** | `qa/regression/out/regression_run.json` (`totals`) |
|
||||
| Matrice de régression (passés) | 607 | **608** | idem (17 skippés inchangés) |
|
||||
|
||||
**Artefacts régénérés** (jamais édités à la main · #6) : `regression_plan.json`,
|
||||
`regression_run.json`, `regression/out/MANIFEST.json`, `audit_4big/out/quality_report.json`.
|
||||
**Prose gatée réalignée** : README module (`17 tests`), fiches `03_agents/{faisabilite,qa,erpnext_backend}`.
|
||||
`run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP** · **0 gate ajouté** (#5) · **aucune commande VPS** (#8).
|
||||
|
||||
---
|
||||
|
||||
## Currency canal stakeholder — `3710253` → HEAD `04f4c21` (+ session `141554`)
|
||||
|
||||
Depuis le jalon *brief.schema* ci-dessus, **six sessions lecture-seule** ont ajouté chacune un **axe
|
||||
de vérification objectif encore jamais balayé** — toutes **CLEAN, 0 édition de production, 0 gate
|
||||
ajouté** (#5). La **structure du gate de merge reste inchangée** : `run_ci.sh` = **33 PASS · 0 FAIL ·
|
||||
0 SKIP** (8 gates statiques + 25 suites) à chaque commit. Aucune n'est opposable au merge — seul le
|
||||
journal `05_activity_log/` est touché — sauf **un durcissement de test** (session `124544`).
|
||||
|
||||
| Commit (session) | Axe NEUF vérifié | Résultat |
|
||||
|---|---|---|
|
||||
| `27cfe2f` (`111534`) | Intégrité référentielle interne des `$ref` des 26 oracles-schémas | CLEAN 108/108 refs |
|
||||
| `6c05538` (`114542`) | Le skip `python -S` de l'oracle jsonschema masque-t-il un écart enum/const ? | CLEAN (1 phantom `{WAG}` = by-design) |
|
||||
| `4aea5bd` (`121542`) | Cohérence interne des 26 oracles (required-vacuité + auto-contradiction const/enum/default) | CLEAN 26/26 (1405 nœuds) |
|
||||
| `bea5d49` (`124544`) | Atteignabilité transitive d'une assertion (aucun test run-only faux-vert) | CLEAN 0/651 **+ 1 durcissement** (implicite→explicite) |
|
||||
| `1839f52` (`131551`) | Cohérence draft/keyword des 26 oracles-schémas (`$schema` vs idiomes cross-draft) | CLEAN 26/26 (tous draft-07) |
|
||||
| `04f4c21` (`134554`) | L'artefact **commité sur disque** conforme à son schéma (≠ objet en-mémoire) | CLEAN 57/57 |
|
||||
|
||||
**Session courante `141554` — axe NEUF « accord numérique INTER-artefacts »** (`out/*.json` ↔ `out/*.json`,
|
||||
distinct de l'intra-artefact et du code/prose↔artefact). Les concepts-singletons plateforme s'accordent
|
||||
tous cross-fichier (`cible_rbac_roles`=50, `ci_modules_count`/`modules`/`suites`=24, `portails`=5…). Seul
|
||||
nom **surchargé** divergent = `roles_couverts` **50** (tous les role-profiles) vs **44** (rôles des 5
|
||||
portails business = 50 − 6 `plateforme`) — **by-design, les deux re-dérivés de `rbac_50_roles.json`**.
|
||||
**CLEAN · 0 édition de production · 0 gate** (#5) · `run_ci.sh` = **33 PASS · 0 FAIL · 0 SKIP**.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Daily Report · 2026-08-11 · Claude Code DTP Worker
|
||||
|
||||
**Session** : `20260811_032800` · **actualisé** `20260811_092842` (fin de journée)
|
||||
**HEAD attesté** : `35de616` (rapport initial émis à `18d8763` · **11 commits worker** ont
|
||||
suivi le même jour — voir §« Jalons worker suivants »)
|
||||
**Phase roadmap** : transition **V12 → V18** (Master Institutional Feasibility &
|
||||
Bankability Engine). La roadmap 8-semaines classique (Sprints 1→8) reste **livrée +
|
||||
gatée + verte** côté dépôt ; la nouvelle directive V18 rouvre le **moteur de
|
||||
faisabilité** — mais sa séquence est **intégralement suspendue à des arbitrages Michel**
|
||||
(voir §« Chemin critique bloqué »).
|
||||
|
||||
## Contexte — le canal stakeholder s'arrêtait au `2026-08-06` (HEAD `a8342e9`)
|
||||
|
||||
Le dernier rapport roadmap datait du **2026-08-06**. Depuis, **cinq commits** ont porté
|
||||
l'arbre au HEAD courant `18d8763`, dont **trois jalons worker `[DTP-Worker]`** que ce
|
||||
canal ne reflétait pas encore. Ce rapport porte le canal stakeholder au point courant et
|
||||
**re-dérive tous les chiffres depuis les artefacts commités** (aucun chiffre figé, #6).
|
||||
|
||||
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **FIX CI RED → VERT** | `0b453c5` (`015744`) | 2 régressions **induites par les commits root V18** de Michel (archivage V10/V11 + 4 docs REFERENCE root-owned mode 600) : lien pendant `README.md` + `check_readme_claims` butant sur `PermissionError`. Repointé le lien vers la directive V18 active ; ajouté un `except PermissionError` **spécifique** (tolère l'illisible « pas notre fichier », garde tout autre `OSError` fatal). | **Opposable** : gate remis **33/0/0** ; 0 artefact reconstruit. |
|
||||
| **AUDIT préalable V18** | `de837fa` (`022753`) | Production de `OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md` — **PREMIÈRE ACTION OBLIGATOIRE** du GO signal V18 (« NE PAS coder avant l'audit »). Mapping **18 sections** point-par-point (2 ✅ · 7 🟠 · 9 🔴), data model « One Master Dataset », 8 risques, séquence 15 phases, **4 arbitrages Michel** avant Phase 1. Fondé sur le **code V12 lisible uniquement** (anti-invention #6 ; les audits deep de Michel sont root-owned illisibles #8). | **Non opposable** (livrable = document · 0 code moteur). |
|
||||
| **REGISTRE décisions V18** | `18d8763` (`025754`) | Consolidation des 4 arbitrages dans `OPEN_DECISIONS_REGISTER.md` → **D-06 → D-08** (+ **D-01 remonté** au chemin critique V18). Le registre **regroupe et source, ne tranche pas**. | **Non opposable** (doc punch-list · #5 ne pas re-surfacer). |
|
||||
|
||||
## Jalons worker suivants du 2026-08-11 (`18d8763` → `35de616`)
|
||||
|
||||
Après l'émission du rapport initial, **huit jalons `[DTP-Worker]`** ont porté l'arbre au
|
||||
HEAD courant `35de616`, tous **sur l'arbre vert** et **hors code moteur V18** (séquence
|
||||
toujours bloquée · #6). Deux catégories :
|
||||
|
||||
| Jalon | Commit | Nature | Opposabilité |
|
||||
|---|---|---|---|
|
||||
| **Exactitude audit V18** | `c3f5664` + `460574a` | Corrigé un claim LOC stale (`generator` 446 → **622**, contradiction interne vs total 1262) puis **Annexe A reproductible** (chaque chiffre de l'audit apparié à la commande qui le re-dérive → gate-doc auto-auditable anti-drift #6). | Non opposable (doc). |
|
||||
| **Bannières statut V18** ×4 modules-origine | `72753a6` `6970294` `f3419f9` `37c1e97` | Bannière `⚠️ Statut migration V18` en tête des **4 modules-origine** ✅/🟠 du mapping audit §3 : fiches `faisabilite/` + `bim/`, READMEs `bancable` + `legal/confotur` — chacun cadré « état commité courant, pas la cible V18 », 3 liens sourcés (directive · audit · registre D-xx). **Alignement des surfaces doc sur le pivot : clos** pour les modules réels impactés. | Non opposable (doc · `quality_report` byte-count régénéré, PASS 24/24 inchangé). |
|
||||
| **Liage index racine** | `344b815` | Les 2 derniers docs de gouvernance V18 tracked+lisibles (`V18_GO_SIGNAL_DEVELOPMENT` · `V18_ADDENDUM_SECTION_60`) liés depuis le README → les **4 documents de gouvernance V18** sont désormais atteignables depuis le point d'entrée du mandat. | Non opposable (doc). |
|
||||
| **FIX correctness `audit_4big`** (1/3 · char-class) | `ac595d0` | **Vrai bug corrigé** : `q4lib/criteria.py` comptait les méthodes `test_*` via une char-class **ASCII** (`[A-Za-z0-9_]`) → loupait le test réel `test_traçabilite_source` (le `ç`, PEP 3131), publiant « 22 méthodes » au lieu de **23** pour `publiciste`. Aligné sur la source sœur `reglib.discovery` (`\w` Unicode-aware) + **teeth** ajoutée. Cascade régénérée : `regression_run.json` **625 → 626 / 608 → 609**. | **Opposable** : fait publié faux (violation cœur anti-invention #6 de l'audit) rendu exact ; gate reste 33/0/0. |
|
||||
| **FIX correctness `audit_4big`** (2/3 · garde `__main__`) | `fd99362` | **Robustesse de contrat** : le critère CLI détectait l'entrypoint `__main__` par un **littéral figé double-quoté** (`'__name__ == "__main__"' in src`) → loupait silencieusement les gardes **simple-quotées** / **« Yoda »** (opérandes inversés) / **sans-espaces**, toutes du Python valide → sous-scorage CLI **−20 pts** d'un module sain. Remplacé par une regex de **forme** (deux styles de guillemets · ordre libre · espacement libre) + **teeth** 3 fixtures. Occurrence latente (les 50 gardes du dépôt sont double-quotées) → `quality_report.json` byte-identique. Cascade : `regression_run.json` **626 → 627 / 609 → 610**. | **Opposable** : logique de détection de l'outil d'audit #6 rendue exacte pour **tout** Python valide ; gate reste 33/0/0. |
|
||||
| **FIX correctness `audit_4big`** (3/3 · séparateur `def`) | `35de616` | **Complète le fix ASCII, laissé à moitié** : le compteur de tests d'`audit_4big` gardait `def ` (**un espace littéral figé**) là où la source sœur `reglib.discovery` a `def\s+` → `def test_x(` (deux espaces) / `def\ttest_x(` (tab), du Python valide, **loupés** = sous-comptage silencieux + désaccord avec l'autorité byte-gatée de la colonne « Tests » des fiches. Aligné `def ` → `def\s+` (forme strictement identique à la sœur) + **teeth**. Occurrence latente (tous les `def` du dépôt en un-espace PEP 8) → `quality_report.json` byte-identique. Cascade : `regression_run.json` **627 → 628 / 610 → 611**. | **Opposable** : les deux compteurs du **même concept** coïncident désormais sur toute graphie valide ; gate reste 33/0/0. |
|
||||
|
||||
> **Événement produit le plus notable du jour** = l'**arc de fixes `audit_4big`**
|
||||
> (`ac595d0` → `fd99362` → `35de616`) : trois défauts de la **même racine** (« détecter
|
||||
> la _forme_, pas une _graphie_ ») **dans l'outil d'audit qualité lui-même** — il
|
||||
> sous-comptait/sous-scorait ses propres faits sur du Python pourtant valide. Chacun
|
||||
> corrigé à la source unique avec une teeth prouvée mordante ; **classe désormais close**
|
||||
> (char-class Unicode · garde `__main__` multi-graphie · séparateur `def` multi-espace).
|
||||
|
||||
## ⛔ Chemin critique bloqué — la séquence moteur V18 attend Michel
|
||||
|
||||
Le GO signal impose : **audit → validation Michel → Phase 1**
|
||||
(`V18_GO_SIGNAL_DEVELOPMENT_20260810.md:79-84`) et la directive **interdit de coder avant
|
||||
l'audit approuvé** (`DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md:90-91`). L'audit
|
||||
**est produit** — mais **aucune ligne de code moteur V18 n'est légitime** tant que ces
|
||||
arbitrages ne sont pas rendus (les coder violerait la directive **et** #6). Les décisions
|
||||
sont consignées dans `05_deliverables_mvp/OPEN_DECISIONS_REGISTER.md` :
|
||||
|
||||
| Item | Décision attendue de Michel | Pourquoi bloquant |
|
||||
|---|---|---|
|
||||
| **D-06** 🟠 | **Approuver l'audit** V12→V18 | Gate d'entrée de **toute** la séquence moteur. |
|
||||
| **D-07** 🟠 | **Fournir les formules financières** (DCF · IRR/VAN · DSCR/LTV/LTC) | `finance.py` est un snapshot **sans** ces ratios (`grep` confirmé) → moteurs Financial/Bankability **4/8 bloqués** · **risque d'invention #6 le plus grave (R1 🔴)**. |
|
||||
| **D-08** 🟠 | Confirmer **Master Intake A1-A20 = sur-ensemble strict du `brief.json`** | Éviter une 2e base de données (R2 🔴), préserver le parser existant. |
|
||||
| **D-01** 🟠 | Trancher le **périmètre juridique Section 13** (CONFOTUR seul, ou aussi Promesa/Fideicomiso/HOA) | Aucun code ne génère aujourd'hui ces contrats ; arbitrage de périmètre, pas un bug worker. |
|
||||
|
||||
> **Action Michel** — l'approbation D-06 + les formules D-07 débloquent la Phase 1
|
||||
> (Master Project Intake / Data Model). Escalation : WhatsApp `+18296296385` ·
|
||||
> `mroy@worldactivitiesrd.com`.
|
||||
|
||||
## État courant · chiffres re-dérivés d'artefacts commités (HEAD `35de616`)
|
||||
|
||||
| Métrique | Valeur | Source (re-calculée ce jour) |
|
||||
|---|---|---|
|
||||
| Gate de merge | **33 PASS · 0 FAIL · 0 SKIP** | `run_ci.sh` (= 8 statiques + 25 suites) |
|
||||
| Gates statiques | **8** | `git ls-files 'ci/*.sh'` hors `lib.sh` |
|
||||
| Suites de modules | **25** | jobs `*-tests` de `gate.needs` (`.gitea/workflows/ci.yml`) |
|
||||
| Matrice de régression | **628 exécutés · 611 passés · 17 skippés · 0 échec** — **PASS** | `qa/regression/out/regression_run.json` (`totals`) |
|
||||
| Audit 4Big | **verdict PASS · 24 modules · score min 100/100** | `qa/audit_4big/out/quality_report.json` (`verdict` · `modules` · `coverage`) |
|
||||
| Bijection couverture | **24 CI ↔ 24 registre** (`ok`) | `quality_report.json` (`coverage`) |
|
||||
|
||||
Les **17 skips** sont l'oracle `jsonschema` OPTIONNEL neutralisé par `python -S` (par
|
||||
design ; lecture stakeholder = **611 passés + 17 skippés**, **pas** 628 — voir baseline
|
||||
régression). La matrice a évolué **quatre fois** depuis le 2026-08-06 : 624→625 / 607→608
|
||||
(wiring de `brief.schema.json` via un test validateur, avant ce rapport), puis **trois
|
||||
teeth successives** ce jour ajoutées par l'arc de fixes `audit_4big` — 625 → 626 / 608 → 609
|
||||
(`ac595d0`, char-class), 626 → 627 / 609 → 610 (`fd99362`, garde `__main__`), 627 → 628 /
|
||||
610 → 611 (`35de616`, séparateur `def`) — **sans nouveau gate** (la divergence de compte est
|
||||
déjà structurellement rougie par `check_readme_claims`).
|
||||
|
||||
## Portée / anti-churn (session `032800`)
|
||||
|
||||
- **0 commande VPS** (#8) · **0 API externe payante** · **0 code moteur V18** (séquence
|
||||
suspendue à Michel).
|
||||
- **0 fichier de production édité** cette session, **0 artefact reconstruit**, **0 gate
|
||||
ajouté** (#5) : le livrable est ce **rapport stakeholder** (currency du canal +
|
||||
remontée explicite du chemin critique bloqué) + le journal `05_activity_log/`.
|
||||
- Tous les chiffres ci-dessus dérivent d'artefacts **commités**, jamais figés (#6).
|
||||
- **Currency `20260811_092842`** : ce rapport re-épinglé de `ac595d0` → `35de616` (2
|
||||
fixes correctness `audit_4big` de plus : `fd99362` garde `__main__` · `35de616`
|
||||
séparateur `def`) ; régression re-dérivée **626/609 → 628/611** depuis `regression_run.json`
|
||||
(`totals`). Aucun code moteur V18 · aucune commande VPS · aucun gate ajouté.
|
||||
|
||||
**Gate au dépôt** : `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP** (inchangé).
|
||||
@@ -0,0 +1,166 @@
|
||||
# Daily Report · 2026-08-12 · Claude Code DTP Worker
|
||||
|
||||
**Session** : `20260812_150201` — rapport du jour, émis initialement en `20260812_043034`
|
||||
puis enrichi au fil des sessions (la **trace horodatée** de chaque commit contributeur est
|
||||
dans les §Jalons ci-dessous ; l'historique des ré-émissions ne se re-liste plus ici).
|
||||
|
||||
**Point d'attestation** — ce rapport **n'épingle volontairement AUCUN hash de HEAD**. Un
|
||||
hash de HEAD périme à chaque commit worker : le ré-épingler à chaque session était la **cause
|
||||
structurelle** de la boucle de re-currency qui a consommé ce canal (le HEAD était re-figé dans
|
||||
l'en-tête, donc périmé au commit suivant, donc « réconcilié » à nouveau, indéfiniment). On
|
||||
supprime la cause plutôt que de la surveiller : le HEAD courant se **lit** (`git log -1`), il
|
||||
ne se **fige** pas. Ce qui **fait foi** pour le stakeholder est la **matrice de régression**,
|
||||
dérivée d'artefacts commités et **stable à `635 / 618 / 17` · PASS** jusqu'au prochain fix de
|
||||
correctness (aucun chiffre figé · #6). Les hashes cités dans les §Jalons sont des **jalons
|
||||
historiques** (immuables), pas une attestation de HEAD courant.
|
||||
**Phase roadmap** : transition **V12 → V18** (Master Institutional Feasibility &
|
||||
Bankability Engine). La roadmap 8-semaines classique (Sprints 1→8) reste **livrée +
|
||||
gatée + verte** au dépôt ; le **moteur de faisabilité** V18 est rouvert par directive
|
||||
mais sa séquence reste **intégralement suspendue à des arbitrages Michel** (voir
|
||||
§« Chemin critique bloqué »). Aucun code moteur V18 n'est légitime tant que l'audit
|
||||
préalable n'est pas approuvé (D-06 · #6).
|
||||
|
||||
## Contexte — le canal stakeholder s'arrêtait au `2026-08-11` (HEAD `35de616`)
|
||||
|
||||
Le dernier rapport roadmap (`2026-08-11`, currency `20260811_092842`) était épinglé à
|
||||
`35de616`. À l'**émission initiale** de ce rapport (session `043034`), trois commits
|
||||
avaient depuis porté l'arbre à `ee88d36`, dont **deux jalons worker `[DTP-Worker]`** que
|
||||
ce canal ne reflétait pas encore. Ce rapport a repris le canal stakeholder à partir de là
|
||||
et **re-dérive tous les chiffres depuis les artefacts commités** (aucun chiffre figé, #6).
|
||||
|
||||
## Jalons depuis le dernier rapport (`35de616` → `ee88d36`)
|
||||
|
||||
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **TEETH publiciste NBSP** | `39def91` (`20260811_095844`) | Ajout d'un test de parsing prix : l'**espace insécable** U+00A0 (inséré par Word/InDesign FR/ES comme séparateur de milliers dans les prix réels des `data_room`) doit être capté par la char-class Unicode-whitespace de `lib/parser.py`. Le test **mord** si la capture est réduite à de l'ASCII pur (seul le 1er chiffre survivrait : `8.0` au lieu de `8 850 000`) — même racine « détecter la **forme**, pas une **graphie** » que l'arc de fixes `audit_4big`. NBSP en escape explicite ` ` (0 octet invisible). | **Opposable** : +1 test verrou ; régression **628/611 → 629/612** (voir §État courant). 0 artefact reconstruit hors les artefacts régression/audit re-générés du même coup. |
|
||||
| **RÉFÉRENCE design (Michel)** | `031b5e7` | Ajout par Michel du PNG `DASHBOARD_LUXURY_MODELE_REFERENCE_20260811` (design canonique OTO CONTRACTOR, dark+doré · #4). **Hors production repo** (asset de référence root-owned mode 600). | **Non opposable** (asset de référence · 0 code · 0 artefact). |
|
||||
| **CLEANUP #5 orphelin `out_fresh/`** | `ee88d36` (`20260812_040034`) | Suppression de `frontend/portails/out_fresh/` (`workspace.json` + `MANIFEST.json`) : scratch-dir **byte-identique** à `out/`, commité une seule fois par accident (`f954cc1`, 2026-08-02), **produit par rien** (le générateur n'écrit que dans `out/`) et **référencé nulle part** (`git grep out_fresh` = 0). Il **échappait à TOUS les gates** (`check_artifacts` découvre par `find -type d -name out` = match exact) → bombe à retardement de staleness. Fix = `git rm -r` (réversible via historique, non créé par le worker, copie autoritaire `out/` intacte + byte-repro vérifié). Occurrence isolée → **pas de nouveau gate** (#5). | **Opposable** : `run_ci` inchangé **33/0/0** ; **0 artefact autoritaire** modifié (seul un doublon orphelin retiré). |
|
||||
|
||||
## Jalons du jour depuis l'émission initiale (`ee88d36` → `37c68ac`)
|
||||
|
||||
Trois commits worker **du même jour** ont suivi l'émission initiale de ce rapport. Tous
|
||||
sont des travaux de **vérification / durcissement** : **0 code moteur V18** (bloqué D-06),
|
||||
**artefacts byte-identiques** (matrice **stable** à `629/612/17` — confirmé par
|
||||
`check_artifacts` ∈ `run_ci` vert).
|
||||
|
||||
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **Déterminisme · ordre d'exécution des suites** | `a17d86a` (`20260812_050034`) | 4ᵉ axe de déterminisme (frères : hash-seed · locale/TZ · forward-compat) : ré-exécution de la matrice avec l'ordre des 24 suites **inversé** → totaux **byte-identiques**, 24/24 suites identiques. Fermé *by design* (chaque suite tourne dans son propre subprocess `cwd=abs_path`, agrégation somme-indépendante-de-l'ordre). Balayage **replayable**, pas un gate (#5). | **Non opposable** (lecture seule `/tmp`, 0 artefact touché). |
|
||||
| **Vérif i18n SEO · asymétrie `fr=87·en=87·es=84`** | `1513e69` (`20260812_053034`) | Angle i18n frais : l'écart per-langue **prouvé NON-DÉFAUT** au byte-level (`country[es]` = « Republica Dominicana » coïncide avec la `localisation` générique de 5 projets → 3 collisions global↔projet dédupliquées en plus qu'en fr/en ; chaque chaîne unique existe exactement une fois). Chiffre déjà gaté par `check_readme_claims`. Livrable = **note explicative** au point de dedup (`keywords.py:106`), anti re-flag ; 0 logique/fixture/artefact touché. | **Opposable** : commentaire pur, `seo/out/*.json` re-build **byte-identique**. |
|
||||
| **Hardening correctness · `criteria.py`** | `37c68ac` (`20260812_060044`) | 3ᵉ détecteur regex `_ADD_PARSER_RE` durci `add_parser\(` → `add_parser\s*\(` (tolère l'espace avant `(`, Python valide) — dernier non durci de la famille « détecter la **forme**, pas une **graphie** » (frères `ac595d0`/`fd99362`/`35de616`). Fix **latent** : aucun module n'utilise l'espace aujourd'hui (`grep` = 0) → `quality_report.json` re-build **byte-identique**. Occurrence one-shot → pas de gate (#5). | **Opposable** : `run_ci` inchangé **33/0/0** ; **0 artefact autoritaire** modifié. |
|
||||
|
||||
## Jalons du jour · 2ᵉ vague (`37c68ac` → `e7cc3c4`) — correctness à impact matrice
|
||||
|
||||
Contrairement à la 1ʳᵉ vague (vérif/hardening à artefacts byte-identiques), ces trois
|
||||
commits corrigent des **bugs latents réels** de correctness de sortie sur des **entrées
|
||||
financières / francophones réalistes RD**. Chacun ajoute un **test de régression à dents**
|
||||
(prouvé qu'il échoue sans le fix), ce qui fait **croître la matrice** `629 → 633`. Tous :
|
||||
**0 code moteur V18** (bloqué D-06), leçon commune « **le contexte de sortie dicte
|
||||
l'encodeur, pas la donnée courante** ».
|
||||
|
||||
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **Fix · rendu publiciste : URL hero percent-encodée** | `aeb30b8` (`20260812_070054`) | `_hero_media` composait `url('/static/projets/{code}/{fichier}')` en CSS mais échappait le nom de fichier via `html.escape` — **mauvais encodeur pour un contexte CSS** : un rendu réel `Vue d'ensemble.png` (FR) devient `Vue d'ensemble.png`, HTML-décodé par le navigateur en `Vue d'ensemble.png` → l'apostrophe **referme la chaîne `url('…')`** → hero **invisible silencieusement**. Fix = `urllib.parse.quote(fichier)` (`'`→`%27`, espace→`%20`) — valide en URL/HTML/CSS. `_esc` conservé pour l'attribut HTML. Test à dents `test_hero_media_url_percent_encode`. Byte-repro intact (ASCII simple inchangé). | **Opposable** : bug de rendu réel corrigé ; suite publiciste **24→25**, matrice **629/612 → 630/613** ; 4 artefacts régénérés (regression×3 + quality_report) + 3 fiches à compteurs = **pure propagation**. |
|
||||
| **Hardening · segment `code` de la même `url('…')`** | `5af86e3` (`20260812_073055`) | Le fix `070054` avait percent-encodé `fichier` mais laissé **le second segment dynamique `code`** brut dans la MÊME `url()` — dernier segment non-encodé (frère exact de `criteria.py` : durcir le SEUL non-durci d'une famille). **Non déclenchable aujourd'hui** (codes `P01..P09`, jamais d'apostrophe → byte-repro inchangé) mais durcissement d'uniformité. Fix = `quote(code)`. Test à dents `test_hero_media_code_segment_percent_encoded`. | **Opposable** : uniformité de construction ; suite publiciste **25→26**, matrice **630/613 → 631/614** ; artefacts = pure propagation de compteur. |
|
||||
| **Fix · commissions : formule `base×taux` cassait sur montants réels** | `e7cc3c4` (`20260812_080104`) | `compute_line` composait le libellé de base via `f"{base:g}"` — casse **deux fois** sur des montants immobiliers RD (unité ≈ **18 000 000 DOP**) : (1) **exponentiel dès 1e6** (`18000000`→`1.8e+07`, non-auditable) ; (2) **arrondi silencieux à 6 chiffres sig.** (`123456.78`→`123457`) = **base fabriquée**, l'invention même interdite #6 dans le module dont la docstring proclame l'anti-invention. Fix = helper `_amount_label` (`f"{x:f}"` + strip zéros), fidèle et identique à l'ancien pour tout montant simple. Code **runtime** (jamais appelé par `commissions_gen.py`) → **0 régénération de l'artefact du module**. 2 tests à dents. | **Opposable** : bug de sortie financière corrigé ; suite commissions **25→27**, matrice **631/614 → 633/616** ; artefacts régression/audit régénérés = pure propagation. |
|
||||
|
||||
## Jalon du jour · 3ᵉ vague (`e7cc3c4` → `85cd625`) — correctness gate financement
|
||||
|
||||
Un 4ᵉ fix worker de correctness a suivi la 2ᵉ vague, dans la même veine (bug **latent
|
||||
réel** sur entrée réaliste, byte-repro-aveugle, test à dents) : **0 code moteur V18**
|
||||
(bloqué D-06).
|
||||
|
||||
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **Fix · `financement_bancaire` : barre pleine ✓ mais dossier BLOQUÉ** | `85cd625` (`20260812_093114`) | `gate.py:97` `_cond_apport` : `pct = _round1(min(100.0, paid/required*100))` arrondit à 1 décimale, tandis que `ok = paid + 1e-9 >= required` est **exact**. Un versement dans la bande **[99.95 %, 100 %[** (p. ex. **19 999 / 20 000 USD** = 99.995 %) remonte `percent: 100.0` (barre frontend « 100 % ✓ ») **tout en bloquant** la soumission (403 · `remaining: 1.0 USD`) — raison de blocage auto-contradictoire. Seul `_cond_apport` est atteignable (les 2 autres conditions = ratios entiers sur petites listes, `percent==100 ⟺ ok` structurel). Fix = garde `if not ok and pct >= 100.0: pct = 99.9` → invariant d'affichage **`percent == 100.0 ⟺ ok`** rétabli. `dossier_exemple` commité à 67.8 % (hors bande) → `gate_status_example.json` **byte-identique**. Test à dents `test_apport_quasi_complet_ninclut_pas_100pct`. | **Opposable** : bug d'affichage user-facing corrigé ; suite financement **35→36**, matrice **633/616 → 634/617** ; artefacts régression/audit régénérés = pure propagation. |
|
||||
|
||||
## Jalon du jour · 4ᵉ vague (`85cd625` → HEAD courant) — même bug, un niveau au-dessus
|
||||
|
||||
Un **5ᵉ fix worker de correctness** prolonge exactement le 4ᵉ : la 3ᵉ vague avait rétabli
|
||||
l'invariant **`percent == 100.0 ⟺ ok`** *au niveau de chaque condition* (`_cond_apport`),
|
||||
mais l'**agrégat** `overall_percent` (la **barre GLOBALE**) le violait toujours — même
|
||||
classe de bug, un cran plus haut, que le fix précédent avait manqué.
|
||||
|
||||
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **Fix · `financement_bancaire` : barre GLOBALE 100 % mais dossier BLOQUÉ** | `20260812_110124` | `gate.py` `gate_status` : `overall = _round1(Σ percents / 4)`. Cas limite = apport plafonné à **99.9** (non ok, cf. fix 3ᵉ vague) **+** les 3 autres conditions à **100.0** (ok) → moyenne `(99.9 + 300)/4 = 99.975` → `round1` = **100.0**. La barre **globale** afficherait « 100 % » tout en refusant la soumission (403) — même auto-contradiction que le fix `_cond_apport`, mais à l'agrégat. Fix = garde `if not can_submit and overall >= 100.0: overall = 99.9` → invariant d'affichage **`overall_percent == 100.0 ⟺ can_submit`**. `gate_status_example.json` commité à `overall_percent: 39.1` (hors bande) → **byte-identique**. Test à dents `test_overall_percent_agrege_ninclut_pas_100pct` (mord `100.0 not less than 100.0` sans la garde). | **Opposable** : bug d'affichage user-facing corrigé ; suite financement **36→37**, matrice **634/617 → 635/618** ; artefacts régénérés = pure propagation. |
|
||||
|
||||
## Jalons de vérification depuis la 4ᵉ vague (depuis le 5ᵉ fix `1992ee6` · au fil de l'eau)
|
||||
|
||||
> **Table roulante.** Après le 5ᵉ (et dernier) fix à impact matrice (`1992ee6`), **tout**
|
||||
> commit worker du jour est **non opposable par construction** — vérification / durcissement
|
||||
> anti-récurrence (commentaire pur), currency de canal, ou sweep en lecture seule. Aucun
|
||||
> n'est un fix de code produit. La matrice reste donc **stable à 635/618/17** quel que soit
|
||||
> le nombre de lignes ci-dessous ; elle ne bougera qu'au **prochain fix de correctness**.
|
||||
> Cette section liste ces jalons au fil de l'eau (**six** à ce jour) sans re-figer le HEAD
|
||||
> ailleurs — chiffres re-dérivés de l'artefact commité, jamais figés (#6) ; `check_artifacts`
|
||||
> ∈ `run_ci` vert · rebuild **byte-identique** de tous les artefacts.
|
||||
|
||||
| Jalon | Commit (session) | Nature | Opposabilité au merge |
|
||||
|---|---|---|---|
|
||||
| **Classe « percent==100 ⟺ ok » FERMÉE (4 conditions)** | `3849375` (`20260812_113134`) | Preuve que `financement_bancaire` est le **seul module à champ `percent`** ; les 2 conditions à domaine borné (documents ≤ 13 · autorisations = 4 ≪ seuil 2000) durcies par note anti-récurrence. Commentaires purs. | **Non opposable** (aucun fix · rebuild byte-identique). |
|
||||
| **Classe « affichage arrondi ⟺ gate booléen » FERMÉE repo-wide** | `ea8458d` (`20260812_120139`) | Preuve d'**immunité structurelle** de `audit_4big` (score-au-seuil 95 = quantité UNIQUE affichage+verdict, ≠ financement à 2 quantités divergentes) + note in-situ `scoring.py`. Commentaire pur. | **Non opposable** (aucun fix · rebuild byte-identique). |
|
||||
| **CURRENCY · réalignement de 2 citations `OPEN_DECISIONS_REGISTER.md`** | `4eb615b` (`20260812_123144`) | D-02 `gate.py:142-145 → :164-167` (les gardes des fixes du jour ont décalé `_cond_validation_wag`) · D-01 confotur `README:8-17 → :32-33` (bannière V18 préfixée) ; sweep complet = autres citations résolvent. Document éditorial. | **Non opposable** (0 code · 0 artefact). |
|
||||
| **Chasse au défaut axe FRAIS date/time + parsing money/percent** | `fb2d3e1` (`20260812_130149`) | 3 candidats **RÉFUTÉS** domaine-borné (`signature_date="0"` corrompu · `banclib._pct` constante canonique · `rate()` séparateur-milliers inatteignable, taux borné 0-100) + note anti-récurrence in-situ sur `rate()` (un Explore l'avait mal-classé CONFIRMED). Commentaire pur. | **Non opposable** (aucun fix · rebuild byte-identique). |
|
||||
| **CURRENCY · remontée des 4 jalons post-4ᵉ-vague dans ce rapport** | `e15129b` (`20260812_133154`) | Le canal s'arrêtait à `1992ee6` tout en s'affirmant « au HEAD courant » (grep-count 0 des 4 hashes) → §« Jalons de vérification » créée + en-tête `HEAD attesté` réaligné. Document éditorial. | **Non opposable** (0 code · 0 artefact). |
|
||||
| **Nouvel axe FRAIS · mode d'arrondi (banquier / round-half-to-even)** | `4a13557` (`20260812_140156`) | Balayage repo-wide des **8 sites `round()`** de production → CLEAN teeth-prouvé, 0 défaut : `scorer.py:57` verdict-immune (`complete ⟹ filled==total ⟹ pts==20` exact) · `:81` immune par inclusion `champs_manquants` · `gate.py` montants au cent + percents déjà gardés (`85cd625`/`1992ee6`) · `parser.py` domaine entier · `audit_4big` immune (`ea8458d`). Sweep en lecture seule (journal only, **aucun mis-flag → pas de note in-situ**). | **Non opposable** (aucun fix · aucun artefact touché). |
|
||||
|
||||
## Chemin critique bloqué — séquence moteur V18 (inchangé)
|
||||
|
||||
La directive V18 (`DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md`) impose une
|
||||
séquence *audit → validation Michel → Phase 1 → validation → Phases 2-15*. L'audit
|
||||
préalable obligatoire (`OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md`) est **produit**
|
||||
mais **en attente de validation** ; rien côté moteur n'est légitime avant l'approbation
|
||||
D-06 (`OPEN_DECISIONS_REGISTER.md`). Les arbitrages ouverts restent :
|
||||
|
||||
| Décision | Objet | Statut |
|
||||
|---|---|---|
|
||||
| **D-06** 🔴 | Approbation de l'audit V18 (gate d'entrée Phase 1) | En attente Michel |
|
||||
| **D-07** 🔴 | Formules financières canoniques (Master Data Model) | En attente Michel |
|
||||
| **D-01** 🟠 | Périmètre juridique Section 13 (CONFOTUR seul, ou aussi Promesa/Fideicomiso/HOA) | Arbitrage de périmètre (aucun code ne génère ces contrats aujourd'hui) |
|
||||
|
||||
> **Action Michel** — l'approbation D-06 + les formules D-07 débloquent la Phase 1
|
||||
> (Master Project Intake / Data Model). Escalation : WhatsApp `+18296296385` ·
|
||||
> `mroy@worldactivitiesrd.com`.
|
||||
|
||||
## État courant · chiffres re-dérivés d'artefacts commités (HEAD courant)
|
||||
|
||||
| Métrique | Valeur | Source (re-calculée ce jour) |
|
||||
|---|---|---|
|
||||
| Gate de merge | **33 PASS · 0 FAIL · 0 SKIP** | `run_ci.sh` (= 8 statiques + 25 suites) |
|
||||
| Gates statiques | **8** | `git ls-files 'ci/*.sh'` hors `lib.sh` |
|
||||
| Suites de modules | **25** exécutées / **24** gatées régression | jobs `*-tests` de `gate.needs` (`.gitea/workflows/ci.yml`) ; la régression s'auto-exclut de son propre recensement |
|
||||
| Matrice de régression | **635 exécutés · 618 passés · 17 skippés · 0 échec** — **PASS** | `qa/regression/out/regression_run.json` (`totals`) |
|
||||
| Audit 4Big | **verdict PASS · 24 modules · score min 100/100** | `qa/audit_4big/out/quality_report.json` (`verdict` · `modules` · `coverage`) |
|
||||
| Bijection couverture | **24 CI ↔ 24 registre** (`ok: true`, 0 manquant) | `quality_report.json` (`coverage`) |
|
||||
|
||||
Les **17 skips** sont l'oracle `jsonschema` OPTIONNEL neutralisé par `python -S` (par
|
||||
design ; lecture stakeholder = **618 passés + 17 skippés**, **pas** 635 — voir baseline
|
||||
régression). Trajectoire de la matrice depuis le rapport précédent (`628/611` à
|
||||
`35de616`) : **+1** au `39def91` (test verrou NBSP publiciste, 628→**629**) ; stable à
|
||||
`629/612` sur la 1ʳᵉ vague de vérif/hardening (`a17d86a`/`1513e69`/`37c68ac`, artefacts
|
||||
byte-identiques) ; puis **2ᵉ vague de correctness** — **+1** au `aeb30b8` (630/613), **+1**
|
||||
au `5af86e3` (631/614), **+2** au `e7cc3c4` (633/616) ; puis **3ᵉ vague** — **+1** au
|
||||
`85cd625` (fix gate financement per-condition, **634/617**) ; puis **4ᵉ vague** — **+1**
|
||||
au `1992ee6` (fix gate financement **agrégat** `overall_percent`, **635/618**) ; puis **six
|
||||
commits de vérification / currency** (`3849375`/`ea8458d`/`4eb615b`/`fb2d3e1`/`e15129b`/`4a13557`)
|
||||
**sans impact matrice** (commentaires purs + currency, rebuild byte-identique · trace dans la
|
||||
table roulante ci-dessus, non ré-épinglée à un hash). Chaque incrément = un **test de
|
||||
régression à dents** ajouté par un fix ; **aucun nouveau gate** (une divergence de compte
|
||||
serait déjà structurellement rougie par `check_readme_claims`, ∈ `run_ci`). Les commits
|
||||
non-test (`031b5e7` PNG · `ee88d36` cleanup orphelin · `cb8069b` notes `:g` pures) **ne
|
||||
touchent aucun test**. Matrice courante **635/618/17** au HEAD courant.
|
||||
|
||||
## Portée / anti-churn (session `20260812_100114` · re-réconcilie `083114` au HEAD `85cd625`)
|
||||
|
||||
- **0 commande VPS** (#8) · **0 API externe payante** · **0 code moteur V18** (séquence
|
||||
suspendue à Michel · #6).
|
||||
- **0 fichier de production édité** cette session, **0 artefact reconstruit**, **0 gate
|
||||
ajouté** (#5) : le livrable est la **re-réconciliation de ce rapport stakeholder** au
|
||||
HEAD courant (`85cd625`) — currency du canal + remontée du **4ᵉ fix de correctness**
|
||||
(`633→634`, gate `financement_bancaire`, commité après la réconciliation `083114`) +
|
||||
chemin critique bloqué inchangé — + le journal `05_activity_log/`. Le commit intercalaire
|
||||
`cb8069b` (notes `:g` pures · session `090114`) est sans impact matrice.
|
||||
- Tous les chiffres ci-dessus dérivent d'artefacts **commités**, jamais figés (#6).
|
||||
|
||||
**Gate au dépôt** : `./run_ci.sh` → **33 PASS · 0 FAIL · 0 SKIP**.
|
||||
@@ -16,7 +16,8 @@ Chaîne de valeur :
|
||||
out/ des modules livrés (workflow, e-CF, CONFOTUR, RBAC, 4Big, portails, …)
|
||||
│ pointeurs RFC 6901 (scenario_spec.json)
|
||||
▼
|
||||
demo_scenario_gen ──► out/run_sheet.json + out/MANIFEST.json
|
||||
demo_scenario_gen ──► out/run_sheet.json + out/run_sheet.md
|
||||
+ out/MANIFEST.json
|
||||
│
|
||||
▼ (VPS · hors périmètre worker · #8)
|
||||
rendu deck / prompteur ─► démo publique otov7.com
|
||||
@@ -28,7 +29,7 @@ Garanties (le CLI REFUSE d'écrire si l'une casse) :
|
||||
· l'artefact livré est `pret=true` (0 citation non résolue · 0 module non gated).
|
||||
|
||||
Sous-commandes :
|
||||
build [-o OUT_DIR] → OUT_DIR/{run_sheet,MANIFEST}.json
|
||||
build [-o OUT_DIR] → OUT_DIR/run_sheet.{json,md} + OUT_DIR/MANIFEST.json
|
||||
validate → manifeste + invariants (sans écrire)
|
||||
"""
|
||||
|
||||
@@ -233,7 +234,7 @@ def build_argparser() -> argparse.ArgumentParser:
|
||||
description=__doc__.splitlines()[0])
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p = sub.add_parser("build", help="scenario_spec.json → out/{run_sheet,MANIFEST}.json")
|
||||
p = sub.add_parser("build", help="scenario_spec.json → out/run_sheet.{json,md} + out/MANIFEST.json")
|
||||
p.add_argument("-o", "--out", default=os.path.join(_HERE, "out"),
|
||||
help="dossier de sortie (défaut: ./out)")
|
||||
p.add_argument("--generated-at", dest="generated_at", default=None)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -47,9 +47,9 @@ chiffrée dans le catalogue de confirmations.
|
||||
| 1 | vps | Prérequis (modules custom OTOV7 + spec RBAC) | `rbac` | — |
|
||||
| 2 | vps | DocTypes custom | `crm/dossier_vente`, `legal/confotur` | 1 |
|
||||
| 3 | worker+vps | RBAC (Role + DocPerm + UP + Role Profile) | `rbac/fixtures_gen`, `rbac/userperm_gen`, `rbac/roleprofile_gen`, `rbac/apply_plan` | 2 |
|
||||
| 4 | worker+vps | Workflow vente + règles métier | `crm/workflow_vente`, `crm/commissions`, `fiscal/ecf_dgii` | 2, 3 |
|
||||
| 4 | worker+vps | Workflow vente + règles métier | `crm/commissions`, `crm/financement_bancaire`, `crm/workflow_vente`, `fiscal/ecf_dgii` | 2, 3 |
|
||||
| 5 | worker+vps | Frontend desk (Workspaces + chat OTOIA) + config mobile | `frontend/portails`, `frontend/chat_otoia`, `mobile/app_config` | 3 |
|
||||
| 6 | worker+vps | Contenu & publication | `publiciste`, `faisabilite/generator`, `faisabilite/bancable`, `seo`, `demo/scenarios` | 4, 5 |
|
||||
| 6 | worker+vps | Contenu & publication | `demo/scenarios`, `faisabilite/bancable`, `faisabilite/generator`, `pie/manifest`, `publiciste`, `seo` | 4, 5 |
|
||||
| 7 | vps | Vérification QA post-déploiement | `qa/acceptance`, `qa/audit_5d`, `qa/audit_4big`, `qa/regression` | 4, 5, 6 |
|
||||
|
||||
**Confirmations préalables VPS** (consolidées, sourcées) : `custom_modules`,
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"rbac/apply_plan": "rbac-apply",
|
||||
"crm/workflow_vente": "workflow-metier",
|
||||
"crm/commissions": "workflow-metier",
|
||||
"crm/financement_bancaire": "workflow-metier",
|
||||
"fiscal/ecf_dgii": "workflow-metier",
|
||||
"frontend/portails": "frontend",
|
||||
"frontend/chat_otoia": "frontend",
|
||||
@@ -88,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",
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
"self_module": "devops/deploy_runbook",
|
||||
"counts": {
|
||||
"phases": 7,
|
||||
"modules": 22,
|
||||
"modules_mapped": 22,
|
||||
"modules": 24,
|
||||
"modules_mapped": 24,
|
||||
"confirmations": 7,
|
||||
"confirmations_referenced": 7
|
||||
},
|
||||
"coverage": {
|
||||
"ci_modules_count": 22,
|
||||
"mapped_modules_count": 22,
|
||||
"ci_modules_count": 24,
|
||||
"mapped_modules_count": 24,
|
||||
"bijective": true,
|
||||
"missing_in_map": [],
|
||||
"extra_in_map": [],
|
||||
|
||||
@@ -93,6 +93,10 @@
|
||||
"module": "crm/commissions",
|
||||
"ci_job": "crm-commissions-tests"
|
||||
},
|
||||
{
|
||||
"module": "crm/financement_bancaire",
|
||||
"ci_job": "crm-financement-bancaire-tests"
|
||||
},
|
||||
{
|
||||
"module": "crm/workflow_vente",
|
||||
"ci_job": "crm-workflow-vente-tests"
|
||||
@@ -184,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"
|
||||
|
||||
@@ -149,7 +149,7 @@ DOIT être daté et sourcé (jamais inventé — contrainte #6).
|
||||
| 4.10 | Devises | 🔴 | **USD + DOP** (contrainte #10) |
|
||||
| 4.11 | Moyen de paiement | 🔴 | **Cardnet** (❌ Stripe · contrainte #10) |
|
||||
| 4.12 | Format documents | 🔴 | **Letter US** (contrainte #10) |
|
||||
| 4.13 | Régime fiscal | 🟡 | CONFOTUR · Fideicomiso (voir volet juridique) | `{{regime_fiscal}}` |
|
||||
| 4.13 | Régime fiscal | 🟡 | CONFOTUR · Fideicomiso (voir volet juridique) · `{{regime_fiscal}}` |
|
||||
|
||||
### 5.3 Cohérence commercial.md (verrou anti-gap)
|
||||
|
||||
|
||||
@@ -2,7 +2,26 @@
|
||||
|
||||
> Livrable **Faisabilité · Sprint 3** — roadmap
|
||||
> [`ROADMAP_8_WEEKS_OR_LESS.md`](../../../04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md)
|
||||
> §Sprint 3 (« `40_llm_outputs/` + rapports bancables FR/EN/ES »).
|
||||
> §Sprint 3 « Faisabilité Auto » (Faisabilité → « génération 4 volets <1h »). Le volet
|
||||
> **financier bancable FR/EN/ES** est la déclinaison S3 planifiée dans les
|
||||
> [`daily_reports`](../../daily_reports) (« `40_llm_outputs/` + rapports bancables
|
||||
> FR/EN/ES ») — un libellé de planification, **pas** un texte du fichier roadmap —
|
||||
> exigée par le **Portail Bancables 4Big** (ci-dessous).
|
||||
|
||||
> ⚠️ **Statut migration V18.** D'après la cartographie de l'audit de migration V12→V18,
|
||||
> **ce module `bancable` est la graine V12 des moteurs financiers V18** : Section 15
|
||||
> **Bankability** (« le plus mûr » du mapping) et, partiellement, Section 12
|
||||
> **Financier/DCF**. Il est à ce titre au **cœur du risque de migration le plus grave
|
||||
> (R1 🔴)** : généraliser ce *snapshot* statique (coût · revenu · marge · point d'équilibre
|
||||
> en unités) vers les **moteurs Financial (4)** et **Bankability (8)** exige les formules
|
||||
> **DCF · IRR/VAN · DSCR/LTV/LTC** — **absentes du code lisible** (`grep` sur `banclib/`
|
||||
> ne rend rien) et **non devinables** sans les rendre lisibles côté Michel (#6). Toute la
|
||||
> séquence moteur V18 est par ailleurs **suspendue** à l'approbation de l'audit. **Ne
|
||||
> coder aucun de ces moteurs avant arbitrage.** La documentation ci-dessous décrit l'**état
|
||||
> V12 commité** — *pas* la cible V18.
|
||||
> Sources : [`DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md`](../../../DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md) ·
|
||||
> [`OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md`](../../../OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md) (§3 mapping · §5 · R1) ·
|
||||
> [`OPEN_DECISIONS_REGISTER.md`](../../OPEN_DECISIONS_REGISTER.md) (D-06 · D-07).
|
||||
|
||||
Remplit le répertoire `50_financier_bancable/` d'une data_room — jusqu'ici **vide**
|
||||
(`.gitkeep` posé par le générateur 4 volets) — que le **Portail Bancables 4Big**
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"""Générateur de dossier financier bancable trilingue FR/EN/ES · Sprint 3.
|
||||
|
||||
Livrable **Faisabilité · Sprint 3** (roadmap `04_roadmap/ROADMAP_8_WEEKS_OR_LESS.md`
|
||||
§Sprint 3 · « 40_llm_outputs/ + rapports bancables FR/EN/ES »). Remplit le
|
||||
§Sprint 3 « Faisabilité Auto » → Faisabilité « génération 4 volets <1h » ; le volet
|
||||
**financier bancable FR/EN/ES** est la déclinaison S3 planifiée dans les
|
||||
`daily_reports` — « 40_llm_outputs/ + rapports bancables FR/EN/ES » — et exigée par le
|
||||
Portail Bancables 4Big, non un libellé du fichier roadmap). Remplit le
|
||||
répertoire `50_financier_bancable/` d'une data_room — jusqu'ici vide (`.gitkeep`)
|
||||
— que le **Portail Bancables 4Big** (`PORTAIL_BANCABLES_4BIG.md`, variante 06
|
||||
retenue par Michel) exige à l'étape 1 de son workflow de déploiement projet.
|
||||
@@ -114,7 +117,9 @@ def _check_derived_arithmetic(brief: dict, manifest: dict) -> list[str]:
|
||||
"""Reproduit indépendamment les 4 agrégats et compare au manifeste.
|
||||
|
||||
Prouve qu'aucune valeur calculée n'a été « posée » : elle DOIT découler des
|
||||
typologies sourcées et du taux canonique 52 %.
|
||||
typologies sourcées et du taux canonique de point d'équilibre. Ce taux est lu
|
||||
à la SOURCE UNIQUE (`CANONICAL` · CLAUDE.md #9, gaté), jamais recopié en dur
|
||||
ici — sinon un changement du mandat (#9) casserait cet oracle en silence.
|
||||
"""
|
||||
motifs: list[str] = []
|
||||
rows = finance.typologies(brief)
|
||||
@@ -128,8 +133,9 @@ def _check_derived_arithmetic(brief: dict, manifest: dict) -> list[str]:
|
||||
if all_q and all(r["prix_usd"] is not None for r in rows) else None)
|
||||
exp_dop = (sum(q * r["prix_dop"] for q, r in zip(qtes, rows))
|
||||
if all_q and all(r["prix_dop"] is not None for r in rows) else None)
|
||||
exp_pe = (float(math.ceil(0.52 * exp_total))
|
||||
if exp_total is not None else None)
|
||||
pe_pct = finance._pct(deps.CANONICAL["point_equilibre_pct"])
|
||||
exp_pe = (float(math.ceil(pe_pct * exp_total))
|
||||
if exp_total is not None and pe_pct is not None else None)
|
||||
|
||||
for cle, exp in (("total_unites", exp_total),
|
||||
("valeur_catalogue_usd", exp_usd),
|
||||
|
||||
@@ -134,6 +134,11 @@ def derived(brief: dict) -> list[dict]:
|
||||
if total_unites is not None and pe_pct is not None:
|
||||
pe_brut = pe_pct * total_unites
|
||||
pe_unites = float(math.ceil(pe_brut))
|
||||
# `:g` sûr ici : pe_brut = 0.52 × NOMBRE d'unités (domaine borné à quelques
|
||||
# centaines en immobilier réel) — jamais ≥ 1e6 (pas d'exponentiel) ni > 6
|
||||
# chiffres significatifs, les deux pièges de `:g`. En prime `:g` nettoie le
|
||||
# bruit flottant du produit (0.52 × 173 = 89.96000…1 → « 89.96 »). Les
|
||||
# MONTANTS non bornés, eux, passent par _money (`,.0f`), jamais par `:g`.
|
||||
pe_formule = (f"{CANONICAL['point_equilibre_pct']} × {int(total_unites)} = "
|
||||
f"{pe_brut:g}")
|
||||
else:
|
||||
|
||||
@@ -33,6 +33,10 @@ def _int(n: Optional[float], placeholder: str) -> str:
|
||||
|
||||
|
||||
def _pct(n: Optional[float], placeholder: str) -> str:
|
||||
# `:g` sûr : un pourcentage (marge, taux) est borné (0-100), donc jamais en
|
||||
# exponentiel (< 1e6) ni > 6 chiffres significatifs — les deux pièges de `:g`
|
||||
# sont hors domaine ; `:g` nettoie en prime le bruit flottant. Les MONTANTS
|
||||
# passent par _money (`,.0f`/`,.2f`), jamais par `:g`. Ne pas « corriger ».
|
||||
if n is None:
|
||||
return "{{" + placeholder + "}}"
|
||||
return f"{n:g} %"
|
||||
|
||||
@@ -93,8 +93,9 @@ Voir [`brief.schema.json`](./brief.schema.json). Les fixtures sont **synthétiqu
|
||||
python3 -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
16 tests `unittest` (stdlib pur, **zéro pip**) : scoring, rétrogradation,
|
||||
paramètres canoniques, et **round-trip end-to-end** generator → parser Publiciste
|
||||
17 tests `unittest` (stdlib pur, **zéro pip**) : scoring, rétrogradation,
|
||||
paramètres canoniques, conformité des fixtures au **contrat d'entrée**
|
||||
(`brief.schema.json`), et **round-trip end-to-end** generator → parser Publiciste
|
||||
→ `projets_master.json` validé contre le schéma. Réutilise le parser + le
|
||||
validateur du Publiciste (aucune duplication · workflow #5). Un oracle
|
||||
`jsonschema` est activé en plus s'il est installé.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"required": ["projet"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"projet": { "type": "string", "pattern": "^P0[1-9]$" },
|
||||
"projet": { "type": "string", "pattern": "^P0[1-9]$", "description": "Code projet canonique (P01..P09) · domaine ancré sur CLAUDE.md §Projets." },
|
||||
"nom": { "type": "string" },
|
||||
"synthetique": { "type": "boolean", "description": "true = fixture de test, jamais publiable." },
|
||||
"sources": {
|
||||
|
||||
@@ -272,6 +272,11 @@ def render_version_json(brief: dict, bilan: dict, generated_at: str,
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Vérification des paramètres canoniques dans le rendu (axe 4, honnêteté).
|
||||
# --------------------------------------------------------------------------- #
|
||||
# ORACLE INDÉPENDANT (à dessein) : ces 6 marqueurs sont une copie DÉLIBÉRÉMENT
|
||||
# distincte de model.CANONICAL — c'est ce qui permet à check_canonical() de
|
||||
# PROUVER que le rendu (bâti depuis CANONICAL) émet bien chaque valeur. Les
|
||||
# dériver de CANONICAL rendrait le contrôle tautologique (vert vacux). La liste
|
||||
# est plutôt ancrée statiquement à CLAUDE.md #9/#10 par ci/check_readme_claims.
|
||||
_CANONICAL_MARKERS = ["3 %", "8.5 %", "52 %", "USD + DOP", "Cardnet", "Letter US"]
|
||||
|
||||
|
||||
|
||||
@@ -175,6 +175,20 @@ class RoundTripTest(unittest.TestCase):
|
||||
errs = pub_validator.validate(version, _schema("version.schema.json"))
|
||||
self.assertEqual(errs, [], f"version.json non conforme : {errs}")
|
||||
|
||||
def test_input_briefs_validate_against_brief_schema(self):
|
||||
# `brief.schema.json` est le « Contrat d'entrée » documenté (README §Contrat
|
||||
# d'entrée) : les deux fixtures DOIVENT s'y conformer. Le brief « incomplet »
|
||||
# l'est au sens SÉMANTIQUE (prix absents → placeholders/rétrogradation), pas au
|
||||
# sens SCHÉMA (le schéma admet null/champ absent, seul `projet` est requis).
|
||||
# Sans ce test, le contrat d'entrée resterait un oracle orphelin : `_validate_brief`
|
||||
# ne garde que le code `projet`, jamais la forme complète du brief.
|
||||
with open(os.path.join(_GEN, "brief.schema.json"), encoding="utf-8") as fh:
|
||||
brief_schema = json.load(fh)
|
||||
_, pub_validator = _pub()
|
||||
for name in ("brief_complete.json", "brief_incomplete.json"):
|
||||
errs = pub_validator.validate(_brief(name), brief_schema)
|
||||
self.assertEqual(errs, [], f"{name} viole brief.schema.json : {errs}")
|
||||
|
||||
def test_full_pipeline_master_validates(self):
|
||||
# Génère 2 projets → parcours build_master → projets_master.json conforme.
|
||||
for name in ("brief_complete.json", "brief_incomplete.json"):
|
||||
|
||||
@@ -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)
|
||||
@@ -18,7 +18,7 @@
|
||||
"projet": {
|
||||
"type": "string",
|
||||
"pattern": "^P0[1-9]$",
|
||||
"description": "Code projet canonique (P01..P09)."
|
||||
"description": "Code projet canonique (P01..P09) · domaine ancré sur CLAUDE.md §Projets."
|
||||
},
|
||||
"template_version": {
|
||||
"type": "string",
|
||||
|
||||
@@ -5,6 +5,24 @@
|
||||
> d'incitation touristique **CONFOTUR** (Ley 158-01, République Dominicaine),
|
||||
> jusqu'ici référencé partout (RBAC, workflow vente) mais jamais généré.
|
||||
|
||||
> **⚠️ Statut migration V18 (2026-08-10) · à lire AVANT le reste de ce module.** Michel a émis
|
||||
> la [`DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md`](../../../DIRECTIVE_V18_MASTER_FEASIBILITY_ENGINE_20260810.md)
|
||||
> — un **Master Institutional Feasibility & Bankability Engine** à 18 sections dont la
|
||||
> **Section 13 · Juridique**. L'audit préalable
|
||||
> [`OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md`](../../../OTO_V18_MIGRATION_ARCHITECTURE_AUDIT_20260810.md)
|
||||
> (V12→V18, produit **avant tout code**) mappe cette Section 13 à **ce module** avec un verdict
|
||||
> **🟠 amorce partielle** (§3) : le générateur ci-dessous produit **le DocType CONFOTUR seul**,
|
||||
> alors que la Section 13 V18 attend aussi les **contrats types Promesa de compraventa ·
|
||||
> Fideicomiso d'adhésion · règlement HOA** — dont **aucun n'est généré aujourd'hui**. Cet écart
|
||||
> de périmètre est l'arbitrage produit **D-01** du
|
||||
> [`OPEN_DECISIONS_REGISTER.md`](../../OPEN_DECISIONS_REGISTER.md) (Section 13 : CONFOTUR seul ou
|
||||
> aussi Promesa/Fideicomiso/HOA), désormais dans le chemin critique V18 ; en amont, **D-06**
|
||||
> (approbation de l'audit de migration par Michel) est le gate d'entrée de **toute** la séquence
|
||||
> moteur. Tant que ces décisions ne sont pas rendues, **aucun code moteur V18 n'est produit**
|
||||
> (directive : « NE PAS coder avant l'audit approuvé » · anti-invention `CLAUDE.md` #6) : le
|
||||
> socle CONFOTUR ci-dessous reste réutilisable, mais **la doc ci-dessous décrit l'état commité
|
||||
> courant, pas la cible finale V18.**
|
||||
|
||||
## Ce que ça produit
|
||||
|
||||
`build` écrit deux fichiers de hand-off dans `out/` :
|
||||
|
||||
@@ -19,6 +19,14 @@ produit uniquement la **config** que l'agent Mobile consomme sur le poste/CI EAS
|
||||
| `app.config` Expo (thème, locales, navigation) | `eas build` (Expo 54) |
|
||||
| `eas.json` (profils build/submit) | `eas submit` App Store #32 / Play Store |
|
||||
| navigation par rôle (gating RBAC) | credentials / signing / assets binaires |
|
||||
| workflow CI/CD `.gitea/workflows/mobile-build.yml` (Sprint 5 P1) | exécution `eas build` sur runner EAS |
|
||||
|
||||
**CI/CD Mobile (Sprint 5 P1 · directive `DIRECTIVE_MOBILE_STORES_20260803.md`).** Le
|
||||
workflow Gitea Actions [`.gitea/workflows/mobile-build.yml`](../../../.gitea/workflows/mobile-build.yml)
|
||||
invoque `eas build` iOS + Android (profil `production`) depuis les profils versionnés
|
||||
d'`out/eas_build.json`. Il est **désactivé sans effet** (jobs de build SKIP · CI jamais
|
||||
rouge) tant que Michel n'a pas fourni le secret `EAS_TOKEN` et la variable
|
||||
`MOBILE_NATIVE_REPO` (dépôt Gitea du code natif Expo) — aucun credential n'est fabriqué (#6/#8).
|
||||
|
||||
## Zéro invention (#6)
|
||||
|
||||
@@ -33,6 +41,7 @@ Ce livrable ne fabrique **aucun fait** :
|
||||
| Tokens de marque `#0a0a12`/`#f0b429` · thème dark | `CLAUDE.md #4` (via `branding.py`) |
|
||||
| Devises USD/DOP | `CLAUDE.md #10` (via `branding.py`) |
|
||||
| Expo SDK **54** | roadmap Sprint 5 l.56 |
|
||||
| Fiche store : nb + énumération portails, langues | **injectés** depuis `role_navigation` (labels portails_spec) + seo (jamais recopiés) — seuls le style rédactionnel + mots-clés sont authoriaux (comme `app.name`) |
|
||||
| **bundleIdentifier · package · projectId EAS · version · credentials · appleId · ascAppId** | **`null` · `a_confirmer`** — jamais fabriqués (#6/#8) |
|
||||
|
||||
**Ancrage de sécurité** : un onglet n'est visible que pour les **rôles RBAC exacts**
|
||||
@@ -44,8 +53,8 @@ Publiciste (zéro `pip`).
|
||||
## Utilisation
|
||||
|
||||
```bash
|
||||
python3 app_config_gen.py build # écrit out/{app_config,eas_build,role_navigation}.json + MANIFEST.json
|
||||
python3 app_config_gen.py validate # schéma + 12 invariants (sort en erreur sinon)
|
||||
python3 app_config_gen.py build # écrit out/{app_config,eas_build,role_navigation,store_listing}.json + MANIFEST.json
|
||||
python3 app_config_gen.py validate # schéma + 15 invariants (sort en erreur sinon)
|
||||
python3 -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
@@ -56,6 +65,10 @@ python3 -m unittest discover -s tests -v
|
||||
- `eas_build.json` — objet `eas.json` (profils `development`/`preview`/`production`
|
||||
+ `submit.production` avec credentials `null`).
|
||||
- `role_navigation.json` — 1 onglet par portail, `roles_allowed` = surface RBAC exacte.
|
||||
- `store_listing.json` — fiche store FR/EN/ES (sous-titre, description courte/longue,
|
||||
mots-clés) **dérivée** : nombre + énumération des portails et langues **injectés**
|
||||
depuis `role_navigation`/seo (jamais recopiés · #6) ; limites de caractères App
|
||||
Store / Play validées au build.
|
||||
- `MANIFEST.json` — traçabilité (comptes, résumé par portail, marque, `a_confirmer`).
|
||||
|
||||
Sortie **déterministe** (tri stable, aucun horodatage) → diffable + re-générable ;
|
||||
@@ -64,8 +77,8 @@ validée par le **validateur maison Publiciste** contre `mobile.schema.json`.
|
||||
## Résultat courant
|
||||
|
||||
**5 onglets** (Ventes/Construction/Achat/Compta/Direction) · **44 rôles** couverts ·
|
||||
**3 langues** · **13 identifiants `a_confirmer`** (jamais fabriqués). Schéma + 12
|
||||
invariants verts.
|
||||
**3 langues** · **13 identifiants `a_confirmer`** (jamais fabriqués) · fiche store
|
||||
FR/EN/ES dérivée. Schéma + 15 invariants verts.
|
||||
|
||||
## Hors périmètre worker (VPS · #8)
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ en :
|
||||
identifiants de store `null` · a_confirmer)
|
||||
- eas_build.json -> objet `eas.json` (profils build/submit, credentials `null`)
|
||||
- role_navigation.json -> 1 onglet par portail, gated par la surface RBAC exacte
|
||||
- store_listing.json -> fiche store FR/EN/ES (sous-titre, descriptions, mots-clés)
|
||||
dérivée : portails/langues injectés depuis les sources (#6)
|
||||
- MANIFEST.json -> traçabilité (comptes, résumé par portail, marque, a_confirmer)
|
||||
|
||||
Sous-commandes :
|
||||
build [-o OUT] -> écrit les 4 fichiers ci-dessus
|
||||
build [-o OUT] -> écrit les 5 fichiers ci-dessus
|
||||
validate [-o OUT] -> (re)génère en mémoire, valide vs mobile.schema.json + invariants
|
||||
|
||||
Sortie déterministe (tri stable, aucun horodatage) -> diffable + re-générable.
|
||||
@@ -179,7 +181,44 @@ def _validate_bundle(bundle: dict) -> list[str]:
|
||||
if c.get(k) != v:
|
||||
errors.append(f"counts.{k} incohérent ({c.get(k)} != {v}).")
|
||||
|
||||
# Invariant 12 · déterminisme : deux builds successifs identiques.
|
||||
# Invariant 12 · fiche store : couvre EXACTEMENT les langues seo (aucune inventée).
|
||||
sl = bundle["store_listing"]
|
||||
sl_spec = spec["store_listing"]
|
||||
if sorted(sl["content"].keys()) != sorted(langs):
|
||||
errors.append("store_listing.content != langues seo (FR/EN/ES).")
|
||||
if sl["langues"] != list(langs):
|
||||
errors.append("store_listing.langues != langues seo.")
|
||||
if sl["limits"] != sl_spec["limits"]:
|
||||
errors.append("store_listing.limits != spec.store_listing.limits.")
|
||||
|
||||
# Invariant 13 · chaque champ de fiche respecte sa limite de caractères store
|
||||
# (App Store / Play — voir limits_source) et n'est jamais vide.
|
||||
for lang, fields in sl["content"].items():
|
||||
for champ, limit in sl_spec["limits"].items():
|
||||
val = fields.get(champ)
|
||||
if not val:
|
||||
errors.append(f"store_listing[{lang}].{champ} vide/absent.")
|
||||
elif len(val) > limit:
|
||||
errors.append(
|
||||
f"store_listing[{lang}].{champ} dépasse la limite store {limit} "
|
||||
f"caractères (={len(val)})."
|
||||
)
|
||||
|
||||
# Invariant 14 · ANTI-DÉRIVE : l'énumération des portails et les langues injectées
|
||||
# dans chaque description longue == la surface RÉELLE (labels role_navigation ·
|
||||
# langues seo) — la copie marketing ne peut pas mentir sur les portails/langues.
|
||||
portails_str = " · ".join(n["label"] for n in nav)
|
||||
langues_str = "/".join(lang.upper() for lang in langs)
|
||||
for lang, fields in sl["content"].items():
|
||||
full = fields.get("full_description", "")
|
||||
if portails_str not in full:
|
||||
errors.append(f"store_listing[{lang}].full_description n'injecte pas les portails réels.")
|
||||
if langues_str not in full:
|
||||
errors.append(f"store_listing[{lang}].full_description n'injecte pas les langues réelles.")
|
||||
if "{" in full or "}" in full:
|
||||
errors.append(f"store_listing[{lang}].full_description : placeholder non substitué.")
|
||||
|
||||
# Invariant 15 · déterminisme : deux builds successifs identiques.
|
||||
again = _build_bundle()
|
||||
if json.dumps(again, sort_keys=True, ensure_ascii=False) != json.dumps(bundle, sort_keys=True, ensure_ascii=False):
|
||||
errors.append("build non déterministe (deux passes divergent).")
|
||||
@@ -201,6 +240,7 @@ def cmd_build(args: argparse.Namespace) -> int:
|
||||
_write_json(os.path.join(out, "app_config.json"), bundle["app_config"])
|
||||
_write_json(os.path.join(out, "eas_build.json"), bundle["eas_build"])
|
||||
_write_json(os.path.join(out, "role_navigation.json"), bundle["role_navigation"])
|
||||
_write_json(os.path.join(out, "store_listing.json"), bundle["store_listing"])
|
||||
_write_json(os.path.join(out, "MANIFEST.json"), bundle["manifest"])
|
||||
|
||||
c = bundle["manifest"]["counts"]
|
||||
@@ -225,7 +265,7 @@ def cmd_validate(args: argparse.Namespace) -> int:
|
||||
c = bundle["manifest"]["counts"]
|
||||
print(
|
||||
f"✅ Validation OK — {c['onglets']} onglets, {c['roles_couverts']} rôles, "
|
||||
f"schéma + 12 invariants verts (identifiants a_confirmer)."
|
||||
f"schéma + 15 invariants verts (identifiants a_confirmer · fiche store dérivée)."
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -236,7 +276,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
pb = sub.add_parser("build", help="génère app_config.json / eas_build.json / role_navigation.json / MANIFEST.json")
|
||||
pb = sub.add_parser("build", help="génère app_config.json / eas_build.json / role_navigation.json / store_listing.json / MANIFEST.json")
|
||||
pb.add_argument("-o", "--out", default=_DEFAULT_OUT, help="dossier de sortie (défaut: ./out)")
|
||||
pb.set_defaults(func=cmd_build)
|
||||
|
||||
|
||||
@@ -3,7 +3,20 @@
|
||||
"title": "Config app mobile OTO Enterprise OS (Expo/EAS + navigation par rôle)",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["app_config", "eas_build", "role_navigation", "manifest"],
|
||||
"required": ["app_config", "eas_build", "role_navigation", "store_listing", "manifest"],
|
||||
"definitions": {
|
||||
"store_listing_lang": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["subtitle", "short_description", "full_description", "keywords"],
|
||||
"properties": {
|
||||
"subtitle": {"type": "string", "minLength": 1},
|
||||
"short_description": {"type": "string", "minLength": 1},
|
||||
"full_description": {"type": "string", "minLength": 1},
|
||||
"keywords": {"type": "string", "minLength": 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"app_config": {
|
||||
"type": "object",
|
||||
@@ -155,6 +168,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"store_listing": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["generated_from", "note", "limits", "limits_source", "langues", "content"],
|
||||
"properties": {
|
||||
"generated_from": {"type": "string", "minLength": 1},
|
||||
"note": {"type": "string", "minLength": 1},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["subtitle", "short_description", "full_description", "keywords"],
|
||||
"properties": {
|
||||
"subtitle": {"type": "integer", "minimum": 1},
|
||||
"short_description": {"type": "integer", "minimum": 1},
|
||||
"full_description": {"type": "integer", "minimum": 1},
|
||||
"keywords": {"type": "integer", "minimum": 1}
|
||||
}
|
||||
},
|
||||
"limits_source": {"type": "string", "minLength": 1},
|
||||
"langues": {"type": "array", "minItems": 1, "items": {"type": "string"}},
|
||||
"content": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["fr", "en", "es"],
|
||||
"properties": {
|
||||
"fr": {"$ref": "#/definitions/store_listing_lang"},
|
||||
"en": {"$ref": "#/definitions/store_listing_lang"},
|
||||
"es": {"$ref": "#/definitions/store_listing_lang"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"manifest": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -33,6 +33,35 @@
|
||||
{"champ": "eas_build.submit.production.ios.appleTeamId", "raison": "identifiant équipe Apple — hors repo (#6/#8)"},
|
||||
{"champ": "eas_build.submit.production.android.serviceAccountKeyPath", "raison": "clé de service Google Play — secret hors repo (#6/#8)"}
|
||||
],
|
||||
"store_listing": {
|
||||
"limits": {"subtitle": 30, "short_description": 80, "full_description": 4000, "keywords": 100},
|
||||
"limits_source": "App Store Connect (sous-titre 30 · mots-clés 100 · description 4000 caractères) · Google Play Console (description courte 80 · complète 4000 caractères)",
|
||||
"placeholders": {
|
||||
"n_portails": "nombre d'onglets = portails métier (len role_navigation)",
|
||||
"portails": "labels des portails joints par ' · ' (source portails_spec via role_navigation) — jamais recopiés à la main",
|
||||
"langues": "langues seo en capitales jointes par '/' (source seo_spec)"
|
||||
},
|
||||
"content": {
|
||||
"fr": {
|
||||
"subtitle": "Vos portails métier en poche",
|
||||
"short_description": "L'app compagnon de votre ERP OTO : {n_portails} portails métier, sécurisés par rôle.",
|
||||
"full_description": "OTO Enterprise OS est l'application mobile compagnon de la plateforme immobilière OTO.\n\nRetrouvez vos {n_portails} portails métier — {portails} — directement sur votre téléphone.\n\nSécurité par rôle (RBAC) : chaque portail n'expose que les fonctions autorisées à votre profil ; aucune donnée hors de votre périmètre.\n\nInterface luxury sombre et dorée, cohérente avec la console web. Disponible en {langues}. Devises USD et DOP.\n\nL'app ne réplique aucune logique métier : elle est la surface mobile sécurisée de votre ERP OTO Enterprise OS.",
|
||||
"keywords": "immobilier,ERP,CRM,ventes,construction,chantier,compta,portail,RBAC,OTO"
|
||||
},
|
||||
"en": {
|
||||
"subtitle": "Business portals on the go",
|
||||
"short_description": "The companion app for your OTO ERP: {n_portails} role-secured business portals.",
|
||||
"full_description": "OTO Enterprise OS is the mobile companion app for the OTO real-estate platform.\n\nAccess your {n_portails} business portals — {portails} — right from your phone.\n\nRole-based security (RBAC): each portal only exposes the features your profile is allowed to use; no data outside your scope.\n\nLuxury dark-and-gold interface, consistent with the web console. Available in {langues}. USD and DOP currencies.\n\nThe app replicates no business logic: it is the secure mobile surface of your OTO Enterprise OS ERP.",
|
||||
"keywords": "real estate,ERP,CRM,sales,construction,site,accounting,portal,RBAC,OTO,property"
|
||||
},
|
||||
"es": {
|
||||
"subtitle": "Tus portales en movilidad",
|
||||
"short_description": "La app complementaria de tu ERP OTO: {n_portails} portales seguros por rol.",
|
||||
"full_description": "OTO Enterprise OS es la aplicación móvil complementaria de la plataforma inmobiliaria OTO.\n\nAccede a tus {n_portails} portales de negocio — {portails} — desde tu teléfono.\n\nSeguridad por rol (RBAC): cada portal solo muestra las funciones que tu perfil tiene permitidas; ningún dato fuera de tu alcance.\n\nInterfaz luxury oscura y dorada, coherente con la consola web. Disponible en {langues}. Monedas USD y DOP.\n\nLa app no replica ninguna lógica de negocio: es la superficie móvil segura de tu ERP OTO Enterprise OS.",
|
||||
"keywords": "inmobiliaria,ERP,CRM,ventas,construcción,obra,contabilidad,portal,RBAC,OTO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notes": [
|
||||
"L'app mobile ne réplique AUCUNE logique métier : elle est la surface mobile des 5 portails rôle ERPNext (mêmes rôles que les Workspaces / le Chat OTOIA).",
|
||||
"Sécurité : un onglet n'est visible que pour les rôles RBAC du portail correspondant (roles_allowed = surface RBAC exacte, ancrée sur le contrat).",
|
||||
|
||||
@@ -117,10 +117,52 @@ def build_navigation(contract: dict, spec: dict, portails_spec: dict) -> list[di
|
||||
return nav
|
||||
|
||||
|
||||
def build_store_listing(spec: dict, nav: list[dict], langs: list[str]) -> dict[str, Any]:
|
||||
"""Fiche store (App Store / Play) DÉRIVÉE des sources · #6.
|
||||
|
||||
Le worker ne SOUMET pas aux stores (agent Mobile · #8) : il produit la fiche
|
||||
versionnable (sous-titre, descriptions, mots-clés FR/EN/ES). Les faits produit
|
||||
prêts à dériver — nombre de portails, énumération des portails, langues — sont
|
||||
INJECTÉS depuis la surface réelle (`role_navigation` = labels portails_spec,
|
||||
langues seo), jamais recopiés à la main : la copie marketing ne peut donc pas
|
||||
mentir sur les portails/langues. Seuls le style rédactionnel et les mots-clés
|
||||
sont des choix authoriaux (mêmes que `app.name`), portés par la spec.
|
||||
"""
|
||||
sl_spec = spec["store_listing"]
|
||||
portails_str = " · ".join(n["label"] for n in nav)
|
||||
langues_str = "/".join(lang.upper() for lang in langs)
|
||||
n_portails = str(len(nav))
|
||||
|
||||
def _render(text: str) -> str:
|
||||
return (
|
||||
text.replace("{n_portails}", n_portails)
|
||||
.replace("{portails}", portails_str)
|
||||
.replace("{langues}", langues_str)
|
||||
)
|
||||
|
||||
content = {
|
||||
lang: {champ: _render(text) for champ, text in sl_spec["content"][lang].items()}
|
||||
for lang in langs
|
||||
}
|
||||
return {
|
||||
"generated_from": "mobile_spec.json · store_listing (gabarits rédactionnels) + role_navigation (portails) + seo_spec (langues)",
|
||||
"note": (
|
||||
"Fiche store DÉRIVÉE : nombre/énumération des portails et langues injectés "
|
||||
"depuis les sources (jamais recopiés · #6). Le worker ne build ni ne submit "
|
||||
"aux stores — hand-off agent Mobile (#8)."
|
||||
),
|
||||
"limits": sl_spec["limits"],
|
||||
"limits_source": sl_spec["limits_source"],
|
||||
"langues": list(langs),
|
||||
"content": content,
|
||||
}
|
||||
|
||||
|
||||
def build_bundle(contract: dict, spec: dict, portails_spec: dict, langs: list[str], default_lang: str) -> dict[str, Any]:
|
||||
app_config = build_app_config(spec, langs, default_lang)
|
||||
eas_build = build_eas(spec)
|
||||
nav = build_navigation(contract, spec, portails_spec)
|
||||
store_listing = build_store_listing(spec, nav, langs)
|
||||
b = deps.branding
|
||||
|
||||
portails_meta = [
|
||||
@@ -180,5 +222,6 @@ def build_bundle(contract: dict, spec: dict, portails_spec: dict, langs: list[st
|
||||
"app_config": app_config,
|
||||
"eas_build": eas_build,
|
||||
"role_navigation": nav,
|
||||
"store_listing": store_listing,
|
||||
"manifest": manifest,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"generated_from": "mobile_spec.json · store_listing (gabarits rédactionnels) + role_navigation (portails) + seo_spec (langues)",
|
||||
"note": "Fiche store DÉRIVÉE : nombre/énumération des portails et langues injectés depuis les sources (jamais recopiés · #6). Le worker ne build ni ne submit aux stores — hand-off agent Mobile (#8).",
|
||||
"limits": {
|
||||
"subtitle": 30,
|
||||
"short_description": 80,
|
||||
"full_description": 4000,
|
||||
"keywords": 100
|
||||
},
|
||||
"limits_source": "App Store Connect (sous-titre 30 · mots-clés 100 · description 4000 caractères) · Google Play Console (description courte 80 · complète 4000 caractères)",
|
||||
"langues": [
|
||||
"fr",
|
||||
"en",
|
||||
"es"
|
||||
],
|
||||
"content": {
|
||||
"fr": {
|
||||
"subtitle": "Vos portails métier en poche",
|
||||
"short_description": "L'app compagnon de votre ERP OTO : 5 portails métier, sécurisés par rôle.",
|
||||
"full_description": "OTO Enterprise OS est l'application mobile compagnon de la plateforme immobilière OTO.\n\nRetrouvez vos 5 portails métier — OTO Ventes · OTO Construction · OTO Achat · OTO Compta · OTO Direction — directement sur votre téléphone.\n\nSécurité par rôle (RBAC) : chaque portail n'expose que les fonctions autorisées à votre profil ; aucune donnée hors de votre périmètre.\n\nInterface luxury sombre et dorée, cohérente avec la console web. Disponible en FR/EN/ES. Devises USD et DOP.\n\nL'app ne réplique aucune logique métier : elle est la surface mobile sécurisée de votre ERP OTO Enterprise OS.",
|
||||
"keywords": "immobilier,ERP,CRM,ventes,construction,chantier,compta,portail,RBAC,OTO"
|
||||
},
|
||||
"en": {
|
||||
"subtitle": "Business portals on the go",
|
||||
"short_description": "The companion app for your OTO ERP: 5 role-secured business portals.",
|
||||
"full_description": "OTO Enterprise OS is the mobile companion app for the OTO real-estate platform.\n\nAccess your 5 business portals — OTO Ventes · OTO Construction · OTO Achat · OTO Compta · OTO Direction — right from your phone.\n\nRole-based security (RBAC): each portal only exposes the features your profile is allowed to use; no data outside your scope.\n\nLuxury dark-and-gold interface, consistent with the web console. Available in FR/EN/ES. USD and DOP currencies.\n\nThe app replicates no business logic: it is the secure mobile surface of your OTO Enterprise OS ERP.",
|
||||
"keywords": "real estate,ERP,CRM,sales,construction,site,accounting,portal,RBAC,OTO,property"
|
||||
},
|
||||
"es": {
|
||||
"subtitle": "Tus portales en movilidad",
|
||||
"short_description": "La app complementaria de tu ERP OTO: 5 portales seguros por rol.",
|
||||
"full_description": "OTO Enterprise OS es la aplicación móvil complementaria de la plataforma inmobiliaria OTO.\n\nAccede a tus 5 portales de negocio — OTO Ventes · OTO Construction · OTO Achat · OTO Compta · OTO Direction — desde tu teléfono.\n\nSeguridad por rol (RBAC): cada portal solo muestra las funciones que tu perfil tiene permitidas; ningún dato fuera de tu alcance.\n\nInterfaz luxury oscura y dorada, coherente con la consola web. Disponible en FR/EN/ES. Monedas USD y DOP.\n\nLa app no replica ninguna lógica de negocio: es la superficie móvil segura de tu ERP OTO Enterprise OS.",
|
||||
"keywords": "inmobiliaria,ERP,CRM,ventas,construcción,obra,contabilidad,portal,RBAC,OTO"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
"""Tests du générateur de config app mobile (Sprint 5 · Mobile).
|
||||
|
||||
Stdlib pur (unittest) — aucune dépendance pip. Couvre :
|
||||
- conformité au schéma + les 12 invariants du CLI (chemin nominal) ;
|
||||
- conformité au schéma + les 15 invariants du CLI (chemin nominal) ;
|
||||
- la cross-cohérence RBAC ↔ portails ↔ seo (onglets, rôles, labels/icônes, langues) ;
|
||||
- la fiche store dérivée (langues seo, limites de caractères, portails/langues
|
||||
injectés depuis la surface réelle) ;
|
||||
- l'anti-invention (#6) par injections négatives (identifiant de store fabriqué,
|
||||
rôles désynchronisés de la surface RBAC, langue inventée, token de marque altéré).
|
||||
rôles désynchronisés de la surface RBAC, langue inventée, token de marque altéré,
|
||||
description store dépassant la limite ou désalignée des portails réels).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -113,6 +116,38 @@ class TestNominal(unittest.TestCase):
|
||||
self.assertEqual(a, b)
|
||||
|
||||
|
||||
class TestStoreListing(unittest.TestCase):
|
||||
"""Fiche store : couverture langues, limites de caractères, injection sourcée."""
|
||||
|
||||
def setUp(self):
|
||||
self.bundle = build()
|
||||
self.sl = self.bundle["store_listing"]
|
||||
|
||||
def test_langues_egal_seo(self):
|
||||
langs, _ = deps.load_langs()
|
||||
self.assertEqual(sorted(self.sl["content"].keys()), sorted(langs))
|
||||
self.assertEqual(self.sl["langues"], list(langs))
|
||||
|
||||
def test_limites_caracteres_store(self):
|
||||
spec = deps.load_spec()
|
||||
limits = spec["store_listing"]["limits"]
|
||||
for lang, fields in self.sl["content"].items():
|
||||
for champ, limit in limits.items():
|
||||
self.assertTrue(fields.get(champ), f"{lang}.{champ} vide")
|
||||
self.assertLessEqual(len(fields[champ]), limit, f"{lang}.{champ} > {limit}")
|
||||
|
||||
def test_portails_et_langues_injectes(self):
|
||||
"""Anti-dérive : la copie cite les portails/langues RÉELS, pas une liste figée."""
|
||||
langs, _ = deps.load_langs()
|
||||
nav = self.bundle["role_navigation"]
|
||||
portails_str = " · ".join(n["label"] for n in nav)
|
||||
langues_str = "/".join(l.upper() for l in langs)
|
||||
for lang, fields in self.sl["content"].items():
|
||||
self.assertIn(portails_str, fields["full_description"])
|
||||
self.assertIn(langues_str, fields["full_description"])
|
||||
self.assertNotIn("{", fields["full_description"])
|
||||
|
||||
|
||||
class TestAntiInvention(unittest.TestCase):
|
||||
"""Injections négatives : chaque garde-fou doit MORDRE (invariant → erreur)."""
|
||||
|
||||
@@ -149,6 +184,18 @@ class TestAntiInvention(unittest.TestCase):
|
||||
errs = gen._validate_bundle(bad)
|
||||
self.assertTrue(any("ordre des onglets" in e for e in errs), errs)
|
||||
|
||||
def test_fiche_store_trop_longue_detecte(self):
|
||||
bad = copy.deepcopy(self.bundle)
|
||||
bad["store_listing"]["content"]["fr"]["subtitle"] = "x" * 200
|
||||
errs = gen._validate_bundle(bad)
|
||||
self.assertTrue(any("limite store" in e for e in errs), errs)
|
||||
|
||||
def test_fiche_store_portails_desalignes_detecte(self):
|
||||
bad = copy.deepcopy(self.bundle)
|
||||
bad["store_listing"]["content"]["en"]["full_description"] = "No portals listed here."
|
||||
errs = gen._validate_bundle(bad)
|
||||
self.assertTrue(any("full_description" in e for e in errs), errs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Caches Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
# Le dossier out/ EST commité (manifest de dépendances versionné) — voir README.
|
||||
@@ -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, 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`.
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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)" }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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é
|
||||
"""
|
||||
@@ -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}
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
import html
|
||||
import os
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from . import branding
|
||||
|
||||
@@ -88,8 +89,18 @@ def _hero_media(projet: dict[str, Any]) -> str:
|
||||
if not hero:
|
||||
return ' <div class="media"></div>'
|
||||
# Convention de chemin public (miroir de /opt/oto/sites/static/projets/pXX/).
|
||||
# Le nom de rendu réel peut contenir espaces/apostrophes/accents (« Vue
|
||||
# d'ensemble.png », « façade nord.webp ») : on percent-encode CHAQUE segment
|
||||
# dynamique de l'URL (code ET fichier) pour qu'elle reste valide DANS le
|
||||
# url('…') CSS. Sans cela, une apostrophe — une fois l'attribut style
|
||||
# HTML-décodé par le navigateur — referme la chaîne CSS et casse
|
||||
# silencieusement le background-image. quote() est le SEUL encodeur correct
|
||||
# pour un segment d'URL (html.escape ne l'est pas) : `code` est un identifiant
|
||||
# contrôlé (P01..P09) aujourd'hui, mais on l'encode par uniformité de
|
||||
# construction — le contexte, pas la donnée courante, dicte l'encodeur.
|
||||
# (_esc protège l'attribut HTML ; quote protège le contexte CSS interne.)
|
||||
code = projet["code"].lower()
|
||||
src = f"/static/projets/{code}/{hero['fichier']}"
|
||||
src = f"/static/projets/{quote(code)}/{quote(hero['fichier'])}"
|
||||
style = f"background-image:url('{_esc(src)}')"
|
||||
return f' <div class="media" style="{style}" role="img" aria-label="{_esc(projet.get("nom",""))}"></div>'
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user