Phase 0 : spécifications, module de coordonnées, sonde ScreenCast - #1
Merged
Merged
Conversation
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>
There was a problem hiding this comment.
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 duREADME.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 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 on lines
+20
to
+22
| - uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| components: rustfmt, clippy |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 laversion initiale ont été invalidés par vérification, sources en annexe A :
« mouse review » d'Orca est cassé ;
GlobalShortcutsn'est pas implémenté sur GNOME.Le moteur de capture repose donc sur
cursor_mode = metadatadu portailScreenCast, 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), enTDD. 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 etn'é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 :
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::Metadataaccepté, 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 decurseur 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.mddit exactement ce que la mesure établit et ce qu'ellen'é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: proprecargo fmt --all -- --check: proprePas encore fait
CI de sécurité de §12.3 :
cargo audit,cargo deny, SBOM CycloneDX, détectionde 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