Skip to content

Phase 0 : spécifications, module de coordonnées, sonde ScreenCast - #1

Merged
NicoBOD merged 15 commits into
mainfrom
phase0/coords-and-screencast-probe
Aug 3, 2026
Merged

NicoBOD merged 15 commits into
mainfrom
phase0/coords-and-screencast-probe

Conversation

@NicoBOD

@NicoBOD NicoBOD commented Aug 3, 2026

Copy link
Copy Markdown
Member

Premier incrément de TutoClic. Aucune interface, rien d'utilisable : la Phase 0
sert à répondre aux questions qui décident de l'architecture avant d'écrire
l'application.

Ce que ça contient

SPEC.md — les spécifications complètes. Trois paris techniques de la
version initiale ont été invalidés par vérification, sources en annexe A :

  • une extension GNOME Shell ne voit pas les clics des fenêtres clientes ;
  • AT-SPI ne livre pas d'événements souris sous Wayland, c'est pourquoi le
    « mouse review » d'Orca est cassé ;
  • le portail GlobalShortcuts n'est pas implémenté sur GNOME.

Le moteur de capture repose donc sur cursor_mode = metadata du portail
ScreenCast, qui livre la position du pointeur par frame sans aucun privilège,
plus une détection de changement de frame comme déclencheur. Pour un tutoriel
c'est même le meilleur signal : on veut la frame où l'interface a répondu, pas
l'instant de l'appui bouton.

crates/tutoclic-coords — conversion entre les quatre repères (§4.5), en
TDD. Zéro dépendance système, donc testable sur un runner nu.

crates/tutoclic-probe — la sonde de la Phase 0. Elle ne capture rien et
n'écrit aucune image.

Un vrai bug trouvé par le test de propriété

Le test de propriété exigé par §15.5 a trouvé une perte silencieuse d'un pixel
sur l'aller-retour de normalisation. Pour une image de 3133 px de large :

px = 1607
n  = 1607 / 3133          = 0.5129269071177784
n * 3133                  = 1606.9999999999998   et non 1607
floor(1606.9999999999998) = 1606                 un pixel perdu

Chaque annotation aurait dérivé d'un pixel, sur certaines largeurs d'image
seulement, sans jamais planter. Un scénario de test manuel ne l'aurait pas vu.
Corrigé par une marge de 1e-9 avant l'arrondi vers le bas.

La Phase 0 n'est pas franchie

La sonde a tourné de bout en bout : portail joignable, CursorMode::Metadata
accepté, jeton de restauration délivré, flux PipeWire connecté, format négocié,
30 frames livrées et inspectées. Tampon MemFd, et aucune métadonnée de
curseur sur les 30 frames.

Mais l'exécution a eu lieu en session X11, alors que la plateforme de
référence est Wayland. Les deux questions de l'annexe B restent donc ouvertes.
docs/phase0-results.md dit exactement ce que la mesure établit et ce qu'elle
n'établit pas.

Prochaine étape, sur Ubuntu GNOME Wayland : cargo run -p tutoclic-probe,
en bougeant la souris, puis relancer pour vérifier que le consentement n'est plus
demandé.

Vérifié

  • cargo test --workspace : 14 tests verts, dont 20 000 cas de propriété
  • cargo clippy --workspace --all-targets -- -D warnings : propre
  • cargo fmt --all -- --check : propre
  • chacun des 6 commits se construit seul

Pas encore fait

CI de sécurité de §12.3 : cargo audit, cargo deny, SBOM CycloneDX, détection
de secrets, suite de fichiers de projet malveillants. Et la voix extérieure de la
revue d'architecture n'a jamais tourné, Codex n'étant pas installé : ce document
n'a reçu qu'une seule perspective de modèle.

🤖 Generated with Claude Code

NicoBOD and others added 6 commits August 3, 2026 21:21
Spécifications complètes du produit, issues d'une session de conception puis
d'une revue d'architecture. Trois paris techniques de la version initiale ont
été invalidés par vérification et corrigés :

