diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9409b04..38e2a17 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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] diff --git a/05_activity_log/2026-08-05.md b/05_activity_log/2026-08-05.md index 819bd7f..fa31a12 100644 --- a/05_activity_log/2026-08-05.md +++ b/05_activity_log/2026-08-05.md @@ -1,5 +1,60 @@ # Activity Log · 2026-08-05 · Claude Code DTP Worker +## Session 181325 · FIX RÉEL défaut locale-runner : le gate est ROUGE en silence sous locale ASCII → **VERROUILLÉ UTF-8** + +**Choix de tâche.** `./run_ci.sh` au démarrage = **33 PASS**, arbre propre, roadmap +`ROADMAP_8_WEEKS_OR_LESS.md` **intégralement livrée/gatée in-repo** : les 15 promesses de +l'`acceptance_matrix` sont **toutes `status=in_repo`**, chaque reste (BIM runtime, builds EAS, +dépôts ONAPI, déploiement prod) explicitement `out_of_scope` VPS (#8). Un fan-out Explore +docstring-vs-code / prose-vs-numérique sur les livrables → **0 dérive** (repo propre). J'ai donc +attaqué un **axe de robustesse absent de la mémoire** et, contrairement aux 3 sweeps déterministes +précédents (hash-seed / locale-TZ / forward-compat, tous **CLEAN → attestation**), celui-ci a +révélé un **défaut RÉEL présent**. + +**La faille — la coercition PEP 538 masquait le vrai cas.** Le sweep locale-TZ (`154314`) a rejoué +les builds sous `LANG=C`, mais Python **coerce silencieusement `C` → `C.UTF-8`** (PEP 538) : sous +`LANG=C` seul, `getpreferredencoding()` = **UTF-8** → le mode d'échec ASCII n'a **jamais été +exercé**. En neutralisant la coercition (`PYTHONUTF8=0 PYTHONCOERCECLOCALE=0 LC_ALL=C`), +l'encodage par défaut redevient **`ANSI_X3.4-1968` (ASCII)** — la config d'un **runner minimal**. + +**Constat — 20 des 21 générateurs `build` PLANTENT.** Réplique de la boucle `check_artifacts` sous +locale ASCII neutralisée : **20/21 builds crashent** (`UnicodeEncodeError`), les 25 suites +échouent aussi. Cause : chaque générateur clôt `cmd_build` par un `print("✅ Bundle … généré" / +accents)` vers **stdout**, dont l'encodage suit la locale. Les fichiers sont écrits sans souci +(`encoding='utf-8'` explicite partout : 37 writes annotés, 0 write nu), mais le **print cosmétique +final** lève `UnicodeEncodeError` **après** écriture → exit non-nul → `check-artifacts` voit +« build a échoué » **ROUGE**. Vert aujourd'hui (runner UTF-8), **rouge en silence** le jour où +l'image runner change = classe « le runner n'est pas ton poste », mais **défaut, pas attestation**. + +**Correctif — single-source à la frontière d'invocation, 0 fichier de production.** `PYTHONUTF8=1` +(PEP 540) impose stdout/stdin/défaut-fichier en UTF-8 **quelle que soit la locale**. Posé **une +fois** en `env:` **au niveau workflow** de `ci.yml` → s'applique à **tous les jobs** (21 builds + +25 suites + gates statiques). Miroir local : `run_ci.sh` `export PYTHONUTF8=1`. **Aucun des 20 +modules touché** (anti-churn #5 — la fragilité est un contrat d'environnement d'invocation, pas 20 +bugs à patcher un par un ; le fix vit là où le runner est configuré, comme le gate `cd`-ait déjà +dans le module pour fixer le contrat de CWD). + +**Verrou — l'absence du fix est INVISIBLE sur runner UTF-8** (retirer l'`env:` reste vert +aujourd'hui) : classe de « vert trompeur » exactement traquée par ce dépôt. J'ancre donc le fix dans +le gate **existant** `check_ci_integrity.sh` (**INV-C**, pas de nouveau fichier gate #5) : il lit +l'`env:` top-level de `ci.yml` et exige `PYTHONUTF8: "1"`. **Teeth mutation-vérifié** : retrait de +la ligne `env:` → INV-C **mord** (exit 1) ; restauré → vert. INV-A/INV-B intacts (l'`env:` est +avant `jobs:` → hors du parseur de jobs). + +**Preuve end-to-end.** `run_ci.sh` sous `env -u PYTHONUTF8 PYTHONCOERCECLOCALE=0 LC_ALL=C` (config +qui était **totalement ROUGE** avant) = **33 PASS · 0 FAIL** grâce au `export` miroir. Build-loop +sous `PYTHONUTF8=1 + LC_ALL=C` = **21 artefacts byte-identiques** (0 dérive, le fix ne change aucun +octet de sortie). `run_ci.sh` normal = **33 PASS**. + +**Portée / anti-churn.** 4 fichiers : `ci.yml` (env), `run_ci.sh` (miroir), `check_ci_integrity.sh` +(INV-C verrou), `ci/README.md` (§2 détail INV-C + « Deux → Trois invariants »). **0 fichier de +production**, **0 artefact rebuild** (aucun `out/` touché → pas de rebuild `quality_report`, cf. +[[audit4big-rebuild-after-doc-edits]]), **0 chiffre inventé** (#6 — 20/21 dérivés de la boucle), +**0 commande VPS** (#8). run_ci = **33 PASS · 0 FAIL · 0 SKIP**. Mémoire neuve +*utf8-locale-runner-fix* (sœur de [[locale-tz-determinism-sweep]] dont elle **corrige l'angle mort**). + +--- + ## Session 164319 · FIX régression RED : `quality_report.json` périmé après édition de doc → **VERT** **Constat au démarrage.** `./run_ci.sh` = **32 PASS · 1 FAIL** — `check-artifacts` RED sur diff --git a/ci/README.md b/ci/README.md index 0f575d8..7fa56ac 100644 --- a/ci/README.md +++ b/ci/README.md @@ -181,7 +181,7 @@ puis commit. ### `check_ci_integrity.sh` Prouve, en lisant `.gitea/workflows/ci.yml`, que le **câblage** du workflow tient — car le job `gate` est le **seul verrou de merge** : un check absent de son `needs:` -ne bloque **rien**, même rouge. Deux invariants : +ne bloque **rien**, même rouge. Trois invariants : - **INV-A** — `gate.needs` == { tous les jobs définis } − { `gate` } − { jobs manuels } (un job manuel = gardé par `if: … workflow_dispatch …`, ex. `e2e-baseline`, légitimement hors du gate push/PR car il exige un serveur live). Détecte un job @@ -193,6 +193,21 @@ ne bloque **rien**, même rouge. Deux invariants : script (lib partagée `ci/lib.sh` — jamais un job propre). Un nouveau gate statique **non câblé** (script mort) ou **décâblé**, ou une **lib morte** (sourcée par personne), casse le check. +- **INV-C** — le workflow force le **mode UTF-8 de Python** (`env: PYTHONUTF8: "1"` + au niveau workflow → tous les jobs). Motif : chaque générateur clôt son `build` + par un `print("✅ …" / accents français)` vers stdout, dont l'encodage suit la + **locale du runner**. Sur une image runner ASCII (`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 et les 25 suites + plantent : **tout le gate passe ROUGE 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 » + (sœur des sweeps hash-seed / locale-TZ / forward-compat, mais ici un **défaut + réel présent**, pas une simple attestation). `PYTHONUTF8=1` (PEP 540) impose + stdout/stdin/défaut-fichier en UTF-8 **partout** → 20 builds qui crashaient + + les suites redeviennent locale-robustes (prouvé : `run_ci.sh` sous + `LC_ALL=C` neutralisé = 33 PASS). Comme l'absence de cet `env:` est **invisible** + sur un runner UTF-8, INV-C l'**ancre** : le retirer casse ce gate au lieu de + dériver en silence. `run_ci.sh` **exporte la même variable** (miroir fidèle). Ferme le trou laissé par les couvertures existantes (`audit_4big/registry`, `qa/regression/discovery`) qui ne prouvent l'appartenance au gate que des jobs de diff --git a/ci/check_ci_integrity.sh b/ci/check_ci_integrity.sh index f893cf5..9b829c6 100755 --- a/ci/check_ci_integrity.sh +++ b/ci/check_ci_integrity.sh @@ -148,6 +148,28 @@ while IFS= read -r script; do fi done < <(git ls-files 'ci/*.sh' | grep -vx 'ci/check_ci_integrity.sh') +# --- INV-C : le workflow force le mode UTF-8 de Python (indépendance locale) -- +# Chaque générateur clôt son `build` par un print(✅/accents) vers stdout ; sous +# une locale ASCII (runner minimal : LC_ALL=C, sans coercition PEP 538), ce print +# lève UnicodeEncodeError → `check-artifacts` ROUGE et les 25 suites plantent, +# EN SILENCE le jour où l'image runner change (VERT aujourd'hui car UTF-8). Le +# correctif est un `env: PYTHONUTF8: "1"` au niveau WORKFLOW (s'applique à tous +# les jobs). Comme son absence est invisible sur un runner UTF-8, on l'ancre ici : +# le retirer casse ce gate au lieu de dériver silencieusement. On lit l'`env:` +# top-level (indenté 2 espaces, AVANT `jobs:` → hors du parseur de jobs). +echo "== INV-C · le workflow force PYTHONUTF8 (I/O UTF-8 · indépendance locale runner) ==" +utf8_mode="$(awk ' + /^jobs:[[:space:]]*$/ { exit } # ne considérer que l’en-tête (avant jobs:) + /^env:[[:space:]]*$/ { inenv=1; next } + inenv && /^[^[:space:]]/ { inenv=0 } # fin du bloc env: (clé non indentée) + inenv && match($0, /^[[:space:]]+PYTHONUTF8:[[:space:]]*["'\'']?1["'\'']?[[:space:]]*$/) { print "1" } +' "$CI")" +if [[ "$utf8_mode" == "1" ]]; then + ok "workflow env: PYTHONUTF8=\"1\" → stdout/défaut-fichier UTF-8 sur tout runner (builds + suites locale-robustes)" +else + report "workflow SANS \`env: PYTHONUTF8: \"1\"\` top-level → gate locale-fragile (RED silencieux sur runner ASCII · cf. print ✅ en fin de build)" +fi + echo if [[ "$FAIL" -eq 0 ]]; then echo -e "\033[32m✅ Câblage CI intègre (gate agrège tout job non-manuel · gates statiques verrouillés).\033[0m" diff --git a/run_ci.sh b/run_ci.sh index 409af4c..74932d6 100755 --- a/run_ci.sh +++ b/run_ci.sh @@ -73,6 +73,13 @@ cd "$ROOT" || exit 3 CI_YML=".gitea/workflows/ci.yml" [ -f "$CI_YML" ] || { echo "run_ci.sh : $CI_YML introuvable." >&2; exit 3; } +# Miroir de l'`env:` workflow de ci.yml — force le mode UTF-8 de Python pour +# tous les jobs rejoués ici, de sorte que ce miroir local reproduise fidèlement +# le gate même si le poste tourne sous une locale ASCII (LC_ALL=C). Sans cela, +# les générateurs (print ✅/accents en fin de `build`) et les suites planteraient +# ici alors qu'ils passent en CI (cf. ci.yml env PYTHONUTF8 · check_ci_integrity INV-C). +export PYTHONUTF8=1 + MODE="all" case "${1:-}" in --static) MODE="static" ;;