Auditer la conformite visuelle — ce que le smoke test ne voit pas

Notebook compagnon du Parcours 1 (Copilot Gutenberg) — axe conformite visuelle. A completer du grain sur la derive de chaine (mesurer-la-derive-dun-copilot.ipynb) : celui-ci mesurait la perte d’information dans une chaine de transformations ; celui-la mesure la conformite du rendu final.

La question

Un Copilot Gutenberg genere ou modifie des templates de pages. Un AI Forms affiche un formulaire sur le frontend. La verification la plus courante est un smoke test structurel : la page repond (simule ici par un statut 200), la balise <main> est presente et non vide, un element d’action (<a> ou <button>) est present. Ce test passe sur une page dont le rendu visuel est casse :

  • un CTA colore en bleu #007bff (primaire Bootstrap) au lieu du ton charte ;
  • un texte sur fond trop clair, sous le seuil de contraste WCAG ;
  • un CTA semi-transparent, sans classe de bouton, qui paraît desactive.

Ce notebook convertit cette constatation en mesure reproductible. Il construit quatre pages synthetiques portant des violations deliberees, ecrit les detecteurs dedies (contraste WCAG, dominance des primaires, affordance des CTA), et montre que le smoke test est aveugle aux trois classes de defauts.

Lien avec docs/reference/verification-verte-systeme-casse.md. Ce document de reference decrit le motif « la sonde ment » pour la classe systeme (HTTP 200 sur un site casse). Le present notebook en demontre la classe visuelle : meme structure, classe differente, detecteurs differents. Le motif recidive, et c’est la lecon.

Comment lire ce notebook

Le notebook suit une pente unique : partir d’une charte ecrite comme des donnees, construire quatre pages dont trois portent un defaut delibere, montrer qu’une sonde usuelle ne voit aucun des trois defauts, puis batir trois detecteurs — un par classe de defaut — et les croiser dans une matrice finale. Chaque etape produit une sortie committee, et chaque chiffre cite dans les sections d’interpretation figure dans l’une de ces sorties : rien n’est affirme qui ne soit imprime par une cellule.

Trois habitudes de lecture avant d’avancer. D’abord, les detecteurs proposes sont des schemas pedagogiques, pas des outils de production : ils lisent le HTML source par expressions regulieres, la ou un audit reel interrogerait le rendu calcule d’un navigateur. Ce choix assume un but : rendre visible la mecanique de chaque detection, ligne par ligne, sans cache technique. Ensuite, la question du notebook n’est pas « ces pages sont-elles conformes ? » mais « quelle sonde voit quel defaut ? » — la conformite des quatre pages est connue d’avance, c’est la variable fixee; la sonde est la variable libre. Enfin, le fil rouge est un incident reel, anonymise et documente ailleurs dans le depot : des audits consecutifs d’un site ont declare « operationnel » sur la foi d’une sonde structurelle, alors que le rendu etait casse. La matrice finale est la reponse methodologique a cet incident — une sonde par classe de defaut, jamais une sonde unique pour toutes. Prerequis et perimetre technique, pour situer l’effort : tout le notebook tourne sur la bibliotheque standard (re seul, en verite) — aucune dependance, aucun reseau, aucune cle, aucun rendu. Ce n’est pas une economie de moyen, c’est un choix de lisibilite : un detecteur de quatorze lignes que l’etudiant peut reecrire de memoire vaut mieux, pour apprendre, qu’une brique d’accessibilite industrielle qui rend son verdict en boite noire. La transposition a un projet reel ne demande ni plus ni moins que de remplir le dictionnaire CHARTE avec les vraies valeurs de la vraie charte — le jour ou cette charte existe sous forme de donnees. C’est la vraie difficulte de l’exercice, et elle n’est pas technique : la plupart des chartes visuelles n’existent qu’en prose, et le premier travail d’audit est de les rendre executables.

1. La charte est une specification, pas un sentiment

Pour qu’un detecteur de conformite puisse dire « conforme » ou « non conforme », il faut une charte explicitement declarée : des couleurs, des polices, des regles d’usage. On travaille sur une instance synthetique — la fiction editoriale « Maison Valmont » — dont la charte est donnee ci-dessous. Aucune couleur ici n’est empruntee a une identite reelle ; la charte est l’instance minimale qui rend la notion de conformite definissable.

