diff --git a/05_activity_log/2026-08-02.md b/05_activity_log/2026-08-02.md new file mode 100644 index 0000000..6abbc60 --- /dev/null +++ b/05_activity_log/2026-08-02.md @@ -0,0 +1,41 @@ +# Activity log · 2026-08-02 · Claude Code DTP Worker + +## Sprint 8 · buffer · Publiciste — gate d'ANCRAGE des DEVISES du site public sur CLAUDE.md #10 + +**Contexte** — Série anti-dérive Sprint 8 (CLAUDE.md #6 « zéro invention de +chiffres »). Le bloc `Publiciste/branding` existant ancre les **4 tokens DESIGN** +de CLAUDE.md #4 (2 couleurs + 2 typographies) sur leurs 4 copies. Mais le MÊME +module publiciste porte une **2ᵉ constante constitutionnelle** — les **devises +CLAUDE.md #10 (« USD + DOP »)** — recopiée à 4 endroits data-derived, **aucun +gaté**, tous invisibles à `check_artifacts` (pas un `out/*.json`) : + +1. `lib/branding.py` `DEVISE_PRIMAIRE`/`DEVISE_SECONDAIRE` — constantes auto- + étiquetées « contrainte #10 · USD + DOP », sans gate ; +2. `lib/generator.py` `_fmt_usd`/`_fmt_dop` — les **formateurs qui IMPRIMENT le + code devise sur la grille de prix du site public** `vente.otov7.com` (sortie + face-public : une devise hors #10 = mandat trahi en silence) ; +3. `lib/generator.py` footer HTML « Prix … en USD + DOP (Cardnet) » ; +4. `tests/test_publiciste.py` — l'oracle hardcodé `assertIn("USD 150 000", …)`, + une copie de plus jamais comparée à CLAUDE.md. + +Aucune suite `tests/` (qui teste des **fonctions** de rendu, pas l'ancre à +CLAUDE.md) n'attrape la dérive : renommer la devise #10 et aligner le module +reste vert alors que le site émettrait la mauvaise devise ; ne rien bouger reste +vert alors que `branding.py` se dit « #10 » en mentant. + +**Gate ajouté** (`ci/check_readme_claims.sh`, bloc « Publiciste/generator+branding +· DEVISES CANONIQUES ») — RE-PARSE les 2 devises depuis la ligne #10 de CLAUDE.md +(indépendamment de `exp` amont · aucun skip hérité) puis exige : +- **(a)** `branding.py` DEVISE_PRIMAIRE/SECONDAIRE == #10 (ordre inclus) ; +- **(b)** l'ensemble des codes émis par les formateurs `generator.py` == #10 ; +- **(c)** le footer HTML cite les 2 devises dans l'ordre + `(Cardnet)` ; +- **(d)** l'oracle du test == devise primaire #10. +Un claim absent échoue AUSSI (traçabilité #6 · anti-évaporation). + +**Vérif** — 5 checks verts sur l'arbre propre. **6 morsures** adversariales : +M1 branding primaire USD→EUR · M2 formateur `_fmt_usd`→EUR (hors #10) · M3 footer +USD+DOP→USD+EUR · M4 oracle test → EUR · **M5 CLAUDE.md #10 USD→EUR = cascade sur +les 4 copies simultanément** (preuve que l'ancre est CLAUDE.md, pas la cohérence +interne) · M6 footer supprimé → INTROUVABLE. Gates re-verts : +`check_readme_claims` · `check_docs` · `guard_constraints` · `check_ci_integrity` +tous exit 0. diff --git a/ci/check_readme_claims.sh b/ci/check_readme_claims.sh index 77d0d33..5fb044d 100755 --- a/ci/check_readme_claims.sh +++ b/ci/check_readme_claims.sh @@ -6131,6 +6131,124 @@ if b4 is not None: + (f" · absents={miss}" if miss else "") + (f" · en trop={extra}" if extra else "") + " → aligner le test") +# ============================================================================ +# Publiciste/generator+branding · DEVISES CANONIQUES du site public « ancrées +# sur CLAUDE.md #10 » (USD + DOP) — les libellés monétaires RÉELLEMENT ÉMIS sur +# vente.otov7.com, jamais gatés. +# ---------------------------------------------------------------------------- +# Le bloc branding #4 ci-dessus ancre les 4 tokens DESIGN. Mais le MÊME module +# porte une 2ᵉ constante constitutionnelle NON gatée : les DEUX devises de +# CLAUDE.md #10 (« **USD + DOP** devises »), recopiées à QUATRE endroits data- +# derived, tous invisibles à check_artifacts (pas un out/*.json) et jamais +# confrontés à #10 : +# (a) lib/branding.py DEVISE_PRIMAIRE/DEVISE_SECONDAIRE — constantes AUTO- +# étiquetées « contrainte #10 · USD + DOP », sans aucun gate ; +# (b) lib/generator.py `_fmt_usd`/`_fmt_dop` — les DEUX formateurs qui +# IMPRIMENT le code devise sur la grille de prix du site public +# (`f"USD {value:,.0f}"` / `f"DOP {value:,.0f}"`) ; c'est la SORTIE +# FACE-PUBLIC : une devise HORS #10 ici = mandat trahi en silence (#6) ; +# (c) lib/generator.py footer (émis dans le HTML) « Prix … en USD + DOP +# (Cardnet) » — la prose du pied de page du site ; +# (d) tests/test_publiciste.py — l'oracle HARDCODÉ « assertIn("USD 150 000", +# …) » : une COPIE de plus de la devise primaire, JAMAIS comparée à #10. +# Aucune suite tests/ (qui teste des FONCTIONS de rendu, pas l'ancre à +# CLAUDE.md) n'attrape la dérive : renommer la devise #10 (USD→EUR) et aligner +# le module reste vert alors que le site émettrait la mauvaise devise ; ne RIEN +# bouger reste vert alors que branding se dit « #10 » en mentant. Même patron +# EXACT que le bloc branding/#4 et faisabilite/#9-#10. On RE-PARSE les 2 devises +# depuis la ligne #10 de CLAUDE.md (indépendamment de `exp` amont · aucun skip +# hérité) et on exige : (a) les 2 constantes == #10 (ordre inclus) ; (b) +# l'ensemble des codes émis par les formateurs == #10 ; (c) le footer cite les 2 +# devises dans l'ordre ; (d) l'oracle du test == devise primaire. Un claim +# absent échoue AUSSI (traçabilité #6). +# ============================================================================ +try: + cm_raw10p = open("CLAUDE.md", encoding="utf-8").read() + l10p = re.search(r"^10\.\s+(.+)$", cm_raw10p, re.M) + if not l10p: + raise ValueError("contrainte #10 INTROUVABLE") + seg10p = [s for s in re.split(r"·", l10p.group(1)) if "devise" in _ai(s)] + mdev = re.search(r"\*\*(.+?)\*\*", seg10p[0]) if seg10p else None + if not mdev: + raise ValueError("segment devises #10 (« **… devises** ») illisible") + pub_cur = re.findall(r"[A-Z]{3}", mdev.group(1)) + if len(pub_cur) != 2: + raise ValueError(f"attendu 2 devises en gras, lu {pub_cur}") +except (OSError, ValueError) as e: + bad(f"Publiciste devises · CLAUDE.md #10 illisible/inattendu : {e}"); pub_cur = None +if pub_cur is not None: + good(f"Publiciste devises · CLAUDE.md #10 sain : primaire={pub_cur[0]} " + f"secondaire={pub_cur[1]}") + PUB = f"{D}/publiciste" + # (a) ANCRAGE : branding.py DEVISE_PRIMAIRE/SECONDAIRE == #10 (ordre inclus) --- + BRD = f"{PUB}/lib/branding.py" + try: + brd_src = open(BRD, encoding="utf-8").read() + except OSError as e: + bad(f"Publiciste devises · {BRD} illisible : {e}"); brd_src = None + if brd_src is not None: + dev_pat = { + "primaire": (r'DEVISE_PRIMAIRE\s*=\s*"([^"]+)"', pub_cur[0]), + "secondaire": (r'DEVISE_SECONDAIRE\s*=\s*"([^"]+)"', pub_cur[1]), + } + ddrift = [] + for k, (pat, want) in dev_pat.items(): + m = re.search(pat, brd_src) + got = m.group(1) if m else None + if got != want: + ddrift.append(f"{k}={got!r} (attendu {want!r})") + if ddrift: + bad(f"Publiciste devises · branding.py CONSTANTES DÉRIVENT de CLAUDE.md #10 : " + f"{' · '.join(ddrift)} → aligner branding.py (ou CLAUDE.md)") + else: + good(f"Publiciste devises · branding.py DEVISE_PRIMAIRE/SECONDAIRE == " + f"CLAUDE.md #10 ({pub_cur[0]}/{pub_cur[1]})") + # (b) SORTIE : les formateurs generator.py émettent EXACTEMENT les 2 devises #10 + GEN = f"{PUB}/lib/generator.py" + try: + gen_src = open(GEN, encoding="utf-8").read() + except OSError as e: + bad(f"Publiciste devises · {GEN} illisible : {e}"); gen_src = None + if gen_src is not None: + emis = re.findall(r'return f"([A-Z]{3}) \{value:[^}]+\}"', gen_src) + if set(emis) != set(pub_cur): + miss = sorted(set(pub_cur) - set(emis)); extra = sorted(set(emis) - set(pub_cur)) + bad(f"Publiciste devises · formateurs generator.py émettent {sorted(set(emis))} " + f"MAIS CLAUDE.md #10 = {sorted(set(pub_cur))}" + + (f" · absents={miss}" if miss else "") + + (f" · HORS #10={extra}" if extra else "") + + " → le site public émettrait une devise non conforme (#6)") + else: + good(f"Publiciste devises · formateurs generator.py émettent {sorted(set(emis))} " + f"== CLAUDE.md #10") + # (c) FOOTER émis dans le HTML : « en + (Cardnet) » ordre inclus --- + mft = re.search(r"en ([A-Z]{3}) \+ ([A-Z]{3}) \(Cardnet\)", gen_src) + if mft is None: + bad(f"Publiciste devises · {GEN} — pied de page « en + (Cardnet) » " + f"INTROUVABLE (claim absent = régression · #6)") + elif [mft.group(1), mft.group(2)] == pub_cur: + good(f"Publiciste devises · footer generator.py cite {pub_cur[0]} + {pub_cur[1]} " + f"== CLAUDE.md #10") + else: + bad(f"Publiciste devises · footer generator.py cite {mft.group(1)} + {mft.group(2)} " + f"MAIS CLAUDE.md #10 = {pub_cur[0]} + {pub_cur[1]} → aligner le footer") + # (d) ORACLE du test : « assertIn(" 150 000", self.html) » == devise primaire + TP = f"{PUB}/tests/test_publiciste.py" + try: + tp_src = open(TP, encoding="utf-8").read() + except OSError as e: + bad(f"Publiciste devises · {TP} illisible : {e}"); tp_src = None + if tp_src is not None: + mto = re.search(r'assertIn\("([A-Z]{3}) [\d ]+", self\.html\)', tp_src) + if mto is None: + bad(f"Publiciste devises · {TP} — oracle « assertIn(\" …\", self.html) » " + f"INTROUVABLE (le contrôle prix du test a disparu · régression)") + elif mto.group(1) == pub_cur[0]: + good(f"Publiciste devises · {TP} oracle prix == devise primaire #10 ({pub_cur[0]})") + else: + bad(f"Publiciste devises · {TP} oracle prix cite {mto.group(1)!r} MAIS devise " + f"primaire #10 = {pub_cur[0]!r} → aligner le test") + # ============================================================================ # Fiches agents · colonne « Job CI » — chaque nom de job cité DOIT exister dans # .gitea/workflows/ci.yml (même classe de « vert trompeur », côté câblage CI).