Aller au contenu

Vérification du wiki#

En bref#

Cinq contrôles automatisables garantissent qu'aucune fiche ne ment sur sa complétude : les liens internes et leurs ancres résolvent, chaque fiche porte ses sources et ses limites, aucune fiche n'est orpheline depuis l'accueil, rien n'est en anglais, et aucune fiche n'est datée après le gel du 31 août 2026. À lancer après chaque lot de rédaction, depuis la racine.

Le cinquième contrôle est le gardien du gel, et il a remplacé un contrôle de fraîcheur devenu absurde dans un corpus figé — le raisonnement est dans les limites, en bas de page.


Le contrôle#

Enregistrer sous /tmp/verifier.py puis lancer python3 /tmp/verifier.py depuis la racine du dépôt. Le script ne modifie rien.

import datetime, pathlib, re, sys

RACINE = pathlib.Path('.')
# Dossiers hors périmètre : dépôt git, dossier de travail de la publication,
# site HTML publié, configuration de l'outillage, documents sources déposés.
# Fichiers hors périmètre : ce sont des documents de pilotage du projet
# (consignes, cahier des charges), pas des fiches du wiki. « REPRISE.md » est
# conservé dans la liste bien qu'il ait été supprimé le 27 août 2026 à la
# clôture : le retirer ne changerait rien au résultat, et le laisser documente
# le périmètre historique du contrôle.
HORS_PERIMETRE = {'.git', '.publication', 'dist', '.claude', 'materiel'}
fiches = sorted(f for f in RACINE.rglob('*.md')
                if not HORS_PERIMETRE & set(f.parts)
                and f.name not in ('CLAUDE.md', 'PROJET.md', 'REPRISE.md'))
anomalies = []

# Les blocs de code contiennent des gabarits, pas de vrais liens : on les retire.
def sans_blocs_de_code(texte):
    return re.sub(r'[`]{3}.*?[`]{3}', '', texte, flags=re.S)

# Identifiant d'ancre tel que le produit la chaîne de publication, reproduit ici :
# on retire le balisage, puis tout caractère qui n'est ni alphanumérique, ni espace,
# ni tiret ; on passe en minuscules ; on remplace **chaque** espace par un tiret — sans
# les regrouper : deux espaces donnent deux tirets.
# Les accents sont conservés. Aucun élagage des bords : un titre finissant par « ? »
# ou par un guillemet produit donc une ancre finissant par un tiret.
def ancre_de(titre):
    t = re.sub(r'\[([^\]]*)\]\([^)]*\)', r'\1', titre)   # [texte](lien) -> texte
    t = t.replace('`', '').replace('**', '').replace('*', '').replace('_', '')
    t = re.sub(r'[^\w\s-]', '', t, flags=re.U).lower()
    return re.sub(r'\s', '-', t)

_cache_ancres = {}
def ancres_de(chemin):
    cle = chemin.resolve()
    if cle not in _cache_ancres:
        texte = sans_blocs_de_code(cle.read_text(encoding='utf-8'))
        _cache_ancres[cle] = {ancre_de(m.group(1))
                              for m in re.finditer(r'^#{1,6}\s+(.*?)\s*$', texte, re.M)}
    return _cache_ancres[cle]

liens_internes = {}
for f in fiches:
    corps = sans_blocs_de_code(f.read_text(encoding='utf-8'))

    # 1. Liens internes, et les ancres qu'ils visent
    cibles = set()
    for cible, ancre in re.findall(r'\]\(([^)\s#]+\.md)(#[^)\s]*)?\)', corps):
        if cible.startswith('http'):
            continue
        cibles.add(cible)
        chemin = (f.parent / cible)
        if not chemin.resolve().exists():
            anomalies.append(f'LIEN CASSÉ      {f} → {cible}')
        elif ancre:
            attendu = ancre[1:]
            if attendu not in ancres_de(chemin):
                anomalies.append(f'ANCRE INTROUVABLE  {f} → {cible}{ancre}')
    liens_internes[f] = cibles

    # 1 bis. Ancres internes à la page elle-même : ](#section)
    for ancre in re.findall(r'\]\(#([^)\s]+)\)', corps):
        if ancre not in ancres_de(f):
            anomalies.append(f'ANCRE INTROUVABLE  {f} → #{ancre}')

    # 2. Sections obligatoires (les gabarits sont des modeles, pas des fiches)
    if 'gabarits' in f.parts:
        continue
    for section in ('## Sources', '## Limites'):
        if not re.search(rf'^{re.escape(section)}', corps, re.M):
            anomalies.append(f'SECTION MANQUANTE  {f} → {section}')

    # 3. Une piste de suite déclarée
    if 'Prochaine piste' not in corps:
        anomalies.append(f'PISTE MANQUANTE    {f}')

# 4. Fiches orphelines : atteignables depuis l'accueil ?
atteintes, pile = {RACINE / 'index.md'}, [RACINE / 'index.md']
while pile:
    courante = pile.pop()
    for cible in liens_internes.get(courante, ()):
        suivante = (courante.parent / cible)
        if suivante.exists():
            suivante = pathlib.Path(suivante.resolve()).relative_to(RACINE.resolve())
            if suivante not in atteintes:
                atteintes.add(suivante)
                pile.append(suivante)
for f in fiches:
    if f not in atteintes and 'gabarits' not in f.parts:
        anomalies.append(f'ORPHELINE          {f}')