# La charte de Maison Valmont (synthetique) : un dictionnaire de contrats.
CHARTE = {
    "fond":           "#faf6ef",  # creme
    "texte":          "#2d4a3e",  # vert profond
    "accent":         "#c9a96e",  # dore
    "cta_fond":       "#2d4a3e",  # CTA principal : fond vert profond
    "cta_texte":      "#ffffff",  # CTA principal : texte blanc
    "police_titres":  "Playfair Display",
}

# Les couleurs primaires Bootstrap : le marqueur d'une esthetique SaaS
# generique qui s'importe avec le framework, pas avec la charte.
PRIMAIRES_BOOTSTRAP = {
    "#007bff",  # blue (primary)
    "#dc3545",  # danger (red)
    "#28a745",  # success (green)
    "#ffc107",  # warning (yellow)
    "#17a2b8",  # info (cyan)
    "#6c757d",  # secondary (gray)
}

print("Charte Maison Valmont definie : " + str(len(CHARTE)) + " contrats couleur.")
print("Marqueur primaire Bootstrap : " + str(len(PRIMAIRES_BOOTSTRAP)) + " couleurs guettees.")
Charte Maison Valmont definie : 6 contrats couleur.
Marqueur primaire Bootstrap : 6 couleurs guettees.

La charte n’est pas negociale : toute couleur hors palette est un suspect. Le detecteur de dominance (section 5) se resume a croiser les couleurs declaredes dans la page avec l’ensemble PRIMAIRES_BOOTSTRAP — rien de plus.

2. Quatre pages, un seul defaut chacune

On construit quatre fragments HTML. Trois portent une violation visuelle deliberee, et une seule — pour isoler chaque classe de defaut. La quatrieme est la reference conforme. Toutes ont une balise <main> non vide et un element d’action : le smoke test les declarera saines.

PAGE_CONFORME = '''<html><body>
<main>
  <h1 style="font-family:'Playfair Display'; color:#2d4a3e">La Collection Valmont</h1>
  <p style="color:#2d4a3e">Trois recits pour la rentree litteraire de septembre.</p>
  <a class="btn cta-principal" href="/collection" style="background-color:#2d4a3e; color:#ffffff">Decouvrir la collection</a>
</main>
</body></html>'''

PAGE_PRIMAIRES = '''<html><body>
<main>
  <h1 style="color:#2d4a3e">La Collection Valmont</h1>
  <p style="color:#2d4a3e">Trois recits pour la rentree litteraire de septembre.</p>
  <a class="btn" href="/collection" style="background-color:#007bff; color:#ffffff">Decouvrir</a>
  <span class="badge" style="background-color:#dc3545; color:#ffffff">Nouveau</span>
  <span class="badge" style="background-color:#28a745; color:#ffffff">En stock</span>
</main>
</body></html>'''

PAGE_CONTRASTE = '''<html><body>
<main>
  <h1 style="color:#2d4a3e">La Collection Valmont</h1>
  <p style="color:#c9a96e">Lisez notre presentation detaillee de la rentree litteraire.</p>
  <a class="btn" href="/collection" style="background-color:#2d4a3e; color:#ffffff">Decouvrir</a>
</main>
</body></html>'''

PAGE_AFFORDANCE = '''<html><body>
<main>
  <h1 style="color:#2d4a3e">La Collection Valmont</h1>
  <p style="color:#2d4a3e">Trois recits pour la rentree litteraire de septembre.</p>
  <a href="/collection" style="color:#2d4a3e; opacity:0.5">Decouvrir la collection</a>
  <a href="/newsletter" style="color:rgba(45,74,62,0.4)">S'abonner a la lettre</a>
</main>
</body></html>'''

PAGES = {
    "conforme":  PAGE_CONFORME,
    "primaires": PAGE_PRIMAIRES,
    "contraste": PAGE_CONTRASTE,
    "affordance": PAGE_AFFORDANCE,
}
print(str(len(PAGES)) + " pages synthetiques preparees (3 defaillantes, 1 conforme).")
4 pages synthetiques preparees (3 defaillantes, 1 conforme).
  • primaires : CTA en bleu Bootstrap, badges en rouge et vert Bootstrap. Couleurs hors charte.
  • contraste : paragraphe en dore #c9a96e sur creme #faf6ef — or sur creme, peu lisible.
  • affordance : deux CTA en <a> brut, sans .btn, l’un a opacity:0.5, l’autre en rgba(...,0.4).