- une extension GNOME Shell ne voit pas les clics des fenêtres clientes ;
- AT-SPI ne livre pas d'événements souris sous Wayland ;
- le portail GlobalShortcuts n'est pas implémenté sur GNOME.

Le moteur de capture repose donc sur cursor_mode = metadata du portail
ScreenCast, qui livre la position du pointeur par frame sans aucun privilège,
plus une détection de changement de frame comme déclencheur.

Sources vérifiées en annexe A, questions ouvertes en annexe B.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le modèle créé à l'initialisation du dépôt visait VisualStudio. Le projet est
en Rust, avec GTK4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SPEC.md §4.5 exige un module unique autorisé à convertir, avec des types
distincts pour que le compilateur refuse un mélange : LogicalPoint,
PhysicalPoint, ImagePoint, NormalizedPoint.

Couvre les échelles fractionnaires, les origines négatives, les moniteurs
multiples, les rotations et la capture de région. Un point hors de la région
capturée renvoie None plutôt que (0, 0) : rendre l'origine placerait le repère
de clic dans le coin de la mauvaise image.

Le test de propriété exigé par §15.5 a trouvé une vraie perte d'un pixel sur
l'aller-retour de normalisation. Pour une image de 3133 px de large,
1607 / 3133 * 3133 vaut 1606.9999999999998 en f64, donc floor rendait 1606.
Corrigé par une marge de 1e-9 avant l'arrondi vers le bas, largement au-dessus
de l'erreur relative de la division f64 et très en dessous d'une frontière de
pixel légitime.

Aucune dépendance système : ce crate se teste sur un runner nu, ce qui est la
propriété que §16 demande à la Phase 1a.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Elle répond aux deux questions qui décident de l'architecture, et à elles
seules (SPEC.md §16, annexe B points 1 et 3) :

1. quel type de tampon PipeWire Mutter négocie-t-il ? MemFd ou MemPtr sont
   lisibles par le CPU et GStreamer reste hors des dépendances ; DmaBuf impose
   GStreamer ou un import EGL ;
2. SPA_META_Cursor arrive-t-il, et sur combien de frames ? Sans lui, le mode
   automatique perd le repère de clic.

Vérifie au passage le jeton de restauration, donc le critère « dix captures
sans nouvelle autorisation » de §15.2 point 4 : un second lancement ne doit
plus rien demander.

Ne capture rien, n'écrit aucune image, ne conserve que le jeton de
restauration. Le seul bloc unsafe emprunte le tampon brut le temps de lire son
type et la position du curseur, puis le rend immédiatement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… PipeWire

Deux jobs séparés, qui reflètent la séparation du workspace. Le job coords
tourne sur un runner nu : si un jour il exige GTK ou PipeWire, c'est que la
propriété d'indépendance de la Phase 1a a été cassée et la CI le dira.

La sonde n'est pas exécutée en CI : elle a besoin d'un portail et d'une session
graphique. Elle se lance à la main, cf. docs/phase0-results.md.

Reste à ajouter, cf. SPEC.md §12.3 : cargo audit, cargo deny, SBOM CycloneDX,
détection de secrets, et la suite de fichiers de projet malveillants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La sonde a tourné de bout en bout : portail joignable, CursorMode::Metadata
accepté, jeton de restauration délivré, flux PipeWire connecté, format négocié,
30 frames livrées et inspectées. Tampon MemFd, et aucune métadonnée de curseur
sur les 30 frames.

Mais l'exécution a eu lieu en session X11, alors que la plateforme de référence
est Wayland. Les deux questions de l'annexe B restent donc ouvertes, et la
Phase 0 n'est pas franchie. docs/phase0-results.md dit exactement ce que la
mesure établit et ce qu'elle n'établit pas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 3, 2026 19:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Ce PR pose les bases de TutoClic – Phase 0 : il formalise l’architecture (spécifications) et ajoute deux premiers incréments techniques vérifiables (conversion de coordonnées + sonde ScreenCast/PipeWire) ainsi qu’une CI minimale.