# 5. Le gel. Ce wiki est volontairement figé au 31 août 2026 — voir la fiche « gel ».
#    Ce contrôle en est le gardien, et il a remplacé un contrôle de fraîcheur.
#
#    Ce qui BLOQUE : une date absente (fiche mal formée) et une date POSTÉRIEURE AU GEL
#    (fiche qui brise la photographie). Les deux sont des anomalies vérifiables, et elles
#    le resteront quelle que soit la date à laquelle ce script est rejoué.
#
#    Ce qui a été RETIRÉ : l'avertissement « fiche non revue depuis plus de 91 jours ».
#    Dans un corpus gelé, il finirait par se déclencher sur toutes les fiches, à chaque
#    publication, pour toujours. Une alarme qui sonne toujours n'est plus une alarme :
#    elle apprend seulement à ignorer les alarmes. L'âge est désormais IMPRIMÉ, jamais
#    signalé — le lecteur qui reconstruit le site en novembre doit voir qu'il manipule
#    une photographie, sans que le script prétende y voir un défaut.
DATE_DE_GEL = datetime.date(2026, 8, 31)
plus_recente = None
for f in fiches:
    if 'gabarits' in f.parts:
        continue
    m = re.search(r'^derniere_maj:\s*(\d{4})-(\d{2})-(\d{2})\s*$',
                  f.read_text(encoding='utf-8'), re.M)
    if not m:
        anomalies.append(f'DATE ABSENTE       {f}')
        continue
    date_fiche = datetime.date(*map(int, m.groups()))
    if date_fiche > DATE_DE_GEL:
        anomalies.append(f'POSTÉRIEURE AU GEL {f} → {date_fiche} > {DATE_DE_GEL}')
    elif plus_recente is None or date_fiche > plus_recente:
        plus_recente = date_fiche

print('\n'.join(anomalies) if anomalies else 'Aucune anomalie.')
print(f'\n{len(fiches)} fiches contrôlées.')
if plus_recente:
    age = (datetime.date.today() - plus_recente).days
    print(f'Corpus gelé au {DATE_DE_GEL}. Fiche la plus récente : {plus_recente}, '
          f'soit {age} jour(s) — information, pas anomalie.')
sys.exit(1 if anomalies else 0)

Les contrôles non automatisables#

À faire à l'œil, à la fin de chaque lot :

  • Français intégral — aucun titre, nom de fichier, section ou terme technique en anglais. Les noms propres et les citations font exception.
  • Traçabilité — toute affirmation non triviale porte une source cotée. Toute source de cote D figure au journal des arbitrages.
  • Symétrie de traitement — la méthode impose que ce qui est examiné chez l'un le soit chez les autres. Se relire en se demandant : un lecteur du camp adverse trouverait-il ce texte injuste, et sur quoi ?
  • Datation — aucune date relative. Tout chiffre porte sa date de mesure.

Sources#

  • Méthode — les règles que ces contrôles font respecter
  • PROJET.md — l'exigence de niveau 3 dont découle le contrôle des « Limites »

Limites#

  • Les gabarits sont exclus du contrôle d'orphelinat : ils sont des modèles, pas des fiches de contenu.
  • Les dossiers .publication/ (travail de publication) et dist/ (site HTML publié) sont hors périmètre : ils contiennent des copies et des fichiers tiers.
  • Le contrôle des liens vérifie désormais les ancres (#section), qu'elles visent une autre fiche ou la page courante — c'était la « prochaine piste » déclarée ici, et elle a été ouverte le 26 août 2026 après la découverte de deux ancres cassées que le contrôle laissait passer. La règle de fabrication des identifiants est reproduite dans le script, elle n'est pas lue de la chaîne de publication : si celle-ci change, le contrôle deviendra faux sans le dire.
  • Le contrôle des liens ne vérifie pas les liens externes, qui peuvent pointer vers une page disparue.
  • La détection de l'anglais n'est pas automatisée.
  • ~~Le contrôle de fraîcheur avertit, il ne bloque pas.~~ Ce contrôle a été retiré le 26 août 2026, quand le wiki a été déclaré figé au 31 août 2026. La raison n'est pas qu'il gênait : c'est que dans un corpus gelé, il finirait par se déclencher sur la totalité des fiches, à chaque publication, pour toujours. Une alarme qui sonne toujours n'est plus une alarme — elle apprend seulement à ignorer les alarmes. À sa place, l'âge du corpus est imprimé et jamais signalé comme une anomalie.
  • Le contrôle bloquant a été rendu plus strict, et surtout stable dans le temps. Il refusait une date postérieure au jour de la vérification — une borne qui bouge chaque jour. Il refuse désormais une date postérieure au 31 août 2026, borne fixe : une fiche modifiée après le gel briserait la photographie, et le script s'y oppose quelle que soit la date à laquelle on le rejoue.
  • Le contrôle du gel porte sur la date déclarée, pas sur le contenu. Une modification faite après le 31 août sans toucher à l'en-tête passerait. L'historique Git la révélerait ; ce script, non. C'est la limite exacte de ce garde-fou, et elle est la même qu'avant : il mesure une déclaration, pas un fait.
  • Prochaine piste, et elle n'est plus pour ce projet. Rapprocher derniere_maj de la date du dernier commit touchant le fichier (git log -1 --format=%ad) reste le seul moyen de détecter un en-tête qui ment — et, depuis le gel, le seul moyen de détecter une retouche clandestine après le 31 août. Ce contrôle n'a pas été écrit avant le gel ; il serait le premier à écrire pour qui voudrait vérifier l'intégrité de cette photographie sans faire confiance à ses en-têtes.

Pour creuser#