# Premier login V2 : chemins de feuilles après Guard Suite du diagnostic : [focus et récupération du 22 octobre](session-v2-focus-recovery-fix-v1.md). Cette page conserve la portée et les résultats de la correction précédente. ## Résultat et portée Correction minimale dans la seule copie `/tmp/labfy-session-v2-path-stability-LrpgZuut`, basée sur `8a4d829ce78b67fe1d4ff910c89d3278d81b6e85`, sans commit ni déploiement. La contradiction entre comparaison post-Guard et admission est corrigée. **Le login graphique physique reste non validé.** Les résultats ci-dessous concernent des applications GTK synthétiques, deux UUID Firefox synthétiques stables et des compositeurs enfants TEST_ONLY. Ils ne valident pas les profils Firefox réels, leurs codecs ni le premier login SwayFX sur le bureau. Le dernier login réel a échoué **après l’intégration des protections P0**. Les validations antérieures ne doivent pas masquer cet échec. Les journaux réels sont préservés : ils n’ont été ni recherchés, ni lus, ni modifiés pendant cette mission. Les faits réels suivants sont uniquement ceux fournis au ticket. ## Faits reçus et hypothèse Vers 16:51, refus `TARGET_OBSERVATION_CHANGED`, phase `cleanup-error`, `operations=0`, `anchors_created=0`, un helper suspendu puis repris. Recovery avait admis Kitty7 et Firefox8 ; aucun déplacement ni swap n’a eu lieu. L’échec à l’admission de Firefox10 est vraisemblable, pas établi par une trace complète de chaque admission. Le checkpoint comporte quatre slots : en 1, `splitv(Firefox1, splith(Firefox2, Kitty))`, et Limusic en 10. Les deux Firefox ont des UUID exacts distincts. La nouvelle session contient Firefox8 et Firefox10 de même PID, Kitty7 et Limusic5. Des parents internes unaires peuvent faire passer les chemins, par exemple, de `[0]` à `[0,0]` ou de `[1,0]` à `[1,0,0]`, sans changer les feuilles, leur ordre ou leur placement. **Hypothèse forte, non preuve historique :** Sway/autotiling a ainsi décalé les chemins entre phase 0 et admission. Les chemins exacts de la preuve PREWARM ne sont pas archivés. La reproduction contrôlée prouve le défaut du code et sa correction dans ce cas ; elle ne reconstruit pas rétroactivement le login. ## Contradiction et correction `Executor._check_prepared(before, after)` autorise déjà les variations de chemin si le lot exact, con_id/PID/starttime/app_id, workspace/output, flottement, géométrie pertinente et ordre relatif des feuilles restent stables. Après Guard, `_apply` faisait cette comparaison puis passait à `Recovery.admit(node, prepared[id])` l’ancien chemin de phase 0. `admit` exige une égalité complète, chemin inclus : il refusait donc une variation que le contrôle précédent venait d’accepter. Le patch conserve l’observation courante post-Guard, la compare au lot de phase 0, puis l’utilise comme lot canonique pour toutes les admissions initiales. Chaque admission relit encore strictement son observation épinglée. Le lot entier est admis avant le premier déplacement. Un changement après cette capture n’est pas accepté par simple réobservation. Aucun changement à Recovery, au journal, à la preuve de focus, aux codecs Firefox ou au catalogue n’est requis. La preuve initiale de focus de phase 0 reste contrôlée strictement à la création de Recovery. La variante « après phase 0 » injecte après cette validation, dans la construction d’Anchors avant Guard ; elle ne renouvelle pas artificiellement la preuve initiale. La variante « pendant Guard » injecte uniquement après la construction réelle de Guard et avant son retour à Executor. Les injections vérifient l’identité du focus actif, sans prétendre que la topologie complète de `focus_proof` est inchangée ni en attribuer la cause. Un changement avant la validation initiale de cette preuve peut donc toujours être refusé par `RESTORE_FOCUS_UNSTABLE`. Après les déplacements possédés, `Recovery.command` actualise les états `expected`, chemins compris, de toutes les fenêtres admises. Les pins post-provider sont pris après ces commandes : les nouveaux chemins possédés sont correctement acceptés. Le provider n’est pas une mutation possédée de placement : une insertion unaire pendant son attente reste refusée par l’égalité stricte de `admit`. Aucune relaxation supplémentaire n’a été ajoutée. ## Régressions et résultats Le test graphique a été écrit et exécuté avant la correction. Sur la baseline, l’injection pendant Guard produit `TARGET_OBSERVATION_CHANGED`, zéro commande, zéro ancre et zéro swap. La reproduction des quatre variantes positives a ensuite été rejouée contre l’Executor de HEAD dans un backend temporaire, avec le même test final : les quatre refusent avant correction, sans commande. Ces échecs attendus sont conservés sous `build/path-stability/avant-final/` et dans `avant-final.log` ; le premier échec est aussi sous `avant/`. Après correction, les variantes suivantes réussissent sur Sway 1.12/Pixman : - Parents unaires horizontaux/verticaux autour des trois fenêtres en 1, uniquement pendant Guard, avec focus actif inchangé. - Insertion après la preuve de phase 0, avant Guard. - Insertions réparties entre ces deux frontières. - Même scénario avec un vrai autotiling enfant, suspendu puis repris une fois. Chaque succès exige quatre swaps, quatre slots, statut `success`, journal `complete`, deux vérifications finales vraies, arbre exact en 1 et 10 et un seul Limusic. Les deux Firefox sont des surfaces du même processus et leurs UUID exacts sont résolus par un provider synthétique stable. L’autotiling testé est un enfant privé ; seuls son PID et son socket sont fournis à la découverte du test. Les splits déterministes sont injectés par le contrôleur de test. Ce scénario ne prouve pas qu’autotiling a causé le refus réel. Les deux nouvelles méthodes backend comportent 17 sous-cas de refus à la frontière Guard ou après la capture canonique : ordre, PID, starttime, remplacement Firefox8 ou Firefox10 par 21 (mêmes app_id/PID et cardinalité), Firefox supplémentaire, workspace, output, flottement, focus tiers, Limusic dupliqué, chemin/identité/membres/focus modifiés après capture et entrée pending. Elles exigent zéro commande et aucun build d’ancres. Une cinquième expérience graphique modifie les chemins pendant le provider et exige le refus `TARGET_OBSERVATION_CHANGED`, zéro swap et aucune commande après cette attente. Les anciens tests de crash mid-IPC, intentions durables et dernière capture provider restent inchangés et passent. Campagne `build/path-stability/campagne/results.json` : **23 groupes réussis**. | Suite | Résultat | | --- | --- | | Backend, anciens P0 et nouvelles frontières | 43 tests OK | | Focus Python | 28 tests OK | | QML Python | 13 tests OK | | QtTest | 5 contrôles OK, aucun ignoré | | Runtime | 35 scénarios OK | | Erreurs contrôlées | 3 scénarios OK | | Crash/SIGKILL | 10 scénarios OK | | Sécurité | 22 contrôles OK | | Runtime Limusic | Succès, absence et duplication contrôlés | | UI QuickShell headless | 7 scénarios OK | | Anciens login Focus | 11 scénarios OK | | P0 headless | 5 contre-exemples refusés comme attendu | | Chemins post-Guard/provider | 4 succès exacts et 1 refus attendu | Les helpers C sont compilés en C17 avec `-Wall -Wextra -Werror` par les fixtures. Les contrôles UTF-8 des pages Session V2 et `git diff --check` sont consignés séparément dans `controles-finaux.log`. L’essai privé sur le SwayFX installé (`/usr/bin/sway`, GLES2) échoue avant création du serveur : `drmGetDevices2 failed`, `no DRM FD available`, `Failed to create renderer`. Le log est `swayfx/resultat.log`. C’est une **validation fonctionnelle indisponible dans le sandbox**, pas un PASS SwayFX. Le binaire Sway 1.12/Pixman préexistant sous `/tmp` a permis la campagne fonctionnelle sans installation ni remplacement système. ## Reproduction sur ArchASP Depuis cette copie, avec le renderer de l’hôte accessible : ```sh python3 -B tests/session_v2/validate_p0.py \ --sway /usr/bin/sway --renderer gles2 \ --evidence build/path-stability/hote-swayfx ``` Cette commande inclut les nouveaux tests de chemins et toutes les suites précédentes. Le harnais définit un environnement positif HOME/XDG privé ; chaque Lab démarre son compositeur headless enfant, son Wayland et son socket Sway explicite TEST_ONLY. Il n’utilise pas le socket du bureau. Aucun reload, logout, redémarrage, changement d’autostart ou de greetd/mosaic n’est requis. Un refus de renderer garde un code non nul et doit être signalé séparément. La campagne équivalente réalisable dans ce sandbox est : ```sh python3 -B tests/session_v2/validate_p0.py \ --sway /tmp/limusic-test-sway-1.12/usr/bin/sway --renderer pixman \ --evidence build/path-stability/reproduction-pixman ``` ## Livraison et conditions restantes `build/path-stability/session-v2-path-stability.patch` contient le patch exact, y compris les nouveaux tests et cette documentation, sans staging ni commit. `manifest.json` contient le SHA de base Git, les SHA-256 des fichiers livrés, du patch et des preuves, les commandes et les résultats. Les artefacts de construction ne font pas partie du patch. Aucun journal utilisateur réel n’est inclus. La correction est bornée à l’incohérence de snapshot démontrée. Une différence d’identité, membres, placement, ordre, focus externe ou une intention ambiguë reste bloquante. Le contrôle GET_TREE puis IPC n’est pas atomique : une mutation postérieure à la dernière observation ne peut être exclue par cette interface. Les chemins inconnus à la prochaine vérification sont refusés ; un crash après intention durable conserve le besoin de recovery. Ne pas transformer ces refus conservateurs en succès par app_id ou cardinalité. Restent à valider séparément : cette nouvelle version sur le renderer SwayFX hôte, puis un véritable login graphique physique et les applications réelles. Aucune action sur la session réelle n’a été effectuée ou autorisée ici.