Changes:

  • Ajout des spécifications complètes (SPEC.md) + documentation des mesures Phase 0 (docs/phase0-results.md) et mise à jour du README.md.
  • Introduction du crate tutoclic-coords (conversion de repères + tests unitaires + proptest de non-régression).
  • Introduction du crate tutoclic-probe (sonde portail ScreenCast + PipeWire) + mise en place d’une CI GitHub Actions.

Reviewed changes

Copilot reviewed 12 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
SPEC.md Spécifications v2.0 (architecture, contraintes, tests, phasage).
README.md Présentation du projet + instructions de build/test et description de la sonde.
docs/phase0-results.md Journal des mesures réellement observées en Phase 0 (par exécution).
crates/tutoclic-probe/src/main.rs Sonde ScreenCast/PipeWire (type de buffers + présence de SPA_META_Cursor).
crates/tutoclic-probe/Cargo.toml Manifest du crate probe (deps ashpd, pipewire, anyhow, async-std).
crates/tutoclic-coords/src/lib.rs Module de conversion de coordonnées + correction de dénormalisation (epsilon).
crates/tutoclic-coords/Cargo.toml Manifest du crate coords (zéro dépendance runtime, proptest en dev).
crates/tutoclic-coords/tests/logical_to_image.rs Tests unitaires pour logical_to_image et tailles d’image vs scale.
crates/tutoclic-coords/tests/normalization.rs Tests normalisation + tests de propriété d’aller-retour.
crates/tutoclic-coords/tests/normalization.proptest-regressions Seed de régression proptest (replay automatique).
Cargo.toml Déclaration workspace (members, MSRV 1.80, deps workspace).
Cargo.lock Lockfile (dépendances Rust).
.gitignore Simplification (Rust + sorties locales sonde/projets + éditeurs).
.github/workflows/ci.yml CI: job coords (runner nu) + job probe (installe PipeWire dev, build/clippy).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/tutoclic-probe/src/main.rs Outdated
Comment on lines +58 to +62
let base = std::env::var_os("XDG_STATE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| {
let mut home = PathBuf::from(std::env::var_os("HOME").unwrap_or_default());
home.push(".local/state");
Comment on lines +75 to +82
fn write_restore_token(token: &str) -> Result<()> {
let path = state_file();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, token)?;
Ok(())
}
Comment thread .github/workflows/ci.yml
Comment on lines +20 to +22
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
NicoBOD and others added 9 commits August 3, 2026 22:08
Deux changements dans le même fichier, donc dans le même commit.

1. LE CORRECTIF DE FOND

Le verdict « aucune métadonnée de curseur » de la première exécution était un
bug de la sonde, pas un refus de la plateforme.

PipeWire n'attache à un tampon que les métadonnées qu'un client a explicitement
réclamées, via des paramètres SPA_PARAM_Meta poussés avec update_params une fois
le format fixé. La sonde n'envoyait qu'un paramètre EnumFormat. Le serveur
n'avait donc aucune raison d'attacher SPA_META_Cursor, et il ne signale pas
l'omission. Le « métadonnées sur la 1re frame : 1 » le montrait déjà : une
métadonnée arrivait, vraisemblablement SPA_META_Header, mais pas le curseur.

Demande désormais SPA_META_Header et SPA_META_Cursor, ce dernier avec une plage
de tailles plutôt qu'une taille fixe, puisque le serveur y écrit éventuellement
un spa_meta_bitmap suivi des pixels du curseur.

Redéclare les métadonnées à chaque changement de format, sans garde « une seule
fois » : un format peut être renégocié quand la résolution du moniteur change
(SPEC.md §4.5), et les métadonnées doivent alors être redemandées.

Nomme aussi les types de métadonnées réellement livrés et le résultat de
update_params, pour qu'un « 0 sur 30 » ne soit plus ambigu entre « pas demandé »
et « pas honoré ».

NON EXÉCUTÉ : l'environnement de développement ne fournit plus de portail
fonctionnel. Compile et passe clippy en -D warnings, mais n'a jamais tourné.

2. RETOURS DE REVUE AUTOMATIQUE SUR LA PR

