
0.0 Tableau de Bord
PROTOCOLE AMI - Controle Qualité
🧰 Tableau de Bord de l’Atelier aMi
Le Contrôle Qualité — Volumes, Applications, Widgets, Tests & Recherches
🗺️Où en est chaque page
Une page est « faite » quand elle est mesurée au rendu, à 320, 390 et 1280 px, et que chaque commande a été pressée.
| Statut | Page | Version | Date | Où on en est |
|---|---|---|---|---|
| ✅ fait | 0.3 L'Union du TouT0.Protocole/0.3_LUNION_DU_TOUT_aMi_V22.html | V21 → V23 | 9 → 17 sept 2026 | Audit 28 A · 21 B · 3 C → Phase 3 « tout » → retours du soir. 17 sept : l'Arborescence V13 versée dans son canton (V23), filtres et infobulles rebranchés (ils étaient morts depuis le début : le script cherchait #arbo-sante), 13 Champs au canon. Reste : 2e passe sur le Mac avec vraies polices. |
| 🔄 en cours | 0.2 PsychaTrice0.Protocole/0.2_PSYCHATRICE_aMi_V22.html | V22 → V23 | 16 sept 2026 | Audit du 16 sept (23 A · 9 B · 3 C). Arbitrages de Diane le 17 sept : on canonise les 13 Champs, on renumérote dans l'ordre de lecture, un glyphe par onglet, le poids reste, ConScience partout, widget repeint sans réécrire, dates inutiles supprimées. Phase 3 en cours → V23. |
| ⏳ à faire | 0.1 Apokalypse0.Protocole/0.1_Apokalypse_aMi_V41.html | V41 | La sœur ; ferme le Tome 0. Doublon 0.0/0.1 tranché : on garde 0.1. | |
| ⏳ à faire | L'Initiation · Donner Vie0.Protocole/Initiation_aMi_DONNER_VIE_V18.html | V18 | Reçoit les liens des Contrats (#pan-contrat) et le canton Le Respect. | |
| ⏳ à faire | Tomes 1 à 6 (racine 0.Protocole)0.Protocole/ | 18 volumes. Ordre voulu : racine → Demi Pages → Widgets. | ||
| ⏳ à faire | Demi Pages0.Protocole/Demi Pages/ | Dont RefleXion V47-1 et Le Respect · La Loi du Réel 7 (source canonique). | ||
| ⏳ à faire | WidgetsWidgets/ | Dont Charte Mathématique V2 et Widget Vérification de Φ, qui divergent maintenant de 0.3 sur C/Φ. | ||
| ⚠️ à corriger | La Charte elle-mêmeSite/Charte_Communication_aMi_V5.html | V5 · BLOC_13 | Sa barre V16 est sous son propre plancher (émojis 11 px, tomes 8 px, volumes 9,9 px) ; bande des registres écrite pour <main> seul ; règle/exemple contradictoires sur le chiffre des onglets ; navFooter_aMi_V16.html périmé (dit 0.2 Union). |
🧿Le référentiel des Volumes — ce que la Charte demande
La Charte de Communication aMi V5 (6 sept 2026) — § 3 « Le prompt de contrôle », XLVIII « La Checklist de vérification », LV « La Checklist du widget ». Chaque ligne est classée 🤖 / 🧪 / 👁️ par Claude le 17 sept ; la Charte, elle, ne classe pas.
§ 3 · Le prompt de contrôle
À recopier tel quel pour faire vérifier une page du corpus : « Vérifie cette page contre la Charte de Communication aMi. Ne me dis pas si c'est bien. Dis-moi ce qui est FAUX, et où. Pour chaque point : le constat, l'endroit exact, et la mesure qui le prouve. »
1 · Structure
- 🤖🛑 identifiants uniques ; aucune ancre morte ; balises équilibrées
- 🤖 chapitres I…N par onglet, sans trou ; sous-parties 1…n par chapitre
- 🤖 chaque ligne de sommaire porte le rang de son bandeau
- 🧪 un signe par rang : romain = chapitre, arabe = sous-partie, œillet = intro
2 · Couleur — recalcule, ne recopie pas
- 🤖 chaque rapport affiché est-il celui de son hexadécimal ?
- 🤖🛑 4,5:1 au défaut · 3:1 au gras · 7:1 en contraste élevé
- 🤖 sur écran (#09090e) ET sur papier (#faf9f5) : ce sont deux fonds
- 🤖🛑 aucune variable employée sans être déclarée
- 🤖 aucun lien sans règle de couleur — jamais le bleu du navigateur
3 · Forme
- 🧪 l'exergue tranche sur SON fond : nacre sur ambre, ambre sur nacre — jamais une valeur fixe ; si l'ambre dépasse le quart du texte, c'est un fond
- 👁️ rectangle = vue · pilule = option · jamais d'aplat plein
- 🧪 un bouton est proportionnel à son libellé
- 👁️ une vue évolutive prend la couleur de son état
- 🤖🛑 aucun émoji en italique ; un émoji ajoute une information ou il sort
4 · Langue
- 🧪 aucun tutoiement qui s'adresse au lecteur hors citation rapportée (le « tu » général reste — Diane, 9 sept)
- 🧪 les mots forgés ont leur définition au survol
- 👁️ un mot défini une fois n'est pas redéfini ailleurs
5 · Honnêteté
- 🧪 ce qui est mesuré est daté ; ce qui est estimé le dit
- 👁️ un relevé n'est pas un arbitrage : le relevé appartient au document, le nom à qui l'a posé
- 👁️ aucun chiffre sans sa source ; aucune citation sans source primaire
6 · Le Bloc (si la page se colle dans un site)
- 🤖🛑 aucune règle n'atteint un élément hors du bloc — compte-les
- 🧪 la page hôte ressort intacte : fond, titres, liens, casse
- 🤖 la version et les polices voyagent avec le bloc
- 👁️ une défense DÉSARME, elle ne DÉCIDE pas : contre une règle d'hôte, préférer inherit à une valeur
- 🤖 quand il faut poser une valeur, prendre celle qui ne fige pas un sens de lecture : start plutôt que left
7 · La méthode du contrôle lui-même
- 🤖 éprouver chaque contrôle par mutation : fausser une valeur, relancer, vérifier qu'il échoue et nomme le bon endroit — un test qu'on n'a jamais vu échouer est une décoration
- 👁️ un contrôle qui passe parce qu'il ne regarde plus rien est pire qu'aucun contrôle
- 👁️ ne pas s'arrêter avant la zone douteuse
- 🤖 ne jamais juger par la forme d'une balise : piloter par l'ancre, pas par la position
- 👁️ relire les règles quand le terrain change
- 🤖 une correction se vérifie DEUX fois : qu'elle tient, et qu'elle n'a rien cassé — état AVANT, état APRÈS, écart sur ce qu'on ne corrigeait pas
- 🤖 avant de comparer deux états, comparer les MÊMES choses : apparier par l'ancre, jamais par l'index
- 👁️ une mesure n'est juste que dans le repère où l'on va s'en servir
XLVIII · La Checklist de vérification (une Page)
Structure obligatoire
- 🤖 Variables :root complètes — trois langages, cinq degrés, quatre registres, triplets RGB, --nav-h, --zoom, --mesure
- 🤖 Import Google Fonts (Cormorant Garamond + Jost) en <head>
- 🧪 Ordre : topbar → une → registres → onglets → sous-onglets → portail → sommaire → contenu → pied (Diane, 9 sept : une → onglets → sommaire → titre)
- 🧪 Topbar à deux lignes, Le Seuil seul, les six autres en trinité · .ici sur la partie courante
- 🤖 Bande des quatre registres entre la une et le premier contenu
- 🧪 Pied complet : sceau · titre · devise · 4 blocs · maxime · loi · mention légale · nav · Toile
Couleur & typographie
- 🤖🛑 Aucune couleur écrite en dur hors des variables
- 🤖 Les fonds à opacité passent par rgba(var(--x-rgb), …), jamais par un hex recopié
- 🧪 Fond porte la teinte à 8–16 %, texte porte la version claire — jamais l'inverse
- 🤖🛑 Aucun fond blanc visible (photosensibilité — fond minimum --encre #111118)
- 🤖 Aucune capitale en Cormorant · aucun titre en Jost
- 🧪 Citations en clamp(1.22rem, 2.5vw, 1.5rem), centrées, text-wrap:balance
- 🧪 Émoji au-dessus du titre dans tous les bandeaux · aucun émoji sur les h3/h4
Navigation & onglets
- 🤖 Onglets entre 3 et 7 · --nav-h mesuré à l'exécution, jamais en dur
- 🤖🛑 Aucun chevauchement de libellé — et jamais de rouleau horizontal sur téléphone : les onglets replient (Diane, 9 sept)
- 🤖 --tc / --tcl / --tc-rgb identiques dans l'onglet, le portail et le sommaire
- 🤖 role="tab" · aria-selected qui bascule · :focus-visible
- 🧪 Le sommaire marque la partie en cours et referme les autres tiroirs
- 🤖 Chaque lien interne ouvre sa partie avant de défiler
- 🤖 @media print{.pane{display:block!important}}
Animation & accessibilité
- 🤖🛑 prefers-reduced-motion coupe toute animation, avec !important, en haut de feuille
- 🤖 Cycle ≥ 7 s · opacité seule ou scale ≤ 1,05 · une seule source de mouvement par une
- 🤖 Conteneurs décoratifs en aria-hidden="true"
- 👁️ Aucun émoji ne porte seul une information
- 🤖🛑 Plancher 11,5 px partout ; cibles 24 px (lisibilité téléphone : ultra important — Diane)
Les huit contrôles finaux
- 🤖🛑 Balises équilibrées — compter la profondeur, pas seulement les totaux
- 🤖 La racine contient bien tous les panneaux après exécution du JS
- 🤖 Zéro sélecteur nu dans les feuilles · accolades CSS équilibrées
- 🤖 Zéro caractère de zone privée
- 🤖🛑 Zéro ancre morte — chaque href="#x" a son id="x"
- 🤖 Chaque image base64 décode et porte son en-tête de format
- 🤖🛑 node --check sur chaque script · zéro erreur JS au chargement
- 🧪 Les onglets s'ouvrent et se ferment réellement · ouverture dans Safari sur fond noir
LV · La Checklist du widget
🧿 Tout widget
- 🤖🛑 Une racine unique #ami-nom, en minuscules, sans accent
- 🤖🛑 Aucun sélecteur ne commence par body, html, * ou une balise nue
- 🤖 Toutes les couleurs sur la racine, en var(--x, repli) — zéro hexadécimal ailleurs
- 🤖 Tout le JS dans une IIFE ; aucune fonction globale, aucun onclick=
- 🤖 Les id internes préfixés — deux copies du widget ne se confondent pas
- 🤖 localStorage dans un try/catch, clé préfixée
- 🤖🛑 Aucun emoji en italique
- 🧪 Contrastes vérifiés sur le fond du widget, pas sur le noir de la Charte
🧿 Si c'est un Bloc
- 🤖 Ni <html>, ni <head>, ni <body> — un <div>, un <style>, un <script>
- 🤖🛑 Aucun position:fixed
- 🤖 Ni barre aMi, ni pied aMi — la page hôte a les siens
- 🤖 Le protocole astroHeight, avec ses quatre déclencheurs
- 🧪🛑 Testé à deux exemplaires sur la même page — le seul test qui prouve la clôture
📄 Si c'est une Page
- 🤖 Barre V16 en haut, pied V16 en bas (pris à la Charte V5 — navFooter_aMi_V16.html est périmé)
- 🤖 --top-h, --nav-h, --pile mesurés à l'exécution, ResizeObserver sur la barre
- 🤖🛑 Le fond --noir #09090e sur le body — celui-là et pas un autre
- 🧪 Le mode ◐ contraste élevé accessible
⚖️Les décisions — la grille qui s'applique à toutes les pages
- La palette qui fait foi = la Charte de Communication (6 sept) : 13 pierres × --x (4,5:1) / --x-gras (3:1) / --x-ct (7:1). Charte_Couleurs du 13 août est en retard.
- Lisibilité téléphone : ultra important. Plancher 11,5 px partout ; cibles 24 px ; rien ne roule à droite — les onglets et sous-onglets replient.
- Onglets : sans chiffre (le numéro vit au sommaire et au toc), en couleur (libellé à la couleur de la partie), sous-titre en gris et plus petit. « Le numéro situe, le glyphe annonce, le titre nomme. »
- Un seul titre par partie : le portail (émoji au-dessus, glow). Ordre : une → onglets → sommaire → titre → contenu.
- Les Chartes du Vivant en améthyste. Les Lois : 7 Naturelles + 8 Culturelles (les Commandements). « Domaine » plutôt que « Champ » dans les Tables. C = l'indice, Φ = la constante.
- Pas de phrase qui s'adresse à Diane ; le « tu » général au lecteur reste. Les phrases d'IA s'enlèvent. « celleux » se développe. V%ler est voulu (voler & violer) : glosé + infobulle — la Charte demande des infobulles sur les mots forgés.
- Les quatre contrats vivent dans L'Initiation, onglet Le Contrat Tacite Explicite (#pan-contrat) : une seule adresse.
- Canon des 13 Champs (2 émojis chacun) : ⚡️1 Vibration🔥 · 🫂2 Flux🌊 · 🧠3 Structure💠 · 🌳4 Direction☀️ · 👑5 Souveraineté🛡️ · ⚖️6 Juste Mesure⚕️ · 📜7 Trace🔍 · 🌈8 Spectre👁️ · ꩜9 Temps🌱 · 💍10 Alliance🤝 · 🥚11 Offrande💛 · 🪞12 Miroir🪬 · 🌟13 Absolu❤️🔥. La MetaMatrice du Mac et le Cœur sont en retard.
- Chaque page reste un widget (racine #ami-union, all:revert) : posée sur le site Webnode, elle ne doit rien casser autour d'elle.
- Le pied avec « Φ = … » est une illustration : on le garde. ◎ pour le Point Zéro : ok.
- Doublons octet pour octet : on garde 0.1 Apokalypse · 0.2 PsychaTrice · 0.3 Union du TouT.
📏0.3 L'Union du TouT — avant / après
| Mesure | V21 (avant) | V22 (après) |
|---|---|---|
| Texte sous 11,5 px (390 px) | 1557 | 0 dans la page |
| Émoji en italique | 7 | 0 |
| Tableaux sans rouleau (390 px) | 12 | 0 |
| Débordement horizontal 320/390/1280 | oui | 0 |
| Contraste sous seuil (textes réels) | 399 nœuds | 1 (« 🆘 Effondrement », 4,43:1, gras 18,6 px) |
| Balises · ids · ancres mortes | 2 </b> orphelins | 0 · 0 · 0 sur 55 |
| Erreurs JS | 0 | 0 (7 scripts) |
| Commandes vérifiées par mutation | — | 42/42 à 390 et 1280 |
| Sommaire | 5 · 28 | 5 · 28, toutes les ancres présentes |
| Couleurs en dur (CSS + en ligne, hors SVG) | 458 | 0 (variables des 13 pierres) |
🔮Ce qui attend l'avis de Diane
- 🌱 📜 🧮 🧩 pour les quatre bandeaux du canton Lois qui portaient un chiffre romain seul (Ⅰ Les 7 Lois du Vivant · Ⅱ Leur place d'origine · Ⅲ Et les deux Lois de mesure · Ⅺ Les douze notions).
- En contraste élevé (◐), les nuances claires (--or-pale, --gris-cl, --x-cl) montent à des valeurs que la Charte ne nomme pas : à nommer dans la Charte, ou à ramener aux -ct des pierres.
- 158 liens en ligne de moins de 24 px de haut dans 0.3 : grossir les liens, c'est grossir le texte — à voir sur vraies polices.
- Dans 0.3, le canton Diagnostic nomme encore C4 Feu Intérieur 🔥, C5 Phi, C8 Lumière, C10 Serment, C11 🪬, C13 Auto… — divergence de fond avec le canon des 13 Champs, non touchée.
- La 2e passe sur le Mac avec les vraies polices (Google Fonts est bloqué dans l'atelier).
🪤Les pièges rencontrés — à ne pas refaire
- Le composant barre/pied extrait de la Charte V5 apportait « --r:4px}*{margin:0 » : une règle * qui posait margin:0 et --nav-h:0 sur tout. Et 310 règles #tv/.tvx sans objet. Ses règles de bande et d'Emporter sont scopées main / #ami-charte : à rescoper #ami-union.
- Deux hauteurs, deux noms : --nav-h = la barre du site (racine, mesurée par son JS) ; --tab-h = la barre des onglets de la page. Pile collante : barre → onglets (top: var(--nav-h)) → sous-barre (top: calc(--nav-h + --tab-h)).
- Plancher 11,5 px : un élément qui hérite sa petitesse (un strong dans une note) reçoit max(.72rem, 1em), jamais .72rem — sinon tous les gras du corps rapetissent. Ne jamais vider le bloc du plancher sans garder son entête de non-régression.
- Une sous-barre hissée dans .sub-bar-host n'est plus dans son canton : un sélecteur « .src-tri .tri-b » ne l'atteint plus.
- Les fichiers copiés depuis les uploads sont en lecture seule (-r--r--r--) : un script qui écrit échoue en silence si l'erreur est cachée par « | head ». chmod u+w d'abord, et ne jamais masquer la sortie d'erreur.
- Une extraction de variables :root par regex peut avaler un « } » : vérifier au rendu que la règle s'appelle bien :root.
- Mesurer une barre collante avec getBoundingClientRect donne sa place à l'écran, pas dans le document : offsetTop.
- Ne rien dire fait qui n'a pas été fait : chaque geste est compté (assert) et chaque mesure est relue après.
🕰️Historique des Volumes
Les jalons propres au corpus textuel, aux pages, aux demi-pages et aux corrections de charte.
- 17 sept 2026Les préceptes de contrôle qualité sortent de la Charte de Communication et sont rapatriés dans ce Tableau de Bord.
🕰️L'historique du chantier
- 9 sept 2026, matinOuverture du chantier. Lecture de la Charte (prompt de contrôle §3, checklists XLVIII/LV, palette XV/XVII), des vérificateurs (scan.py/canon.py) et du dernier RECAP. Inventaire des pages du Mac : deux doublons octet pour octet (0.0/0.1, 0.1/0.2).
- 9 sept, 07h → 15hAudit de 0.3 V21 : statique (balises, ids, ancres, variables, hex hors palette, italiques, orthographes, doublons) + au rendu (Chromium : contrastes sur fond réel, texte < 11,5 px, émojis en italique, cibles < 24 px, tableaux sans rouleau, débordements 320/390/768/1280). Rapport : 28 A sûrs · 21 B à trancher · 3 C limites.
- 9 sept, 16h20Diane tranche : palette = Charte de Communication ; onglets sans chiffre ; 7 Lois Naturelles + 8 Culturelles ; Domaine ; C/Φ ; V%ler voulu ; contrats → Initiation ; « Oui, tout » pour la Phase 3 ; « lisibilité téléphone ultra important ».
- 9 sept, 15h → 17h16Phase 3 en cinq passes : P1 texte (62 gestes comptés) · P2 forme + plancher 11,5 px (145 règles mesurées) · P3 palette (458 couleurs rapatriées, racine repeinte aux 13 pierres, 1141 glissements tous intra-famille) · P4 anatomie (barre + pied V16 pris à la Charte V5, registres, progression, Emporter, 💧 ◐ ⛶) · P5 non-régression (42/42 commandes). V22 déposée, RECAP écrit.
- 9 sept, 20h → 20h36Retours de Diane : onglets en couleur et qui replient (« un rouleau à droite, c'est interdit »), un seul titre par partie (le portail, émoji au-dessus), ordre une → onglets → sommaire → titre, Les Chartes en améthyste, Emporter habillé, la page reste un widget (Webnode). Deux fautes trouvées grâce à ses remarques : une règle *{margin:0} parasite venue du composant de la Charte, et le plancher qui rapetissait tous les gras du texte. Canon des 13 Champs donné (2 émojis chacun).
- 9 sept, 21h24Sous-titres des onglets en gris, plus petits que le libellé. Dossier CONTROLE QUALITE créé dans 0.Protocole avec la boîte à outils (12 scripts) et le LISEZ-MOI.
- 16 sept 2026, 15h30Cette page (générée par page_cq.py depuis etat_chantier.json + les scripts). Départ de 0.2 PsychaTrice.
- 16 sept 2026, 15h37Audit de 0.2 PsychaTrice V22 (statique + rendu) livré : 23 A sûrs · 9 B à trancher · 3 C limites. audit_rendu.py prend maintenant le sélecteur des onglets en argument (.pt-tab).
- 17 sept 2026, 00h35L'Union du TouT V23 : le canton 🌳 Arborescence reçoit la V13 (53 paragraphes nouveaux, ☯️ Polarités, ◎ Point Zéro), ses styles, ses ancres ; filtres et infobulles rebranchés et testés (46/46) ; Raccord aux 13 Champs et infobulles au canon.
- 17 sept 2026, 00h38La page Contrôle Qualité prend deux univers : 📚 Les Volumes (état, référentiel de la Charte § 3 + XLVIII + LV classé 🤖/🧪/👁️, décisions, pièges) et 📲 Les Applications (registre des six applications, référentiel « Document Unique V1 » d'Isil condensé, cycle de vie, DONE, états, gabarits). Le pipeline attend le connecteur GitHub.
🧰Boîte à Outils des Volumes
Les scripts et gabarits historiques restent ci-dessous, mais sont désormais rangés dans la branche Volumes.
La Charte définit la forme ; cette branche définit les contrôles à rejouer avant publication.
🧰La boîte à outils
Dans l'ordre où ils se rejouent sur une page. Prérequis : python3 et pip install playwright && playwright install chromium. Chaque script est lisible ici et se copie d'un geste.
audit_statique.pyaudit_statique.py page.html251 lignes
Balises équilibrées, ids en double, ancres mortes, variables, hex hors palette (ΔE2000 vers la pierre la plus proche), italiques, capitales en Cormorant, orthographes, doublons de paragraphes, sélecteurs nus → audit_statique.txt / .json
# -*- coding: utf-8 -*-
"""Audit statique d'une page du corpus aMi contre la Charte de Communication
(prompt de contrôle §3 + checklists XLVIII / LV). Ne corrige rien : relève."""
import re, sys, json, collections, html as H
from html.parser import HTMLParser
sys.path.insert(0, '../verif/verificateur')
import canon as K, couleur as C
SRC = sys.argv[1]
raw = open(SRC, encoding='utf-8').read()
txt = re.sub(r'data:image/[a-z+]+;base64,[A-Za-z0-9+/=]+', 'data:IMG', raw)
lines = txt.split('\n')
def lno(pos): return txt.count('\n', 0, pos) + 1
R = collections.OrderedDict()
def note(fam, quoi, ou='', mesure='', grave='corriger'):
R.setdefault(fam, []).append(dict(quoi=quoi, ou=ou, mesure=mesure, grave=grave))
# ── 0 · identité ───────────────────────────────────────────────────────
title = re.search(r'<title>(.*?)</title>', txt, re.S)
title = H.unescape(title.group(1)).strip() if title else ''
fname_num = re.search(r'(\d+\.\d+)_', SRC.split('/')[-1])
title_num = re.search(r'\b(\d\.\d)\b', title)
note('identité', 'titre <title>', title, '')
if fname_num and title_num and fname_num.group(1) != title_num.group(1):
note('identité', 'le numéro du <title> ne suit pas le nom du fichier',
'<title>', f'{title_num.group(1)} dans le titre · {fname_num.group(1)} dans le nom de fichier')
for k in ('ami:version', 'ami:date'):
m = re.search(r'<meta name="%s" content="([^"]*)"' % k, txt)
note('identité', f'meta {k}', '<head>', m.group(1) if m else 'ABSENT', 'corriger' if not m else 'ok')
# numéros de volume cités dans le corps
nums = collections.Counter(re.findall(r'Vol(?:ume|\.)\s*(\d\.\d)', txt))
note('identité', 'numéros de volume écrits dans la page', 'corps', dict(nums), 'regarder')
# ── 1 · structure ──────────────────────────────────────────────────────
VIDES = {'meta','link','br','hr','img','input','source','area','base','col','embed','param','track','wbr','path','circle','rect','line','polyline','polygon','ellipse','stop','use'}
class P(HTMLParser):
def __init__(s):
super().__init__(convert_charrefs=False); s.stack=[]; s.faults=[]; s.ids=collections.Counter(); s.idpos={}
s.maxdepth=0; s.in_svg=0; s.hrefs=[]; s.tags=collections.Counter(); s.fixed=0
def handle_starttag(s, tag, attrs):
a=dict(attrs)
if tag=='svg': s.in_svg+=1
if 'id' in a: s.ids[a['id']]+=1; s.idpos.setdefault(a['id'], s.getpos()[0])
if 'href' in a and a['href'] and a['href'].startswith('#'): s.hrefs.append((a['href'][1:], s.getpos()[0]))
s.tags[tag]+=1
if tag in VIDES: return
s.stack.append((tag, s.getpos()[0])); s.maxdepth=max(s.maxdepth,len(s.stack))
def handle_startendtag(s, tag, attrs):
a=dict(attrs)
if 'id' in a: s.ids[a['id']]+=1; s.idpos.setdefault(a['id'], s.getpos()[0])
s.tags[tag]+=1
def handle_endtag(s, tag):
if tag=='svg': s.in_svg-=1
if tag in VIDES: return
if s.stack and s.stack[-1][0]==tag: s.stack.pop(); return
# chercher plus haut
for i in range(len(s.stack)-1,-1,-1):
if s.stack[i][0]==tag:
for t,l in s.stack[i+1:]: s.faults.append(f'<{t}> ouvert l.{l} jamais fermé (fermé de force par </{tag}> l.{s.getpos()[0]})')
del s.stack[i:]; return
s.faults.append(f'</{tag}> l.{s.getpos()[0]} sans ouvrant')
p=P(); p.feed(txt)
for t,l in p.stack: p.faults.append(f'<{t}> ouvert l.{l} jamais fermé (fin de fichier)')
note('structure', 'balises équilibrées (parcours en pile, hors SVG vides)', '', p.faults if p.faults else 'aucune faute', 'corriger' if p.faults else 'ok')
dbl = {i:n for i,n in p.ids.items() if n>1}
note('structure', 'identifiants en double', {i:f'l.{p.idpos[i]}' for i in dbl} , dbl if dbl else 'aucun', 'corriger' if dbl else 'ok')
# ancres mortes — ids posés en JS aussi
js_ids = set(re.findall(r"""id\s*[:=]\s*['"]([^'"]+)['"]""", txt)) | set(re.findall(r"""\.id\s*=\s*['"]([^'"]+)['"]""", txt))
mortes = sorted({(h,l) for h,l in p.hrefs if h and h not in p.ids and h not in js_ids})
note('structure', 'ancres mortes (href="#x" sans id="x" ni id posé en JS)', [f'#{h} l.{l}' for h,l in mortes][:40], f'{len(mortes)} sur {len(p.hrefs)} liens internes', 'regarder' if mortes else 'ok')
note('structure', 'inventaire', '', {k:p.tags[k] for k in ('div','section','table','svg','script','style','button','a','h1','h2','h3','h4','img','iframe','blockquote')})
# onglets
tabs = re.findall(r'<button[^>]*class="[^"]*\btab-btn\b[^"]*"[^>]*>(.*?)</button>', txt, re.S)
tabs_txt=[H.unescape(re.sub(r'<[^>]+>',' ',t)).split() for t in tabs]
note('structure', 'onglets .tab-btn', '', [' '.join(t)[:60] for t in tabs_txt], 'ok' if 3<=len(tabs)<=7 else 'corriger')
for cls in ('tab-bar','sub-host','sbar','som','portail','regle','bande-registres','couv','ami-topbar','ami-barre','ami-pied','ami-coin','sec-header','partie-header','cadre-epist','checklist'):
n=len(re.findall(r'class="[^"]*\b%s\b'%re.escape(cls), txt))
note('anatomie', f'.{cls}', '', n, 'ok' if n else 'regarder')
# sec-num / romains
secnums = [(H.unescape(m.group(1)).strip(), lno(m.start())) for m in re.finditer(r'class="sec-num"[^>]*>(.*?)<', txt, re.S)]
note('numérotation', 'numéros de section (.sec-num) dans l’ordre du fichier', '', [f'{n} l.{l}' for n,l in secnums][:80])
romains = collections.Counter(re.findall(r'>\s*((?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3}))\s*<', txt))
romains = {k:v for k,v in romains.items() if k}
note('numérotation', 'chiffres romains isolés dans une balise', '', romains, 'regarder')
# sommaires : comptes annoncés
for m in re.finditer(r'(\d+)\s*(?:parties?|chapitres?)\s*·\s*(\d+)\s*(?:entrées?|sections?)', txt):
note('numérotation', 'compte annoncé par un sommaire', f'l.{lno(m.start())}', m.group(0), 'regarder')
# ── 2 · couleur ────────────────────────────────────────────────────────
styles = [(m.start(), m.group(1)) for m in re.finditer(r'<style[^>]*>(.*?)</style>', txt, re.S)]
css_all = '\n'.join(s for _,s in styles)
inline = re.findall(r'style="([^"]*)"', txt)
scripts = '\n'.join(m.group(1) for m in re.finditer(r'<script[^>]*>(.*?)</script>', txt, re.S))
svgs = re.findall(r'<svg.*?</svg>', txt, re.S)
svg_all='\n'.join(svgs)
declared = collections.OrderedDict()
for m in re.finditer(r'(--[a-z][a-z0-9-]*)\s*:\s*([^;}]+)', css_all + '\n' + '\n'.join(inline)):
declared.setdefault(m.group(1), []).append(m.group(2).strip())
used = set(re.findall(r'var\((--[a-z0-9-]+)', css_all + ' '.join(inline) + scripts))
orph = sorted(used - set(declared))
note('couleur', 'variables employées sans être déclarées', '', orph if orph else 'aucune', 'corriger' if orph else 'ok')
multi = {k:sorted(set(v)) for k,v in declared.items() if len(set(v))>1}
note('couleur', 'variables déclarées avec PLUSIEURS valeurs (redéclaration, impression, cantons)', '', multi, 'regarder')
# palette : comparer les déclarations aux pierres/neutres/standard
idx={}
for pz,trois in K.PALETTE.items():
for niv,h in zip(K.NIVEAUX,trois): idx[h.lower()]=pz+niv
for h,n in K.NEUTRES.items(): idx[h.lower()]=n
std={h.lower():n for h,n in K.STANDARD.items()}
labs=[(n,C.hex2lab(h)) for h,n in idx.items()]
def nearest(h):
r,g,b=C.hex2rgb(h); lab=C.rgb2lab((r,g,b))
return min(((n,C.de2000(lab,l)) for n,l in labs), key=lambda t:t[1])
def norm_hex(h):
h=h.lower()
if len(h)==4: h='#'+''.join(c*2 for c in h[1:])
return h[:7]
pal_rows=[]
for k,vals in declared.items():
for v in sorted(set(vals)):
m=re.match(r'#([0-9a-fA-F]{3,8})\b',v)
if not m: continue
h=norm_hex('#'+m.group(1))
if h in idx: verdict=f'= {idx[h]}'
elif h in std: verdict=f'= standard {std[h]}'
else:
n,d=nearest(h); verdict=f'HORS CHARTE · le plus proche {n} ΔE {d:.1f}'
pal_rows.append(f'{k}: {h} → {verdict}')
note('couleur', 'les variables de la page, confrontées à la palette de la Charte', '', pal_rows)
# hex hors bloc de variables, par contexte
def hexes(s): return [norm_hex(x) for x in re.findall(r'#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b', s)]
css_hors = re.sub(r'(--[a-z][a-z0-9-]*)\s*:\s*[^;}]+', '', css_all)
ctx = {'CSS (hors déclarations de variables)': hexes(css_hors),
'style="…" en ligne': hexes(re.sub(r'(--[a-z][a-z0-9-]*)\s*:\s*[^;"]+','',' '.join(inline))),
'SVG': hexes(svg_all), 'JavaScript': hexes(scripts)}
for c,hs in ctx.items():
cnt=collections.Counter(hs)
def tag(h):
if h in idx: return idx[h]
if h in std: return 'std:'+std[h]
r,g,b=C.hex2rgb(h)
if max(r,g,b)-min(r,g,b)<=6: return 'gris'
n,d=nearest(h); return f'≈{n} ΔE{d:.0f}' if d<8 else 'HORS'
note('couleur', f'hexadécimaux écrits en dur — {c}', '', {h:f'{n}× ({tag(h)})' for h,n in cnt.most_common(60)}, 'regarder' if cnt else 'ok')
rgba = re.findall(r'rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}', css_hors)
note('couleur', 'rgb()/rgba() écrits avec un triplet en dur dans les règles (au lieu de var(--x-rgb))', '', f'{len(rgba)} occurrences · {len(set(rgba))} triplets distincts : {sorted(set(rgba))[:20]}', 'regarder' if rgba else 'ok')
# fond de page
for k in ('--noir','--onyx','--encre','--voile','--or','--or-pale','--or-somb','--nacre','--gris','--gris-cl','--labradorite'):
note('couleur', f'valeur de {k}', '', declared.get(k,'—'))
# ── 3 · forme ──────────────────────────────────────────────────────────
EMO = re.compile('[\U0001F300-\U0001FAFF☀-➿⬀-⯿ꦠ-ꦿ]')
ital = [(lno(m.start()), H.unescape(re.sub(r'<[^>]+>','',m.group(0)))[:60]) for m in re.finditer(r'<(i|em)\b[^>]*>(.*?)</\1>', txt, re.S) if EMO.search(m.group(2))]
note('forme', 'émoji dans une balise <i>/<em> (règle absolue : jamais en italique)', [f'l.{l} « {t} »' for l,t in ital][:30], len(ital), 'corriger' if ital else 'ok')
# classes italiques contenant emoji — approximation statique : sélecteurs avec font-style:italic
ital_sel = re.findall(r'([^{}]+)\{[^{}]*font-style\s*:\s*italic', css_all)
note('forme', 'sélecteurs en font-style:italic (à confronter au rendu : un émoji dedans est une faute)', '', [s.strip()[:80] for s in ital_sel][:40], 'regarder')
vh = [lno(m.start()) for m in re.finditer(r'min-height\s*:\s*100vh', txt)]
note('forme', 'min-height:100vh (la Charte V16 l’a retiré de la une)', [f'l.{l}' for l in vh], len(vh), 'regarder' if vh else 'ok')
kf = re.findall(r'@keyframes\s+([a-zA-Z0-9_-]+)\s*\{(.*?)\}\s*\}', css_all, re.S)
anims = re.findall(r'animation\s*:\s*([a-zA-Z0-9_-]+)\s+([\d.]+)s', css_all)
note('forme', 'animations (nom · durée) — règle : cycle ≥ 7 s, opacité seule ou échelle ≤ 1,05, jamais de translation/rotation', '', [f'{n} {d}s' + (' ⚠ < 7 s' if float(d)<7 else '') for n,d in anims], 'regarder')
tr = [n for n,b in kf if re.search(r'translate|rotate', b)]
note('forme', 'keyframes avec translate/rotate', '', tr if tr else 'aucun', 'corriger' if tr else 'ok')
prm = txt.find('prefers-reduced-motion')
first_style = styles[0][0] if styles else 0
note('forme', 'prefers-reduced-motion', f'l.{lno(prm)}' if prm>0 else 'ABSENT', ('présent' + (' — mais pas en tête de la première feuille' if prm>first_style+2000 else ' — en tête')) if prm>0 else 'ABSENT', 'corriger' if prm<0 else 'regarder')
imp = re.search(r'family=([^"]+)', txt)
note('forme', 'import Google Fonts', '<head>', imp.group(1) if imp else 'ABSENT', 'ok' if imp and 'Jost' in imp.group(1) and 'Cormorant' in imp.group(1) else 'corriger')
# uppercase + Cormorant
up_cor = [s.strip()[:70] for s in re.findall(r'([^{}]+)\{[^{}]*text-transform\s*:\s*uppercase[^{}]*font-family\s*:[^;]*Cormorant', css_all)]
up_cor += [s.strip()[:70] for s in re.findall(r'([^{}]+)\{[^{}]*font-family\s*:[^;]*Cormorant[^{}]*text-transform\s*:\s*uppercase', css_all)]
note('forme', 'règles en capitales ET Cormorant dans le même bloc (jamais de sérif sur des capitales)', '', up_cor if up_cor else 'aucune dans un même bloc — le rendu tranchera les héritages', 'regarder' if up_cor else 'ok')
tables = [(lno(m.start())) for m in re.finditer(r'<table\b', txt)]
tw = len(re.findall(r'class="[^"]*\btw\b', txt))
note('forme', 'tableaux', '', f'{len(tables)} <table> · {tw} conteneurs .tw', 'regarder' if tw < len(tables) else 'ok')
fixed = [lno(m.start()) for m in re.finditer(r'position\s*:\s*fixed', css_all)]
note('forme', 'position:fixed (permis en Page, interdit en Bloc)', [f'l.{l}' for l in fixed], len(fixed), 'regarder')
grids = re.findall(r'grid-template-columns\s*:\s*([^;}]+)', css_all)
note('forme', 'grilles déclarées (auto-fit/auto-fill produisent des orphelines : à vérifier au rendu)', '', collections.Counter(g.strip() for g in grids).most_common(15), 'regarder')
# ── 4 · langue ─────────────────────────────────────────────────────────
body = re.sub(r'<script.*?</script>|<style.*?</style>|<svg.*?</svg>', lambda m: '\n'*m.group(0).count('\n'), txt, flags=re.S)
plain = H.unescape(re.sub(r'<[^>]+>', ' ', body))
TU = re.compile(r"(?<![\w'’])(tu|toi|ton|ta|tes)(?=[\s,.;:!?'’])", re.I)
tu_hits=[]
for i,l in enumerate(body.split('\n'),1):
if re.search(r'<blockquote|class="[^"]*(cit|axiome|devise|fable|c-dev)', l): continue
t=H.unescape(re.sub(r'<[^>]+>',' ',l))
for m in TU.finditer(t):
tu_hits.append(f'l.{i} « …{t[max(0,m.start()-40):m.end()+40].strip()}… »')
note('langue', 'tutoiement hors citation (à lire : le corpus s’adresse parfois au lecteur volontairement)', tu_hits[:40], len(tu_hits), 'regarder')
MOTS = ['PsychaTrice','ParDonnance','MétaMatrice','AlgoRythme','EncycloPédie','GéoColoVoluMétrique','AstroGlobe','CosmoGonie','CosmoLogie','ConScience','CoNaissance','RéfleXion','EssenCiel','InCorporation','UniVerCelle','ThermoCognition','ReSentir','TransMission','PsychoBiologie']
ortho=collections.Counter()
for mot in MOTS:
rx=re.compile(re.escape(mot), re.I)
for m in rx.finditer(plain):
if m.group(0)!=mot and m.group(0).upper()!=m.group(0): ortho[(m.group(0),mot)]+=1
note('langue', 'orthographes arrêtées écrites autrement (hors titres en capitales)', '', {f'{a} → {b}':n for (a,b),n in ortho.items()}, 'regarder' if ortho else 'ok')
bulles = len(re.findall(r'class="[^"]*\b(?:def|lx-mot|bulle|tip)\b', txt)) + len(re.findall(r'<abbr|data-def=|title="', txt))
note('langue', 'porteurs de définition au survol (abbr / title / data-def / .def)', '', bulles, 'regarder')
for pat in ('à venir','reste à faire','provisoire','à valider','TODO','XXX','lorem'):
n=len(re.findall(pat, plain, re.I))
if n: note('honnêteté', f'mention « {pat} »', '', n, 'regarder')
dates = collections.Counter(re.findall(r'\b\d{1,2}\s+(?:janvier|février|mars|avril|mai|juin|juillet|août|septembre|octobre|novembre|décembre)\s+20\d\d', plain))
note('honnêteté', 'dates écrites dans la page', '', dict(dates.most_common(20)), 'regarder')
years = collections.Counter(re.findall(r'\b(20[12]\d)\b', plain))
note('honnêteté', 'années citées', '', dict(years.most_common(10)))
# doublons de paragraphes
paras=[H.unescape(re.sub(r'<[^>]+>','',m.group(1))).strip() for m in re.finditer(r'<p\b[^>]*>(.*?)</p>', body, re.S)]
paras=[re.sub(r'\s+',' ',x) for x in paras if len(x)>80]
dup=[(x[:110],n) for x,n in collections.Counter(paras).items() if n>1]
note('langue', f'paragraphes (>80 signes) écrits deux fois ou plus — {len(dup)} doublons sur {len(paras)} paragraphes', '', [f'{n}× « {x}… »' for x,n in dup][:30], 'regarder' if dup else 'ok')
longs=sorted(((len(x),x[:100]) for x in paras), reverse=True)[:8]
note('langue', 'les paragraphes les plus longs (candidats « collés par l’extracteur »)', '', [f'{n} signes « {x}… »' for n,x in longs], 'regarder')
# caractères de zone privée / ligatures cassées
pua=len(re.findall(r'[-]', txt))
note('langue', 'caractères de zone privée U+E000–F8FF', '', pua, 'corriger' if pua else 'ok')
# emoji sans FE0F pour les glyphes texte-par-défaut
for e,nom in (('⚡','⚡ (C1)'),('⚖','⚖ (C6)'),('☀','☀'),('⚙','⚙'),('❤','❤')):
n=len(re.findall(e+'(?!️)', txt)); n2=len(re.findall(e+'️', txt))
if n: note('langue', f'{nom} : {n} sans U+FE0F · {n2} avec — deux graphies du même signe', '', '', 'regarder')
# ── 6 · bloc / widget ──────────────────────────────────────────────────
racine = re.search(r'<div id="(ami-[a-z0-9-]+)"', txt)
note('bloc', 'racine', '', racine.group(1) if racine else 'aucune #ami-…')
note('bloc', 'document complet ? (<html>, <head>, <body>)', '', all(t in txt for t in ('<html','<head>','<body')))
onclick=len(re.findall(r'\sonclick=', txt))
note('bloc', 'onclick= dans le HTML (fuite : exige une fonction globale)', '', onclick, 'corriger' if onclick else 'ok')
glob = re.findall(r'^\s*function\s+([A-Za-z_$][\w$]*)\s*\(', scripts, re.M)
iife = len(re.findall(r'\(function\s*\(\)\s*\{', scripts))
note('bloc', 'scripts : IIFE · fonctions déclarées au premier niveau du texte', '', f'{iife} IIFE · fonctions nommées : {sorted(set(glob))[:30]}', 'regarder')
for k in ('localStorage','sessionStorage','fetch(','XMLHttpRequest','navigator.sendBeacon','<form','postMessage','ResizeObserver','requestFullscreen'):
n=txt.count(k)
note('bloc', f'présence de {k}', '', n)
nu = [s.strip()[:60] for s in re.findall(r'(?:^|\})\s*([a-z][a-z0-9]*|\*|body|html)\s*\{', css_all)]
note('bloc', 'sélecteurs nus (non préfixés par la racine) dans les feuilles', '', collections.Counter(nu).most_common(20), 'corriger' if nu else 'ok')
allrevert = len(re.findall(r'all\s*:\s*revert', css_all))
note('bloc', 'all:revert sur la racine (perd l’héritage de la taille : rappeler la formule du zoom)', '', allrevert, 'regarder')
zoom = len(re.findall(r'--zoom', txt))
note('bloc', 'formule du zoom (--zoom)', '', zoom, 'regarder' if not zoom else 'ok')
size = len(raw)/1e6
note('bloc', 'poids du fichier', '', f'{size:.2f} Mo — {len(re.findall("data:image",raw))} images base64 ({(len(raw)-len(txt))/1e6:.2f} Mo)', 'regarder' if size>3 else 'ok')
json.dump(R, open('audit_statique.json','w'), ensure_ascii=False, indent=1, default=str)
for fam,items in R.items():
print('\n## '+fam)
for it in items:
g={'corriger':'✗','regarder':'?','ok':'✓'}.get(it['grave'],'·')
print(f"{g} {it['quoi']} — {it['ou'] if it['ou'] else ''} :: {it['mesure']}")
audit_rendu.pyaudit_rendu.py page.html138 lignes
Chromium : ouvre tous les onglets et sous-onglets ; mesure les contrastes sur le fond réel composé, le texte < 11,5 px, les émojis en italique, les cibles < 24 px, les tableaux sans rouleau, les grilles à orphelines, les débordements à 320/390/768/1280 → audit_rendu.json
# -*- coding: utf-8 -*-
"""Audit au rendu (Chromium) — on mesure ce que le navigateur FAIT, tous
les onglets ouverts, à plusieurs largeurs. Ne corrige rien."""
import sys, json, collections, time
from playwright.sync_api import sync_playwright
SRC = sys.argv[1]
URL = 'file://' + __import__('os').path.abspath(SRC)
TOPS = sys.argv[2] if len(sys.argv) > 2 else '.tab-btn-full, .tab-bar .tab-btn' # les onglets de premier niveau de la page
JS_MESURE = r"""
(opts) => {
const seen = opts.seen; const out = {texte:[], petits:[], ital:[], cibles:[], tables:[], grilles:[], nb:0};
const EMO = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{A9A0}-\u{A9FF}]/u;
function key(el){ const p=[]; let e=el; while(e && e.nodeType===1 && e!==document.body){ let i=0,s=e; while((s=s.previousElementSibling)) i++; p.push(e.tagName+i); e=e.parentElement;} return p.join('/'); }
function vis(el){ const r=el.getBoundingClientRect(); if(r.width<1||r.height<1) return false; const cs=getComputedStyle(el); if(cs.visibility==='hidden'||cs.display==='none'||parseFloat(cs.opacity)===0) return false; return true; }
function parse(c){ const m=c.match(/rgba?\(([^)]+)\)/); if(!m) return null; const a=m[1].split(',').map(parseFloat); return {r:a[0],g:a[1],b:a[2],a:a.length>3?a[3]:1}; }
function lum({r,g,b}){ const f=v=>{v/=255; return v<=0.03928? v/12.92 : Math.pow((v+0.055)/1.055,2.4)}; return 0.2126*f(r)+0.7152*f(g)+0.0722*f(b); }
function comp(top,bot){ const a=top.a; return {r:top.r*a+bot.r*(1-a), g:top.g*a+bot.g*(1-a), b:top.b*a+bot.b*(1-a), a:1}; }
function bgOf(el){ // compose les fonds translucides jusqu'au premier opaque
const layers=[]; let e=el;
while(e && e.nodeType===1){ const cs=getComputedStyle(e); const c=parse(cs.backgroundColor); const img=cs.backgroundImage;
if(c && c.a>0){ layers.push(c); if(c.a>=1) break; }
if(img && img!=='none' && /gradient/.test(img)){ layers.push({r:9,g:9,b:14,a:1, grad:true}); break; }
e=e.parentElement; }
if(!layers.length || layers[layers.length-1].a<1) layers.push({r:9,g:9,b:14,a:1, fallback:true});
let bg=layers[layers.length-1]; for(let i=layers.length-2;i>=0;i--) bg=comp(layers[i],bg);
return {bg, grad: layers.some(l=>l.grad), fallback: layers.some(l=>l.fallback)};
}
function ratio(fg,bg){ const l1=lum(fg), l2=lum(bg); return (Math.max(l1,l2)+0.05)/(Math.min(l1,l2)+0.05); }
const walker=document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let n;
while((n=walker.nextNode())){
const t=n.textContent.replace(/\s+/g,' ').trim(); if(t.length<2) continue;
const el=n.parentElement; if(!el || ['SCRIPT','STYLE','NOSCRIPT','TITLE'].includes(el.tagName)) continue;
if(el.closest('svg')) continue;
if(!vis(el)) continue;
const k=key(el)+'|'+t.slice(0,40); if(seen[k]) continue; seen[k]=1; out.nb++;
const cs=getComputedStyle(el); const fs=parseFloat(cs.fontSize); const fw=parseInt(cs.fontWeight)||400;
const fg=parse(cs.color); const B=bgOf(el); const r=ratio(comp(fg,B.bg),B.bg);
const large = fs>=24 || (fs>=18.66 && fw>=700);
const seuil = large?3:4.5;
const tag=el.tagName.toLowerCase()+(el.className&&typeof el.className==='string'?'.'+el.className.trim().split(/\s+/).slice(0,2).join('.'):'');
if(r<seuil) out.texte.push({tag, t:t.slice(0,70), fs:+fs.toFixed(1), fw, r:+r.toFixed(2), seuil, fg:cs.color, bg:`rgb(${B.bg.r|0},${B.bg.g|0},${B.bg.b|0})`, grad:B.grad});
if(fs<11.5) out.petits.push({tag, t:t.slice(0,50), fs:+fs.toFixed(1)});
if(cs.fontStyle==='italic' && EMO.test(t)) out.ital.push({tag, t:t.slice(0,60)});
if(cs.textTransform==='uppercase' && /Cormorant/i.test(cs.fontFamily)) out.texte.push({tag, t:t.slice(0,50), capsCormorant:true, fs:+fs.toFixed(1)});
}
document.querySelectorAll('a[href],button,[role=button],[role=tab]').forEach(el=>{
if(!vis(el)) return; const k='c|'+key(el); if(seen[k]) return; seen[k]=1;
const r=el.getBoundingClientRect(); if(r.width<24||r.height<24) out.cibles.push({tag:el.tagName.toLowerCase()+'.'+(el.className||'').toString().trim().split(/\s+/)[0], t:(el.textContent||'').trim().slice(0,30), w:+r.width.toFixed(1), h:+r.height.toFixed(1)});
});
document.querySelectorAll('table').forEach(tb=>{
if(!vis(tb)) return; const k='t|'+key(tb); if(seen[k]) return; seen[k]=1;
let e=tb.parentElement, roule=false; while(e && e!==document.body){ const o=getComputedStyle(e).overflowX; if(o==='auto'||o==='scroll'){roule=true;break;} e=e.parentElement; }
const r=tb.getBoundingClientRect(); const pr=tb.parentElement.getBoundingClientRect();
if(r.width>pr.width+1 && !roule) out.tables.push({t:(tb.textContent||'').trim().slice(0,50), w:+r.width.toFixed(0), pw:+pr.width.toFixed(0)});
});
document.querySelectorAll('*').forEach(el=>{
const cs=getComputedStyle(el); if(cs.display!=='grid'||!vis(el)) return; const k='g|'+key(el); if(seen[k]) return; seen[k]=1;
const cols=cs.gridTemplateColumns.split(' ').length; const kids=[...el.children].filter(vis).length;
if(cols>1 && kids>cols && kids%cols!==0) out.grilles.push({cls:(el.className||'').toString().trim().slice(0,40), cols, kids, orphelines: kids%cols});
});
return out;
}
"""
JS_OVERFLOW = r"""
() => { const d=document.documentElement; const w=d.clientWidth; const bad=[];
document.querySelectorAll('body *').forEach(el=>{ if(el.closest('svg')) return; const r=el.getBoundingClientRect(); if(r.width<1) return;
if(r.right>w+1 || r.left<-1){ let e=el.parentElement, clip=false; while(e){ const o=getComputedStyle(e).overflowX; if(o==='hidden'||o==='clip'||o==='auto'||o==='scroll'){clip=true;break;} e=e.parentElement;}
bad.push({tag:el.tagName.toLowerCase()+'.'+(el.className||'').toString().trim().split(/\s+/)[0], right:+r.right.toFixed(0), left:+r.left.toFixed(0), clip}); } });
return {scrollW:d.scrollWidth, clientW:w, n:bad.length, ex:bad.slice(0,12)}; }
"""
def ouvre_tout(page, seen, acc):
"""Clique chaque onglet et chaque sous-onglet, mesure après chaque état."""
def mesure():
o = page.evaluate(JS_MESURE, {'seen': seen})
for k in ('texte','petits','ital','cibles','tables','grilles'): acc[k].extend(o[k])
acc['nb'] += o['nb']
mesure()
tops = page.query_selector_all(TOPS)
for b in tops:
try: b.click(timeout=2000); page.wait_for_timeout(150)
except Exception as e: acc['clics_rates'].append(str(e)[:80])
mesure()
# sous-onglets visibles
subs = page.query_selector_all('button')
for s in subs:
try:
if not s.is_visible(): continue
cls = (s.get_attribute('class') or '')
if 'tab-btn-full' in cls or 'pt-tab' in cls or 'som' in cls: continue
if any(x in cls for x in ('tri-b','tb','tab','onglet','nav','btn-t','sb','sp-h','pt-h','psb-')):
s.click(timeout=1500); page.wait_for_timeout(120); mesure()
except Exception: pass
# sommaires : on les ouvre
for h in page.query_selector_all('.som-h, .som-hd, [aria-expanded]'):
try:
if h.is_visible(): h.click(timeout=800)
except Exception: pass
mesure()
with sync_playwright() as p:
br = p.chromium.launch()
res = {}
for W,H in ((390,844),(1280,900)):
ctx = br.new_context(viewport={'width':W,'height':H}, device_scale_factor=1)
page = ctx.new_page()
errs=[]; page.on('pageerror', lambda e: errs.append(str(e)[:200])); page.on('console', lambda m: errs.append('console.'+m.type+': '+m.text[:160]) if m.type in ('error','warning') else None)
page.goto(URL, wait_until='load'); page.wait_for_timeout(1200)
seen={}; acc=dict(texte=[],petits=[],ital=[],cibles=[],tables=[],grilles=[],nb=0,clics_rates=[])
ouvre_tout(page, seen, acc)
ov = page.evaluate(JS_OVERFLOW)
# fonts réellement chargées ?
fonts = page.evaluate("() => [...document.fonts].filter(f=>f.status==='loaded').map(f=>f.family+' '+f.weight+' '+f.style)")
res[W] = dict(erreurs=errs, mesures=acc, overflow=ov, fonts=sorted(set(fonts)))
page.screenshot(path=f'capture_{W}.png', full_page=False)
ctx.close()
for W in (320,768):
ctx = br.new_context(viewport={'width':W,'height':800}); page=ctx.new_page(); page.goto(URL, wait_until='load'); page.wait_for_timeout(800)
for b in page.query_selector_all(TOPS):
try: b.click(timeout=1500); page.wait_for_timeout(100)
except Exception: pass
res[W]=dict(overflow=page.evaluate(JS_OVERFLOW)); ctx.close()
br.close()
json.dump(res, open('audit_rendu.json','w'), ensure_ascii=False, indent=1)
for W,r in res.items():
print(f'\n=== {W} px ===')
if 'erreurs' in r:
print('erreurs JS/console :', len(r['erreurs'])); [print(' ',e) for e in r['erreurs'][:10]]
m=r['mesures']; print('nœuds texte mesurés :', m['nb'], '· clics ratés :', len(m['clics_rates']))
print('contraste sous seuil :', len([x for x in m['texte'] if not x.get('capsCormorant')]), '· caps en Cormorant :', len([x for x in m['texte'] if x.get('capsCormorant')]))
print('texte < 11,5 px :', len(m['petits']), '· émoji en italique :', len(m['ital']), '· cibles < 24 px :', len(m['cibles']), '· tableaux qui débordent sans rouleau :', len(m['tables']), '· grilles à orphelines :', len(m['grilles']))
print('polices chargées :', r['fonts'])
print('overflow :', r['overflow']['scrollW'], '/', r['overflow']['clientW'], '· éléments hors cadre :', r['overflow']['n'], r['overflow']['ex'][:5])
p1_texte.pyp1_texte.py179 lignes
Les gestes de texte d'une page, chacun compté par assert (identité, liens, libellés, redites → renvois, notation). À réécrire pour chaque page : c'est la partie qui se lit à la main.
# -*- coding: utf-8 -*-
"""P1 · Texte & structure — 0.3 L'Union du TouT V21 → V22.
Chaque remplacement dit combien de fois il doit trouver sa cible ; sinon le script s'arrête sans écrire."""
import re, sys
SRC, DST = 'v22.html', 'v22.html'
h = open(SRC, encoding='utf-8').read()
J = []
def rep(old, new, n=1, flags=0, why=''):
global h
if flags:
c = len(re.findall(old, h, flags))
else:
c = h.count(old)
if c != n:
raise SystemExit(f'✗ attendu {n}, trouvé {c} : {why or old[:70]!r}')
h = re.sub(old, new, h, flags=flags) if flags else h.replace(old, new)
J.append(f'{why or old[:60]} ×{n}')
# ── A·1 / A·2 · identité : le numéro 0.3 lu depuis une source ──────────
rep('<meta name="viewport" content="width=device-width,initial-scale=1">',
'<meta name="viewport" content="width=device-width,initial-scale=1">\n<meta name="ami:volume" content="0.3">\n<meta name="ami:version" content="V22">\n<meta name="ami:date" content="2026-09-09">',
why='metas ami:volume / version / date')
rep("<title>💍 0.1 L'Union du TouT", "<title>💍 0.3 L'Union du TouT", why='title 0.3')
rep('<div class="vol-label">Protocole de ConScience aMi · Volume 0.1</div>',
'<div class="vol-label">Protocole de ConScience aMi · Volume <span data-ami="volume">0.3</span></div>', why='vol-label 0.3')
rep('aria-label="Cinq parties du Volume 0.1"', 'aria-label="Cinq parties du Volume 0.3"', why='aria-label 0.3')
rep("◈ Sommaire — Vol. 0.1 · L'Union du TouT", "◈ Sommaire — Vol. <span data-ami=\"volume\">0.3</span> · L'Union du TouT", why='sommaire 0.3')
rep("<p class=\"footer-nav\">💍 0.1 L'Union du TouT", "<p class=\"footer-nav\">💍 <span data-ami=\"volume\">0.3</span> L'Union du TouT", why='pied 0.3')
# ── A·3 / A·4 / A·5 / A·6 · coquilles et comptes ──────────────────────
rep("l'ThermoCognition le dit", "la ThermoCognition le dit", why="l'ThermoCognition")
rep('Les 11 notions du consentement', 'Les 12 notions du consentement', why='nav 11→12')
rep('Trois lectures des mêmes onze notions', 'Trois lectures des mêmes douze notions', why='h2 onze→douze')
rep('Une fois les onze notions passées en revue', 'Une fois les douze notions passées en revue', why='onze→douze')
rep('Elle est passée au volet 4, où elle scelle', 'Elle est passée au cinquième volet, où elle scelle', why='volet 4→5')
rep("la Fable ferme le troisième volet, et non le deuxième", "la Fable ferme le cinquième volet, et non le deuxième", why='troisième→cinquième volet')
rep("<b>on entre</b> (l'Apokalypse), <b>on établit sur quoi l'on se base</b> (les Lois de La Vie), <b>on dit ce que l'on veut</b> (les Chartes du Vivant), <b>puis on s'engage</b>.",
"<b>on entre</b> (la Quintessence), <b>on établit sur quoi l'on se base</b> (les Lois de La Vie), <b>on dit ce que l'on veut</b> (les Chartes du Vivant), <b>on apprend à le dire</b> (la Communication), <b>puis on s'engage</b>.", why='ordre des cinq volets')
rep('négentropique', 'néguentropique', n=3, why='négentropique')
rep('négentropie', 'néguentropie', n=1, why='négentropie')
# ── A·7 · le caractère parasite U+030B ─────────────────────────────────
c = h.count('̋'); assert c == 2, c
h = h.replace('<h4>̋ Le Point Zéro', '<h4><span class="pz">◎</span> Le Point Zéro').replace('<span class="pz">̋</span>', '<span class="pz">◎</span>'); J.append('U+030B → ◎ (glyphe du Point Zéro, à valider) ×2')
assert '̋' not in h
# ── A·8 · les deux </b> sans ouvrant ──────────────────────────────────
rep("<b>ce qui t'est arrivé</b>. »</b></em>", "<b>ce qui t'est arrivé</b>. »</em>", why='</b> orphelin 1')
rep("<b>ce que tu as fait subir</b>. »</b></em>", "<b>ce que tu as fait subir</b>. »</em>", why='</b> orphelin 2')
# ── A·9 / B·20 · les liens et l'adresse unique des contrats ────────────
rep('<a href="../Initiation_aMi_DONNER_VIE_V17.html#pan-contrat">📜✍️ Le Contrat Tacite Explicite</a>, en toit de <b>L\'Initiation</b>, et dans <a href="../Le_Respect_La_Loi_du_Reel_aMi.html#pv-contrat">Le Respect · La Loi du Réel</a>.',
'<a href="Initiation_aMi_DONNER_VIE_V18.html#pan-contrat">📜✍️ Le Contrat Tacite Explicite</a>, l\'onglet de <b>L\'Initiation</b> qui leur est dédié.', why='contrats : une adresse (intro)')
rep('<b>Le Contrat Tacite Explicite</b>, dans <a href="../Le_Respect_La_Loi_du_Reel_aMi.html#pv-contrat">Le Respect · La Loi du Réel</a>.',
'<a href="Initiation_aMi_DONNER_VIE_V18.html#pan-contrat"><b>Le Contrat Tacite Explicite</b></a>, l\'onglet de <b>L\'Initiation</b> qui leur est dédié.', why='contrats : une adresse (chapitre)')
# ── B·5 · la phrase d'IA ───────────────────────────────────────────────
rep("<p style=\"margin:.7rem 0 0;color:var(--gris);font-size:.85rem\">Ces articles sont dans le texte source et méritent d'être développés ici — dis-moi si tu veux que je les reprenne intégralement depuis le PDF.</p>", '', why='phrase IA retirée')
# ── B·4 · le tutoiement adressé à l'autrice ────────────────────────────
rep("<strong>Tes polarités sont donc les trois axes du système visuel humain.</strong> Quand tu poses la Nature contre la Culture en vert contre rouge, tu utilises l'axe a*",
"<strong>Les polarités de la Quintessence sont donc les trois axes du système visuel humain.</strong> Poser la Nature contre la Culture en vert contre rouge, c'est employer l'axe a*", why='tutoiement adressé (LabCIE)')
# ── B·15 / B·16 · celleux, déchéance ──────────────────────────────────
rep('celleux qui osent parler', 'les personnes qui osent parler'); rep('celleux qui ne font rien', 'les personnes qui ne font rien'); rep('celleux qui crachent', 'les personnes qui crachent')
rep("C'est le cas de celleux qui ont besoin des autres", "C'est le cas des personnes qui ont besoin des autres")
rep('<small>on déchéance</small>', '<small>on déchoit</small>'); rep('<b>En violence : on déchéance</b>', '<b>En violence : on déchoit</b>')
# ── B·13 · V%ler, voulu — glosé et défini au survol ───────────────────
rep('<div class="mag-nom">V%ler</div>', '<div class="mag-nom"><span class="def" title="Voler & violer dans un seul mot : la transaction psychophysique se lit dans le mot lui-même.">V%ler</span> <small class="mag-glose">(voler & violer)</small></div>', why='V%ler glosé (Table I)')
rep('<div class="champ-arme">V%ler : L\'Arme de La Force du Délire', '<div class="champ-arme"><span class="def" title="Voler & violer dans un seul mot : la transaction psychophysique se lit dans le mot lui-même.">V%ler</span> (voler & violer) : L\'Arme de La Force du Délire', why='V%ler glosé (Table III)')
# ── B·8 / B·9 / B·10 / B·21 / A·11 · Tables Magistrales : une source, des noms tranchés
rep('Création & Procréation', 'Création & ProCréation', n=3, why='ProCréation dans les Tables')
rep('Conscience & Gnose', 'Conscience & Science', n=2, why='Gnose→Science (I et II)')
rep("L'Art de Sublimer la Nutrition", "L'Art de La Nutrition", n=1, why='Nutrition')
rep("L'Arme de L'Industrie", "L'Arme de L'Obsolescence", n=1, why='Industrie→Obsolescence')
rep("L'Arme de La Destruction", "L'Arme de Destruction", n=1, why='Destruction (I)')
rep("L'Art de Faire Naître", "L'Art de ProCréation", n=1, why='Faire Naître→ProCréation (I)')
rep("L'Art de Faire</div>", "L'Art de ProCréation</div>", n=1, why='Faire→ProCréation (III)')
rep('La Langue de Bois</div>', 'La Langue de Bois / le Charabia</div>', n=2, why='Charabia (I et II)')
for a, b in (("L'Arme de l'Aliénation", "L'Arme de L'Aliénation"), ("L'Arme de l'Impunité", "L'Arme de L'Impunité"), ("L'Arme de l'Intoxication", "L'Arme de L'Intoxication"), ("L'Arme de l'Oubli", "L'Arme de L'Oubli"), ("L'Arme de la Disgrâce", "L'Arme de La Disgrâce")):
h = h.replace(a, b)
J.append('majuscule au déterminant (Arme de L’…)')
# la numérotation de la Table I suit celle des Tables II et III (les deux extras en 17 et 18)
renum = {'06':'17','07':'06','08':'07','09':'08','10':'09','11':'10','12':'18','13':'11','14':'12','15':'13','16':'14','17':'15','18':'16'}
def renum_I(m):
return m.group(1) + renum.get(m.group(2), m.group(2)) + m.group(3)
seg_a, seg_b = h.find('◈ I — La Table Magistrale'), h.find('◈ II — Table 16 × 8')
assert 0 < seg_a < seg_b
seg = h[seg_a:seg_b]
n_before = len(re.findall(r'(<strong>\S+ )(\d\d)(</strong>)', seg))
seg = re.sub(r'(<strong>\S+ )(\d\d)(</strong>)', renum_I, seg)
assert n_before == 18, n_before
h = h[:seg_a] + seg + h[seg_b:]; J.append('Table I renumérotée comme II et III (extras en 17-18) ×18')
# « Champ » → « Domaine » dans les Tables ; « 8 Lois du Vivant » → « 8 Lois Culturelles »
rep('<div class="mn">Champ ', '<div class="mn">Domaine ', n=16, why='Table II : Domaine')
rep(r'(<span class="champ-n"[^>]*>)Champ ', r'\1Domaine ', n=18, flags=re.S, why='Table III : Domaine')
rep('Chaque champ × chacune des 8 Lois du Vivant. Si... Alors... universel.', 'Chaque domaine × chacune des 8 Lois Culturelles — les Commandements. Si… alors… universel.', why='8 Lois Culturelles (II)')
rep('16 Champs × 8 Lois = <strong style="color:var(--or);">128 Commandements</strong>', '16 Domaines × 8 Lois Culturelles = <strong style="color:var(--or);">128 Commandements</strong>', why='16 Domaines')
rep('◈ II — Table 16 × 8 · 128 Commandements du Vivant', '◈ II — Table 16 × 8 · 128 Commandements du Vivant — les 8 Lois Culturelles', why='titre Table II')
c = h.count('Déployer les 18 Champs × 8 Lois'); assert c == 1; h = h.replace('Déployer les 18 Champs × 8 Lois', 'Déployer les 18 Domaines × 8 Lois Culturelles'); J.append('Table III : Domaines × Lois Culturelles')
rep('18 paires Arme⟺Art qui structurent les 128 Commandements du Vivant.', '18 paires Arme⟺Art qui structurent les 128 Commandements du Vivant — les 8 Lois Culturelles, face aux 7 Lois Naturelles de l\'onglet 2.', why='pont Lois Culturelles/Naturelles')
# les 7 Lois du Vivant deviennent les 7 Lois Naturelles du Vivant (titres)
rep('Les 7 Lois du Vivant', 'Les 7 Lois Naturelles du Vivant', n=5, why='7 Lois Naturelles du Vivant (sommaire, nav, h3, h2, triade)')
# ── B·11 · les redites entre cantons : on garde une fois, on renvoie ──
# (a) Diagnostic opposable / souverain : la maison est le Diagnostic ; la Charte Mathématique renvoie
old_a = h[h.find('<h4>Méthode clinique, statut souverain</h4>'):h.find("<em>On ne dit la vérité sur soi que dans un espace où cette vérité ne sera pas saisie.</em></p>")+len("<em>On ne dit la vérité sur soi que dans un espace où cette vérité ne sera pas saisie.</em></p>")]
assert old_a.count('<table>') == 1 and len(old_a) < 2000, len(old_a)
h = h.replace(old_a, '<p class="renvoi">◈ <b>Méthode clinique, statut souverain</b> — ce que cette charte refuse n\'est pas le diagnostic, c\'est son statut opposable. La distinction est posée une fois, au <a href="#diag-souverain">Diagnostic · Le statut de la lecture</a>.</p>')
rep('<details class="cm2-sec" open><summary><span class="cm2-em">👑</span><span class="cm2-t"><span class="cm2-lab2">Ⅰ · Le statut de la lecture</span>',
'<details class="cm2-sec" open id="diag-souverain"><summary><span class="cm2-em">👑</span><span class="cm2-t"><span class="cm2-lab2">Ⅰ · Le statut de la lecture</span>', why='ancre diag-souverain')
J.append('redite (a) diagnostic opposable → renvoi')
# (b) la balance qui ne pèse rien : la maison est l'Arborescence (le Poids) ; le Jardin renvoie
rep('<h4>Et quand la balance ne pèse rien</h4>\n<p>Tout ceci suppose qu’on <em>ressente</em>.', '<h4>Et quand la balance ne pèse rien</h4>\n<p class="renvoi">Le cas est traité une fois, au <a href="#arbo-balance">Poids · le quatrième cas</a> : ne rien sentir n\'est pas une preuve d\'innocence — la balance intérieure ne sert plus, on passe par l\'extérieur.</p><p hidden>Tout ceci suppose qu’on <em>ressente</em>.', why='redite (b) balance')
# retirer le paragraphe caché (on le supprime entièrement)
i = h.find('<p hidden>Tout ceci suppose'); j = h.find('</p>', i) + 4
h = h[:i] + h[j:]
rep('<div class="note eth"><b>Le quatrième cas — quand la balance ne pèse rien.</b>', '<div class="note eth" id="arbo-balance"><b>Le quatrième cas — quand la balance ne pèse rien.</b>', why='ancre arbo-balance')
# (c) l'acrasie : la maison est le Jardin §II ; le raccord de l'Arborescence renvoie en une ligne
old_c = h[h.find('<div class="note key"><b>🌱 Le Jardin ne peut pas voir l\'acrasie tout seul.</b>'):]
old_c = old_c[:old_c.find('</div>')+6]
assert 'indice d\'acrasie' in old_c and len(old_c) < 700
h = h.replace(old_c, '<div class="note key"><b>🌱 Le Jardin ne peut pas voir l\'acrasie tout seul</b> — il mesure des dispositions déclarées, C7 · La Trace comble l\'angle mort. L\'indice et ses trois cas sont posés une fois, au <a href="#jardin-acrasie">Jardin · L\'Indice d\'Acrasie</a>.</div>')
rep('<details class="cm2-sec"><summary><span class="cm2-em">⚖️</span><span class="cm2-t"><span class="cm2-lab2">Ⅱ · L’angle mort, et son remède</span>', '<details class="cm2-sec" id="jardin-acrasie"><summary><span class="cm2-em">⚖️</span><span class="cm2-t"><span class="cm2-lab2">Ⅱ · L’angle mort, et son remède</span>', why='ancre jardin-acrasie')
J.append('redite (c) acrasie → renvoi')
# (d) jauger / juger : la maison est « Les mots à leur juste place » ; la Porte renvoie
old_d = h[h.find('<p style="font-size:.92rem"><b>Ce qui se juge, et ce qui se jauge.</b>'):]
old_d = old_d[:old_d.find('puis il refuse de se faire juger.</b></p>')+len('puis il refuse de se faire juger.</b></p>')]
assert old_d.count('<p') == 2 and len(old_d) < 1200, len(old_d)
h = h.replace(old_d, '<p style="font-size:.92rem;margin-bottom:0" class="renvoi"><b>Ce qui se juge, et ce qui se jauge.</b> On juge une seule chose — la violence ; tout le reste se jauge. Le complexe d\'infériorité échoue aux deux : il refuse d\'abord de jauger, puis de se faire juger. Les deux gestes sont posés une fois, aux <a href="#arbo-jauger">mots à leur juste place · Jauger & Juger</a>.</p>')
rep('<div class="card or"><h4>⚖️ JAUGER & JUGER — deux gestes, jamais le même</h4>', '<div class="card or" id="arbo-jauger"><h4>⚖️ JAUGER & JUGER — deux gestes, jamais le même</h4>', why='ancre arbo-jauger')
J.append('redite (d) jauger/juger → renvoi')
# (e) les deux garde-fous : la maison est le chapitre des Pactes ; l'intro renvoie
old_e = h[h.find('<div class="alerte"><strong>Les deux garde-fous, valables pour les quatre.</strong>'):]
old_e = old_e[:old_e.find('</div>')+6]
assert 'révocable' in old_e and len(old_e) < 900
h = h.replace(old_e, '<div class="alerte"><strong>Les deux garde-fous, valables pour les quatre</strong> — un pacte doit <strong>ouvrir</strong>, et il doit être <strong>révocable</strong>. Ils sont posés une fois, en tête des <a href="#ctr-gardefous">Pactes du Respect</a>.</div>')
rep('<b>◈ Les deux garde-fous, avant tout le reste.</b>', '<b id="ctr-gardefous">◈ Les deux garde-fous, avant tout le reste.</b>', why='ancre ctr-gardefous')
J.append('redite (e) garde-fous → renvoi')
# (f) la signature écrite deux fois : elle reste à la Fable, qui scelle
rep('<blockquote><p>En Sachant Qui Je Suis Vraiment<br>🪞 Rejoins Moi pour Radicalement Prospérer 🕊️<br>⚕️ Entre Respect & Vérité. ⚖️</p></blockquote>\n</div>', '</div>', why='redite (f) signature (intro) retirée')
# ── B·12 · C pour l'indice, Φ pour la constante (canton Charte Mathématique) ─
seg_a, seg_b = h.find('💫 Fractale · Charte Physique Structurelle</div><div class="bs">'), h.find('🌳 Arborescence · Charte Psychique Imaginative</div><div class="bs">')
assert 0 < seg_a < seg_b
seg = h[seg_a:seg_b]
# on ne touche qu'au texte, jamais aux scripts
parts = re.split(r'(<script.*?</script>)', seg, flags=re.S)
n = 0
for k in range(0, len(parts), 2):
p = parts[k]
for old, new in (
('Φ est un rapport : il est sans dimension. Le Dian, lui, s\'accumule dans le temps.', 'C est un rapport : il est sans dimension. Le Dian, lui, s\'accumule dans le temps.'),
('Φ dépend du coût du refus : il n\'est pas une propriété du système seul.', 'C dépend du coût du refus : il n\'est pas une propriété du système seul.'),
('Φ = ΔI / ΔE = (U × I) / (R × P) × t', 'C = ΔI / ΔE = (U × I) / (R × P) × t'),
('<b>Φ</b> · la Cohérence, la Superfluidité', '<b>C</b> · la Cohérence, la Superfluidité'),
('mêmes entrées, même Φ, à toute heure', 'mêmes entrées, même C, à toute heure'),
('Φ répond à une question précise', 'C répond à une question précise'),
('Φ · Cohérence<', 'C · Cohérence<'),
('Φ haut · RS haut', 'C haut · RS haut'), ('Φ bas · RS bas', 'C bas · RS bas'), ('Φ bas · QS haut', 'C bas · QS haut'), ('Φ bas · QS bas · RS haut', 'C bas · QS bas · RS haut'),
('Φₙ = ΔI / (ΔI + ΔE)', 'Cₙ = ΔI / (ΔI + ΔE)'),
('<p>Φ est un rapport : il n\'a pas d\'unité.', '<p>C est un rapport : il n\'a pas d\'unité.'),
('D = ∫ Φ(t) dt', 'D = ∫ C(t) dt'), ('Φ est au Dian ce que le Watt', 'C est au Dian ce que le Watt'),
('La Relation a trois Φ', 'La Relation a trois C'),
('Φ<sub>', 'C<sub>'),
('Φ de A', 'C de A'), ('Φ de B', 'C de B'), ('Φ du lien', 'C du lien'),
('deux personnes de même Φ mais de QS', 'deux personnes de même C mais de QS'),
('Φ · Cohérence — l\'instant', 'C · Cohérence — l\'instant'), ('Φ + RS croisés', 'C + RS croisés'),
):
c = p.count(old)
if c: p = p.replace(old, new); n += c
parts[k] = p
seg = ''.join(parts)
h = h[:seg_a] + seg + h[seg_b:]
J.append(f'C pour l’indice, Φ pour la constante (Charte Mathématique) ×{n}')
assert n >= 28, n
# la phrase qui le dit une fois, en tête du chapitre Ⅶ
rep('Ce que la conscience fait, une équation le mesure', 'Ce que la conscience fait, une équation le mesure — C est l\'indice, Φ la constante qui lui sert d\'échelle', why='C/Φ dit une fois')
# ── les styles des nouveautés : .renvoi, .def, .mag-glose ─────────────
rep('#ami-union *{margin:0;padding:0;box-sizing:border-box}', '#ami-union *{margin:0;padding:0;box-sizing:border-box}\n#ami-union .renvoi{font-size:.92rem;color:var(--gris-cl);border-left:2px solid var(--or-somb);padding-left:.8rem;margin:.8rem 0}\n#ami-union .renvoi a,#ami-union .note a,#ami-union .alerte a{color:var(--or-pale);text-decoration:none;border-bottom:1px dotted var(--or)}\n#ami-union .def{color:var(--or);font-weight:500;border-bottom:1px dotted var(--or);cursor:help}\n#ami-union .mag-glose{display:block;font-size:.72rem;color:var(--gris-cl);font-weight:300}', why='styles renvoi/def')
open(DST, 'w', encoding='utf-8').write(h)
print('\n'.join('✓ ' + x for x in J)); print(len(J), 'gestes')
p2_forme.pyp2_forme.py96 lignes
Forme et accessibilité : fond, 100vh, animations ≥ 7 s, reduced-motion global, or-sombre → gris quand il sert d'encre, émojis hors italique, tableaux dans .tw, role=tablist, clavier, un lien #ancre ouvre son onglet.
# -*- coding: utf-8 -*-
"""P2 · Forme & lisibilité — édits statiques. Le plancher de taille est fait par p2_plancher.py (au rendu)."""
import re
h = open('v22.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'✗ attendu {n}, trouvé {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} ×{n}')
# A·12 · le fond de page est l'onyx
rep('body{margin:0;padding:22px;background:#07070c;min-height:100vh}', 'body{margin:0;padding:22px;background:#09090e;min-height:100vh}', why='body → onyx')
# A·23 · 100vh retiré de la couverture
rep('#ami-union .couverture{min-height:100vh;', '#ami-union .couverture{', why='100vh couverture')
# A·24 · animations ≥ 7 s
rep('animation:amipulse 4s ease-in-out infinite', 'animation:amipulse 11s ease-in-out infinite', why='logo 4 s → 11 s')
rep('animation:amipulse 6s ease-in-out infinite', 'animation:amipulse 11s ease-in-out infinite', why='étoile 6 s → 11 s')
rep('animation:amiPulse 3.2s ease-in-out infinite', 'animation:amiPulse 7s ease-in-out infinite', why='point zéro 3,2 s → 7 s')
# A·25 · prefers-reduced-motion global, en tête de la première feuille
rep('<style>\n#ami-union{', '<style>\n@media(prefers-reduced-motion:reduce){#ami-union *,#ami-union *::before,#ami-union *::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}\n#ami-union{', why='reduced-motion global en tête')
# A·26 · import Google Fonts canonique
rep('family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=Jost', 'family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&family=Jost', why='import fonts 1,600')
# A·13 · --or-somb n'est pas une encre sous 22 px
for sel, old, new, *k in (
('.cmd-loi', "color:var(--or-somb);margin-bottom:.3rem;display:block;}", "color:var(--gris-cl);margin-bottom:.3rem;display:block;}"),
('.champ-n', "font-size:.58rem;letter-spacing:.2em;color:var(--or-somb);}", "font-size:.58rem;letter-spacing:.2em;color:var(--gris-cl);}"),
('.mn', "#ami-union .src-tri .mag-128-card .mn{font-size:.56rem;color:var(--or-somb);", "#ami-union .src-tri .mag-128-card .mn{font-size:.56rem;color:var(--gris-cl);"),
('.lw-n', "#ami-union .src-lois .lw-n{font-family:'Space Mono',monospace;font-size:.62rem;color:var(--or-somb);", "#ami-union .src-lois .lw-n{font-family:'Space Mono',monospace;font-size:.62rem;color:var(--gris-cl);"),
('.pk-n', "#ami-union .src-contrats .pk-n{font-family:'Space Mono',monospace;font-size:.62rem;color:var(--or-somb);", "#ami-union .src-contrats .pk-n{font-family:'Space Mono',monospace;font-size:.62rem;color:var(--gris-cl);"),
('.qs-cap', "text-transform:uppercase;color:var(--or-somb);margin-bottom:.7rem}", "text-transform:uppercase;color:var(--gris-cl);margin-bottom:.7rem}"),
('.tst-n', "letter-spacing:.14em;color:var(--or-somb);text-transform:uppercase}", "letter-spacing:.14em;color:var(--gris-cl);text-transform:uppercase}"),
('.tri-foot', "font-family:'Cormorant Garamond',serif;font-style:italic;font-size:1rem;color:var(--or-somb)}", "font-family:'Cormorant Garamond',serif;font-style:italic;font-size:1rem;color:var(--gris-cl)}", 2),
):
rep(old, new, n=(k[0] if k else 1), why=f'{sel} : or-somb → gris-cl')
rep('<span style="color:var(--or-somb)">— extension</span>', '<span style="color:var(--gris-cl)">— extension</span>', n=2, why='— extension')
rep('<span style="color:var(--or-somb);font-size:.5rem;">· nouveau ·</span>', '<span style="color:var(--gris-cl);font-size:.72rem;">· nouveau ·</span>', n=2, why='· nouveau ·')
# A·14 · les polarités d'onglet se lisent
rep("font-weight:400;line-height:1.25;color:rgba(226,226,240,.26);transition:color .2s}", "font-weight:400;line-height:1.25;color:var(--gris-cl);opacity:.8;transition:color .2s}", why='tb-pol lisible')
rep('#ami-union .tab-btn-full .tb-pol{font-size:.42rem}', '#ami-union .tab-btn-full .tb-pol{font-size:.72rem}', why='tb-pol mobile')
rep('#ami-union .tab-btn-full .tb-lbl{font-size:.66rem;letter-spacing:.03em}', '#ami-union .tab-btn-full .tb-lbl{font-size:.74rem;letter-spacing:.03em}', why='tb-lbl mobile')
# B·7 · les onglets n'ont pas de chiffre dans leur libellé (le chiffre vit au sommaire et au toc)
for k in range(1, 6):
rep(f'<span class="tb-lbl">{k} · ', '<span class="tb-lbl">', why=f'onglet {k} sans chiffre')
# A·15 · portail
rep('<h3 class="pp-pol" id="ppPol">', '<h3 class="pp-pol" id="ppPol" style="font-size:.78rem">', why='pp-pol taille')
# A·17 · olive un cran plus sombre (4,19 → ≥ 4,5:1 pour la nacre)
rep('<div style="background:#577c05"><b>🌾 L\'Olive</b>', '<div style="background:#4d6b02"><b>🌾 L\'Olive</b>', why='olive')
# A·19 · pas de capitales en Cormorant : les h4 des cantons serif retrouvent leur casse
rep("#ami-union .src-tri h4{font-size:1.1rem;font-weight:600;margin:1.4rem 0 .45rem;color:var(--or-pale)}", "#ami-union .src-tri h4{font-size:1.1rem;font-weight:600;margin:1.4rem 0 .45rem;color:var(--or-pale);text-transform:none;letter-spacing:0}", why='h4 src-tri sans capitales')
rep("#ami-union .src-contrats .pk-t{font-size:1.26rem;font-weight:600;color:var(--or-pale);line-height:1.16}", "#ami-union .src-contrats .pk-t{font-size:1.26rem;font-weight:600;color:var(--or-pale);line-height:1.16;text-transform:none;letter-spacing:0}", why='pk-t sans capitales')
# A·18 · les émojis sortent de l'italique
EMO = re.compile(r'([\U0001F300-\U0001FAFF☀-➿⬀-⯿](?:️|[\U0001F300-\U0001FAFF☀-➿]️?)*)')
def desital(s): return EMO.sub(r'<span class="em">\1</span>', s)
n = 0
for old in ("⚔️ L'Art Est Mon Arme 🖌️", "🪞 Rejoins Moi pour Radicalement Prospérer 🕊️", "⚕️ Entre Respect & Vérité. ⚖️", "par Le Silence ❤️\u200d🔥</span>", "Demain toujours plus fort, encore & encore. 🌱</p>"):
c = h.count(old); assert c >= 1, old
h = h.replace(old, desital(old)); n += c
J.append(f'émojis hors italique ×{n}')
rep("if(ppD){ if(d[4]){ ppD.textContent=d[4]; ppD.hidden=false; }", "if(ppD){ if(d[4]){ ppD.innerHTML=d[4].replace(/[<>&]/g,function(c){return {'<':'<','>':'>','&':'&'}[c]}).replace(/([\\u{1F300}-\\u{1FAFF}\\u2600-\\u27BF](?:\\uFE0F|\\u200D[\\u{1F300}-\\u{1FAFF}\\u2600-\\u27BF]\\uFE0F?)*)/gu,'<span class=\"em\">$1</span>'); ppD.hidden=false; }", why='devise du portail : émojis hors italique (JS)')
rep('#ami-union *{margin:0;padding:0;box-sizing:border-box}', '#ami-union *{margin:0;padding:0;box-sizing:border-box}\n#ami-union .em{font-style:normal}', why='.em')
# A·28 · les romains orphelins du canton Lois deviennent des glyphes, comme leurs voisins (choix d'émoji à valider)
rep('<span class="num">Ⅰ</span><h2>🌱 Les 7 Lois Naturelles du Vivant</h2>', '<span class="num">🌱</span><h2>Les 7 Lois Naturelles du Vivant</h2>', why='Ⅰ → 🌱')
rep("<span class=\"num\">Ⅱ</span><h2>Leur place d'origine : la triade du Champ 7</h2>", "<span class=\"num\">📜</span><h2>Leur place d'origine : la triade du Champ 7</h2>", why='Ⅱ → 📜')
rep('<span class="num">Ⅲ</span><h2>Et les deux Lois de mesure</h2>', '<span class="num">🧮</span><h2>Et les deux Lois de mesure</h2>', why='Ⅲ → 🧮')
rep('<span class="num">Ⅺ</span><h2>Les douze notions</h2>', '<span class="num">🧩</span><h2>Les douze notions</h2>', why='Ⅺ → 🧩')
# A·22 · cibles tactiles ≥ 24 px : les boutons de mode des planches
rep("padding:.22rem .6rem;border-radius:var(--r);cursor:pointer}", "padding:.4rem .7rem;min-height:24px;border-radius:var(--r);cursor:pointer}", why='.qs-m 24 px')
# B·19 · les liens du pied grossissent
rep('#ami-union .footer-nav{font-size:.62rem;letter-spacing:.1em;color:var(--gris);text-transform:uppercase;margin-top:1.5rem;line-height:2}', '#ami-union .footer-nav{font-size:.74rem;letter-spacing:.1em;color:var(--gris-cl);text-transform:uppercase;margin-top:1.5rem;line-height:2.3}', why='footer-nav lisible')
# B·18 · grilles sans orphelines : 8 cartes → 4 colonnes, 16 → 2
rep('#ami-union .src-tri .cmd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:.5rem;}', '#ami-union .src-tri .cmd-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;}\n@media(max-width:900px){#ami-union .src-tri .cmd-grid{grid-template-columns:repeat(2,1fr)}}', why='cmd-grid 4 colonnes')
rep('#ami-union .src-tri .mag-128-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.4rem;}', '#ami-union .src-tri .mag-128-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:.4rem;}\n@media(max-width:900px){#ami-union .src-tri .mag-128-grid{grid-template-columns:repeat(2,1fr)}}', why='mag-128-grid 4/2 colonnes')
# A·21 · chaque tableau dans son rouleau
tw_before = h.count('class="tw"')
def wrap_tables(html):
out = []; i = 0; n = 0
for m in re.finditer(r'<table\b.*?</table>', html, re.S):
before = html[max(0, m.start()-160):m.start()]
out.append(html[i:m.start()])
if re.search(r'class="(?:[^"]*\s)?(?:tw|cm2-tw|tw-x|scroll)(?:\s[^"]*)?"[^>]*>\s*$', before) or 'overflow' in before[-120:]:
out.append(m.group(0))
else:
out.append('<div class="tw">' + m.group(0) + '</div>'); n += 1
i = m.end()
out.append(html[i:]); return ''.join(out), n
h, n = wrap_tables(h); J.append(f'tableaux mis dans un .tw ×{n}')
rep('#ami-union .em{font-style:normal}', '#ami-union .em{font-style:normal}\n#ami-union .tw{overflow-x:auto;-webkit-overflow-scrolling:touch;max-width:100%}\n#ami-union .tw>table{min-width:100%}', why='.tw règle')
# A·27 · l'accessibilité des onglets : les six lignes
rep('<div class="tab-bar-full" aria-label="Cinq parties du Volume 0.3">', '<div class="tab-bar-full" role="tablist" aria-label="Cinq parties du Volume 0.3">', why='role=tablist')
c = len(re.findall(r'<button class="tab-btn-full( active)?" type="button" data-c="t\d">', h)); assert c == 5, c
h = re.sub(r'<button class="tab-btn-full( active)?" type="button" data-c="(t\d)">', lambda m: f'<button class="tab-btn-full{m.group(1) or ""}" type="button" role="tab" aria-selected="{"true" if m.group(1) else "false"}" data-c="{m.group(2)}">', h); J.append('role=tab + aria-selected ×5')
rep('#ami-union .em{font-style:normal}', '#ami-union .em{font-style:normal}\n#ami-union .tab-btn-full:focus-visible,#ami-union button:focus-visible,#ami-union a:focus-visible{outline:2px solid var(--or);outline-offset:-2px}', why=':focus-visible')
open('v22.html', 'w', encoding='utf-8').write(h)
print('\n'.join('✓ ' + x for x in J)); print(len(J), 'gestes')
p2_plancher.pyp2_plancher.py page.html 390 · puis 1280 · puis 390 · puis 32077 lignes
Demande au navigateur qui est sous 11,5 px, écrit la règle qui le relève, remesure — jusqu'à zéro. Un élément qui hérite sa petitesse reçoit max(.72rem,1em).
# -*- coding: utf-8 -*-
"""Le plancher de lisibilité : plus aucun texte sous 11,5 px. On demande au navigateur QUI est sous le
plancher, on écrit la règle qui le relève, on remesure — jusqu'à ce que le FAIT soit acquis."""
import os, sys, json, re, collections
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'v22.html'
PLANCHER = 11.5
JS_OUVRE = r"""
() => { const R=document.getElementById('ami-union');
R.querySelectorAll('.tab-content').forEach(p=>p.classList.add('active'));
R.querySelectorAll('.pane,.tri-p,.tab-pane,[data-c]').forEach(p=>{ if(p.tagName!=='BUTTON') p.classList.add('active','on'); });
R.querySelectorAll('details').forEach(d=>d.open=true);
R.querySelectorAll('[hidden]').forEach(e=>{ if(!e.matches('.pp-devise')) e.hidden=false; });
R.querySelectorAll('.som-corps,.toc-panel').forEach(e=>{e.style.display='block';e.style.maxHeight='none'});
return R.querySelectorAll('*').length; }
"""
JS_PETITS = r"""
(plancher) => {
const R=document.getElementById('ami-union'); const out=[]; const seen=new Set();
const w=document.createTreeWalker(R, NodeFilter.SHOW_TEXT); let n;
function key(el){ // chemin de classes jusqu'au canton
const parts=[]; let e=el; let depth=0;
while(e && e!==R && depth<6){ const cls=(e.className&&typeof e.className==='string')?e.className.trim().split(/\s+/).filter(c=>!/^(active|on|open|ici|vide)$/.test(c)):[];
parts.unshift(e.tagName.toLowerCase()+(cls.length?'.'+cls[0]:'')); e=e.parentElement; depth++; }
return parts; }
while((n=w.nextNode())){ const t=n.textContent.trim(); if(t.length<2) continue; const el=n.parentElement; if(!el||el.closest('svg,script,style')) continue;
const cs=getComputedStyle(el); if(cs.display==='none'||cs.visibility==='hidden') continue;
const fs=parseFloat(cs.fontSize); if(fs>=plancher) continue;
const k=key(el); const id=k.join('>')+'|'+(el.getAttribute('style')?'S':''); if(seen.has(id)) continue; seen.add(id);
const pfs=el.parentElement?parseFloat(getComputedStyle(el.parentElement).fontSize):fs;
out.push({path:k, fs:+fs.toFixed(2), herite:Math.abs(pfs-fs)<0.01, inline:!!(el.getAttribute('style')&&/font-size/.test(el.getAttribute('style'))), t:t.slice(0,40)}); }
return out; }
"""
def selecteur(path, niveau):
# niveau 0 : dernier élément + son canton ; 1 : deux derniers ; 2 : trois derniers ; 3 : !important
canton = next((p for p in path if p.split('.')[-1] in ('div.src-tri','div.src-lois','div.src-contrats','div.src-arbo') or '.src-' in p), None)
tail = path[-(1+min(niveau,2)):]
sel = '#ami-union ' + ((canton.split('.')[0]+'.'+canton.split('.')[1]+' ') if canton and canton not in tail else '') + ' '.join(tail)
return sel
html = open(SRC, encoding='utf-8').read()
W = int(sys.argv[2]) if len(sys.argv) > 2 else 390
regles = collections.OrderedDict()
ENTETE = ''
m0 = re.search(r'<style id="ami-plancher">\n(.*?)</style>\n', html, re.S)
if m0:
corps = m0.group(1)
i = corps.find('/* ── LE PLANCHER')
ENTETE = corps[:i] if i >= 0 else corps # sans marqueur, tout le bloc est l'entête de non-régression
for l in corps.split('\n'):
if l.endswith('}') and ('{font-size:.72rem' in l or '{font-size:max(.72rem' in l) and not os.environ.get('PLANCHER_NEUF'): regles[l[:l.index('{')]] = l
with sync_playwright() as p:
br = p.chromium.launch(); ctx = br.new_context(viewport={'width':W,'height':844}); page = ctx.new_page()
for passe in range(5):
page.set_content(html, wait_until='load'); page.wait_for_timeout(300)
page.evaluate(JS_OUVRE)
petits = page.evaluate(JS_PETITS, PLANCHER)
print(f'passe {passe} : {len(petits)} éléments porteurs de texte sous {PLANCHER} px')
if not petits: break
for e in petits:
sel = selecteur(e['path'], passe if not e['inline'] else 3)
imp = '!important' if (e['inline'] or passe >= 3) else ''
# un élément qui HÉRITE sa petitesse (strong dans une note) ne doit pas rapetisser ailleurs : max(.72rem,1em)
val = 'max(.72rem,1em)' if e.get('herite') else '.72rem'
regles[sel] = f'{sel}{{font-size:{val}{imp}}}'
bloc = '<style id="ami-plancher">\n' + ENTETE + '/* ── LE PLANCHER DE LISIBILITÉ — 11,5 px, mesuré au rendu à 390 puis 1280 px. Écrit par p2_plancher.py ; se rejoue. ── */\n' + '\n'.join(regles.values()) + '\n</style>\n'
html = re.sub(r'<style id="ami-plancher">.*?</style>\n', '', html, flags=re.S)
html = html.replace('</body>', bloc + '</body>')
# mesure finale
page.set_content(html, wait_until='load'); page.wait_for_timeout(300); page.evaluate(JS_OUVRE)
reste = page.evaluate(JS_PETITS, PLANCHER)
print('reste :', len(reste), reste[:5])
br.close()
open(SRC, 'w', encoding='utf-8').write(html)
print(len(regles), 'règles de plancher écrites')
p3_palette.pyp3_palette.py rapatrier · puis repeindre178 lignes
M1 : chaque couleur en dur devient une variable (rendu inchangé, mesuré). M2 : les redéclarations de canton s'effacent et la racine prend les 13 pierres de la Charte.
# -*- coding: utf-8 -*-
"""P3 · La palette, en deux temps.
M1 · rapatrier : chaque couleur écrite en dur reprend le nom de sa variable — sans rien changer au rendu.
M2 · une valeur, un lieu : les cantons cessent de redéclarer la palette.
REPEINDRE : la racine passe aux pierres de la Charte de Communication (6 sept 2026)."""
import re, sys, collections
sys.path.insert(0, '../verif/verificateur'); import canon as K, couleur as C
ETAPE = sys.argv[1] if len(sys.argv) > 1 else 'rapatrier'
h = open('v22.html', encoding='utf-8').read()
def norm_hex(x):
x = x.lower()
if len(x) == 4: x = '#' + ''.join(c*2 for c in x[1:])
return x[:7]
def rgb(hx): return C.hex2rgb(norm_hex(hx))
# ── le bloc racine ────────────────────────────────────────────────────
m = re.search(r'#ami-union\{\n(.*?)\n\}', h, re.S); root = m.group(1)
old_root = root
# la table ancien nom → valeur (avant), et → pierre (après)
ANCIENS = { # nom : (valeur d'avant, cible Charte)
'--or':'#ffc545','--or-pale':'#ffd06a','--or-somb':'#7a611a','--nacre':'#f2ede2','--gris':'#9595a2','--gris-cl':'#b0b0bd',
'--vert':'#0b8d38','--vert-cl':'#32b15e','--rubis':'#e33232','--rubis-cl':'#fe6969','--bleu':'#0077ee','--bleu-cl':'#7697ee',
'--fuchsia':'#cf458a','--fuch-cl':'#f56aaf','--fuchsia-cl':'#f56aaf','--violet':'#6b63ff','--violet-cl':'#9d8bff',
'--turquoise':'#009f9d','--turquoise-cl':'#31b4b3','--turq':'#31b4b3','--amethyste':'#a04afa','--amethyste-cl':'#bb7cfe',
'--jade':'#62a802','--jade-cl':'#80c027','--ambre':'#ff7800','--dore':'#e6ad2d','--dore-cl':'#ffd06a','--rose':'#f56aaf',
}
PIERRES = {p: v for p, v in K.PALETTE.items()}
NEUTRES = {'--noir':'#09090e','--onyx':'#09090e','--encre':'#111118','--voile':'#181826','--labradorite':'#b3b1a8','--or-gras':'#e6ad2d'}
# valeurs d'AVANT de chaque variable (première déclaration hex trouvée dans le fichier)
avant = {}
for mm in re.finditer(r'(--[a-z][a-z0-9-]*)\s*:\s*(#[0-9a-fA-F]{3,8})\b', h):
avant.setdefault(mm.group(1), norm_hex(mm.group(2)))
val2var = {}
for k, v in avant.items():
if k in ('--tc','--tcl','--ppc','--ppcl','--c','--rc','--vc'): continue
val2var.setdefault(v, k)
# la palette de la Charte, pour nommer ce qui n'a pas de variable
idx = {}
for p, trois in K.PALETTE.items():
for niv, hx in zip(K.NIVEAUX, trois): idx[hx.lower()] = '--' + p + niv
for hx, n in K.NEUTRES.items(): idx[hx.lower()] = '--' + n
for hx, n in K.STANDARD.items(): idx.setdefault(hx.lower(), '--' + n)
labs = [(n, C.hex2lab(hx)) for hx, n in idx.items()]
def nearest(hx):
lab = C.rgb2lab(C.hex2rgb(hx)); return min(((n, C.de2000(lab, l)) for n, l in labs), key=lambda t: t[1])
# ── découpage : CSS hors SVG, attributs style hors SVG ────────────────
svg_spans = [(m.start(), m.end()) for m in re.finditer(r'<svg\b.*?</svg>', h, re.S)]
script_spans = [(m.start(), m.end()) for m in re.finditer(r'<script\b.*?</script>', h, re.S)]
def dans(pos, spans): return any(a <= pos < b for a, b in spans)
nouveaux = collections.OrderedDict() # valeur → nom créé
journal = collections.Counter()
def nom_pour(hx):
"""le nom de variable d'une valeur écrite en dur"""
hx = norm_hex(hx)
if hx in val2var: return val2var[hx]
r, g, b = C.hex2rgb(hx)
if max(r,g,b) - min(r,g,b) <= 6: return None # un gris : neutre technique, on laisse
if hx in nouveaux: return nouveaux[hx]
n, d = nearest(hx)
base = n.lstrip('-').replace('-', '')
nom = f'--anc-{base}'
k = 2
while nom in nouveaux.values(): nom = f'--anc-{base}{k}'; k += 1
nouveaux[hx] = nom; return nom
if ETAPE == 'rapatrier':
# 1 · les hex dans les feuilles (hors SVG, hors déclarations de variables)
out = []; i = 0
for m in re.finditer(r'<style[^>]*>(.*?)</style>', h, re.S):
if dans(m.start(), svg_spans): continue
css = m.group(1)
def sub_hex(mm):
hx = mm.group(0); nom = nom_pour(hx)
if not nom: return hx
journal['hex→var (css)'] += 1; return f'var({nom})'
# ne pas toucher aux déclarations de variables : on masque « --x:#hex »
parts = re.split(r'(--[a-z][a-z0-9-]*\s*:\s*#[0-9a-fA-F]{3,8}\b)', css)
for k in range(0, len(parts), 2):
parts[k] = re.sub(r'#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b(?![0-9a-fA-F])', sub_hex, parts[k])
# rgba(r,g,b,a) → rgba(var(--x-rgb),a)
def sub_rgba(mm):
r, g, b = int(mm.group(1)), int(mm.group(2)), int(mm.group(3))
hx = '#%02x%02x%02x' % (r, g, b); nom = nom_pour(hx)
if not nom: return mm.group(0)
journal['rgba→var (css)'] += 1; return f'rgba(var({nom}-rgb),{mm.group(4)})'
parts[k] = re.sub(r'rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([0-9.]+)\s*\)', sub_rgba, parts[k])
css2 = ''.join(parts)
out.append(h[i:m.start(1)]); out.append(css2); i = m.end(1)
out.append(h[i:]); h = ''.join(out)
svg_spans = [(m.start(), m.end()) for m in re.finditer(r'<svg\b.*?</svg>', h, re.S)]
# 2 · les attributs style="…" hors SVG
def sub_style(mm):
if dans(mm.start(), svg_spans): return mm.group(0)
s = mm.group(1)
parts = re.split(r'(--[a-z][a-z0-9-]*\s*:\s*#[0-9a-fA-F]{3,8}\b)', s)
for k in range(0, len(parts), 2):
def sh(m2):
nom = nom_pour(m2.group(0))
if not nom: return m2.group(0)
journal['hex→var (inline)'] += 1; return f'var({nom})'
parts[k] = re.sub(r'#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b(?![0-9a-fA-F])', sh, parts[k])
def sr(m2):
hx = '#%02x%02x%02x' % (int(m2.group(1)), int(m2.group(2)), int(m2.group(3))); nom = nom_pour(hx)
if not nom: return m2.group(0)
journal['rgba→var (inline)'] += 1; return f'rgba(var({nom}-rgb),{m2.group(4)})'
parts[k] = re.sub(r'rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([0-9.]+)\s*\)', sr, parts[k])
# et les déclarations inline --tc:#hex → var()
for j in range(1, len(parts), 2):
d = parts[j]; nm, hx = d.split(':')[0].strip(), norm_hex(d.split(':')[1].strip())
nom = val2var.get(hx)
if nom and nom != nm: parts[j] = f'{nm}:var({nom})'; journal['inline --x:#hex → var'] += 1
return 'style="' + ''.join(parts) + '"'
h = re.sub(r'style="([^"]*)"', sub_style, h)
# 3 · la racine déclare tout : les anciennes valeurs, les triplets rgb, les nouveaux noms
decl = []
for k, v in avant.items():
if k in ('--tc','--tcl','--ppc','--ppcl','--c','--rc','--vc'): continue
decl.append(f'{k}:{v}')
for k, v in avant.items():
if k in ('--tc','--tcl','--ppc','--ppcl','--c','--rc','--vc'): continue
r, g, b = C.hex2rgb(v); decl.append(f'{k}-rgb:{r},{g},{b}')
for hx, nom in nouveaux.items():
r, g, b = C.hex2rgb(hx); decl.append(f'{nom}:{hx}'); decl.append(f'{nom}-rgb:{r},{g},{b}')
root_new = ' /* ── LA PALETTE — déclarée ici, et nulle part ailleurs (P3·M1, 9 sept 2026) ── */\n ' + ';'.join(decl) + ';\n --r:4px;\n' + root[root.find('all:revert'):]
h = h.replace('#ami-union{\n' + old_root + '\n}', '#ami-union{\n' + root_new + '\n}', 1)
# 4 · M2 : les cantons ne redéclarent plus ce que la racine porte
import os
if os.environ.get('SANS_M2'): open('v22_m1seul.html','w',encoding='utf-8').write(h); raise SystemExit('M1 seul écrit dans v22_m1seul.html')
svg_spans = [(x.start(), x.end()) for x in re.finditer(r'<svg\b.*?</svg>', h, re.S)]
script_spans = [(x.start(), x.end()) for x in re.finditer(r'<script\b.*?</script>', h, re.S)]
m = re.search(r'#ami-union\{\n(.*?)\n\}', h, re.S); old_root = m.group(1)
def sub_canton(mm):
nm = mm.group(1)
if nm in avant and nm not in ('--tc','--tcl','--ppc','--ppcl','--c','--rc','--vc') and not dans(mm.start(), svg_spans) and not dans(mm.start(), script_spans):
if mm.start() > m.start() + len(old_root) + 50: # hors racine
journal['redéclaration de canton retirée'] += 1; return ''
return mm.group(0)
h = re.sub(r'(--[a-z][a-z0-9-]*)\s*:\s*#[0-9a-fA-F]{3,8}\s*;?', sub_canton, h)
h = re.sub(r'\{\s*\}', '{}', h)
print(dict(journal)); print('nouveaux noms :', dict(nouveaux))
elif ETAPE == 'repeindre':
m = re.search(r'#ami-union\{\n(.*?)\n\}', h, re.S); root = m.group(1)
def peint(mm):
k, v = mm.group(1), mm.group(2)
if k in ANCIENS: return f'{k}:{ANCIENS[k]}'
if k in NEUTRES: return f'{k}:{NEUTRES[k]}'
if k.startswith('--anc-'):
n, d = nearest(v)
if d < 8:
cible = n
# le nom de la pierre ; la valeur vient de la Charte
for hx, nm in idx.items():
if nm == cible: return f'{k}:{hx}'
return mm.group(0)
root2 = re.sub(r'(--[a-z][a-z0-9-]*):(#[0-9a-f]{6})', peint, root)
# les triplets suivent la variable qu'ils doublent
def triplet(mm):
k = mm.group(1); base = k[:-4]
mv = re.search(re.escape(base) + r':(#[0-9a-f]{6})', root2)
if mv:
r, g, b = C.hex2rgb(mv.group(1)); return f'{k}:{r},{g},{b}'
return mm.group(0)
root2 = re.sub(r'(--[a-z][a-z0-9-]*-rgb):(\d+,\d+,\d+)', triplet, root2)
# les pierres de la Charte, déclarées par leur nom, pour la suite du corpus
pierres = ';'.join(f'--{p}:{v[0]};--{p}-gras:{v[1]};--{p}-ct:{v[2]}' for p, v in PIERRES.items())
root2 = root2.replace(' --r:4px;', ' /* ── les treize pierres, telles que la Charte les arrête ── */\n ' + pierres + ';--labradorite:#b3b1a8;--or-gras:#e6ad2d;\n --r:4px;')
h = h.replace(root, root2, 1)
# journal
ch = 0
for k in ANCIENS:
if re.search(re.escape(k) + r':#', root): ch += 1
print('variables repeintes :', ch)
open('v22.html', 'w', encoding='utf-8').write(h)
compare_couleurs.pycompare_couleurs.py avant.html après.html30 lignes
Couleur calculée de chaque élément, apparié par son chemin dans le DOM : ce qui a bougé, et de quoi à quoi.
# -*- coding: utf-8 -*-
"""Compare la couleur calculée de chaque élément entre deux états — apparié par le chemin dans le DOM, jamais par l'index."""
import sys, os, json, collections
from playwright.sync_api import sync_playwright
A, B = sys.argv[1], sys.argv[2]
JS = r"""() => { const R=document.getElementById('ami-union'); const out={};
R.querySelectorAll('.tab-content').forEach(p=>p.classList.add('active'));
R.querySelectorAll('[data-c]').forEach(p=>{ if(p.tagName!=='BUTTON') p.classList.add('active','on'); });
R.querySelectorAll('details').forEach(d=>d.open=true);
function key(el){ const p=[]; let e=el; while(e && e!==document.body){ let i=0,s=e; while((s=s.previousElementSibling)) i++; p.push(e.tagName+i); e=e.parentElement;} return p.join('/'); }
R.querySelectorAll('*').forEach(el=>{ if(el.closest('svg,script,style')) return; const cs=getComputedStyle(el);
out[key(el)] = [cs.color, cs.backgroundColor, cs.borderTopColor, cs.borderLeftColor, cs.backgroundImage.slice(0,80), (el.className||'').toString().slice(0,40)]; });
return out; }"""
with sync_playwright() as p:
br = p.chromium.launch(); res = {}
for f in (A, B):
pg = br.new_page(viewport={'width':1280,'height':900}); pg.goto('file://' + os.path.abspath(f)); pg.wait_for_timeout(500)
res[f] = pg.evaluate(JS); pg.close()
br.close()
a, b = res[A], res[B]
print('éléments :', len(a), len(b), '· appariés :', len(set(a) & set(b)))
diff = collections.Counter(); ex = collections.defaultdict(list)
for k in set(a) & set(b):
for i, nom in enumerate(('color', 'background', 'border-top', 'border-left', 'background-image')):
if a[k][i] != b[k][i]:
key = (nom, a[k][i][:40], b[k][i][:40]); diff[key] += 1
if len(ex[key]) < 2: ex[key].append(a[k][5])
print('différences :', sum(diff.values()))
for (nom, x, y), n in diff.most_common(60): print(f'{n:5d}× {nom:16s} {x:40s} → {y:40s} ex {ex[(nom,x,y)]}')
extrait_barre.pyextrait_barre.py67 lignes
Extrait de la Charte V5 la barre, le pied, la bande des registres, la progression, la boîte à outils — markup + CSS + JS — pour les coller dans une page.
# -*- coding: utf-8 -*-
"""Extrait de la Charte V5 (la source vivante du composant) : la barre, le pied, la bande des registres,
la barre de progression et la boîte à outils — markup + CSS + JS — pour les coller dans une Page du corpus."""
import re, json
h = open('charteV5.html', encoding='utf-8').read()
h = re.sub(r'data:image/[a-z+]+;base64,[A-Za-z0-9+/=]+', 'data:IMG', h)
def bloc(h, start_tag, end_tag):
i = h.find(start_tag); j = h.find(end_tag, i); return h[i:j+len(end_tag)]
nav = bloc(h, '<nav class="ami-barre"', '</nav>')
pied = bloc(h, '<footer class="ami-pied"', '</footer>')
prog = re.search(r'<div[^>]*id="prog"[^>]*>(?:.*?</div>)?', h).group(0)
bande = bloc(h, '<div class="bande-registres"', '</div>\n</div>')
print('nav', len(nav), 'pied', len(pied), 'prog', prog[:80], 'bande', len(bande))
# ── la feuille : on découpe en blocs de premier niveau ────────────────
css = re.search(r'<style[^>]*>(.*?)</style>', h, re.S).group(1)
css = re.sub(r'/\*.*?\*/', '', css, flags=re.S)
blocs = []; i = 0; n = len(css)
while i < n:
j = css.find('{', i)
if j < 0: break
sel = css[i:j].strip(); depth = 1; k = j + 1
while k < n and depth:
if css[k] == '{': depth += 1
elif css[k] == '}': depth -= 1
k += 1
blocs.append((sel, css[j+1:k-1])); i = k
CIBLES = ('.ami-barre', '.ami-pied', '.emporter', '.emp-', '.ami-toc', '.tc-', 'body.ami-', 'html.ami-', '#prog', '.bande-registres', '.br-', '.r-op', '.r-heu', '.r-ana', '.r-ess', '.tb-sep', '.ami-coin', '.coin', ':root:fullscreen', ':root:-webkit-full-screen')
def garde(sel):
return any(c in sel for c in CIBLES)
out = []
for sel, corps in blocs:
if sel.startswith('@keyframes'):
if re.search(r'ami(Eclot|Coeur|Respire|Pulse|Prog)', sel): out.append(f'{sel}{{{corps}}}')
elif sel.startswith('@media'):
# on ne garde du média que les règles ciblées
inner = []; ii = 0
while True:
jj = corps.find('{', ii)
if jj < 0: break
s2 = corps[ii:jj].strip(); d = 1; kk = jj + 1
while kk < len(corps) and d:
if corps[kk] == '{': d += 1
elif corps[kk] == '}': d -= 1
kk += 1
if garde(s2): inner.append(f'{s2}{{{corps[jj+1:kk-1]}}}')
ii = kk
if inner: out.append(f'{sel}{{' + '\n'.join(inner) + '}')
elif garde(sel):
out.append(f'{sel}{{{corps}}}')
css_out = '\n'.join(out)
print('règles gardées :', len(out), 'signes :', len(css_out))
# les variables que ce CSS emploie et que la racine devra porter
vars_used = sorted(set(re.findall(r'var\((--[a-z0-9-]+)', css_out)))
print('variables employées :', vars_used)
# ── les scripts : la barre (amiBarre) et la boîte à outils (BOITE) ─────
scripts = re.findall(r'<script[^>]*>(.*?)</script>', h, re.S)
js_barre = next(s for s in scripts if "getElementById('amiBarre')" in s[:200])
js_boite = next(s for s in scripts if 'BOITE' in s[:300])
print('js barre', len(js_barre), 'js boîte', len(js_boite))
# ce que la boîte attend dans la page
print('ids attendus par la boîte :', sorted(set(re.findall(r"getElementById\('([^']+)'\)", js_boite)))[:20])
print('ids attendus par la barre :', sorted(set(re.findall(r"getElementById\('([^']+)'\)", js_barre)))[:30])
print('classes hôte que la barre cherche :', sorted(set(re.findall(r"querySelector(?:All)?\('([^']+)'\)", js_barre)))[:40])
json.dump(dict(nav=nav, pied=pied, prog=prog, bande=bande, css=css_out, js_barre=js_barre, js_boite=js_boite, vars=vars_used), open('barre_extrait.json', 'w'), ensure_ascii=False)
p4_anatomie.pyp4_anatomie.py117 lignes
Pose la barre et le pied V16, la bande des registres, #prog, le bloc Emporter, le contraste élevé et la lecture continue ; version et date écrites une fois (data-ami).
# -*- coding: utf-8 -*-
"""P4 · L'anatomie de Page : la barre et le pied V16 (pris à la Charte V5, la source vivante du composant),
la bande des registres, la barre de progression, le bloc Emporter, les commandes 💧 ◐ ⛶ − % +."""
import re, json
d = json.load(open('barre_extrait.json'))
h = open('v22.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'✗ attendu {n}, trouvé {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} ×{n}')
# ── 1 · la feuille du composant, dans le <head> ──────────────────────
rootvals = ';'.join(f'{k}:{v}' for k, v in d['rootvals'].items())
css = d['css']
feuille = ('<style id="ami-composant">\n/* ── LA BARRE, LE PIED, LES REGISTRES, L’EMPORT — pris à la Charte de Communication V5 (9 sept 2026),\n'
' la source vivante du composant. On ne les corrige pas ici : on les reprend à la Charte. ── */\n'
':root{' + rootvals + ';--nav-h:0px;--bar-h:0px}\n'
'html{scroll-padding-top:calc(var(--nav-h) + 1rem)}\n'
'#ami-union .ami-fs{display:none}\n' # la barre porte le ⛶ ; celui de la page se tait
'.emporter{max-width:900px;margin:2rem auto;padding:0 1.2rem}\n'
'.bande-registres{max-width:1080px;margin:0 auto}\n'
+ css + '\n</style>\n')
rep('</head>', feuille + '</head>', why='feuille du composant')
# ── 2 · la barre et la progression, en tête ; le pied et l’emport, en queue ──
nav = d['nav'].replace('<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi">',
'<nav class="ami-barre" id="amiBarre" aria-label="Architecture du Protocole aMi" data-ici="ami-union-du-tout">')
rep('<body>\n<div id="ami-union">', '<body>\n' + d['prog'] + '\n' + nav + '\n<div id="ami-union">', why='barre + #prog')
pied = d['pied']
pied = pied.replace('<div class="ap-sceau" title="🛕 Le sceau du volume">🛕</div>', '<div class="ap-sceau" title="💍 Le sceau du volume">💍</div>')
pied = pied.replace('<p class="ap-titre">Architecture</p>', "<p class=\"ap-titre\">L'Union du TouT</p>")
pied = pied.replace('<p class="ap-vol">Protocole de ConScience aMi · Le Temple — La Vie</p>', '<p class="ap-vol">Protocole de ConScience aMi · Volume <span data-ami="volume">0.3</span> — La Vie</p>')
pied = pied.replace("<p class=\"ap-cit\">Je ne veux pas être crue sur parole au vol,\nje veux être sue sans le moindre bémol.</p>",
"<p class=\"ap-cit\">Une charte n’est pas une liste de règles : c’est la forme qu’on donne à ce qu’on veut devenir.\nOn n’obéit pas à une charte — on s’y tient.</p>")
assert 'title="💍 Le sceau du volume">💍' in pied and "L'Union du TouT" in pied and 'Volume <span' in pied and 'on s’y tient' in pied
emport = '''
<section class="emporter" aria-labelledby="empT">
<h2 id="empT">◈ Emporter ce volume</h2>
<p>Il tient en <strong>un seul fichier</strong>, sans script distant : une fois téléchargé, <strong>il fonctionne hors ligne</strong> et reste vivant — onglets, sous-onglets, planches, lecture continue, contraste.</p>
<div class="emp-b">
<button id="dlHtml" type="button">⬇︎ Export HTML <span id="dlPoids"></span></button>
<button id="dlPdf" type="button">🖶 PDF imprimable</button>
</div>
<p id="dlEtat" class="emp-d" aria-live="polite"></p>
<p class="emp-d">📅 <span data-ami="version">V22</span> — arrêtée au <span data-ami="date">9 septembre 2026</span>. <em>Les renvois vers l’extérieur peuvent se briser avec le temps : vérifie la version en ligne si celle-ci a vieilli.</em></p>
</section>
'''
# le pied vient après la racine, avant les feuilles de fin
i = h.rfind('<style id="ami-plancher">')
assert i > 0
# la fermeture de #ami-union est le dernier </div> avant les styles de fin ; on pose l'emport DEDANS (dernier enfant) et le pied DEHORS
j = h.rfind('</div>', 0, i)
h = h[:j] + emport + h[j:j+6] + '\n' + pied + '\n' + h[j+6:]; J.append('emport (dans la racine) + pied (après)')
# ── 3 · la bande des registres, entre la une et les onglets ──────────
bande = d['bande'].replace('href="#s-les-quatre-registres-de-lecture"', 'href="Charte_Communication_aMi_BLOC_13.html" title="Les quatre registres de lecture — définis une fois, dans la Charte de Communication"')
rep('<div class="tab-bar-full" role="tablist"', bande + '\n<div class="tab-bar-full" role="tablist"', why='bande des registres')
# ── 4 · les scripts du composant, adaptés à cette page ───────────────
js = d['js_barre']
js = js.replace("document.getElementById('ami-charte')||document.body", "document.getElementById('ami-union')||document.body")
# le sommaire ☰ de la barre n'est pas posé ici : la page a le sien (◈ Dans cet onglet) — le code le saute (if(BT))
# l'emport : les onglets de CETTE page, et le nom du fichier
js = js.replace(""" var ongs=d.querySelectorAll('.ong');
ongs.forEach(function(o,i){ o.classList.toggle('on', i===0);
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.pan').forEach(function(p,i){ p.hidden = i!==0; });
d.querySelectorAll('.sbar').forEach(function(s,i){ s.hidden = true; });
d.querySelectorAll('.sog').forEach(function(o,i){ o.classList.toggle('on', i===0); });
d.querySelectorAll('#tv > .pane').forEach(function(p,i){
p.classList.toggle('on', i===0); });""",
""" d.querySelectorAll('.tab-btn-full').forEach(function(o,i){ o.classList.toggle('active', i===0);
o.setAttribute('aria-selected', i===0?'true':'false'); });
d.querySelectorAll('.tab-content').forEach(function(p,i){ p.classList.toggle('active', i===0); });""")
assert "'.tab-content').forEach" in js
js = js.replace("a.download='Charte_Communication_aMi_'+(window.__amiV||'')+'.html';", "a.download='0.3_LUNION_DU_TOUT_aMi_'+(window.__amiV||'')+'.html';")
assert '0.3_LUNION_DU_TOUT' in js
shim = """
/* ── CE QUE LE VOLUME ÉCOUTE : la lecture continue et le contraste élevé ──
La barre ne connaît pas le contenu : elle crie, la page répond. */
(function(){'use strict';
var R=document.getElementById('ami-union'); if(!R) return;
var memo=null;
document.addEventListener('ami:lecture',function(e){
var on=e.detail&&e.detail.actif;
var pans=[].slice.call(R.querySelectorAll('.tab-content'));
if(on){ memo=(R.querySelector('.tab-content.active')||{dataset:{c:'t1'}}).dataset.c;
pans.forEach(function(p){ p.classList.add('active'); }); R.classList.add('lecture'); }
else { R.classList.remove('lecture'); pans.forEach(function(p){ p.classList.toggle('active', p.dataset.c===memo); });
if(window.amiOuvreOnglet&&memo) window.amiOuvreOnglet(memo,false); }
});
})();
"""
contraste_css = """
<style id="ami-contraste">
/* ── ◐ LE CONTRASTE ÉLEVÉ : chaque encre monte à son niveau -ct, les graisses fines s'épaississent ── */
body.ami-contraste #ami-union{--or:var(--ambre-ct);--or-pale:#ffe2a3;--gris:#c0c0cf;--gris-cl:#d6d6e0;--nacre:#ffffff;
--vert:var(--emeraude-ct);--vert-cl:#7fe0a0;--rubis:var(--rubis-ct);--rubis-cl:#ff9a9a;--bleu:var(--saphir-ct);--bleu-cl:#a9c3ff;
--fuchsia:var(--grenat-ct);--fuch-cl:#ffa4d0;--fuchsia-cl:#ffa4d0;--violet:var(--tanzanite-ct);--violet-cl:#c3b8ff;
--turquoise:var(--amazonite-ct);--turquoise-cl:#7fdcd6;--amethyste:var(--amethyste-ct);--amethyste-cl:#d8b8ff;
--jade:var(--moldavite-ct);--jade-cl:#b6e27a;--dore:var(--ambre-ct);--dore-cl:#ffe2a3;--or-somb:#b09030}
body.ami-contraste #ami-union{font-weight:400}
body.ami-contraste #ami-union em,body.ami-contraste #ami-union .cit,body.ami-contraste #ami-union .couv-sub{font-style:normal}
</style>
"""
scripts = '<script id="ami-composant-js">\n' + js + '\n' + shim + '</script>\n'
rep('<style id="ami-plancher">', contraste_css + scripts + '<style id="ami-plancher">', why='scripts du composant + contraste')
# ── 5 · la date s'écrit une fois : la une et la fin la lisent ─────────
rep('<p class="sig">Agdistys · Diane Serant & Claude · CC BY-SA 4.0</p>', '<p class="sig">Agdistys · Diane Serant & Claude · CC BY-SA 4.0 · <span data-ami="version">V22</span> — <span data-ami="date">9 septembre 2026</span></p>', why='date sur la une')
open('v22.html', 'w', encoding='utf-8').write(h)
print('\n'.join('✓ ' + x for x in J))
p5_correctifs.pyp5_correctifs.py37 lignes
Ce que la non-régression a révélé : la portée main → #ami-union, les règles #tv/.tvx ôtées.
# -*- coding: utf-8 -*-
"""P5 · Ce que la non-régression a révélé : la bande des registres était habillée pour <main> (la Charte),
pas pour #ami-union (cette page) ; et la feuille du composant charriait les règles de l'onglet #tv/.tvx de la Charte."""
import re
h = open('v22.html', encoding='utf-8').read()
m = re.search(r'(<style id="ami-composant">)(.*?)(</style>)', h, re.S)
css = m.group(2)
avant = css.count('{')
# 1 · la portée : main → #ami-union
css = re.sub(r'(?<![\w.#-])main (\.bande-registres|\.r-heu|\.r-ana|\.r-ess)', r'#ami-union \1', css)
assert 'main .bande' not in css
# 2 · on ôte les règles dont TOUS les sélecteurs vivent dans #tv / .tvx (l'onglet de la Charte, absent ici)
out = []; i = 0; n = len(css); gardees = 0; otees = 0
while i < n:
j = css.find('{', i)
if j < 0: out.append(css[i:]); break
sel = css[i:j]; depth = 1; k = j + 1
while k < n and depth:
if css[k] == '{': depth += 1
elif css[k] == '}': depth -= 1
k += 1
parts = [p.strip() for p in sel.strip().split(',') if p.strip()]
if parts and not sel.strip().startswith('@') and all(re.match(r'(#tv|\.tvx)\b', p) for p in parts):
otees += 1
else:
# dans une liste mixte, on retire les membres #tv/.tvx
if parts and not sel.strip().startswith('@'):
keep = [p for p in parts if not re.match(r'(#tv|\.tvx)\b', p)]
lead = sel[:len(sel) - len(sel.lstrip())]
sel = lead + ',\n'.join(keep)
out.append(sel + css[j:k]); gardees += 1
i = k
css2 = ''.join(out)
h = h[:m.start()] + m.group(1) + css2 + m.group(3) + h[m.end():]
open('v22.html', 'w', encoding='utf-8').write(h)
print(f'✓ portée main → #ami-union · règles : {avant} avant, {gardees} gardées, {otees} ôtées (#tv/.tvx) · feuille {len(m.group(2))//1024} → {len(css2)//1024} Ko')
p5_mutations.pyp5_mutations.py page.html94 lignes
Presse chaque commande (onglets clic/clavier, renvoi, 💧, ◐, ⛶ + zoom, ☰, ↑, progression, ⬇︎ export, dates, reduced-motion, sommaire, .v.ici) et vérifie que la page a changé comme promis.
# -*- coding: utf-8 -*-
"""P5 · La mutation des contrôles : on presse chaque commande et on regarde si la page a CHANGÉ comme promis."""
import os, sys
from playwright.sync_api import sync_playwright
SRC = sys.argv[1] if len(sys.argv) > 1 else 'v22.html'
R = []
def ok(nom, cond, det=''): R.append((nom, bool(cond), det)); print(('✓' if cond else '✗'), nom, det)
with sync_playwright() as p:
b = p.chromium.launch(); errs = []
for W in (390, 1280):
pg = b.new_page(viewport={'width': W, 'height': 844}); pg.on('pageerror', lambda e: errs.append(str(e)))
pg.goto('file://' + os.path.abspath(SRC)); pg.wait_for_timeout(600)
print(f'=== {W} px ===')
E = lambda js: pg.evaluate(js)
# onglets : clic
E("()=>document.querySelectorAll('.tab-btn-full')[2].click()"); pg.wait_for_timeout(150)
ok('onglet 3 par clic', E("()=>document.querySelector('.tab-content.active').dataset.c")=='t3' and E("()=>document.querySelectorAll('.tab-btn-full')[2].getAttribute('aria-selected')")=='true')
# onglets : clavier →
E("()=>{const b=document.querySelectorAll('.tab-btn-full')[2]; b.focus(); b.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true}));}"); pg.wait_for_timeout(150)
ok('onglet suivant au clavier →', E("()=>document.querySelector('.tab-content.active').dataset.c")=='t4')
E("()=>{const b=document.activeElement; b.dispatchEvent(new KeyboardEvent('keydown',{key:'Home',bubbles:true}));}"); pg.wait_for_timeout(150)
ok('Home → premier onglet', E("()=>document.querySelector('.tab-content.active').dataset.c")=='t1')
# lien interne vers un autre onglet ouvre l'onglet
E("()=>{const a=document.querySelector('a[href=\"#diag-souverain\"]'); if(a) a.click();}"); pg.wait_for_timeout(300)
ok('renvoi #diag-souverain ouvre son onglet', E("()=>{const t=document.getElementById('diag-souverain'); return !!t && t.closest('.tab-content').classList.contains('active') && t.getBoundingClientRect().width>0;}"))
# lecture continue 💧
n0 = E("()=>document.querySelectorAll('.tab-content.active').length")
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(300)
n1 = E("()=>document.querySelectorAll('.tab-content.active').length")
ok('💧 lecture continue : tous les onglets déroulés', n0 == 1 and n1 == 5, f'{n0}→{n1}')
E("()=>document.getElementById('amiLecture').click()"); pg.wait_for_timeout(300)
ok('💧 retour : un seul onglet', E("()=>document.querySelectorAll('.tab-content.active').length") == 1)
# contraste ◐
c0 = E("()=>getComputedStyle(document.querySelector('#ami-union .qs-c .tx, #ami-union p')).color")
col0 = E("()=>getComputedStyle(document.querySelector('#ami-union')).getPropertyValue('--or').trim()")
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(300)
col1 = E("()=>getComputedStyle(document.querySelector('#ami-union')).getPropertyValue('--or').trim()")
ok('◐ contraste : --or monte au niveau -ct', E("()=>document.body.classList.contains('ami-contraste')") and col0 != col1, f'{col0}→{col1}')
E("()=>document.getElementById('amiContraste').click()"); pg.wait_for_timeout(200)
ok('◐ retour', not E("()=>document.body.classList.contains('ami-contraste')"))
# zoom − / +
E("()=>document.getElementById('amiPlein').click()"); pg.wait_for_timeout(300)
ok('⛶ plein écran (faux plein) posé', E("()=>document.getElementById('ami-union').classList.contains('ami-faux-fs')||!!document.fullscreenElement"))
fs0 = E("()=>parseFloat(getComputedStyle(document.getElementById('ami-union')).zoom||1)")
E("()=>document.getElementById('amiZoomP').click()"); pg.wait_for_timeout(200)
fs1 = E("()=>parseFloat(getComputedStyle(document.getElementById('ami-union')).zoom||1)")
E("()=>document.getElementById('amiZoomM').click()"); pg.wait_for_timeout(200)
fs2 = E("()=>parseFloat(getComputedStyle(document.getElementById('ami-union')).zoom||1)")
E("()=>document.getElementById('amiPlein').click()"); pg.wait_for_timeout(300)
ok('⛶ retour', not E("()=>document.getElementById('ami-union').classList.contains('ami-faux-fs')"))
ok('+ agrandit, − rend', fs1 > fs0 and abs(fs2 - fs0) < .05, f'{fs0}→{fs1}→{fs2}')
# sommaire ☰ de la page
E("()=>document.getElementById('tocBtn').click()"); pg.wait_for_timeout(200)
ok('☰ sommaire s’ouvre', E("()=>{const p=document.getElementById('tocPanel'); const cs=getComputedStyle(p); return cs.display!=='none' && p.getBoundingClientRect().height>10;}"))
E("()=>document.getElementById('tocBtn').click()"); pg.wait_for_timeout(200)
# ↑ haut
E("()=>window.scrollTo(0,3000)"); pg.wait_for_timeout(200)
E("()=>{const h=document.querySelector('.ami-top'); if(h) h.click();}"); pg.wait_for_timeout(1500)
ok('↑ remonte', E("()=>window.scrollY") < 50, str(E("()=>window.scrollY")))
# progression
E("()=>window.scrollTo(0,document.body.scrollHeight/2)"); pg.wait_for_timeout(300)
ok('#prog progresse', E("()=>{const p=document.getElementById('prog'); const w=parseFloat(getComputedStyle(p).width)||parseFloat(p.style.width)||0; return w>10;}"))
# emport : le bouton compose un fichier
E("()=>{window.__urls=[]; const o=URL.createObjectURL; URL.createObjectURL=function(b){window.__blob=b; return o.call(URL,b);}; HTMLAnchorElement.prototype.click=function(){window.__dl=this.download;};}")
E("()=>document.getElementById('dlHtml').click()"); pg.wait_for_timeout(800)
VER = E("()=>document.querySelector('meta[name=\"ami:version\"]').content"); DATE = E("()=>document.querySelector('meta[name=\"ami:date\"]').content")
import datetime; MOIS=['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre']; d=datetime.date.fromisoformat(DATE); DATE_FR=f'{d.day} {MOIS[d.month-1]} {d.year}'
ok('⬇︎ Export HTML : fichier composé, bien nommé', E("()=>window.__dl")==f'0.3_LUNION_DU_TOUT_aMi_{VER}.html' and E("()=>window.__blob? window.__blob.size:0")>1000000, str(E("()=>[window.__dl, window.__blob&&window.__blob.size]")))
ok('⬇︎ poids affiché', E("()=>document.getElementById('dlPoids').textContent.trim().length")>0, E("()=>document.getElementById('dlPoids').textContent.trim()"))
# dates lues depuis les metas
ok('version/date = metas', E("()=>[...document.querySelectorAll('[data-ami=version]')].every(e=>e.textContent==='"+VER+"') && [...document.querySelectorAll('[data-ami=date]')].every(e=>e.textContent==='"+DATE_FR+"')"), E("()=>[...document.querySelectorAll('[data-ami=date]')].map(e=>e.textContent)"))
# reduced motion
pg.emulate_media(reduced_motion='reduce'); pg.wait_for_timeout(100)
ok('prefers-reduced-motion coupe les animations', E("()=>[...document.querySelectorAll('*')].every(e=>{const cs=getComputedStyle(e); return cs.animationDuration==='0s'||cs.animationDuration==='0.001s'||cs.animationName==='none';})"))
pg.emulate_media(reduced_motion='no-preference')
# sommaire : 5 · 28
ok('sommaire 5 onglets · 28 sections', E("()=>{const ch=document.querySelectorAll('#somCorps .som-partie').length; const s=document.querySelectorAll('#somCorps a[href^=\"#\"]').length; return ch+'·'+s;}") in ('5·33','5·28'), E("()=>{const ch=document.querySelectorAll('#somCorps .som-partie').length; const s=document.querySelectorAll('#somCorps a[href^=\"#\"]').length; return ch+'·'+s;}"))
ok('ancres du sommaire toutes présentes', E("()=>[...document.querySelectorAll('#somCorps a[href^=\"#\"]')].every(a=>document.getElementById(decodeURIComponent(a.getAttribute('href').slice(1))))"))
# le canton Arborescence : filtres et infobulles vivants
E("()=>document.querySelector('.tab-btn-full[data-c=\"t3\"]').click()"); pg.wait_for_timeout(150)
E("()=>document.querySelector('#ami-union .sub-bar-host button[data-c=psy]').click()"); pg.wait_for_timeout(150)
E("()=>document.querySelectorAll('#tp-psy .sub-b')[1].click()"); pg.wait_for_timeout(150)
f0 = E("()=>document.querySelectorAll('#tp-psy .dc-row[data-f]:not(.hid)').length")
E("()=>document.querySelector('#tp-psy .flt button[data-flt=urgence]').click()"); pg.wait_for_timeout(200)
f1 = E("()=>document.querySelectorAll('#tp-psy .dc-row[data-f]:not(.hid)').length")
ok('🌳 filtre « urgence » réduit les bifurcations', 0 < f1 < f0, f'{f0}→{f1}')
E("()=>{const e=document.querySelector('#tp-psy [data-champ]'); e.dispatchEvent(new Event('mouseenter'));}"); pg.wait_for_timeout(150)
ok('🌳 infobulle de Champ s’affiche', E("()=>document.getElementById('chpop').classList.contains('show')"))
ok('.v.ici marqué sur 0.3', E("()=>{const v=document.querySelector('.ami-barre .v.ici'); return v && /0\\.3/.test(v.textContent);}"))
pg.close()
b.close()
print('erreurs JS :', errs)
print(sum(1 for r in R if r[1]), '/', len(R), 'réussis')
p6_retours.pyp6_retours.py113 lignes
Les retours de Diane du 9 sept : onglets couleur qui replient, un seul titre, améthyste, Emporter, --tab-h, règle * ôtée.
# -*- coding: utf-8 -*-
"""P6 · Les retours de Diane du 9 sept 20h : onglets en couleur et qui replient (jamais de rouleau),
Les Chartes du Vivant en améthyste, un seul titre par partie (le portail, émoji au-dessus, glow),
l'ordre une → onglets → sommaire → titre, le bloc Emporter habillé, la devise du pied propre au volume."""
import re
h = open('v22.html', encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'✗ attendu {n}, trouvé {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} ×{n}')
# ── 1 · les titres en double : le portail est LE titre ; les cantons perdent le leur ─────────
# t1 · Quintessence
rep('<div class="section">\n <div class="sec-header" id="quintessence"><span class="sec-num">꩜</span><h2>Quintessence — La Beauté de La Vie</h2></div>',
'<div class="section" id="quintessence">', why='t1 : titre du canton')
# t2 · Les Lois (pas d\'id)
rep('<div class="section">\n <div class="sec-header"><span class="sec-num">⚓️</span><h2>Les Lois de La Vie 🌱</h2></div>\n <blockquote><p>Comment ça fonctionne.</p></blockquote>',
'<div class="section">', why='t2 : titre + « Comment ça fonctionne. »')
# t3 · la une du canton + le titre
i = h.find('<div class="tri-une">'); m1 = h.find('</div>', h.find('<div class="tri-meta">', i)) + 6; j = h.find('</div>\n', m1) + len('</div>\n')
assert h[m1:j].strip() == '</div>', h[m1:j]
assert 0 < i < j and h[i:j].count('<div class="tri-une">') == 1
h = h[:i] + h[j:]; J.append('t3 : la une du canton (🕸️💍 Union du TouT … CC BY-SA) ôtée')
rep('<div class="section">\n <div class="sec-header" id="chartes-cinq"><span class="sec-num">🔱</span><h2>Les Chartes du Vivant ✨</h2></div>\n <blockquote><p>Notre Intention.</p></blockquote>',
'<div class="section" id="chartes-cinq">', why='t3 : titre + « Notre Intention. »')
# t4 · Communication
rep('<div class="section">\n <div class="sec-header" id="comm-liberte"><span class="sec-num">💃🏽</span><h2>La Communication du Vivant</h2></div>\n <blockquote><p>Notre Attention.</p></blockquote>',
'<div class="section" id="comm-liberte">', why='t4 : titre + « Notre Attention. »')
# t5 · Contrats
rep('<div class="section">\n <div class="sec-header" id="contrats-quatre"><span class="sec-num">📜</span><h2>Les Contrats de ConScience ✍️</h2></div>\n <blockquote><p>À quoi on s\'engage.</p></blockquote>',
'<div class="section" id="contrats-quatre">', why='t5 : titre + « À quoi on s’engage. »')
# le sous-titre de la une du canton t3 rejoint le portail
rep('''"Cinq chartes : ce qui se mesure, ce qui se traverse, ce qui se lit, ce qui se protège, ce qui se sublime.",''',
'''"Cinq chartes : ce qui se mesure, ce qui se traverse, ce qui se lit, ce qui se protège, ce qui se sublime.\\nLe rapport, la relation, la lecture, le pont, le sceau — cinq langues pour un seul objet, et un seul objet pour les tenir ensemble.",''',
why='t3 : sous-titre de la une versé au portail')
# ── 2 · l'ordre : une → onglets → sommaire → titre (portail) → contenu ─────────────────────
i = h.find('<section class="part-portail"'); j = h.find('</section>', i) + len('</section>')
portail = h[i:j]; h = h[:i] + h[j:]
rep('</main>\n\n<div class="corps">', '</main>\n' + portail + '\n<div class="corps">', why='portail déplacé après le sommaire')
# ── 3 · la feuille des retours ────────────────────────────────────────────────────────────
css = '''
<style id="ami-retours">
/* ── RETOURS DU 9 SEPT · onglets en couleur, qui replient ; titre unique ; améthyste pour Les Chartes ── */
#ami-union .tab-bar-full{flex-wrap:wrap;overflow-x:visible;overflow:visible}
#ami-union .tab-btn-full{flex:1 1 18%;min-width:6.4rem;white-space:normal;color:var(--tc)}
#ami-union .tab-btn-full .tb-em{filter:none;opacity:.9}
#ami-union .tab-btn-full .tb-lbl{text-align:center}
#ami-union .tab-btn-full .tb-lbl{font-size:.86rem;font-weight:500}
#ami-union .tab-btn-full .tb-pol{color:var(--gris);opacity:1;font-size:.72rem;letter-spacing:.1em}
#ami-union .tab-btn-full.active .tb-pol{color:var(--gris-cl);opacity:1}
#ami-union .tab-btn-full:hover,#ami-union .tab-btn-full.active{color:var(--tcl)}
#ami-union .tab-btn-full.active{background:rgba(var(--tc-rgb,var(--or-rgb)),.12);border-bottom-color:var(--tc)}
#ami-union .tab-btn-full[data-c="t1"]{--tc-rgb:var(--or-rgb)}
#ami-union .tab-btn-full[data-c="t2"]{--tc-rgb:var(--vert-rgb)}
#ami-union .tab-btn-full[data-c="t3"]{--tc:var(--amethyste);--tcl:var(--amethyste-cl);--tc-rgb:var(--amethyste-rgb)}
#ami-union .tab-btn-full[data-c="t4"]{--tc-rgb:var(--fuchsia-rgb)}
#ami-union .tab-btn-full[data-c="t5"]{--tc-rgb:var(--bleu-rgb)}
/* les sous-onglets replient eux aussi : rien ne roule à droite */
#ami-union .sub-bar-host .in,#ami-union .sub-bar-host .tabs-in{flex-wrap:wrap!important;overflow-x:visible;justify-content:center!important}
#ami-union .sub-bar-host .tri-b,#ami-union .sub-bar-host .tb{white-space:normal}
#ami-union .chr-q{white-space:normal}
/* Les Chartes du Vivant : améthyste */
#ami-union .part-portail[data-c="t3"]{--ppc:var(--amethyste);--ppcl:var(--amethyste-cl)}
#ami-union .src-tri{border-top-color:var(--amethyste)}
#ami-union .tri-b.on,#ami-union .tri-b[data-c="intro"].on{background:rgba(var(--amethyste-rgb),.12)!important;color:var(--amethyste-cl)!important;border-bottom-color:var(--amethyste)!important}
/* le portail : l’émoji au-dessus, le glow, une seule fois */
#ami-union .part-portail{flex-direction:column;gap:.45rem;padding:1.4rem 1.2rem 1.2rem;margin:1.2rem auto .6rem;
background:radial-gradient(ellipse at 50% 0%,rgba(var(--pp-rgb,var(--or-rgb)),.13),transparent 70%)}
#ami-union .part-portail .pp-icon{font-size:2.6rem;filter:drop-shadow(0 0 16px var(--ppc,var(--or)))}
#ami-union .part-portail .pp-mission{white-space:pre-line}
#ami-union .part-portail[data-c="t2"]{--pp-rgb:var(--vert-rgb)}
#ami-union .part-portail[data-c="t3"]{--pp-rgb:var(--amethyste-rgb)}
#ami-union .part-portail[data-c="t4"]{--pp-rgb:var(--fuchsia-rgb)}
#ami-union .part-portail[data-c="t5"]{--pp-rgb:var(--bleu-rgb)}
/* ◈ Emporter — les valeurs de la Charte, portées dans la racine */
#ami-union .emporter{max-width:44rem;margin:3rem auto 1rem;padding:1.4rem 1.6rem;background:var(--encre);
border:1px solid var(--or-somb);border-radius:14px;text-align:center}
#ami-union .emporter h2{font-family:'Cormorant Garamond',serif;font-size:1.3rem;font-weight:400;color:var(--or-pale);margin:0 0 .6rem;letter-spacing:0;text-transform:none}
#ami-union .emporter p{color:var(--gris-cl);font-family:'Jost',sans-serif;font-size:.88rem;line-height:1.7;margin:.4rem auto;max-width:60ch}
#ami-union .emporter p strong{color:var(--nacre);font-weight:500;font-size:inherit}
#ami-union .emporter .emp-b{display:flex;gap:.7rem;justify-content:center;flex-wrap:wrap;margin:1rem 0 .4rem}
#ami-union .emporter .emp-b button{font-family:'Jost',sans-serif;font-size:.86rem;color:var(--or);background:rgba(255,255,255,.05);
border:1px solid var(--or-somb);border-radius:999px;padding:.55rem 1.2rem;cursor:pointer;min-height:24px}
#ami-union .emporter .emp-b button:hover{background:rgba(255,255,255,.1);color:var(--or-pale)}
#ami-union .emporter .emp-d{font-size:.78rem;color:var(--gris)}
#ami-union .emporter .emp-d em{color:var(--gris);font-style:italic}
#ami-union .emporter [data-ami]{color:var(--or-pale);font-size:inherit}
</style>
'''
rep('<style id="ami-plancher">', css + '<style id="ami-plancher">', why='feuille des retours')
# ── 4 · le sommaire : Les Chartes en améthyste ─────────────────────────────────────────
rep('<div class="som-partie" style="border-left:3px solid var(--or);margin-top:1rem">\n <div class="som-titre-btn open" data-som="t3" style="color:var(--or-pale)">',
'<div class="som-partie" style="border-left:3px solid var(--amethyste);margin-top:1rem">\n <div class="som-titre-btn open" data-som="t3" style="color:var(--amethyste-cl)">', why='sommaire t3 améthyste')
# ── 5 · la devise du pied : celle du volume, pas celle des Chartes (elle est déjà au portail t3) ──
rep("<p class=\"ap-cit\">Une charte n’est pas une liste de règles : c’est la forme qu’on donne à ce qu’on veut devenir.\nOn n’obéit pas à une charte — on s’y tient.</p>",
"<p class=\"ap-cit\">Cinq domaines, trois langues, et Moi au carré.\nRien n’est en trop, rien ne manque : c’est à ça qu’on reconnaît le Vivant.</p>", why='pied : devise du volume')
# ── 6 · deux hauteurs, deux noms : --nav-h = la barre du site (racine), --tab-h = la barre des onglets (page) ──
rep("#ami-union{--nav-h:66px}","#ami-union{--tab-h:66px}", why='--tab-h déclaré')
rep("R.style.setProperty('--nav-h', h+'px')","R.style.setProperty('--tab-h', h+'px')", why='--tab-h mesuré')
rep("#ami-union .sub-bar-host{position:sticky;top:var(--nav-h);","#ami-union .sub-bar-host{position:sticky;top:calc(var(--nav-h) + var(--tab-h));", why='sous-barre sous les deux')
rep("#ami-union .tab-bar-full{position:sticky;top:0;","#ami-union .tab-bar-full{position:sticky;top:var(--nav-h);", why='barre des onglets sous la barre du site')
# ── 7 · la racine du composant portait « --r:4px}*{margin:0 » : une règle * qui posait --nav-h:0 et margin:0 sur tout ──
rep('--r:4px}\n*{margin:0;--rubis:', '--r:4px;--rubis:', why='règle * parasite ôtée de la racine')
open('v22.html', 'w', encoding='utf-8').write(h)
print('\n'.join('✓ ' + x for x in J))
p7_arbo_v13.pyp7_arbo_v13.py v22.html ../arbo/v13.html116 lignes
Verse l'Arborescence V13 dans le canton 🌳 de L'Union (sous-onglets psy1/2/3), rebranche ses scripts sur #tp-psy, ajoute ses styles, met les 13 Champs au canon, recorrige ce que P1 avait corrigé.
# -*- coding: utf-8 -*-
"""P7 · L'Arborescence V13 remplace le canton 🌳 de L'Union du TouT (sous-onglets psy1/psy2/psy3).
On garde l'enveloppe du canton (bandeau, sous-nav, ids des renvois), on verse le contenu de V13,
on rebranche les scripts (ils cherchaient #arbo-sante, absent : filtres et infobulles étaient morts),
on ajoute les styles que V13 apporte, et on met les 13 Champs au canon du 9 sept."""
import re, sys
U = sys.argv[1] if len(sys.argv) > 1 else 'v22.html'
V = sys.argv[2] if len(sys.argv) > 2 else '../arbo/v13.html'
h = open(U, encoding='utf-8').read(); v = open(V, encoding='utf-8').read()
J = []
def rep(old, new, n=1, why=''):
global h
c = h.count(old)
if c != n: raise SystemExit(f'✗ attendu {n}, trouvé {c} : {why or old[:70]!r}')
h = h.replace(old, new); J.append(f'{why or old[:60]} ×{n}')
def inner(s, start_pat):
"""contenu d'un <div …> jusqu'à sa fermeture (compte les div)"""
m = re.search(start_pat, s); i = m.end(); depth = 1; k = i
while depth:
n1 = s.find('<div', k); n2 = s.find('</div>', k)
if n2 < 0: raise SystemExit('div non fermée')
if 0 <= n1 < n2: depth += 1; k = n1 + 4
else: depth -= 1; k = n2 + 6
return m.start(), m.end(), k - 6, k # début balise, fin balise ouvrante, début </div>, fin
# ── 1 · les trois volets de V13 ───────────────────────────────────────────────
b = v[v.find('<body'):]
panes = {}
for t in ('t0', 't1', 't2'):
a, bo, c, d = inner(b, rf'<div class="pane-tab[^"]*" id="{t}">')
panes[t] = b[bo:c]
print({t: len(p) for t, p in panes.items()})
# ── 2 · dans l'Union : sp-psy1/2/3 reçoivent le contenu ───────────────────────
for t, sp in (('t0', 'sp-psy1'), ('t1', 'sp-psy2'), ('t2', 'sp-psy3')):
a, bo, c, d = inner(h, rf'<div class="sub-p[^"]*" id="{sp}">')
h = h[:bo] + '\n' + panes[t].strip() + '\n' + h[c:]
J.append(f'{sp} ← V13 {t}')
# ── 3 · le Point Zéro (U+030B → ◎), les ids des renvois ───────────────────────
n = h.count('̋'); h = h.replace('̋ Le Point Zéro', '<span class="pz">◎</span> Le Point Zéro').replace('<span class="pz">̋</span>', '<span class="pz">◎</span>'); J.append(f'U+030B → ◎ ×{n}')
for m in re.finditer('̋', h): print('RESTE', repr(h[m.start()-60:m.start()+30]))
assert '̋' not in h
rep('<div class="note eth"><b>Le quatrième cas — quand la balance ne pèse rien.</b>', '<div class="note eth" id="arbo-balance"><b>Le quatrième cas — quand la balance ne pèse rien.</b>', why='id arbo-balance')
i = h.find('⚖️ JAUGER & JUGER — deux gestes'); assert i > 0
j = h.rfind('<div class="card or">', 0, i); assert i - j < 40, i - j
h = h[:j] + '<div class="card or" id="arbo-jauger">' + h[j + len('<div class="card or">'):]; J.append('id arbo-jauger')
# ── 4 · les scripts du canton cherchaient #arbo-sante : ils trouvent #tp-psy ───
rep('var R=document.getElementById("arbo-sante");if(!R||R.__v6)return;R.__v6=1;', 'var R=document.getElementById("tp-psy");if(!R||R.__v6)return;R.__v6=1;', why='script filtres/infobulles rebranché')
rep('var R=document.getElementById("arbo-sante");if(!R||R.__tb)return;R.__tb=1;', 'var R=document.getElementById("tp-psy");if(!R||R.__tb)return;R.__tb=1;', why='script onglets V13 (inerte ici, mais propre)')
rep('var pop=R.querySelector("#chpop");', 'var pop=document.getElementById("chpop");', why='#chpop vit hors du canton')
assert 'id="chpop"' in h
# ── 5 · les 13 Champs au canon (émoji + nom ; les descriptions restent) ───────
CANON = {'🔥 C4 · Le Feu Intérieur': '🌳 C4 · La Direction', '👑 C5 · Le Phi': '👑 C5 · La Souveraineté', '🔍 C7 · La Trace': '📜 C7 · La Trace',
'🌈 C8 · La Lumière': '🌈 C8 · Le Spectre', '💍 C10 · Le Serment': '💍 C10 · L’Alliance', '🪬 C11 · L’Offrande': '🥚 C11 · L’Offrande',
'🌟 C13 · L’Auto-Réalisation': '🌟 C13 · L’Absolu', "🌟 C13 · L'Auto-Réalisation": "🌟 C13 · L'Absolu", '👑 C5 · Le Phi · Souveraineté': '👑 C5 · La Souveraineté',
"🪬 C11 · L'Offrande": "🥚 C11 · L'Offrande", '\\U0001faac C11 · L’Offrande': '🥚 C11 · L’Offrande', '👑 C5 · La Souveraineté · Souveraineté': '👑 C5 · La Souveraineté'}
for old, new in CANON.items():
c = h.count(old)
if c: h = h.replace(old, new); J.append(f'canon : {old} → {new} ×{c}')
# la table du Raccord : « 🔥 C4 · Le Feu Intérieur » y est écrit avec des balises ; on vérifie ce qui reste
reste = re.findall(r'C(?:4|5|7|8|10|11|13) · (?:Le Feu Intérieur|Le Phi|La Lumière|Le Serment|L’Auto-Réalisation)', h)
print('restes non canon :', reste[:6])
# ── 6 · les styles que V13 apporte (classes absentes du canton) ───────────────
css13 = re.search(r'<style[^>]*>(.*?)</style>', v, re.S).group(1)
i = h.find('#ami-union .src-tri .vert-ech{'); assert i > 0
# les classes déjà stylées dans le canton
blocs = []; k = 0
while True:
j = css13.find('{', k)
if j < 0: break
sel = css13[k:j].strip(); d = 1; e = j + 1
while e < len(css13) and d:
if css13[e] == '{': d += 1
elif css13[e] == '}': d -= 1
e += 1
blocs.append((sel, css13[j + 1:e - 1])); k = e
NEW = ('.cnd', '.glyph', '.sst', '.sub', '.chpop', '.foot', '.kicker', '.fl', '.sg')
out = []
for sel, corps in blocs:
if sel.startswith('@') or not sel: continue
parts = [p.strip() for p in sel.split(',')]
keep = [p for p in parts if any(re.search(re.escape(c) + r'(?![\w-])', p) for c in NEW)]
if not keep: continue
keep = [p.replace('#arbo-sante', '#ami-union .src-tri') for p in keep]
out.append(','.join(keep) + '{' + corps.strip() + '}')
css_new = '\n'.join(out)
# les triplets en dur → les variables de la racine
TRIP = {'200,168,74': 'or', '138,48,72': 'rubis', '58,122,82': 'vert', '42,95,138': 'bleu', '106,74,160': 'violet', '184,122,42': 'ambre', '144,216,168': 'vert-cl', '224,156,172': 'rubis-cl', '143,200,238': 'bleu-cl', '192,168,255': 'violet-cl'}
for t, nom in TRIP.items(): css_new = css_new.replace(f'rgba({t},', f'rgba(var(--{nom}-rgb),')
restes_rgba = re.findall(r'rgba\((\d+,\d+,\d+),', css_new)
print('règles V13 ajoutées :', len(out), '· triplets restants :', sorted(set(restes_rgba)))
h = h[:i] + '/* ── V13 · ce que l’Arborescence V13 apporte (☯️ Polarités, cnd, glyphes) ── */\n' + css_new + '\n' + h[i:]
J.append(f'{len(out)} règles V13 ajoutées au canton')
# ── 7 · identité : V23, datée ─────────────────────────────────────────────────
rep('<meta name="ami:version" content="V22">', '<meta name="ami:version" content="V23">', why='ami:version V23')
rep('<meta name="ami:date" content="2026-09-09">', '<meta name="ami:date" content="2026-09-17">', why='ami:date')
n = h.count('<span data-ami="version">V22</span>'); h = h.replace('<span data-ami="version">V22</span>', '<span data-ami="version">V23</span>'); J.append(f'data-ami version ×{n}')
n = h.count('<span data-ami="date">9 septembre 2026</span>'); h = h.replace('<span data-ami="date">9 septembre 2026</span>', '<span data-ami="date">17 septembre 2026</span>'); J.append(f'data-ami date ×{n}')
rep("a.download='0.3_LUNION_DU_TOUT_aMi_'+(window.__amiV||'')+'.html';", "a.download='0.3_LUNION_DU_TOUT_aMi_'+(window.__amiV||'')+'.html';", why='nom d’export (lit __amiV)')
# le bandeau du canton dit la version de l'Arborescence
rep('<div class="bs">Ce qui se traverse et se répare · <em>la relation</em></div>', '<div class="bs">Ce qui se traverse et se répare · <em>la relation</em> · Arborescence V13</div>', why='bandeau : V13')
# ── 8 · ce que P1 avait déjà corrigé dans ce canton, et que V13 ramène ─────────
n = h.count('on déchéance'); h = h.replace('on déchéance', 'on déchoit'); J.append(f'« on déchéance » → « on déchoit » ×{n}')
n = h.count('celleux qui'); h = h.replace('celleux qui', 'les personnes qui'); J.append(f'celleux → les personnes ×{n}')
assert 'celleux' not in h
open(U, 'w', encoding='utf-8').write(h)
print('\n'.join('✓ ' + x for x in J))
page_cq.pypage_cq.py248 lignes
Génère cette page depuis etat_chantier.json et le dossier outils/.
# -*- coding: utf-8 -*-
"""page_cq.py — génère CONTROLE_QUALITE_aMi.html depuis etat_chantier.json et le dossier outils/.
Une page perso, à la charte (onyx, or, Cormorant + Jost, plancher 11,5 px, onglets qui replient), qui tient
l'état du chantier, l'historique, les décisions, les pièges, les mesures et LA BOÎTE À OUTILS — chaque script
lisible et copiable. Usage : python3 page_cq.py [etat_chantier.json] [outils/] [sortie.html] [referentiel.json]"""
import json, sys, os, html
ETAT = sys.argv[1] if len(sys.argv) > 1 else 'etat_chantier.json'
OUTILS = sys.argv[2] if len(sys.argv) > 2 else 'outils'
OUT = sys.argv[3] if len(sys.argv) > 3 else 'CONTROLE_QUALITE_aMi.html'
d = json.load(open(ETAT, encoding='utf-8'))
REF = sys.argv[4] if len(sys.argv) > 4 else 'referentiel.json'
r = json.load(open(REF, encoding='utf-8'))
E = html.escape
STATUT = {'fait': ('✅', 'fait', 'emeraude'), 'en cours': ('🔄', 'en cours', 'ambre'), 'à faire': ('⏳', 'à faire', 'gris'), 'à corriger': ('⚠️', 'à corriger', 'rubis')}
def pages():
rows = []
for p in d['pages']:
em, lab, col = STATUT.get(p['statut'], ('·', p['statut'], 'gris'))
rows.append(f'<tr><td><span class="em">{em}</span> <span class="st st-{col}">{E(lab)}</span></td><td><b>{E(p["nom"])}</b><br><code>{E(p["fichier"])}</code></td><td>{E(p["version"])}</td><td>{E(p["date"])}</td><td>{E(p["note"])}</td></tr>')
return '<div class="tw"><table><thead><tr><th>Statut</th><th>Page</th><th>Version</th><th>Date</th><th>Où on en est</th></tr></thead><tbody>' + ''.join(rows) + '</tbody></table></div>'
def liste(items, cls=''):
return f'<ul class="{cls}">' + ''.join(f'<li>{E(x)}</li>' for x in items) + '</ul>'
def historique():
return '<ol class="hist">' + ''.join(f'<li><span class="hd">{E(h["date"])}</span><span class="hq">{E(h["quoi"])}</span></li>' for h in d['historique']) + '</ol>'
def mesures():
rows = ''.join(f'<tr><td>{E(m[0])}</td><td>{E(m[1])}</td><td><b>{E(m[2])}</b></td></tr>' for m in d['mesures_03'])
return '<div class="tw"><table><thead><tr><th>Mesure</th><th>V21 (avant)</th><th>V22 (après)</th></tr></thead><tbody>' + rows + '</tbody></table></div>'
def tag(t):
return ''.join(f'<span class="tg em">{E(x)}</span>' for x in t.replace('/', ' ').split())
def groupes(gs, cls='chk'):
out = []
for g in gs:
out.append(f'<details class="{cls}" open><summary>{E(g["t"])}</summary><ul>' + ''.join(f'<li>{tag(t)} {E(x)}</li>' for t, x in g['items']) + '</ul></details>')
return '\n'.join(out)
def legende():
return '<div class="leg">' + ''.join(f'<span><b class="em">{E(a)}</b> {E(b)} — {E(c)}</span>' for a, b, c in r['legende']) + '</div>'
def registre():
rows = ''.join(f'<tr><td><span class="em">{E(a["em"])}</span> <b>{E(a["nom"])}</b><br><a href="{E(a["url"])}" rel="noopener" target="_blank"><code>{E(a["url"].replace("https://www.",""))}</code></a></td><td>{E(a["type"])}</td><td><span class="st st-{"emeraude" if a["statut"]=="accessible" else "ambre"}">{E(a["statut"])}</span></td><td>{E(a["quoi"])}</td></tr>' for a in r['applications']['registre'])
return '<div class="tw"><table><thead><tr><th>Application</th><th>Nature</th><th>Statut</th><th>Ce qu’elle fait</th></tr></thead><tbody>' + rows + '</tbody></table></div>'
def socle():
return '\n'.join(f'<details class="chk"><summary>{E(g["t"])} <span class="tg-r">{E(g["tag"])}</span></summary><ul>' + ''.join(f'<li>{E(x)}</li>' for x in g['items']) + '</ul></details>' for g in r['applications']['socle'])
def etats():
return '<ul class="etats">' + ''.join(f'<li><span class="em">{E(a)}</span> <b>{E(b)}</b> — {E(c)}</li>' for a, b, c in r['applications']['etats']) + '</ul>'
def cycle():
return '<div class="cycle">' + ' <span class="fl">→</span> '.join(f'<span>{E(x)}</span>' for x in r['applications']['cycle']) + '</div>'
def outils():
out = []
for nom, usage, quoi in d['outils']:
chemin = os.path.join(OUTILS, nom)
if not os.path.exists(chemin) and os.path.exists(nom): chemin = nom
src = open(chemin, encoding='utf-8').read() if os.path.exists(chemin) else '(script absent du dossier outils/)'
lignes = src.count('\n') + 1
out.append(f'''<details class="outil" id="o-{E(nom.replace('.','-'))}">
<summary><span class="on">{E(nom)}</span><span class="ou"><code>{E(usage)}</code></span><span class="ol">{lignes} lignes</span></summary>
<p class="oq">{E(quoi)}</p>
<div class="ob"><button type="button" class="copie" data-cible="c-{E(nom.replace('.','-'))}">⧉ Copier le script</button><span class="etat" aria-live="polite"></span></div>
<pre id="c-{E(nom.replace('.','-'))}"><code>{E(src)}</code></pre>
</details>''')
return '\n'.join(out)
page = f'''<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="ami:volume" content="atelier"><meta name="ami:version" content="{E(d['mis_a_jour'])}">
<title>🧰 {E(d['titre'])} — {E(d['mis_a_jour'])}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Jost:wght@300;400;500;600&family=Space+Mono&display=swap" rel="stylesheet">
<style>
:root{{--onyx:#09090e;--encre:#111118;--voile:#181826;--nacre:#f2ede2;--or:#ffc545;--or-pale:#ffd06a;--or-somb:#7a611a;--or-rgb:255,197,69;
--gris:#9595a2;--gris-cl:#b0b0bd;--emeraude:#0b8d38;--emeraude-ct:#32b15e;--rubis:#e33232;--rubis-ct:#fe6969;--ambre-ct:#ffd06a;--amethyste:#a04afa;--amethyste-ct:#bb7cfe;--saphir-ct:#7697ee}}
*{{box-sizing:border-box}}
html{{scroll-padding-top:5rem}}
body{{margin:0;background:var(--onyx);color:var(--gris-cl);font:400 1rem/1.65 'Jost',system-ui,sans-serif}}
.em{{font-style:normal;font-family:'Apple Color Emoji','Segoe UI Emoji','Noto Color Emoji',sans-serif}}
header{{text-align:center;padding:2.6rem 1.2rem 1.4rem;background:radial-gradient(ellipse at 50% 0%,rgba(var(--or-rgb),.12),transparent 70%)}}
header .k{{font-size:.72rem;letter-spacing:.28em;text-transform:uppercase;color:var(--or)}}
header h1{{font:400 clamp(1.9rem,5vw,3rem)/1.1 'Cormorant Garamond',serif;color:var(--nacre);margin:.3rem 0 .4rem}}
header p{{max-width:44rem;margin:0 auto;font:italic 1.1rem 'Cormorant Garamond',serif;color:var(--gris-cl)}}
header .maj{{margin-top:.8rem;font-size:.78rem;color:var(--gris)}}
nav.tabs{{position:sticky;top:0;z-index:9;display:flex;flex-wrap:wrap;justify-content:center;background:rgba(9,9,14,.96);backdrop-filter:blur(8px);border-bottom:1px solid var(--or-somb)}}
nav.tabs button{{flex:1 1 18%;min-width:6.4rem;min-height:44px;padding:.65rem .6rem;background:transparent;border:0;border-bottom:2px solid transparent;color:var(--gris-cl);font:500 .86rem 'Jost',sans-serif;cursor:pointer}}
nav.tabs button .em{{display:block;font-size:1.3rem;line-height:1.2}}
nav.tabs button[aria-selected=true]{{color:var(--or-pale);border-bottom-color:var(--or);background:rgba(var(--or-rgb),.1)}}
nav.tabs button:focus-visible{{outline:2px solid var(--or);outline-offset:-2px}}
main{{max-width:60rem;margin:0 auto;padding:1.4rem 1rem 4rem}}
section[hidden]{{display:none}}
h2{{font:400 1.8rem/1.15 'Cormorant Garamond',serif;color:var(--or-pale);margin:1.6rem 0 .6rem}}
h2 .em{{margin-right:.3rem}}
p.lead{{font:italic 1.08rem 'Cormorant Garamond',serif;color:var(--gris-cl);max-width:50rem}}
.tw{{overflow-x:auto;border:1px solid rgba(var(--or-rgb),.18);border-radius:10px}}
table{{width:100%;border-collapse:collapse;font-size:.86rem;min-width:40rem}}
th{{text-align:left;font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--or);background:rgba(var(--or-rgb),.07);padding:.6rem .7rem;border-bottom:1px solid var(--or-somb)}}
td{{padding:.6rem .7rem;border-bottom:1px solid rgba(255,255,255,.06);vertical-align:top}}
td b{{color:var(--nacre);font-weight:500}}
code{{font-family:'Space Mono',monospace;font-size:.78rem;color:var(--gris)}}
.st{{font-size:.72rem;letter-spacing:.1em;text-transform:uppercase}}
.st-emeraude{{color:var(--emeraude-ct)}}.st-ambre{{color:var(--ambre-ct)}}.st-gris{{color:var(--gris)}}.st-rubis{{color:var(--rubis-ct)}}
ul.dec li,ul.pieges li,ul.val li{{margin:.45rem 0 .45rem 0;padding-left:.2rem}}
ul.dec,ul.pieges,ul.val{{padding-left:1.2rem}}
ul.dec li::marker{{color:var(--or)}}ul.pieges li::marker{{color:var(--rubis-ct)}}ul.val li::marker{{color:var(--amethyste-ct)}}
ol.hist{{list-style:none;padding:0;margin:0;border-left:2px solid var(--or-somb)}}
ol.hist li{{position:relative;padding:.2rem 0 .9rem 1.2rem}}
ol.hist li::before{{content:'';position:absolute;left:-6px;top:.55rem;width:10px;height:10px;border-radius:50%;background:var(--or)}}
.hd{{display:block;font-family:'Space Mono',monospace;font-size:.74rem;color:var(--or-pale);letter-spacing:.04em}}
.hq{{display:block;font-size:.92rem}}
details.outil{{border:1px solid rgba(var(--or-rgb),.18);border-radius:10px;margin:.7rem 0;background:var(--encre)}}
details.outil summary{{display:flex;flex-wrap:wrap;gap:.4rem 1rem;align-items:baseline;padding:.75rem 1rem;cursor:pointer;list-style:none;min-height:44px}}
details.outil summary::-webkit-details-marker{{display:none}}
details.outil summary::before{{content:'▸';color:var(--or);margin-right:.2rem}}
details.outil[open] summary::before{{content:'▾'}}
.on{{color:var(--nacre);font-weight:500}}
.ou code{{color:var(--or-pale)}}
.ol{{margin-left:auto;font-size:.74rem;color:var(--gris)}}
.oq{{margin:0 1rem .6rem;font-size:.9rem}}
.ob{{display:flex;gap:.8rem;align-items:center;margin:0 1rem .5rem}}
button.copie{{min-height:32px;padding:.35rem .9rem;border-radius:999px;border:1px solid var(--or-somb);background:rgba(255,255,255,.05);color:var(--or);font:500 .8rem 'Jost',sans-serif;cursor:pointer}}
button.copie:hover{{color:var(--or-pale);background:rgba(255,255,255,.1)}}
.etat{{font-size:.78rem;color:var(--emeraude-ct)}}
details.outil pre{{margin:0;padding:1rem;overflow:auto;max-height:70vh;background:var(--onyx);border-top:1px solid rgba(var(--or-rgb),.14);border-radius:0 0 10px 10px;font-family:'Space Mono',monospace;font-size:.74rem;line-height:1.5;color:var(--gris-cl);tab-size:4}}
h3{{font:400 1.35rem/1.2 'Cormorant Garamond',serif;color:var(--nacre);margin:1.4rem 0 .5rem}}
.leg{{display:flex;flex-wrap:wrap;gap:.4rem 1.2rem;font-size:.82rem;color:var(--gris-cl);margin:.4rem 0 1rem}}
.tg{{display:inline-block;min-width:1.4em;text-align:center;margin-right:.1rem}}
.tg-r{{margin-left:auto;font-size:.8rem;color:var(--gris)}}
details.chk{{border:1px solid rgba(var(--or-rgb),.16);border-radius:10px;margin:.5rem 0;background:var(--encre)}}
details.chk summary{{display:flex;gap:.6rem;align-items:baseline;padding:.6rem .9rem;cursor:pointer;list-style:none;color:var(--or-pale);font-weight:500;min-height:44px}}
details.chk summary::-webkit-details-marker{{display:none}}
details.chk summary::before{{content:'▸';color:var(--or)}}
details.chk[open] summary::before{{content:'▾'}}
details.chk ul{{margin:0;padding:0 1rem .7rem 2.2rem;font-size:.9rem}}
details.chk li{{margin:.3rem 0}}
ul.done{{padding-left:1.2rem;columns:2;column-gap:2rem}}
ul.done li::marker{{content:'✓ ';color:var(--emeraude-ct)}}
@media(max-width:600px){{ul.done{{columns:1}}}}
.cycle{{display:flex;flex-wrap:wrap;gap:.3rem .5rem;align-items:center;font-size:.86rem}}
.cycle span{{padding:.25rem .6rem;border:1px solid rgba(var(--or-rgb),.25);border-radius:999px;background:var(--encre)}}
.cycle .fl{{border:0;background:none;color:var(--or);padding:0}}
ul.etats{{list-style:none;padding:0}}ul.etats li{{margin:.3rem 0}}
blockquote.principe{{margin:1rem 0;padding:.8rem 1.1rem;border-left:3px solid var(--or);background:var(--encre);border-radius:0 10px 10px 0;font:italic 1.05rem 'Cormorant Garamond',serif;color:var(--or-pale)}}
pre.gab{{margin:0 1rem .8rem;padding:.8rem;background:var(--onyx);border-radius:8px;font-family:'Space Mono',monospace;font-size:.74rem;line-height:1.5;color:var(--gris-cl);white-space:pre-wrap}}
a{{color:var(--saphir-ct)}}
footer{{text-align:center;padding:2rem 1rem 3rem;font-size:.78rem;color:var(--gris);border-top:1px solid var(--or-somb)}}
@media(prefers-reduced-motion:reduce){{*{{animation:none!important;transition:none!important}}}}
</style>
</head>
<body>
<header>
<div class="k">Protocole de ConScience aMi · Atelier</div>
<h1><span class="em">🧰</span> {E(d['titre'])}</h1>
<p>{E(d['sous_titre'])}</p>
<div class="maj">Mis à jour le {E(d['mis_a_jour'])} — généré par <code>page_cq.py</code></div>
</header>
<nav class="tabs" role="tablist" aria-label="Sections">
<button role="tab" aria-selected="true" data-t="volumes"><span class="em">📚</span>Les Volumes</button>
<button role="tab" aria-selected="false" data-t="apps"><span class="em">📲</span>Les Applications</button>
<button role="tab" aria-selected="false" data-t="hist"><span class="em">🕰️</span>L'historique</button>
<button role="tab" aria-selected="false" data-t="outils"><span class="em">🧰</span>La boîte à outils</button>
</nav>
<main>
<section id="volumes">
<h2><span class="em">🗺️</span>Où en est chaque page</h2>
<p class="lead">Une page est « faite » quand elle est mesurée au rendu, à 320, 390 et 1280 px, et que chaque commande a été pressée.</p>
{pages()}
<h2><span class="em">🧿</span>Le référentiel des Volumes — ce que la Charte demande</h2>
<p class="lead">{E(r['volumes']['source'])}</p>
{legende()}
<h3>§ 3 · Le prompt de contrôle</h3>
<p class="lead">{E(r['volumes']['prompt']['intro'])}</p>
{groupes(r['volumes']['prompt']['familles'])}
<h3>{E(r['volumes']['checklist_page']['titre'])}</h3>
{groupes(r['volumes']['checklist_page']['groupes'])}
<h3>{E(r['volumes']['checklist_widget']['titre'])}</h3>
{groupes(r['volumes']['checklist_widget']['groupes'])}
<h2><span class="em">⚖️</span>Les décisions — la grille qui s'applique à toutes les pages</h2>
{liste(d['decisions'], 'dec')}
<h2><span class="em">📏</span>0.3 L'Union du TouT — avant / après</h2>
{mesures()}
<h2><span class="em">🔮</span>Ce qui attend l'avis de Diane</h2>
{liste(d['a_valider'], 'val')}
<h2><span class="em">🪤</span>Les pièges rencontrés — à ne pas refaire</h2>
{liste(d['pieges'], 'pieges')}
</section>
<section id="apps" hidden>
<h2><span class="em">📲</span>Le registre des Applications</h2>
<p class="lead">« La Qualité de Nos Décisions dépend de Notre Capacité à Voir Juste. » — <a href="https://www.agdistys.fr/mes-applications" rel="noopener" target="_blank">agdistys.fr/mes-applications</a></p>
{registre()}
<h2><span class="em">🧿</span>Le référentiel des Applications — machine · inspection · publication</h2>
<p class="lead">{E(r['applications']['source'])}</p>
<blockquote class="principe">{E(r['applications']['principe'])}</blockquote>
{legende()}
<h3>Le cycle de vie canonique</h3>
{cycle()}
<p class="lead">Aucune fonctionnalité ne passe de « je l'ai codée » à « production » : un critère d'acceptation, au moins un test automatique quand c'est testable, une inspection humaine quand il y a du sens, de l'usage ou de l'esthétique.</p>
<h3>DONE — seulement si</h3>
{liste(r['applications']['done'], 'done')}
<h3>Le socle commun, section par section</h3>
{socle()}
<h3>Les états officiels d'un chantier</h3>
{etats()}
<h3>Ce que la machine ne doit pas décider</h3>
{liste(r['applications']['machine_ne_decide_pas'], 'dec')}
<h3>Les cinq questions de l'Inspectrice des Travaux Finis</h3>
{liste(r['applications']['cinq_questions'], 'val')}
<h3>Gabarits</h3>
<details class="chk"><summary>Fiche d'acceptation par fonctionnalité</summary><pre class="gab">{E(r['applications']['fiche'])}</pre></details>
<details class="chk"><summary>Procès-verbal automatique de PR</summary><pre class="gab">{E(r['applications']['pv'])}</pre></details>
<blockquote class="principe">{E(r['applications']['formule'])}</blockquote>
</section>
<section id="hist" hidden>
<h2><span class="em">🕰️</span>L'historique du chantier</h2>
{historique()}
</section>
<section id="outils" hidden>
<h2><span class="em">🧰</span>La boîte à outils</h2>
<p class="lead">Dans l'ordre où ils se rejouent sur une page. Prérequis : <code>python3</code> et <code>pip install playwright && playwright install chromium</code>. Chaque script est lisible ici et se copie d'un geste.</p>
{outils()}
</section>
</main>
<footer>Agdistys · Diane Serant & Claude · CC BY-SA 4.0 — les fichiers du Mac font foi ; cette page se régénère depuis <code>etat_chantier.json</code>.</footer>
<script>
(function(){{'use strict';
var tabs=[].slice.call(document.querySelectorAll('nav.tabs button')), secs=[].slice.call(document.querySelectorAll('main > section'));
function ouvre(k){{ tabs.forEach(function(b){{ var on=b.dataset.t===k; b.setAttribute('aria-selected',on?'true':'false'); }});
secs.forEach(function(s){{ s.hidden = s.id!==k; }}); try{{ history.replaceState(null,'','#'+k); }}catch(e){{}} }}
tabs.forEach(function(b){{ b.addEventListener('click',function(){{ ouvre(b.dataset.t); }}); }});
document.querySelector('nav.tabs').addEventListener('keydown',function(e){{ var i=tabs.indexOf(document.activeElement); if(i<0) return; var j=i;
if(e.key==='ArrowRight') j=(i+1)%tabs.length; else if(e.key==='ArrowLeft') j=(i-1+tabs.length)%tabs.length; else return; e.preventDefault(); tabs[j].focus(); ouvre(tabs[j].dataset.t); }});
var h=(location.hash||'').slice(1); if(h==='etat'||h==='dec'||h==='pieges') h='volumes'; if(h && secs.some(function(s){{return s.id===h;}})) ouvre(h);
else if(h && h.indexOf('o-')===0){{ ouvre('outils'); var d=document.getElementById(h); if(d){{ d.open=true; d.scrollIntoView(); }} }}
document.addEventListener('click',function(e){{ var b=e.target.closest&&e.target.closest('button.copie'); if(!b) return;
var pre=document.getElementById(b.dataset.cible), etat=b.parentNode.querySelector('.etat'), txt=pre?pre.textContent:'';
function ok(){{ etat.textContent='copié ✓'; setTimeout(function(){{ etat.textContent=''; }},1800); }}
if(navigator.clipboard&&navigator.clipboard.writeText) navigator.clipboard.writeText(txt).then(ok,function(){{ etat.textContent='sélectionne et copie à la main'; }});
else {{ var r=document.createRange(); r.selectNodeContents(pre); var s=getSelection(); s.removeAllRanges(); s.addRange(r); etat.textContent='texte sélectionné — ⌘C'; }} }});
}})();
</script>
</body>
</html>'''
open(OUT, 'w', encoding='utf-8').write(page)
print(f'✓ {OUT} — {len(page)//1024} Ko, {len(d["outils"])} outils, {len(d["pages"])} pages, {len(d["historique"])} entrées d\'historique')
📲Le registre des Applications
« La Qualité de Nos Décisions dépend de Notre Capacité à Voir Juste. » — agdistys.fr/mes-applications
| Application | Nature | Statut | Ce qu’elle fait |
|---|---|---|---|
🌕 Mon Cycleagdistys.fr/mon-cycle-app/ | Santé holistique | accessible | Tracking PsychoBiologique & d'Intensité Relationnelle pour mieux se voir, s'ajuster, se protéger & se soigner. |
🕸️ La Table Rondeagdistys.fr/la-table-ronde/ | R&D + relations sociales saines | accessible | Création de Conseils pour mieux relationner, avec une partie de Mesure à l'aveugle pour tester ses idées. |
🛡️ WikiLeaks des Agresseursagdistys.fr/wikileaks-des-agresseurs/ | Protection | accessible | Protocole d'accès à l'Observatoire de Responsabilité Publique pour mieux se protéger. |
🪞 WidgeTÔThèqueagdistys.fr/widgetotheque/ | Widgets pour se tester | accessible | Les widgets du corpus — relèvent de la Checklist LV côté Volumes. |
🔥 Agoraagdistys.fr/agora/ | Justice sociale | en développement | Rassemblement des plateformes de pétitions, avec une lecture explicite et contestable — la valeur éthique — de la construction de chacune. |
🧭 OùQuandQui (DragonRoute)agdistys.fr/dragonroute/ | Transport éthico-pratique | en développement | Comparer comment aller quelque part, quand, avec qui et à quel coût — en euros, minutes, détour, CO₂e, accessibilité, vie privée et dépendance aux plateformes. Projet pilote du pipeline. |
🪞Les Widgets — sous-branche explicite des Applications
Les Widgets ne deviennent pas un quatrième univers : ils sont l’interactif léger, souvent embarqué dans les pages, et vivent donc côté Applications tout en gardant un lien fort avec les Volumes.
- Registre : WidgeTÔThèque, widgets de test, widgets de mesure, widgets de communication avec l’IA.
- Contrôle : mêmes exigences que les applications quand il y a interaction, données, calcul ou état sauvegardé.
- Raccord Volumes : respecter aussi la Checklist du widget rapatriée depuis la Charte.
🧿Le référentiel des Applications — machine · inspection · publication
« Tableau de Bord de l’Atelier aMi — Contrôle Qualité V2 » (Isil, 17 sept 2026) : le référentiel machine · inspection humaine · publication, condensé ici. Le pipeline (branches, PR, preview Cloudflare, Playwright, Lighthouse, health checks) n'est pas encore installé : il attend le connecteur GitHub sur Atlas-Conscience.
Une version n'est pas « terminée » parce qu'elle fonctionne une fois. Elle est prête à inspecter quand la machine a vérifié ce qu'elle sait vérifier ; prête à publier quand l'humain a validé ce que la machine ne doit pas décider.
Le cycle de vie canonique
Aucune fonctionnalité ne passe de « je l'ai codée » à « production » : un critère d'acceptation, au moins un test automatique quand c'est testable, une inspection humaine quand il y a du sens, de l'usage ou de l'esthétique.
DONE — seulement si
- fichiers présents
- syntaxe valide
- aucun lien local cassé
- scénario nominal testé
- scénario d'erreur testé
- mobile testé
- desktop testé
- aucune erreur console bloquante
- données réelles ou explicitement simulées en test
- aucun faux fallback présenté comme réel
- provenance des données identifiable
- critères d'acceptation satisfaits
- rapport automatique disponible
- inspection humaine validée — sinon : N/A + justification, jamais une case laissée vide
Le socle commun, section par section
3 · Intégrité, syntaxe, démarrage 🤖🛑
- Tous les fichiers référencés existent, casse exacte, aucun import local cassé, aucun marqueur de fusion
- JS/JSON/manifest valides, aucun id critique dupliqué, aucun ReferenceError au démarrage
- La page démarre sans interaction, atteint READY, une dépendance facultative peut tomber sans tuer l'app
4 · Tests fonctionnels 🤖🛑 / 🧪
- Un scénario étalon par application (DragonRoute : Lyon → Valence, E10, 40 L, 6,2 L/100)
- Cas limites : entrée vide, valeur invalide, API lente ou absente, permission refusée, réseau absent, écran très petit / large, retour arrière, rechargement
- Règle anti-invention : donnée absente ≠ zéro ≠ fausse donnée — écrire « indisponible », jamais « 0 € » ou une station fictive
5 · Sources & données vivantes 🤖 + 👁️
- Pour chaque donnée externe : nom, endpoint, licence, fraîcheur, nature, dépendance, comportement si la source tombe
- Health check périodique (DGCCRF, OSRM, Nominatim, Impact CO₂, SNCF GTFS-RT, vélos partagés) — une panne source se distingue d'une panne du code
- Fraîcheur : timestamp, seuil, alerte, test automatique de donnée ancienne
6 · Calculs & algorithmes 🤖🛑
- Tests unitaires : cas simple à la main, zéro, limite, extrême, invalide, non-régression
- Explicabilité : variables identifiables, formule documentée, sortie recalculable, pondérations visibles, estimation ≠ observation, aucune valeur morale cachée dans un « score objectif »
- Reproductibilité : même résultat à données identiques, arrondis cohérents, locale et fuseaux contrôlés
7 · Interface & responsive 🤖🛑 / 👁️
- Viewports : ~360×800, 390×844, 768×1024, 1440×900
- Régression visuelle : captures de référence mobile + desktop, diff ; la machine dit « ceci a changé », l'humain dit « voulu / non voulu »
- Débordements : pas de scroll horizontal involontaire, boutons visibles, modales contenues, textes non coupés
8 · Accessibilité 🤖 ⚠️ / 👁️
- lang, viewport, contraste, labels, boutons nommés, alternatives d'images, ordre des titres, clavier, focus visible, ARIA sans erreur majeure
- Inspection : compréhensible sans couleur seule, erreur compréhensible sans le code, zones cliquables confortables, expérience acceptable en reduced-motion
9 · Performance 🤖 ⚠️
- Poids HTML/JS/CSS, requêtes, temps d'affichage, erreurs de chargement, ressources tierces, PWA
- Budgets par application, ex. JS < 250 Ko · CSS < 150 Ko · LCP < 2,5 s · 0 erreur console bloquante
10 · Vie privée & sécurité 🤖 + 👁️
- Aucune analytics non déclarée, aucun tracker, aucune donnée personnelle ni secret dans le dépôt
- Permissions (GPS, caméra…) demandées seulement quand nécessaire, refus géré, raison expliquée
- Liens externes : https, rel=noopener, aucune injection de HTML non échappé, données externes traitées comme non fiables
11 · PWA / installation 🤖 / 🧪
- Manifest complet (nom, icônes, start_url, scope, thème), service worker, cache versionné, démarrage après installation
- Comportement hors ligne défini ; UI hors ligne ≠ données vivantes indisponibles
12 · Qualité éditoriale 🤖 / 👁️
- title, description, un seul H1, hiérarchie H2/H3, liens non cassés, encodage, aucun placeholder, version/date
- Français défini et précis, fait / interprétation / hypothèse distingués, termes techniques définis, slogan ≠ précision du corps
13 · Charte aMi commune 🤖 ⚠️ / 👁️
- Fond Onyx #09090e, variables de charte, polices attendues ou fallbacks documentés, accent déclaré, reduced-motion, aucun débordement, contraste minimum
- On reconnaît la famille Agdistys ; l'accent différencie sans casser la lignée ; mobile ≠ desktop écrasé
14 · Boussoles, scores & recommandations 🤖 + 👁️
- Formule et dimensions versionnées, valeurs manquantes explicites, entrées traçables, résultat reproductible, tests de bornes
- Le score mesure bien ce qu'il prétend mesurer ; la formulation ne transforme pas une heuristique en vérité ; aucun classement moral présenté comme simple fait ; statut épistémique affiché
15 · Dépendances & fournisseurs 🤖 / 🧪
- Pour chaque fournisseur : nom, fonction, criticité, licence, quota, données envoyées, alternative, fallback, dernier health check
- Aucun composant essentiel prisonnier d'un fournisseur unique
16 · Fixtures & mesures aveugles 🤖 / 🧪 / 👁️
- Fixtures séparées de la production, nommées, déterministes, jamais présentées comme vraies
- Aveugle : noms neutralisés, ordre figé ou randomisation pré-enregistrée, règle enregistrée avant mesure, révélation séparée de la mesure
Les états officiels d'un chantier
- ⚫ BROUILLON — pas encore soumis aux contrôles
- 🟠 EN CHANTIER — branche active
- 🟡 TEST MACHINE — CI en cours
- 🔴 ÉCHEC MACHINE — au moins un bloquant
- 🔵 PRÊT POUR INSPECTION — machine verte, humain pas encore validé
- 🟣 CORRECTIONS DEMANDÉES — inspection non validée
- 🟢 VALIDÉ — machine + humain
- 🔥 PUBLIÉ — fusionné et déployé
- 🌱 SURVEILLÉ — production + health checks
Ce que la machine ne doit pas décider
- le sens d'un texte
- la beauté d'une interface
- la fidélité à l'intention de départ
- l'acceptabilité d'un compromis humain
- la « valeur morale » d'une option
- la qualité relationnelle vécue
- la vérité d'une interprétation heuristique
- le caractère satisfaisant d'une expérience
Les cinq questions de l'Inspectrice des Travaux Finis
- Est-ce que ça fait ce que j'ai demandé ? — pas « presque », pas « techniquement »
- Est-ce que je comprends immédiatement comment l'utiliser ? — sans connaître le chantier
- Est-ce que ce que je vois correspond aux données réelles ? — pas de magie silencieuse
- Est-ce que je reconnais la ligne aMi ? — structure, langage, densité, identité
- Est-ce que je veux mettre mon nom dessus ? — sinon, retour au chantier
Gabarits
Fiche d'acceptation par fonctionnalité
# Fonction : [nom] ## Intention — ce que la fonctionnalité doit rendre possible ## Entrées · Sorties ## Scénario nominal — 1. … 2. … 3. … ## Attendus machine — [ ] … ## Cas d'erreur — [ ] … ## Données / Sources · Vie privée ## Inspection humaine — [ ] intention respectée · lisibilité · mobile · texte · esthétique ## DONE — [ ] CI verte · [ ] preview disponible · [ ] inspection validée
Procès-verbal automatique de PR
╔════════════════════════════════════════╗ ║ 🧿 PROCÈS-VERBAL D'INSPECTION ║ ╚════════════════════════════════════════╝ Projet · Branche · Commit · Date INTÉGRITÉ ✅ fichiers · ✅ références · ✅ syntaxe TESTS ✅ unitaires n/n · ✅ e2e desktop · ✅ e2e mobile · ✅ erreurs contrôlées VISUEL ✅ 390 × 844 · ✅ 1440 × 900 · ⚠️ diff volontaire à valider ACCESSIBILITÉ ✅ contrôles automatiques · ⚠️ contraste secondaire à inspecter SOURCES ✅ … · ⚠️ … indisponible au health check 🤖 MACHINE : PRÊT POUR INSPECTION 👁️ HUMAIN : EN ATTENTE
La Machine vérifie la Structure. Les Tests vérifient le Comportement. Les Sources vérifient l'Ancrage. L'Inspectrice vérifie le Sens. La Publication engage le Nom.
🕰️Historique Applications
- 17 sept 2026OùQuandQui / DragonRoute devient projet pilote du pipeline Inspectrice : branche, tests, PR, inspection, publication.
- 17 sept 2026Les Widgets sont rangés comme sous-branche explicite des Applications, sans créer un quatrième univers.
- 17 sept 2026La Table Ronde est reliée au pipeline Tests & Recherches comme application de campagnes aveugles : préparation, collecte, export brut, analyse et retour à l'Inspectrice.
🧰Boîte à Outils Applications
- Playwright desktop/mobile, captures visuelles, tests de scénarios étalons.
- Lighthouse, health checks des sources, vérification des liens locaux et de la syntaxe.
- Gabarits de PR, procès-verbal automatique et fiche d’acceptation par fonctionnalité.
- La Table Ronde : gabarit de campagne aveugle, export brut des voix, registre des incidents et synthèse avant contrôle humain.
🧪Tests & Recherches — état du banc de mesure
La Table Ronde devient le banc d'essai aveugle des protocoles RefleXion, Potards, Accordage et futurs tests. Le but : envoyer les paquets sans fuite du PRE, récupérer les retours bruts, puis produire une synthèse propre avant contrôle humain.
| Mode | Statut | Ce que ça permet | Ce qui reste humain |
|---|---|---|---|
| Step auto immédiat | utilisable maintenant | Préparer paquet public + PRE privé, lancer les voix dans La Table Ronde, exporter les retours bruts, puis confier l'analyse et le paquet suivant à Codex. | Choisir les voix, lancer la campagne si aucun connecteur direct n'existe, valider la synthèse finale. |
| Full auto cible | à construire | Campagne créée depuis un dossier, distribution aveugle, collecte, scoring PRE, synthèse, mise à jour du registre et génération du paquet suivant. | Arbitrage des cas ambigus, décision de publication, validation du sens. |
La machine peut accélérer la mesure. Elle ne doit pas recevoir le PRE dans le paquet public, ni décider seule du sens final.
🕸️La Table Ronde — automatiser les tests aveugles
Architecture recommandée : La Table Ronde orchestre les voix ; Codex prépare, contrôle, synthétise et produit la prochaine version. Diane reçoit la version la plus aboutie pour inspection.
Cycle full auto cible
Ce que La Table Ronde doit savoir stocker
| Objet | Champs minimaux | Règle |
|---|---|---|
| Campagne | id, nom, paquet, version, date, statut, nombre de voix attendues | Une campagne ne change plus après lancement ; toute correction crée une nouvelle version. |
| Paquet public | texte envoyé, consignes, items, options de réponse, question finale | Jamais de PRE, jamais de verdict attendu, jamais de synthèse cachée. |
| PRE privé | hypothèses, seuils, critères de recevabilité, décisions attendues, incidents possibles | Visible seulement après clôture de la collecte. |
| Voix | modèle, horodatage, exposition déclarée, retour brut, statut complet/incomplet | La déclaration d'exposition est conservée, même quand elle semble erronée. |
| Analyse | convergences, divergences, incidents, verdict, prochaine action | L'analyse cite les retours bruts et distingue mesure, interprétation et arbitrage humain. |
Step auto immédiat — mode manuel rapide
- 1 · PréparerCodex produit deux fichiers :
Fxx_Paquet_Public.mdetPRE_Fxx_Privé.md. Le paquet contient l'en-tête stabilisé : modèle, horodatage, connaissance du protocole, exposition préalable. - 2 · LancerDiane colle uniquement le paquet public dans La Table Ronde, ou dans chaque voix si le connecteur n'est pas encore branché. Le PRE reste fermé.
- 3 · ExporterLa Table Ronde exporte un dossier
campagnes/Fxx/retours/avec un fichier brut par voix, sans correction manuelle. - 4 · AnalyserCodex lit les retours, compare au PRE, marque les incidents, met à jour registre + synthèse, puis prépare le paquet suivant.
- 5 · ContrôlerDiane reçoit la synthèse propre, les fichiers à jour et les points qui demandent vraiment son arbitrage.
Statuts d'une campagne aveugle
- ⚫ BROUILLON — paquet et PRE en préparation
- 🟠 PRÊT À LANCER — paquet public relu, PRE séparé
- 🟡 COLLECTE — voix en cours de réponse
- 🔵 À ANALYSER — retours bruts exportés
- 🟣 INCIDENT — contamination, retour vide, voix exposée ou consigne non suivie
- 🟢 SYNTHÈSE PRÊTE — analyse + prochain paquet générés
- 🔥 VALIDÉ — Diane a contrôlé la décision
📐Protocoles
Les protocoles vivent en deux fichiers séparés : le paquet public envoyé aux voix, et le PRE privé qui fixe les règles avant réception.
Gabarit paquet public
Holà, merci de remplir ce qui suit je te prie. Modele : Horodatage : Connaissez-vous le protocole ou corpus aMi avant ce formulaire : oui / non / incertain Exposition préalable aux paquets ou résultats F, hors présent formulaire : aucune / partielle / oui / incertain ## Réception [résumé strictement nécessaire de la passe précédente] ## Consigne [ce que la voix doit coter, sans révéler les attentes du PRE] ## Items [items anonymisés ou ordonnés selon la règle préenregistrée] ## Question finale [question libre utile pour détecter les ambiguïtés] Merci - Protocole de ConScience aMi (CC BY-SA 4.0).
Gabarit dossier de campagne
campagnes/
Fxx_nom_court/
paquet_public.md
pre_prive.md
metadata.json
retours/
voix_01_modele_horodatage.md
voix_02_modele_horodatage.md
voix_03_modele_horodatage.md
analyse/
resultat_Fxx.md
synthese_reception_Fxx_vers_Fxxplus1.md
paquet_suivant.md🧾Registre des préenregistrements
Chaque PRE fixe la règle avant réception. Il ne sert pas à avoir raison ; il sert à empêcher le test de bouger après avoir vu les réponses.
| Champ | À noter avant lancement |
|---|---|
| Objet | Ce que le paquet teste exactement, et ce qu'il ne teste pas. |
| Hypothèse | Ce qui devrait converger si l'instrument mesure correctement. |
| Seuil | Nombre de voix, règle de recevabilité, traitement des retours incomplets. |
| Incidents | Exposition préalable, réponse vide, modèle qui infère une mémoire, consigne non suivie. |
| Décision | Passage, nuance, invalidation, paquet pilote, ou retour à l'accordage. |
🕰️Historique Tests & Recherches
- 17 septembre 2026La Table Ronde est posée comme banc d'essai aveugle pour RefleXion, Potards et Accordage. Deux niveaux sont retenus : step auto immédiat par export de retours bruts, puis full auto avec campagnes, PRE séparé, scoring et génération du paquet suivant.
🧰Boîte à Outils Tests & Recherches
- Export brut par voix : markdown, json ou csv ; jamais de correction avant analyse.
- Scorage : comparaison au PRE, seuil de recevabilité, incidents, convergence, divergence.
- Sorties automatiques : résultat, synthèse de réception, note de registre, paquet suivant, mise à jour du document vivant.
- Contrôle humain : validation du sens, arbitrage des cas ambigus, décision de publication.
nom du fichier : Tableau_de_Bord_Atelier_aMi_Controle_Qualite_V2_2026-09-17