3. Le smoke test : la sonde qui ne voit rien

Le smoke test est la verification minimale qu’on deploye apres une modification de template. Il ne regarde que la structure : la balise <main> est-elle presente et non vide ? Un element d’action est-il present ? La page est-elle « servie » (simule par un statut 200) ?

import re

def smoke_test(html_str):
    """Verifie la presence structurelle. Ne regarde AUCUNE couleur."""
    main_non_vide = bool(re.search(r"<main[^>]*>\s*\S", html_str, re.IGNORECASE))
    action_present = bool(re.search(r"<(a|button)[^>]*>", html_str, re.IGNORECASE))
    return {
        "statut":      200,
        "structure":   "PASS" if main_non_vide else "FAIL",
        "action":      "PASS" if action_present else "FAIL",
    }

for nom, page in PAGES.items():
    print(nom.ljust(11), smoke_test(page))
conforme    {'statut': 200, 'structure': 'PASS', 'action': 'PASS'}
primaires   {'statut': 200, 'structure': 'PASS', 'action': 'PASS'}
contraste   {'statut': 200, 'structure': 'PASS', 'action': 'PASS'}
affordance  {'statut': 200, 'structure': 'PASS', 'action': 'PASS'}

Le smoke test passe sur les quatre pages, y compris les trois cassees visuellement. C’est exactement le scenario recurrent : un agent regenere un template, le smoke test reste vert, le deploiement est declare « operationnel », et le rendu public est casse. Le smoke test mesure le contenant (la structure est la), pas la conformite (le contenu respecte-t-il la charte ?).

Il nous faut donc trois autres detecteurs, un par classe de defaut visuel.

4. Detecteur de contraste (WCAG)

Le contraste percu depend de la luminance relative des deux couleurs, pas de leur teinte. Le ratio WCAG est defini par le W3C : on linearise chaque canal RGB (correction gamma), on combine en luminance, puis on forme le ratio (L_clair + 0,05) / (L_fonce + 0,05). Le seuil AA est 4,5:1 pour du texte normal (3:1 pour du grand texte).

def hex_vers_rgb(c):
    c = c.lstrip("#")
    return tuple(int(c[i:i+2], 16) for i in (0, 2, 4))

def _canal_lineaire(c):
    c = c / 255.0
    return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4

def luminance(rgb):
    r, g, b = rgb
    return 0.2126 * _canal_lineaire(r) + 0.7152 * _canal_lineaire(g) + 0.0722 * _canal_lineaire(b)

def contraste_w3c(avant, arriere):
    la = luminance(hex_vers_rgb(avant))
    lr = luminance(hex_vers_rgb(arriere))
    clair, fonce = max(la, lr), min(la, lr)
    return round((clair + 0.05) / (fonce + 0.05), 2)

SEUIL_AA_NORMAL = 4.5