- Le chemin d'état retombait sur un chemin relatif quand ni XDG_STATE_HOME ni
  HOME n'étaient définis, écrivant silencieusement dans le répertoire courant.
  Échoue maintenant explicitement.
- Le jeton de restauration était écrit avec les permissions de l'umask, mesurées
  à 0664. Un jeton de restauration rouvre une session de partage d'écran sans
  redemander le consentement, donc 0600 forcé à la création et sur un fichier
  préexistant. SPEC.md §4.2 exige de toute façon que l'application, elle, passe
  par le Secret Service : la sonde documente pourquoi elle ne le fait pas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le workspace annonçait rust-version = 1.80. Vérification faite, le workspace ne
compile pas en 1.80, ni en 1.82, ni en 1.85. Le plancher réel est 1.86, et il
n'est pas fixé par le code de TutoClic mais par son arbre de dépendances :

    ashpd -> zbus -> url -> idna -> idna_adapter -> icu_* (exige rustc 1.86)

proptest tire par ailleurs getrandom, qui exige aussi plus que 1.80. Le code de
tutoclic-coords compile, lui, dès 1.80 : c'est sa bibliothèque seule qui tient,
pas ses tests.

Vérifié : cargo +1.86 build --workspace --locked puis cargo +1.86 test passent,
14 tests verts.

Ajoute un job de CI qui construit ET teste à la version exacte annoncée. Un job
sur `stable` ne vérifie rien : il laisserait passer l'usage d'une API stabilisée
après le MSRV déclaré et rendrait la ligne fausse sans que personne ne le voie.
C'est exactement ce qui s'est produit ici.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…stion 2

Exécution sur pc-fixe, vraie session Wayland, moniteur 2560x1440 à la position
logique (1920, 0).

QUESTION 1 DE L'ANNEXE B : TRANCHÉE. Tampons MemFd, lisibles par le CPU, sur une
vraie session Wayland. GStreamer reste hors des dépendances, le repli de §3.1
point 2 n'a pas à être activé, et la liste de §13.1 tient telle quelle. C'était
le point qui changeait la liste des dépendances.

QUESTION 3 DE L'ANNEXE B : ROUVERTE. Le « 0 sur 30 » n'était pas un refus de la
plateforme, la sonde ne demandait pas la métadonnée. La mesure est invalide et
reste à refaire avec la sonde corrigée.

Consigne aussi ce qui n'a PAS été testé et que la sortie pouvait laisser croire
testé : le consentement, puisque les deux lancements ont réutilisé un jeton de
restauration et qu'aucun dialogue n'est apparu, donc §15.2 point 5 n'est pas
vérifié.

L'exécution 1 est déclassée : le type de session rapporté a changé au cours de la
même séance, l'environnement n'était pas fiable, aucune conclusion n'en est tirée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…taille fixe

Deux corrections, toutes deux issues de la mesure sur pc-fixe qui renvoyait
["Busy", "Header"] sans Cursor, sans erreur et sans fermeture de session.

1. LA VÉRIFICATION QUI MANQUAIT DEPUIS LE DÉBUT

Le portail expose AvailableCursorModes, un masque des modes qu'il implémente
réellement. La sonde ne l'interrogeait pas et affichait « CursorMode::Metadata
accepté par le portail : OUI » sur la seule base d'un SelectSources sans erreur.
C'était faux : ça ne prouvait que « pas rejeté ».

La sonde affiche désormais les modes annoncés, et les types de source, avant même
de créer la session. Le verdict distingue trois cas au lieu d'un :

- mode non annoncé : cause identifiée, ce bureau ne l'implémente pas, et les
  replis sont listés dans la sortie ;
- annoncé mais non livré : anomalie du compositeur, à remonter en amont ;
- absent alors que la sonde n'a pas su le demander : indéterminé.

À noter : la spécification du portail dit que demander un mode non annoncé ferme
la session. Or la session de pc-fixe n'a pas été fermée et a livré 30 frames, ce
qui laisse penser que Metadata EST annoncé. Cette exécution le dira.

