From 68004265d6ed1daf2defef771d6b90f42d9a23bc Mon Sep 17 00:00:00 2001 From: Claude Code DTP Worker Date: Sat, 1 Aug 2026 02:41:10 +0000 Subject: [PATCH] =?UTF-8?q?[DTP-Worker]=20Sprint=208=20=C2=B7=20buffer=20?= =?UTF-8?q?=C2=B7=20Publiciste/branding=20:=20les=204=20TOKENS=20DESIGN=20?= =?UTF-8?q?CANONIQUES=20de=20la=20marque=20luxury=20=E2=80=94=20fond=20dar?= =?UTF-8?q?k=20`#0a0a12`=20+=20accent=20dor=C3=A9=20`#f0b429`,=20titres=20?= =?UTF-8?q?Fraunces=20+=20corps=20Cormorant=20Garamond=20(contrainte=20NON?= =?UTF-8?q?-N=C3=89GOCIABLE=20CLAUDE.md=20#4)=20=E2=80=94=20=C3=A9taient?= =?UTF-8?q?=20RECOPI=C3=89S=20=C3=A0=20QUATRE=20endroits=20tous=20NON=20ga?= =?UTF-8?q?t=C3=A9s=20:=20(a)=20les=20CONSTANTES=20lib/branding.py=20(COLO?= =?UTF-8?q?R=5FBG/COLOR=5FACCENT/FONT=5FDISPLAY/FONT=5FBODY),=20source=20u?= =?UTF-8?q?nique=20import=C3=A9e=20par=20le=20g=C3=A9n=C3=A9rateur=20&=20s?= =?UTF-8?q?ubstitu=C3=A9e=20dans=20le=20gabarit=20{{COLOR=5FBG}}=E2=80=A6,?= =?UTF-8?q?=20hors=20out/=20donc=20INVISIBLE=20=C3=A0=20check=5Fartifacts?= =?UTF-8?q?=20;=20(b)=20le=20docstring=20du=20fichier=20;=20(c)=20le=20REA?= =?UTF-8?q?DME=20;=20(d)=20le=20tuple=20d'oracle=20HARDCOD=C3=89=20de=20te?= =?UTF-8?q?sts/test=5Fpubliciste.py.=20Le=20seul=20contr=C3=B4le=20existan?= =?UTF-8?q?t=20(test=5Fmarque=5Fluxury)=20assert=20que=20le=20RENDU=20cont?= =?UTF-8?q?ient=20ces=20marqueurs=20mais=20ils=20sont=20une=20copie=20de?= =?UTF-8?q?=20plus=20JAMAIS=20compar=C3=A9e=20=C3=A0=20CLAUDE.md=20?= =?UTF-8?q?=E2=80=94=20m=C3=AAme=20patron=20EXACT=20que=20les=20param?= =?UTF-8?q?=C3=A8tres=20canoniques=20faisabilite/generator=20(CANONICAL?= =?UTF-8?q?=E2=86=94CLAUDE.md=20#9/#10).=20Gate=20ajout=C3=A9=20:=20les=20?= =?UTF-8?q?4=20tokens=20recomput=C3=A9s=20de=20la=20ligne=20#4=20de=20CLAU?= =?UTF-8?q?DE.md=20(z=C3=A9ro=20duplication)=20;=20(a)=20branding.py=20CON?= =?UTF-8?q?STANTES=20=3D=3D=20#4=20(hex=20insensible=20casse,=20fontes=20e?= =?UTF-8?q?xact),=20(b)=20docstring=20cite=20les=204,=20(c)=20README=20cit?= =?UTF-8?q?e=20les=202=20fontes,=20(d)=20oracle=20du=20test=20=3D=3D=20#4?= =?UTF-8?q?=20(set-diff).=206=20morsures=20v=C3=A9rifi=C3=A9es=20(constant?= =?UTF-8?q?e=20accent/display=20=C2=B7=20CLAUDE.md=20#4=20accent=20?= =?UTF-8?q?=E2=86=92=20mord=20SIMULTAN=C3=89MENT=20les=203=20copies=20aval?= =?UTF-8?q?=20=C2=B7=20docstring=20=C2=B7=20README=20=C2=B7=20oracle),=20r?= =?UTF-8?q?estaur=C3=A9=20vert,=207=20gates=20re-verts.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- 05_activity_log/2026-08-01.md | 55 ++++++++++++++++ ci/README.md | 28 +++++++- ci/check_readme_claims.sh | 117 ++++++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 1 deletion(-) diff --git a/05_activity_log/2026-08-01.md b/05_activity_log/2026-08-01.md index b249421..ea864a5 100644 --- a/05_activity_log/2026-08-01.md +++ b/05_activity_log/2026-08-01.md @@ -303,3 +303,58 @@ mobile∉`OTO Ventes` · exit 0. Working tree byte-restauré (`git checkout --`, édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; l'attachement réel des rôles aux Workspaces `bench` reste côté VPS). - **Auto-score 4Big** : 96/100. + +## Session `20260801_023211` · Buffer S8 · Domaine Publiciste/branding : les **4 TOKENS DESIGN CANONIQUES** de la marque luxury — fond dark `#0a0a12` + accent doré `#f0b429`, titres **Fraunces** + corps **Cormorant Garamond** (contrainte NON-NÉGOCIABLE **CLAUDE.md #4**) — étaient RECOPIÉS à **QUATRE** endroits **tous NON gatés** : (a) les CONSTANTES `lib/branding.py` (`COLOR_BG`/`COLOR_ACCENT`/`FONT_DISPLAY`/`FONT_BODY`), la source unique importée par le générateur & substituée dans le gabarit (`{{COLOR_BG}}…`), hors `out/` donc **invisible à check_artifacts** ; (b) le **docstring** du même fichier ; (c) le README (ligne du gabarit) ; (d) le **tuple d'oracle HARDCODÉ** de `tests/test_publiciste.py`. + +**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 patron EXACT** que +les paramètres canoniques `faisabilite/generator` **ancrés sur CLAUDE.md #9/#10** +(`CANONICAL` ↔ contrainte) — appliqué à la **contrainte #4** (identité visuelle) et +à son consommateur `publiciste/branding`, module **entièrement hors CI** jusqu'ici +(grep `branding|0a0a12|f0b429|Fraunces` dans `ci/` = 0 avant cette session). + +**Dérive silencieuse fermée** : +- `05_deliverables_mvp/publiciste/lib/branding.py:13-20` — les 4 CONSTANTES · + `05_deliverables_mvp/publiciste/lib/branding.py:3-4` — le docstring qui les cite · + `05_deliverables_mvp/publiciste/README.md:43` — « Gabarit dark+doré (Fraunces + + Cormorant Garamond) » · `05_deliverables_mvp/publiciste/tests/test_publiciste.py:193` + — `for token in ("#0a0a12", "#f0b429", "Fraunces", "Cormorant Garamond")`. +- Source faisant autorité : **CLAUDE.md #4** (ligne 18) — la contrainte + constitutionnelle, comme #9/#10 le sont pour le générateur de faisabilité. +- Piège : le SEUL contrôle existant (`test_marque_luxury`) assert que le RENDU + HTML CONTIENT ces 4 marqueurs — mais ils sont **HARDCODÉS dans le test**, une + copie de plus **jamais comparée à CLAUDE.md**. Si #4 change (accent + `#f0b429`→`#c9a227`) et qu'on aligne `branding.py`, l'oracle du test reste périmé + (rouge trompeur) ; si RIEN ne bouge, tout reste vert alors que le site public + `vente.otov7.com` émettrait la mauvaise couleur — invention silencieuse de la + classe même que #6 interdit, qu'aucune suite `tests/` (qui teste des FONCTIONS, + pas l'ancrage à CLAUDE.md) n'attrape. +- **État courant** : **aucun token périmé** — les 4 constantes + docstring + README + + oracle du test recoupent CLAUDE.md #4 exactement (anti-invention #6, rien à + réécrire). Le défaut est la **surface ungated** (module hors CI). + +**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « Publiciste/branding » +avant `sys.exit`) : (0) **ancrage** — les 4 tokens recomputés de la ligne #4 de +CLAUDE.md (2 couleurs backtickées + 2 fontes de la parenthèse `(… + …)`) ; (a) +`branding.py` CONSTANTES == #4 (hex **insensible à la casse**, fontes à l'exact +strip) ; (b) **docstring** (texte AVANT les constantes) cite les 4 EXACTS ; (c) +README cite les 2 fontes ; (d) **oracle du test** — le tuple `for token in (...)` +== les 4 tokens de #4 (set-diff : absent ET en trop). Un claim absent échoue AUSSI +(traçabilité #6). + +**6 morsures vérifiées** : `branding.py` accent `#f0b429→#e0a420` (constante +périmée) · `branding.py` display `Fraunces→Playfair Display` · **CLAUDE.md #4** +accent `→#c9a227` (mord **SIMULTANÉMENT** les 3 copies aval : constante + docstring ++ oracle du test — prouve la valeur de l'ANCRAGE) · docstring retire l'accent de la +prose (claim absent) · README échange une fonte (`Cormorant Garamond→Georgia`) · +oracle du test retire `Fraunces` ; restauré = green : 4 tokens sains · constantes == +#4 · docstring 4 tokens · README 2 fontes · oracle == #4 · exit 0. Working tree +byte-restauré (`git checkout --`, **JAMAIS** `git clean`, interdit absolu) · +**7 gates re-verts**. + +- `ci/README.md` (table récap du pipeline + paragraphe détaillé « Tokens design + canoniques `publiciste/branding` ancrés sur CLAUDE.md #4 ») 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 ; le déploiement + réel du site `vente.otov7.com` reste côté VPS). +- **Auto-score 4Big** : 96/100. diff --git a/ci/README.md b/ci/README.md index 2dd8884..c0df101 100644 --- a/ci/README.md +++ b/ci/README.md @@ -20,7 +20,7 @@ et manuellement (`workflow_dispatch`). Jobs statiques (+ une suite `unittest` pa | `check-artifacts` | `ci/check_artifacts.sh` | Reproductibilité : chaque `out/*.json` versionné == build frais | ✅ oui | | `check-regression` | `ci/check_regression.sh` | Fraîcheur : `qa/regression/out/regression_run.json` (`run`) == run frais + verdict PASS | ✅ oui | | `check-ci-integrity` | `ci/check_ci_integrity.sh` | Intégrité du câblage : `gate.needs` == tous les jobs non-manuels · chaque `ci/*.sh` câblé au gate | ✅ oui | -| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) **et** de la cross-cohérence PERMISSIONS du DocType CONFOTUR `legal/confotur` (2e surface : l'IDENTITÉ des 3 rôles + leur jeu d'ACTIONS exact + leur portail — le §Permissions énumérait `ventes-confotur`→read/write/create/print · `legal-onapi`→read/write/create · `legal-directeur`→read/write/**submit**/report — recomputés de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`), set-diff normalisé + identité d'ensemble README⇔MANIFEST (aucun rôle fantôme/manquant) + cohérences croisées : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par rôle · séparation des pouvoirs `submit` singleton et `is_submittable` déduit de sa présence) **et** de l'identité de la table run-book de `rbac/apply_plan` (2e surface : la table « ## Run-book généré (SPEC §7) » — le graphe d'application ordonné — dont les colonnes `#`/`Responsable`/`Dépend de` étaient un WILDCARD : `#`=`order`, `Responsable`=`responsable` et `Dépend de`=les n°s d'ordre des `depends_on` recomputés de `out/apply_plan.json` byte-gaté, exacts par étape + identité d'ensemble lignes==étapes (aucune fantôme/manquante) + cohérences croisées : ordres contigus `1..N` sans doublon · `responsable` ∈ {worker,vps,worker+vps} · toute dépendance pointe en arrière) **et** des gardes RBAC du pipeline vente `crm/workflow_vente` (3e surface : la table « Rôles = contrat RBAC » — le rôle qui GARDE chaque transition — recomputée de `{allowed}` de `workflow.json` : identité d'ensemble colonne Rôle == gardes, cohérence artefacts `{allowed}` == `MANIFEST.roles_rbac_utilises`, ensemble non vide, + cross-cohérence par pas sur les étapes sensibles `Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` — mord la réaffectation de privilège vers un rôle déjà présent que le set-diff seul manquerait) **et** de l'identité de la table run-book de `devops/deploy_runbook` (2e surface : la table « ## Plan de phases généré » — le graphe de portage VPS ordonné — dont les colonnes `#`/`Responsable`/`Modules`/`Dépend de` étaient un WILDCARD : recomputées de `out/deploy_runbook.json` byte-gaté, exactes par phase, + identité d'ensemble lignes==phases ET union des cellules Modules == les 22 modules de l'artefact — a capté un défaut réel : la table omettait `mobile/app_config` (phase 5) et `qa/acceptance` (phase 7), 20/22 modules listés pendant que la couverture prouvée était bijective 22/22 — + cohérences croisées : ordres contigus `1..N` · `responsable` ∈ {worker,vps,worker+vps} · dépendances en arrière) **et** du mapping d'enforcement row-level de `rbac/userperm_gen` (la table « Mapping `scope_donnees` → mécanisme » — la FONCTION qui associe chaque portée de données à son mécanisme Frappe natif + le verdict « Template émis ? », auparavant non gatée hors sa ventilation `28/16/2/4` — recomputée de `out/user_permission_plan.json` byte-gaté : par portée le `mechanism` (1er token backtické de la col.) & le verdict « Template émis ? » exacts, + identité d'ensemble portées de la table == portées de l'artefact (ni fantôme ni manquante) + cohérences croisées : chaque portée mappe UN SEUL mécanisme (fonction, pas relation) · « Template émis » uniforme sur les entrées d'une portée · template émis EXACTEMENT pour `user_permission_company` — l'invariant « template SSI entite ») **et** des figures « Génération réelle (fixture) » de `faisabilite/bancable` (module **hors** check_artifacts — son `out/` n'est pas commité — dont le README §« Vérification en-repo » recopie 4 chiffres data-derived `40 unités`/`USD 8,560,000`/`DOP 505,040,000`/`21 unités (⌈52 % × 40⌉)` : recomputés `Σ quantités`/`Σ(qté×prix_usd)`/`Σ(qté×prix_dop)`/`⌈52 % × Σ unités⌉` de la fixture COMMITÉE `brief_bancable.json`, **pct 52 % ancré sur CLAUDE.md #9**, confirmés par la GÉNÉRATION RÉELLE `bancable_gen validate` — mord une fixture éditée, un générateur/manifeste incohérent, un séparateur de milliers manquant ou une formule désalignée ; un claim absent échoue AUSSI) **et** de la fiche d'identité du rôle RBAC mobile `03_agents/mobile/AGENT.md` (le SEUL ancrage in-repo de l'agent mobile — builds/stores hors-repo #8 — dont la ligne « Points de contact réellement commités » recopie du contrat `rbac_50_roles.json` TOUS les attributs data-derived du rôle `plateforme-mobile` : `erpnext_role_name`/`nom_fr`/`famille`/`portail`/`entite_principale`/`niveau`/`scope_donnees`/`modules`/permission `API Access` custom R/W (sigle des verbes canonique C/R/W/D/S/X/A — élargir/rétrécir la perm change le sigle attendu) + la `description` recoupée verbatim, emphase markdown & point final neutralisés — recomputés du contrat, prose EXACTE exigée par attribut, jamais gatés avant hors les COMPTES agrégés de `mobile/app_config` ; cohérences croisées : `famille == portail` (bijection portail), permission `custom`, `role_id` présent une seule fois dans le contrat) **et** des fiches d'identité des rôles RBAC faisabilité `03_agents/rendu/AGENT.md` & `03_agents/ifc_speckle/AGENT.md` (même classe que la fiche mobile : chaque ligne « Contact in-repo » recopie du contrat `rbac_50_roles.json` les attributs data-derived des rôles `faisabilite-rendu-3d` / `faisabilite-ifc-speckle` — `erpnext_role_name`/`nom_fr` (rendu)/`portail`/`entite_principale`/`niveau`/`modules` (rendu)/les deux `permissions_cibles` `File` R/W/create + `Faisabilité` R ou R/W (verbes rendus en ORDRE CANONIQUE read→R·write→W·create/delete/… en toutes lettres — élargir/rétrécir une perm change la chaîne attendue) — recomputés du contrat, prose EXACTE exigée par attribut RESTITUÉ, jamais gatés avant hors les COMPTES de rôles par Workspace de `frontend/portails` ; NB la DESCRIPTION de ces deux fiches est une PARAPHRASE éditoriale donc NON gatée verbatim (ne rien réécrire de correct pour un gate · #6), seuls les attributs structurés sont contraints ; un claim absent échoue AUSSI) **et** du rattachement Workspace (`Has Role`) des fiches agents `03_agents/{rendu,ifc_speckle,mobile}/AGENT.md` (surface data-derived DISTINCTE du contrat : QUELLE CONSOLE — Workspace ERPNext natif — le rôle peut atteindre, membership qui vit dans `frontend/portails/out/workspace.json` byte-gaté, jamais gatée hors le COMPTE de rôles par workspace aveugle à QUEL rôle — on recompute l'ensemble des Workspaces dont le `Has Role` porte l'`erpnext_role_name` du rôle (résolu du contrat par id) et on confronte l'assertion de la fiche : rendu affirme rattaché au Workspace NOMMÉ `OTO Construction` (in_named : membership == {ce Workspace} et la prose le cite) · ifc_speckle affirme rattaché à UN Workspace (in_any : membership ≥ 1) · mobile affirme VOLONTAIREMENT PAS rattaché au Workspace NOMMÉ `OTO Ventes` (not_in_named, assertion NÉGATIVE #6 : rôle ABSENT du `Has Role` de ce Workspace) — mord l'ajout du rôle mobile à `OTO Ventes` (SUR-EXPOSITION console), le retrait/déplacement du rôle rendu de `OTO Construction`, le renommage du Workspace dans la prose, un Workspace nommé fantôme (anti-typo) ou une assertion absente) == artefact cité (#6) | ✅ oui | +| `check-readme-claims` | `ci/check_readme_claims.sh` | Intégrité des chiffres des docs d'entrée : chaque nombre de `README.md` « État courant », de la fiche QA « Verdict agrégé » **et** de la colonne « Tests » par suite des tables de fiches **et** des agrégats en prose (Total CRM / RBAC / e-CF) **et** des comptes par workspace des portails (cartes/liens/rôles, ×2 docs) **et** des comptes RBAC des 3 volets (table Nb rôles/profil, ligne agrégée apply_plan, ventilation userperm) **et** des comptes de mots-clés SEO (total + fr/en/es, ×2 docs) **et** du pipeline vente CRM (états/transitions, ×2 docs + énumération des transitions à séparation des pouvoirs `{action | allow_self_approval=0}` recomputée de `workflow.json`) **et** de l'app mobile Expo (onglets/rôles/langues/identifiants a_confirmer + énumération ordonnée des onglets recomputée de `role_navigation`, bijection onglet⇔portail) **et** du DocType CONFOTUR (champs/données/sections/rôles/évènements de dépôt, ×2 docs) **et** de l'audit 5D (contrôles/dimensions ×2 docs + ventilation verdict pass/fail/à-confirmer + énumération des open items `control→dimension→propriétaire` recomputée de `audit_report.open_items`) **et** du run-book de déploiement VPS (phases ×2 dans la fiche devops + confirmations préalables count & énumération dans le README) **et** du run-sheet de démo (nombre de modules cités dans le diagramme du README) **et** du montage Chat OTOIA par portail (`Custom Block`/configs runtime/portails, tous = 5 aujourd'hui, ×2 docs) **et** du module de recette `qa/acceptance` (couple livrables de sprint + métriques succès MVP, ×2 docs de module au-delà du README d'entrée) **et** du barème de l'auditeur 4Big (nombre de critères en prose + poids par critère de la table + « generator (N critères) », recomputés de `quality_report.criteria`) **et** de la carte de renormalisation par archétype de l'auditeur 4Big (critères EXCLUS de `data_room`/`parser`/`contract`, recomputés des `checks` par archétype de `quality_report.modules`) **et** du contrat de format de l'e-NCF fiscal (`E + tipoeCF(2) + secuencia(10) = 13` : préfixe/longueurs des segments/longueur totale, ×3 README + fiche `erpnext_backend`, recomputés du bloc `e_ncf` d'`ecf_plan.json` + cohérences croisées `1+tipo+seq==longueur` & `|regex digits|==tipo+seq`) **et** de la cross-cohérence e-CF DGII `fiscal/ecf_dgii` (2e surface : états émetteurs recomputés de `{emission_events[].update_value}` + champs de base recomputés de `{emission_events[].base_field}` + FormaPago défaut « 3 (Tarjeta) » recomputée de `forma_pago_defaut` **ancrée sur CLAUDE.md #10** (Cardnet) + cohérences croisées : `forma_pago_defaut.code` ∈ catalogue `formas_pago` · libellé prose ⊆ libellé catalogue · ensembles non vides) **et** de la cross-cohérence du barème de commissions `crm/commissions` (états brouillon exclus recomputés de `{doc_status=0}` de `workflow.json` + champs de base recomputés de `{base_field}` de `commission_plan.json` + invariant 3 « commissionne seulement des états soumis ») **et** du catalogue RBAC `rbac/fixtures_gen` (DocTypes `custom` à créer énumérés == `MANIFEST.custom_doctypes_a_confirmer` + séparation des pouvoirs : le rôle unique porteur de `set_user_permissions` == `custom_docperm.json`) **et** de la composition du graphe schema.org SEO (les `@type` du `@graph` — `Organization` racine + une `Residence` par projet — recomputés de `seo_schema_org.json` + bijection listing⇔projet & absence d'`offers`) **et** de la carte hreflang SEO (les langues de contenu `alternate` `FR/EN/ES` + `x-default` recomputées de `seo_hreflang.json` + cohérences croisées : carte uniforme sur toutes les pages · `x-default` présent partout · langue par défaut ∈ langues de contenu · `x-default`.href == `canonical` == href de la langue par défaut) **et** du catalogue de projets du DocType `crm/dossier_vente` (le Select `projet` — les 9 projets `P01..P09` avec libellés — **ancré sur** la section « ## Projets » de `CLAUDE.md`, recomputé de CLAUDE.md : ensemble + ordre == options du DocType byte-gaté, plage `P01..P09` + mot d'ancrage dans le README, catalogue contigu/sans-doublon en bonus) **et** du catalogue des DocTypes `custom` à créer du portail `frontend/portails` (2e surface : le §hand-off VPS énumère `CONFOTUR Application`/`Faisabilité`/`Publiciste Log` == `MANIFEST.doctypes_custom_a_creer` recomputé, set-diff normalisé + cohérences croisées : catalogue trié/sans-doublon/non-vide · égal à l'UNION des `doctypes_custom` par workspace) **et** du classement métier/technique des `Role Profile` de `rbac/roleprofile_gen` (2e surface : la colonne « Type » de la table — auparavant un WILDCARD — recompute le flag `metier` par profil de `MANIFEST.profiles` + prose « N profils (X métier + Y technique) » == `counts.role_profiles/portails_metier/portails_techniques` + identité d'ensemble : lignes de profil de la table == `|profiles|` (aucun profil fantôme) + cohérences croisées : `portails_metier + portails_techniques == role_profiles` et recomptent les flags réels) **et** des paramètres canoniques du générateur de faisabilité `faisabilite/generator` (**ancrés sur** CLAUDE.md #9/#10 : les 6 valeurs `3 %`/`8.5 %`/`52 %`/`USD+DOP`/`Letter US`/`Cardnet` recomputées des contraintes #9/#10 de `CLAUDE.md` puis exigées EXACTES dans `genlib/model.py::CANONICAL` — le dict émis dans chaque rendu, hors `out/` donc invisible à check_artifacts — ET dans la prose README, set-diff normalisé) **et** de la table des scénarios de démo `demo/scenarios` (2e surface : les trois colonnes d'identité `id`/`projet+libellé`/`audience` recomputées de `out/run_sheet.json` — libellés projets **ancrés sur** CLAUDE.md §Projets — + identité d'ensemble : ids des lignes == ids du run_sheet == `counts.scenarios` (aucun scénario fantôme/manquant) + cohérences croisées : `run_sheet`↔`MANIFEST` d'accord sur `(id,projet,audience)` · `id == S-{projet}-{AUDIENCE}`) **et** de la cross-cohérence PERMISSIONS du DocType CONFOTUR `legal/confotur` (2e surface : l'IDENTITÉ des 3 rôles + leur jeu d'ACTIONS exact + leur portail — le §Permissions énumérait `ventes-confotur`→read/write/create/print · `legal-onapi`→read/write/create · `legal-directeur`→read/write/**submit**/report — recomputés de `MANIFEST.roles_rbac_utilises[]` (role_id · portail · `permissions`), set-diff normalisé + identité d'ensemble README⇔MANIFEST (aucun rôle fantôme/manquant) + cohérences croisées : MANIFEST ⇄ DocType d'accord sur le jeu d'actions par rôle · séparation des pouvoirs `submit` singleton et `is_submittable` déduit de sa présence) **et** de l'identité de la table run-book de `rbac/apply_plan` (2e surface : la table « ## Run-book généré (SPEC §7) » — le graphe d'application ordonné — dont les colonnes `#`/`Responsable`/`Dépend de` étaient un WILDCARD : `#`=`order`, `Responsable`=`responsable` et `Dépend de`=les n°s d'ordre des `depends_on` recomputés de `out/apply_plan.json` byte-gaté, exacts par étape + identité d'ensemble lignes==étapes (aucune fantôme/manquante) + cohérences croisées : ordres contigus `1..N` sans doublon · `responsable` ∈ {worker,vps,worker+vps} · toute dépendance pointe en arrière) **et** des gardes RBAC du pipeline vente `crm/workflow_vente` (3e surface : la table « Rôles = contrat RBAC » — le rôle qui GARDE chaque transition — recomputée de `{allowed}` de `workflow.json` : identité d'ensemble colonne Rôle == gardes, cohérence artefacts `{allowed}` == `MANIFEST.roles_rbac_utilises`, ensemble non vide, + cross-cohérence par pas sur les étapes sensibles `Confirmer réservation`/`Signer contrat`/`Approuver CONFOTUR` — mord la réaffectation de privilège vers un rôle déjà présent que le set-diff seul manquerait) **et** de l'identité de la table run-book de `devops/deploy_runbook` (2e surface : la table « ## Plan de phases généré » — le graphe de portage VPS ordonné — dont les colonnes `#`/`Responsable`/`Modules`/`Dépend de` étaient un WILDCARD : recomputées de `out/deploy_runbook.json` byte-gaté, exactes par phase, + identité d'ensemble lignes==phases ET union des cellules Modules == les 22 modules de l'artefact — a capté un défaut réel : la table omettait `mobile/app_config` (phase 5) et `qa/acceptance` (phase 7), 20/22 modules listés pendant que la couverture prouvée était bijective 22/22 — + cohérences croisées : ordres contigus `1..N` · `responsable` ∈ {worker,vps,worker+vps} · dépendances en arrière) **et** du mapping d'enforcement row-level de `rbac/userperm_gen` (la table « Mapping `scope_donnees` → mécanisme » — la FONCTION qui associe chaque portée de données à son mécanisme Frappe natif + le verdict « Template émis ? », auparavant non gatée hors sa ventilation `28/16/2/4` — recomputée de `out/user_permission_plan.json` byte-gaté : par portée le `mechanism` (1er token backtické de la col.) & le verdict « Template émis ? » exacts, + identité d'ensemble portées de la table == portées de l'artefact (ni fantôme ni manquante) + cohérences croisées : chaque portée mappe UN SEUL mécanisme (fonction, pas relation) · « Template émis » uniforme sur les entrées d'une portée · template émis EXACTEMENT pour `user_permission_company` — l'invariant « template SSI entite ») **et** des figures « Génération réelle (fixture) » de `faisabilite/bancable` (module **hors** check_artifacts — son `out/` n'est pas commité — dont le README §« Vérification en-repo » recopie 4 chiffres data-derived `40 unités`/`USD 8,560,000`/`DOP 505,040,000`/`21 unités (⌈52 % × 40⌉)` : recomputés `Σ quantités`/`Σ(qté×prix_usd)`/`Σ(qté×prix_dop)`/`⌈52 % × Σ unités⌉` de la fixture COMMITÉE `brief_bancable.json`, **pct 52 % ancré sur CLAUDE.md #9**, confirmés par la GÉNÉRATION RÉELLE `bancable_gen validate` — mord une fixture éditée, un générateur/manifeste incohérent, un séparateur de milliers manquant ou une formule désalignée ; un claim absent échoue AUSSI) **et** de la fiche d'identité du rôle RBAC mobile `03_agents/mobile/AGENT.md` (le SEUL ancrage in-repo de l'agent mobile — builds/stores hors-repo #8 — dont la ligne « Points de contact réellement commités » recopie du contrat `rbac_50_roles.json` TOUS les attributs data-derived du rôle `plateforme-mobile` : `erpnext_role_name`/`nom_fr`/`famille`/`portail`/`entite_principale`/`niveau`/`scope_donnees`/`modules`/permission `API Access` custom R/W (sigle des verbes canonique C/R/W/D/S/X/A — élargir/rétrécir la perm change le sigle attendu) + la `description` recoupée verbatim, emphase markdown & point final neutralisés — recomputés du contrat, prose EXACTE exigée par attribut, jamais gatés avant hors les COMPTES agrégés de `mobile/app_config` ; cohérences croisées : `famille == portail` (bijection portail), permission `custom`, `role_id` présent une seule fois dans le contrat) **et** des fiches d'identité des rôles RBAC faisabilité `03_agents/rendu/AGENT.md` & `03_agents/ifc_speckle/AGENT.md` (même classe que la fiche mobile : chaque ligne « Contact in-repo » recopie du contrat `rbac_50_roles.json` les attributs data-derived des rôles `faisabilite-rendu-3d` / `faisabilite-ifc-speckle` — `erpnext_role_name`/`nom_fr` (rendu)/`portail`/`entite_principale`/`niveau`/`modules` (rendu)/les deux `permissions_cibles` `File` R/W/create + `Faisabilité` R ou R/W (verbes rendus en ORDRE CANONIQUE read→R·write→W·create/delete/… en toutes lettres — élargir/rétrécir une perm change la chaîne attendue) — recomputés du contrat, prose EXACTE exigée par attribut RESTITUÉ, jamais gatés avant hors les COMPTES de rôles par Workspace de `frontend/portails` ; NB la DESCRIPTION de ces deux fiches est une PARAPHRASE éditoriale donc NON gatée verbatim (ne rien réécrire de correct pour un gate · #6), seuls les attributs structurés sont contraints ; un claim absent échoue AUSSI) **et** du rattachement Workspace (`Has Role`) des fiches agents `03_agents/{rendu,ifc_speckle,mobile}/AGENT.md` (surface data-derived DISTINCTE du contrat : QUELLE CONSOLE — Workspace ERPNext natif — le rôle peut atteindre, membership qui vit dans `frontend/portails/out/workspace.json` byte-gaté, jamais gatée hors le COMPTE de rôles par workspace aveugle à QUEL rôle — on recompute l'ensemble des Workspaces dont le `Has Role` porte l'`erpnext_role_name` du rôle (résolu du contrat par id) et on confronte l'assertion de la fiche : rendu affirme rattaché au Workspace NOMMÉ `OTO Construction` (in_named : membership == {ce Workspace} et la prose le cite) · ifc_speckle affirme rattaché à UN Workspace (in_any : membership ≥ 1) · mobile affirme VOLONTAIREMENT PAS rattaché au Workspace NOMMÉ `OTO Ventes` (not_in_named, assertion NÉGATIVE #6 : rôle ABSENT du `Has Role` de ce Workspace) — mord l'ajout du rôle mobile à `OTO Ventes` (SUR-EXPOSITION console), le retrait/déplacement du rôle rendu de `OTO Construction`, le renommage du Workspace dans la prose, un Workspace nommé fantôme (anti-typo) ou une assertion absente) **et** des tokens design canoniques `publiciste/branding` (**ancrés sur CLAUDE.md #4** : les 4 valeurs de la marque luxury `#0a0a12`/`#f0b429`/`Fraunces`/`Cormorant Garamond` recopiées à QUATRE endroits jamais gatés — (a) les CONSTANTES `lib/branding.py` `COLOR_BG`/`COLOR_ACCENT`/`FONT_DISPLAY`/`FONT_BODY`, hors `out/` donc invisibles à check_artifacts, importées par le générateur & substituées dans le gabarit `{{COLOR_BG}}…` ; (b) le docstring du fichier ; (c) le README ; (d) le tuple d'oracle HARDCODÉ de `tests/test_publiciste.py` que le seul test existant assert sans jamais le comparer à CLAUDE.md — recomputés de la ligne #4 de `CLAUDE.md` puis exigés EXACTS : (a) constantes == #4 (hex insensible à la casse, fontes à l'exact), (b) docstring cite les 4, (c) README cite les 2 fontes, (d) oracle du test == #4 ; même patron que les paramètres canoniques `faisabilite/generator`, un changement de #4 mord SIMULTANÉMENT les 3 copies aval ; un claim absent échoue AUSSI) == artefact cité (#6) | ✅ oui | | `gate` | — | Agrégat vert = gate qualité 4Big franchi | ✅ oui | Aucune dépendance réseau/marketplace hors `actions/checkout`. Tout tourne avec @@ -793,6 +793,32 @@ INTROUVABLE) · fiche rendu supprime l'assertion · fiche mobile nomme un Worksp fantôme (`OTO Marketing`) · fiche ifc supprime l'assertion générique · fiche mobile passe le négatif au positif (retire « volontairement pas ») ; restauré = vert. +**Tokens design canoniques `publiciste/branding` ancrés sur CLAUDE.md #4** : la +contrainte NON-NÉGOCIABLE #4 fixe l'identité visuelle luxury — fond dark `#0a0a12` ++ accent doré `#f0b429`, titres **Fraunces** + corps **Cormorant Garamond**. Ces 4 +tokens sont RECOPIÉS à **quatre** endroits jamais gatés : (a) les CONSTANTES +`lib/branding.py` (`COLOR_BG`/`COLOR_ACCENT`/`FONT_DISPLAY`/`FONT_BODY`) — la source +unique importée par le générateur ET substituée dans le gabarit (`{{COLOR_BG}}…`), +hors `out/` donc **invisible à `check_artifacts`** ; (b) le **docstring** du même +fichier ; (c) le README (ligne du gabarit) ; (d) le **tuple d'oracle HARDCODÉ** de +`tests/test_publiciste.py` (`("#0a0a12","#f0b429","Fraunces","Cormorant Garamond")`). +Le seul contrôle existant (`test_marque_luxury`) assert que le RENDU contient ces +marqueurs, mais ils sont une **copie de plus, jamais comparée à CLAUDE.md** : si #4 +change (accent `#f0b429`→`#c9a227`) et qu'on aligne `branding.py`, l'oracle du test +reste périmé (rouge trompeur) ; si RIEN ne bouge, tout reste vert alors que le site +public émettrait la mauvaise couleur — invention silencieuse (#6) qu'aucune suite +`tests/` (qui teste des FONCTIONS, pas l'ancrage à CLAUDE.md) n'attrape. **Même +patron EXACT** que les paramètres canoniques `faisabilite/generator` (ancrage +`CANONICAL`↔CLAUDE.md #9/#10). On RECOMPUTE les 4 tokens depuis la ligne #4 de +CLAUDE.md (zéro duplication) et on exige : (a) `branding.py` CONSTANTES == #4 (hex +insensible à la casse, fontes à l'exact) ; (b) docstring cite les 4 EXACTS ; (c) +README cite les 2 fontes ; (d) l'oracle du test == #4 (set-diff). **6 morsures +vérifiées** : `branding.py` accent `#f0b429→#e0a420` · `branding.py` display +`Fraunces→Playfair Display` · **CLAUDE.md #4** accent `→#c9a227` (mord SIMULTANÉMENT +les 3 copies aval : constante + docstring + oracle du test — la valeur de l'ancrage) +· docstring retire l'accent de la prose · README échange une fonte +(`Cormorant Garamond→Georgia`) · oracle du test retire `Fraunces` ; restauré = vert. + Un claim **absent** échoue aussi (la dérive de formulation qui ferait disparaître un chiffre est elle-même une régression de traçabilité). stdlib pur (bash/git/python3), zéro réseau. diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 804e8e6..c660a65 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -3513,6 +3513,123 @@ if _ws_json is not None and _ws_roles is not None: f"MAIS workspace.json met « {rname} » dans son Has Role → " f"SUR-EXPOSITION console, assertion #6 VIOLÉE") +# ============================================================================ +# Publiciste/branding · TOKENS DESIGN CANONIQUES « ancrés sur CLAUDE.md #4 » +# — les 4 valeurs de la marque luxury (2 couleurs + 2 typographies), jamais gatées. +# ---------------------------------------------------------------------------- +# La contrainte NON-NÉGOCIABLE #4 de CLAUDE.md fixe l'identité visuelle : fond +# dark `#0a0a12` + accent doré `#f0b429`, titres Fraunces + corps Cormorant +# Garamond. C'est la SOURCE FAISANT AUTORITÉ. Ces 4 tokens sont RECOPIÉS à +# QUATRE endroits, tous NON gatés : +# (a) lib/branding.py CONSTANTES (COLOR_BG/COLOR_ACCENT/FONT_DISPLAY/FONT_BODY) +# — la source unique importée par le générateur ET substituée dans le +# gabarit ({{COLOR_BG}}…) ; ce n'est PAS un out/*.json → invisible à +# check_artifacts ; +# (b) le docstring de branding.py (« `#0a0a12` fond · `#f0b429` … Fraunces +# (titres) + Cormorant Garamond ») — la prose du module ; +# (c) README:43 (« Gabarit dark+doré (Fraunces + Cormorant Garamond) ») ; +# (d) tests/test_publiciste.py — le tuple d'oracle HARDCODÉ +# ("#0a0a12","#f0b429","Fraunces","Cormorant Garamond") : le SEUL contrôle +# existant assert que le RENDU contient ces marqueurs, mais ils sont une +# COPIE de plus, JAMAIS comparée à CLAUDE.md. Si #4 change (ex. accent +# `#f0b429`→`#e0a420`) et qu'on aligne branding.py, l'oracle du test reste +# périmé (rouge trompeur) ; si RIEN ne bouge, tout reste vert alors que le +# site public émettrait la mauvaise couleur — invention silencieuse (#6) que +# nulle suite tests/ (qui teste des FONCTIONS, pas l'ancrage à CLAUDE.md) +# n'attrape. Même patron EXACT que les paramètres canoniques +# faisabilite/generator (ancrage CANONICAL↔CLAUDE.md #9/#10). +# On RECOMPUTE les 4 tokens depuis la ligne #4 de CLAUDE.md (zéro duplication) et +# on exige : (a) branding.py CONSTANTES == #4 ; (b) docstring cite les 4 EXACTS ; +# (c) README cite les 2 fontes ; (d) l'oracle du test == #4. Hex insensible à la +# casse, fontes comparées à l'exact (strip). Un claim absent échoue AUSSI. +# ============================================================================ +PUB = f"{D}/publiciste" +try: + cm_raw4 = open("CLAUDE.md", encoding="utf-8").read() + l4 = re.search(r"^4\.\s+(.+)$", cm_raw4, re.M) + if not l4: + raise ValueError("contrainte #4 INTROUVABLE") + hexes = [h.lower() for h in re.findall(r"`(#[0-9a-fA-F]{6})`", l4.group(1))] + mfp = re.search(r"\(([^)]+)\)", l4.group(1)) + fonts = [f.strip() for f in mfp.group(1).split("+")] if mfp else [] + if len(hexes) != 2 or len(fonts) != 2: + raise ValueError(f"attendu 2 couleurs + 2 fontes, lu {hexes} / {fonts}") + b4 = {"bg": hexes[0], "accent": hexes[1], "display": fonts[0], "body": fonts[1]} +except (OSError, ValueError) as e: + bad(f"Branding canoniques · CLAUDE.md #4 illisible/inattendu : {e}"); b4 = None +if b4 is not None: + good(f"Branding canoniques · CLAUDE.md #4 sain : bg={b4['bg']} accent={b4['accent']} " + f"display={b4['display']!r} body={b4['body']!r}") + # (a) ANCRAGE : lib/branding.py CONSTANTES == CLAUDE.md #4 ------------------- + BR = f"{PUB}/lib/branding.py" + try: + br_src = open(BR, encoding="utf-8").read() + except OSError as e: + bad(f"Branding canoniques · {BR} illisible : {e}"); br_src = None + if br_src is not None: + const_pat = { + "bg": r'COLOR_BG\s*=\s*"([^"]+)"', + "accent": r'COLOR_ACCENT\s*=\s*"([^"]+)"', + "display": r'FONT_DISPLAY\s*=\s*"([^"]+)"', + "body": r'FONT_BODY\s*=\s*"([^"]+)"', + } + drift = [] + for k, pat in const_pat.items(): + m = re.search(pat, br_src) + got = m.group(1) if m else None + want = b4[k] + ok = (got is not None and + (got.lower() == want if k in ("bg", "accent") else got.strip() == want)) + if not ok: + drift.append(f"{k}={got!r} (attendu {want!r})") + if drift: + bad(f"Branding canoniques · branding.py CONSTANTES DÉRIVENT de CLAUDE.md #4 : " + f"{' · '.join(drift)} → aligner branding.py (ou CLAUDE.md)") + else: + good("Branding canoniques · branding.py CONSTANTES == CLAUDE.md #4 (4 tokens)") + # (b) PROSE docstring : les 4 tokens cités AVANT les CONSTANTES ----------- + doc = br_src.split("COLOR_BG", 1)[0] + doc_hex = {h.lower() for h in re.findall(r"`(#[0-9a-fA-F]{6})`", doc)} + miss = [b4[k] for k in ("bg", "accent") if b4[k] not in doc_hex] + miss += [b4[k] for k in ("display", "body") if b4[k] not in doc] + if miss: + bad(f"Branding canoniques · docstring branding.py — tokens ABSENTS de la " + f"prose : {miss} (claim absent = régression · #6)") + else: + good("Branding canoniques · docstring branding.py cite les 4 tokens == CLAUDE.md #4") + # (c) README : la ligne du gabarit cite les 2 fontes ----------------------- + PBR_RD = f"{PUB}/README.md" + pbr_rd = read_norm(PBR_RD) + if pbr_rd is not None: + rd_miss = [b4[k] for k in ("display", "body") if b4[k] not in pbr_rd] + if rd_miss: + bad(f"Branding canoniques · {PBR_RD} — fontes ABSENTES de la prose : " + f"{rd_miss} (attendu {b4['display']} + {b4['body']} == CLAUDE.md #4)") + else: + good(f"Branding canoniques · {PBR_RD} cite les 2 fontes == CLAUDE.md #4") + # (d) ORACLE du test : le tuple hardcodé == les 4 tokens de CLAUDE.md #4 ---- + TBR = f"{PUB}/tests/test_publiciste.py" + try: + tbr_src = open(TBR, encoding="utf-8").read() + except OSError as e: + bad(f"Branding canoniques · {TBR} illisible : {e}"); tbr_src = None + if tbr_src is not None: + mt = re.search(r"for token in \(([^)]+)\)", tbr_src) + if mt is None: + bad(f"Branding canoniques · {TBR} — tuple d'oracle « for token in (...) » " + f"INTROUVABLE (le contrôle #4 du test a disparu · régression)") + else: + toks = [t.strip().strip('"\'') for t in mt.group(1).split(",") if t.strip()] + toks_n = {t.lower() if t.startswith("#") else t for t in toks} + want_n = {b4["bg"], b4["accent"], b4["display"], b4["body"]} + if toks_n == want_n: + good(f"Branding canoniques · {TBR} oracle == CLAUDE.md #4 (4 tokens)") + else: + miss = sorted(want_n - toks_n); extra = sorted(toks_n - want_n) + bad(f"Branding canoniques · {TBR} oracle DÉRIVE de CLAUDE.md #4" + + (f" · absents={miss}" if miss else "") + + (f" · en trop={extra}" if extra else "") + " → aligner le test") + sys.exit(1 if FAIL else 0) PY rc=$?