# Quelques mesures de bon sens avant l'audit.
print("Blanc / vert profond (#2d4a3e) : ", contraste_w3c("#ffffff", "#2d4a3e"))
print("Blanc / bleu primaire (#007bff) : ", contraste_w3c("#ffffff", "#007bff"))
print("Dore / creme (#c9a96e sur #faf6ef) : ", contraste_w3c("#c9a96e", "#faf6ef"))
Blanc / vert profond (#2d4a3e) :  9.72
Blanc / bleu primaire (#007bff) :  3.98
Dore / creme (#c9a96e sur #faf6ef) :  2.08

Le dore sur creme (2,08) est bien sous le seuil — c’est le defaut de la page contraste. Maintenant l’audit lui-meme : on extrait la couleur de chaque paragraphe et titre, et on la compare au fond declare dans la charte.

def audit_contraste_texte(html_str, fond):
    """Cherche les couleurs des balises de texte, les compare au fond.
    Renvoie le ratio minimal et son verdict (le pire cas decide)."""
    ratios = []
    for m in re.finditer(r'<(p|h[1-6])[^>]*style="([^"]*)"[^>]*>', html_str, re.IGNORECASE):
        style = m.group(2)
        cm = re.search(r"color:\s*(#[0-9a-fA-F]{6})", style)
        if cm:
            ratios.append(contraste_w3c(cm.group(1), fond))
    if not ratios:
        return None, "N/A"
    mini = min(ratios)
    return mini, "PASS" if mini >= SEUIL_AA_NORMAL else "FAIL"

for nom, page in PAGES.items():
    mini, verdict = audit_contraste_texte(page, CHARTE["fond"])
    detail = ("ratio min " + str(mini)) if mini is not None else "aucun texte couleur"
    print(nom.ljust(11), "contraste :", verdict, "(" + detail + ")")
conforme    contraste : PASS (ratio min 9.02)
primaires   contraste : PASS (ratio min 9.02)
contraste   contraste : FAIL (ratio min 2.08)
affordance  contraste : PASS (ratio min 9.02)

Seule la page contraste echoue — son paragraphe dore sur creme. Les trois autres pages ont un texte lisible. Ce detecteur voit ce que le smoke test ignorait.

5. Detecteur de dominance des primaires

Deuxieme classe de defaut : une page qui affiche des couleurs SaaS generiques (les primaires Bootstrap) la ou la charte exige des tons tamises. Le detecteur recense les couleurs declarees dans la page et signale celles qui figurent dans l’ensemble PRIMAIRES_BOOTSTRAP.

_HEX = re.compile(r"#([0-9a-fA-F]{6})\b")
_RGB = re.compile(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)")

def couleurs_declarees(html_str):
    trouv = []
    for m in _HEX.finditer(html_str):
        trouv.append("#" + m.group(1).lower())
    for m in _RGB.finditer(html_str):
        trouv.append("#%02x%02x%02x" % (int(m.group(1)), int(m.group(2)), int(m.group(3))))
    return trouv

def audit_dominance(html_str):
    utilisees = couleurs_declarees(html_str)
    primaires = sorted(set(c for c in utilisees if c in PRIMAIRES_BOOTSTRAP))
    return primaires

for nom, page in PAGES.items():
    primaires = audit_dominance(page)
    verdict = "PASS" if not primaires else "FAIL"
    detail = str(len(primaires)) + " primaire(s)" if primaires else "0 primaire"
    print(nom.ljust(11), "dominance :", verdict, "(" + detail + ")")
conforme    dominance : PASS (0 primaire)
primaires   dominance : FAIL (3 primaire(s))
contraste   dominance : PASS (0 primaire)
affordance  dominance : PASS (0 primaire)

Allowlist, denylist, et la normalisation des notations

La sortie est la plus tranchee du notebook : trois primaires sur la page primaires, zero partout ailleurs. Trois decisions de conception meritent d’etre lues dans le code avant de croire le verdict.

Premiere decision : le detecteur compare les couleurs declarees a la denylist de la section 1, pas a la charte. Une couleur hors charte et hors denylist — un violet maison, un gris importe d’ailleurs — passerait. Le choix est assume : le detecteur traque le temoin d’import framework, pas l’exhaustivite de conformite. Deuxieme decision : la normalisation. #2d4a3e et rgb(45, 74, 62) decrivent la meme couleur ; sans la conversion rgb-vers-hex, la page affordance — dont le second CTA est pose en rgba(45,74,62,0.4) — compterait ses couleurs dans deux mondes differents. Le _HEX porte un \b apres les six chiffres : sans lui, un identifiant plus long pourrait livrer six hexadecimaux parasites. Details petits, mais chacun est la difference entre un detecteur et une intuition.

Troisieme decision, la plus discutable et donc la plus instructive : sorted(set(...)) dedouble et ordonne. Le compte « 3 primaire(s) » signifie trois teintes distinctes, pas trois occurrences — un CTA et dix badges bleus compteraient une seule primaire. Pour un temoin d’import, c’est le bon choix (une teinte suffit a prouver la provenance) ; pour mesurer l’envahissement, ce serait le mauvais (il faudrait compter les occurrences). Le detecteur repond a « cette page porte-t-elle l’esthetique importee ? », pas a « a quel point ? ». Deux questions legitimes, deux compteurs differents — le verdict texte n’en porte qu’un. Que faudrait-il pour un veritable detecteur d’exhaustivite — chaque couleur declaree presente dans la charte ? L’idee se heurte vite a la realite des interfaces fonctionnelles : les gris neutres (bordures, separateurs, textes secondaires), les etats (hover, focus, disabled), les ombres — aucun projet reel ne vit avec six couleurs exactement. L’allowlist stricte est donc inapplicable telle quelle ; ce qui s’applique est un graphe de tolerance (charte, plus une zone neutre bornee en luminance), dont l’ecriture est un vrai travail de design, pas un raffinement technique. C’est la raison profonde du choix denylist de ce banc : il ne faut pas une charte enrichie pour tourner, et il attrape le signal le plus grave — l’esthetique importee — des le premier jour. L’audit complet des deux cotes (denylist pour la provenance, allowlist tolerante pour la completude) est une progression, pas un tout-or-rien.

Seule la page primaires echoue, avec ses trois couleurs Bootstrap. Notons que ces couleurs sont parfaitement lisibles (blanc sur bleu Bootstrap a un contraste correct pour un grand bouton) : le detecteur de contraste ne les voit pas comme un defaut. C’est volontaire — chaque detecteur capture une classe distincte, et aucun ne peut seul certifier la conformite globale.

6. Detecteur d’affordance des CTA

Troisieme classe : un lien qui devrait se comporter comme un bouton mais n’en a ni la classe (.btn) ni l’opacite pleine — il semble desactive. Le detecteur repere les elements d’action (liens et boutons au texte oriente action) et verifie qu’ils portent une classe de bouton et une opacite suffisante.

TEXTES_ACTION = (
    "decouvrir", "lire la suite", "soumettre", "s'abonner",
    "en savoir plus", "telecharger", "je m'inscris",
)

def extraire_cta(html_str):
    cta = []
    for m in re.finditer(r"<(a|button)([^>]*)>(.*?)</\1>", html_str, re.DOTALL | re.IGNORECASE):
        tag, attrs, texte = m.group(1), m.group(2).lower(), re.sub(r"<[^>]+>", "", m.group(3)).strip()
        texte_bas = texte.lower()
        if ("btn" in attrs) or ("cta" in attrs) or any(t in texte_bas for t in TEXTES_ACTION):
            cta.append({"tag": tag, "attrs": attrs, "texte": texte})
    return cta

def _opacite_effective(attrs):
    om = re.search(r"opacity:\s*([0-9.]+)", attrs)
    if om:
        return float(om.group(1))
    rm = re.search(r"rgba\([^)]*,\s*([0-9.]+)\s*\)", attrs)
    if rm:
        return float(rm.group(1))
    return 1.0

def audit_affordance(html_str):
    verdicts = []
    for cta in extraire_cta(html_str):
        a_bouton = ("btn" in cta["attrs"]) or (cta["tag"] == "button")
        opac = _opacite_effective(cta["attrs"])
        ok = a_bouton and opac >= 0.7
        verdicts.append((cta["texte"][:30], a_bouton, opac, "PASS" if ok else "FAIL"))
    return verdicts

for nom, page in PAGES.items():
    verdicts = audit_affordance(page)
    global_v = "PASS" if all(v[3] == "PASS" for v in verdicts) and verdicts else "FAIL"
    print(nom.ljust(11), "affordance :", global_v, "(" + str(len(verdicts)) + " CTA)")
conforme    affordance : PASS (1 CTA)
primaires   affordance : PASS (1 CTA)
contraste   affordance : PASS (1 CTA)
affordance  affordance : FAIL (2 CTA)

La page affordance echoue : ses deux CTA n’ont pas de classe de bouton, et leurs opacites (0,5 et 0,4) sont sous le seuil. Un visiteur ne comprend pas que ce sont des actions. La encore, le smoke test avait seulement verifie qu’un <a> existait — il n’a aucune idee de son rendu.

7. La matrice — ce que chaque sonde voit

On croise maintenant les quatre detecteurs sur les quatre pages. C’est le resultat central du notebook.

def conformite_globale(page):
    sm = smoke_test(page)
    smoke_ok = (sm["structure"] == "PASS" and sm["action"] == "PASS")
    _, v_contraste = audit_contraste_texte(page, CHARTE["fond"])
    v_dominance = "PASS" if not audit_dominance(page) else "FAIL"
    aff = audit_affordance(page)
    v_affordance = "PASS" if (aff and all(x[3] == "PASS" for x in aff)) else "FAIL"
    return {
        "smoke":      "PASS" if smoke_ok else "FAIL",
        "contraste":  v_contraste,
        "dominance":  v_dominance,
        "affordance": v_affordance,
    }

print("page".ljust(11), "smoke", "contraste", "dominance", "affordance", "  GLOBAL", sep="   ")
print("-" * 70)
for nom, page in PAGES.items():
    v = conformite_globale(page)
    global_v = "PASS" if all(x == "PASS" for x in v.values()) else "FAIL"
    print(nom.ljust(11), v["smoke"].ljust(6), v["contraste"].ljust(9),
          v["dominance"].ljust(10), v["affordance"].ljust(10), " ", global_v)
page          smoke   contraste   dominance   affordance     GLOBAL
----------------------------------------------------------------------
conforme    PASS   PASS      PASS       PASS         PASS
primaires   PASS   PASS      FAIL       PASS         FAIL
contraste   PASS   FAIL      PASS       PASS         FAIL
affordance  PASS   PASS      PASS       FAIL         FAIL

Lecture. La colonne smoke est verte partout — y compris sur les trois pages cassees. Chaque page defaillante est rathee par exactement un des trois detecteurs visuels, et conforme pour tous les autres. La colonne GLOBAL (conformite visuelle) est la seule qui discrimine, et elle exige la reunion des quatre sondes.

C’est la lecon, mesuree :

Un smoke test structurel ne peut pas, par construction, detecter un defaut de conformite visuelle. Il mesure le contenant (la structure est presente), pas le contenu (la structure respecte-t-elle la charte ?). Declaring une page « operationnelle » sur la foi du seul smoke test, c’est declarer sain ce qu’on n’a pas regarde.

Et comme l’ecrit le document de reference cite en introduction : le motif recidive. La classe systeme (HTTP 200 sur un site casse) et la classe visuelle (smoke structurel vert sur une UI cassee) sont deux instances du meme defaut de methode : confondre « la sonde a passe » avec « l’etat est vrai ».

8. Pourquoi l’agent ne peut pas s’auto-certifier

Un Copilot Gutenberg qui regenere un template peut-il, lui-meme, certifier que le rendu est conforme ? Non, pour deux raisons que la matrice rend tangibles.

  1. L’agent optimise ce qu’on mesure. Si l’auto-certification porte sur le smoke test (structure presente), l’agent produira des pages qui passent le smoke test — bleu Bootstrap compris. Le detecteur devient un objectif, et l’objectif est trop pauvre pour garantir la conformite.

  2. La conformite visuelle est un jugement externe. Elle suppose une charte declaree (section 1) et des detecteurs dedies (sections 4 a 6) qu’un agent generique n’embarque pas. La charte est un contrat entre le lieu d’edition et le lieu de rendu ; l’agent operate au deuxieme sans connaitre le premier.

La consequence pratique : apres toute modification de template par un agent, un humain (ou un harnais d’audit dedie) doit faire tourner les detecteurs visuels avant de declarer le rendu operationnel. Confier cette etape a l’agent lui-meme, c’est lui demander de noter sa propre copie.

9. Exercices

Les trois exercices suivants sont laisses a completer. Convention : un stub retourne None ou pass — il ne levere pas d’exception, et s’execute sans erreur.

# Exercice 1 — Enumerer les primaires avec leur contexte.
# Ecrire detecteur_primaire_contexte(html_str) qui retourne une liste de
# tuples (element, couleur) indiquant dans quelle balise chaque primaire
# Bootstrap apparait. Retourner [] si aucune.
def detecteur_primaire_contexte(html_str):
    # Parcourir les balises, extraire leur couleur de fond ou de texte,
    # croiser avec PRIMAIRES_BOOTSTRAP, retourner le contexte.
    return []
# Exercice 2 — Rapport agrege.
# Ecrire rapport_conformite(html_str, charte) qui retourne un dictionnaire
# {contraste, dominance, affordance} donnant le verdict et un resume humain
# pour chacun. Retourner un dict vide si la page est vide.
def rapport_conformite(html_str, charte):
    # Reunir les trois detecteurs en un rapport lisible.
    return {}
# Exercice 3 — Le piege du smoke vert.
# Etant donne une page qui PASS le smoke_test, ecrire piege_smoke_vert(page)
# qui retourne la classe du defaut visuel parmi {"contraste", "dominance",
# "affordance", None}. Retourner None si la page est conforme.
def piege_smoke_vert(page):
    # On suppose deja que smoke_test(page) passe ; chercher le defaut.
    return None

Ce qu’il faut retenir

  • Le smoke test structurel mesure le contenant, pas la conformite. Une page au rendu casse le passe tranquillement.
  • Chaque classe de defaut visuel demande son propre detecteur : contraste WCAG (luminance, pas teinte), dominance des primaires (croisement avec la charte), affordance des CTA (classe + opacite). Aucun ne suffit seul.
  • La conformite visuelle est un jugement externe : elle suppose une charte declaree et ne peut pas etre auto-certifiee par l’agent qui genere le rendu.
  • Ce motif est la classe visuelle du « probe menteur » decrit dans docs/reference/verification-verte-systeme-casse.md (classe systeme). La structure recidive : une sonde qui passe ne prouve pas que l’etat est vrai.

Fixture synthetique « Maison Valmont » — couleurs, polices et textes fictifs. Aucune donnee client, aucun nom reel, aucun secret. Toutes les fonctions sont deterministes (stdlib seul : re, pas de cle, pas de reseau).

D’ou vient cette matrice

Une derniere lecture, en guise de provenance. La structure de ce notebook — une sonde usuelle aveugle, puis une sonde par classe de defaut — n’est pas nee d’une speculation : elle condense un incident documente dans le depot, ou des audits consecutifs d’un site reel ont declare « operationnel » en se fondant sur une sonde structurelle, chaque episode corrigeant la sonde precedente sans jamais corriger la question posee. L’etude de cas complete vit dans docs/reference/verification-verte-systeme-casse.md — six classes de sondes menteuses y sont cataloguees avec leur signature et leur contre-mesure ; la classe visuelle y est la soeur de la classe systeme (un statut 200 sur un site casse).

Ce notebook en est la mise en forme executable pour la classe visuelle : quatre pages, trois defauts, une sonde aveugle par conception, trois detecteurs orthogonaux, une matrice qui refuse l’agregat flatteur. La transposition a un projet reel suit exactement les sections : ecrire la charte en donnees (section 1), denombrer les classes de defaut observes (section 2), nommer la question exacte a laquelle chaque sonde existante repond (section 3), puis une sonde par classe, chacune avec son perimetre explicite (sections 4 a 6), et un verdict non compensatoire (section 7).

Et la regle la plus portable reste celle de la section 3 : avant d’invoquer une sonde comme preuve, nommer la question a laquelle elle repond. Le smoke test n’a jamais menti dans cette histoire — il a toujours repondu exactement, sincerement, a une question que personne n’avait formulee. Le defaut etait dans la citation, pas dans la mesure. Une precision de citation, pour finir. Ce notebook est un schema de preuve, pas un precedent : une equipe qui l’applique a son projet ne doit pas ecrire « conforme au notebook » mais nommer ses colonnes (« contraste : pire cas 9.02 ; dominance : zero primaire ; affordance : 2 CTA verifies »), ses perimetres et ses N/A — la matrice sert a rendre le rapport falsifiable, pas a le raccourcir. Quant a l’ordre des sections, il n’est pas pedagogique seulement : il suit l’ordre historique des decouvertes de l’incident origine — la sonde usuelle d’abord, puis chaque classe de defaut le jour ou elle a ete vue, puis la matrice le jour ou l’on a compris qu’aucune sonde isolee ne suffirait. Un audit qui commence par la matrice gagne du temps ; un audit qui commence par l’incident comprend pourquoi la matrice a cette forme — et saura en changer la forme le jour ou une septieme classe de defaut apparaitra.

Retour au sommet