2. LE DIFFÉRENTIEL QUI ACCUSE LA SONDE

Dans la même liste de paramètres, Header demandé avec une taille fixe a été
honoré, tandis que Cursor demandé avec une Choice::Range a été ignoré en silence.
La seule variable qui différait était Int contre Choice.

Cursor est donc demandé avec une taille fixe, dimensionnée pour un curseur de
256 par 256, ce qui couvre l'échelle 200 %. Un tampon plus grand que nécessaire
ne gêne pas le serveur.

NON EXÉCUTÉ : l'environnement de développement ne fournit plus de portail
fonctionnel. Compile, passe clippy en -D warnings, jamais tourné.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CONSENTEMENT, observé directement par l'utilisateur sur la machine de référence en
session Wayland : un dialogue de partage d'écran apparaît aux lancements sans
jeton enregistré, et pas au lancement avec jeton. Le mécanisme restore_token plus
PersistMode::ExplicitlyRevoked de §4.2 fonctionne comme spécifié.

Conséquences sur §15.2 :
- point 5, rien n'est capturé avant consentement : franchi au niveau du portail ;
- point 4, captures sans nouvelle autorisation : franchi au niveau du portail. Le
  décompte littéral des dix étapes relève de la Phase 1b, la sonde observant des
  frames et n'écrivant pas d'étapes ;
- points 1 à 3 : franchis, source sélectionnée, flux reçu, 30 frames décodées.

ANNEXE B POINT 1 : TRANCHÉ. Tampons MemFd sur Wayland en 2560x1440, trois
exécutions identiques. GStreamer n'entre pas dans les dépendances, le repli de
§3.1 point 2 n'est pas activé, la liste de §13.1 tient telle quelle. Marqué comme
résolu dans l'annexe plutôt que laissé en question ouverte.

ANNEXE B POINT 3 : toujours ouvert, mais le champ des causes s'est resserré à
deux, et la sonde de l'exécution 4 les distingue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Metadata EST annoncé par le portail sur la machine de référence, et la taille
fixe n'a rien changé. Trois causes sont donc écartées : métadonnée non demandée,
mode non supporté, paramètre mal formé.

Reste une cause instrumentale, et c'est un défaut de la sonde.

Le moniteur capturé est celui à la position logique (1920, 0), soit l'écran
secondaire, alors que le terminal de lancement est sur l'écran principal. Mutter
n'a de position de curseur à rapporter que si le curseur se trouve SUR la zone
capturée. La consigne « bouge la souris » était par ailleurs inapplicable : la
fenêtre d'observation valait 30 frames, soit environ une seconde.

Corrigé :

- fenêtre d'observation en TEMPS, 15 secondes, au lieu d'un compte de frames ;
- affichage de la géométrie exacte de la zone capturée avant l'observation, avec
  la consigne d'y amener le pointeur, et la marche à suivre pour capturer un
  autre écran ;
- sortie anticipée dès que 5 frames portent la métadonnée, pour qu'une réponse
  positive soit immédiate ;
- le verdict négatif demande maintenant explicitement si le pointeur était bien
  sur la zone, au lieu d'accuser la plateforme.

NON EXÉCUTÉ : l'environnement de développement ne fournit plus de portail.
Compile, passe clippy en -D warnings, jamais tourné.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…étadonnées

Cause instrumentale précédente éliminée : 1923 frames observées sur l'écran où se
trouve le terminal, donc pointeur bien sur la zone capturée, et toujours zéro
métadonnée de curseur.

Mais une inférence gratuite traînait depuis trois exécutions. Je concluais que la
demande de métadonnées fonctionnait parce que Header figurait dans les
métadonnées reçues. Or Busy y figurait aussi sans avoir jamais été demandé : le
serveur attache donc des métadonnées de son propre chef, et Header pouvait très
bien être dans ce lot. update_params renvoyant Ok ne prouve que « les pods ont
été analysés », pas « le serveur a honoré la demande ».

Header n'est donc plus demandé, et VideoCrop l'est à sa place, comme témoin.
Lecture du résultat :

- VideoCrop présent : le mécanisme fonctionne, et Cursor est refusé
  spécifiquement, ce qui pointe une anomalie en amont ;
- VideoCrop absent et Header toujours présent : update_params n'a aucun effet
  observable, et tout verdict sur Cursor rendu jusqu'ici est sans valeur.

La sonde affiche ce diagnostic AVANT le verdict sur le curseur, parce que le
second n'a aucun sens si le premier dit « inopérant ».

NON EXÉCUTÉ : l'environnement de développement ne fournit plus de portail.
Compile, passe clippy en -D warnings, jamais tourné.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mutter n'attache jamais SPA_META_Cursor, alors que le portail annonce le mode
Metadata. Établi, et non plus supposé.

Ce qui rend cette exécution concluante là où les cinq précédentes ne l'étaient
pas : le témoin de contrôle. Header, cessant d'être demandé, a disparu des
métadonnées reçues. VideoCrop, demandé à sa place, est apparu. Le mécanisme
SPA_PARAM_Meta est donc démontré opérant par un témoin, dans la même liste de
paramètres, par le même code, au même instant que la demande de Cursor. La seule
variable qui diffère entre le paramètre honoré et le paramètre ignoré est le type
de métadonnée.

Cinq explications instrumentales éliminées une par une : métadonnée non demandée,
mode non annoncé, taille malformée, pointeur hors de la zone capturée, mécanisme
inopérant. 1326 frames observées sur l'écran du terminal.

Ce que §0.3 perd, et ce qu'il garde : la thèse avait deux moitiés et une seule
tombe. Le déclencheur par différence de frames, qui décide QUAND capturer, ne
dépend pas du curseur et repose sur des tampons MemFd confirmés disponibles.
C'était la moitié difficile. Seul le repère de clic, qui décide OÙ placer le
badge, n'est plus calculable. Le mode automatique reste constructible ; c'est la
précision du repère qui se dégrade.

Quatre contournements consignés, du changement d'une constante au helper
privilégié, à trancher avec l'utilisateur avant de modifier la spécification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nt indisponible

Applique la décision prise après la mesure : boîte de changement pour placer le
badge, curseur caché par défaut, Embedded en option de session.

Huit passages de SPEC.md mis en accord avec ce qui a été mesuré :

- §0.3, la thèse : elle avait deux moitiés et une seule tombe. Le déclencheur par
  différence de frames, qui décide QUAND capturer, survit intact et repose sur des
  tampons MemFd confirmés. Seul le repère de clic, qui décide OÙ, n'est plus
  calculable. La section le dit maintenant au lieu de promettre la position exacte.
- §4.1, option de curseur : hidden par défaut, embedded en option de session,
  metadata tenté puis abandonné automatiquement. hidden est le défaut parce qu'un
  curseur composité est cuit dans les pixels de l'original, ce qui contredirait le
  modèle non destructif de §7.2.
- §4.2 : demander Metadata d'abord, vérifier sur la première frame, basculer en
  silence. Consigne explicite de ne jamais se fier à AvailableCursorModes seul.
- §4.3.3 point 6 : le repère est le coin haut-gauche de la boîte de changement, un
  menu ou un dialogue s'ouvrant vers le bas à droite du point cliqué. Au-delà de
  40 % de l'image modifiée, aucun repère n'est placé et l'étape demande un
  placement manuel, plutôt que de recevoir un badge faux.
- §4.6, §7.1, §15.2 point 3, annexe B point 3 : alignés, avec les critères
  historiques conservés pour mémoire plutôt que supprimés.

La sonde continue de tester Metadata à chaque exécution : si une version future de
GNOME corrige la lacune, la position exacte remplacera l'heuristique sans autre
changement, et la sonde le signalera.

Ajoute docs/upstream-mutter-cursor-meta.md, brouillon de signalement en amont, en
anglais, non publié. Il contient le cas de reproduction minimal, le témoin de
contrôle, et le tableau des cinq hypothèses éliminées avant de conclure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@NicoBOD
NicoBOD merged commit c795551 into main Aug 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants