Vers la 2.1 : suiveur audio, grille de presets, code d'accès, replis — et la suite de tests qui mesure enfin ce qu'elle promet - #1
Draft
pymenvert wants to merge 27 commits into
Draft
Conversation
…ait verte `findBrowser()` ne regardait que les chemins système. Les conteneurs installent Chromium sous `PLAYWRIGHT_BROWSERS_PATH`, jamais dans `/usr/bin` : la suite d'interface s'y annonçait « ignorée », la sortie restait verte, et personne ne voyait que 32 tests sur 262 ne tournaient pas. C'est le même mode de panne que la palette spatiale morte en silence le 28/07. Deux voies ajoutées avant les chemins système : `CASCADE_NAVIGATEUR` pour imposer un binaire à la main, puis un balayage de l'installation Playwright (le sous-dossier porte la révision, donc on cherche plutôt que de deviner ; le Chromium complet est préféré au `headless_shell`). Chromium refuse ensuite de démarrer en root sans `--no-sandbox`, et les conteneurs tournent en root. Le drapeau n'est ajouté que dans ce cas : sur une machine de développement le bac à sable reste en place. En CI, l'absence de navigateur n'était qu'un `echo`. Elle échoue maintenant : en local le repli gracieux se défend, en CI c'est un angle mort. Les 32 tests passent — aucune régression ne se cachait derrière le skip. 262 tests, zéro échec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
`ETAT-DU-PROJET.md` est le fichier que le projet impose de lire en premier. Il annonçait la version 1.6.0 en tête et 1.2.0 six lignes plus bas, « `main` est la 1.6.0 ; le front de développement est `v2` », 111 tests, un server.js de 1400 lignes, et un schéma d'état sans rien de la v2 — ni `p3`, ni `field`, ni palette, ni crossfader, ni vues. Il se termine pourtant par : « le laisser mentir coûte une demi-journée à celui qui reprend ». Le plus coûteux était ailleurs. `CLAUDE.md` présentait deux défauts comme ouverts et « déjà en v1 » : la couche couleur qui allume les barres qu'elle ne pilote pas, et la phase non préservée au changement de tempo. **Les deux sont corrigés depuis fin juillet** — vérifié dans le code, pas dans les docs : `mixLevel()` décide barre par barre, `phaseContinue()` intègre `dt / period`. Tous deux listés « Corrigé » au CHANGELOG et couverts par des tests. Ils venaient d'une relecture arrêtée avant sa phase de vérification ; la consigne envoyait donc rechasser du gibier mort. Reste une piste, elle, bien ouverte et non couverte, notée à la place : `resolveBars()` replie sur TOUTES les barres actives quand le groupe d'une couche est vide — seule voie vivante qui reproduit le symptôme d'origine. Le reste, par fichier : - comptes remis d'aplomb partout : 262 tests (pas 248 ni 111), 27 mutants (pas 21), tailles réelles des sources, badges du README ; - le pivot de `output/rot` est mesuré depuis le 29/07, mais quatre documents le disaient encore « jamais fait » : propagé, et il ne reste que DEUX mesures MadMapper, pas trois ; - `V2-TESTS-MADMAPPER.md` : le haut du fichier contredisait le bas. T11 à T16, T18 et T21 étaient mesurés dans les sections de résultat mais leurs cases restaient vides — cochées et reliées à leur résultat. On a failli refaire T14 ; - `PLAN-V2.md` : une « Étape 4 — à faire » traînait juste au-dessus d'une « Étape 4 ✅ » ; la boîte du champ y était encore décrite en coques de Tchebychev alors que c'est un pavé mobile ; trois items « non faits » sont livrés depuis la 2.0. Le fichier est marqué historique ; - `V1-V2.md` organisait la cohabitation de `main` et d'une branche `v2` qui n'existe plus, et disait de committer dessus. Remplacé par une pierre tombale ; - trois pièges ajoutés : une suite verte ne prouve pas que tout a tourné, les tests d'interface ne sont pas Playwright, et les fichiers hors dépôt sont absents des clones frais — donc des sessions distantes et de la CI. Les tests d'interface étaient d'ailleurs listés « à faire » alors qu'ils existent depuis la 2.0. Troisième piste faite qui traînait dans cette liste, après la synchro de phase Link et le `dist/` généré : la purger fait partie du travail de livraison. 262 tests, zéro échec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Six commits attendaient depuis la 2.0.1, dont un correctif bloquant : le crossfader posé en butée — la position de repos de tout fader — rallumait le plateau entier. Ils sortent. Version passée à 2.0.2 dans package.json, server.js et dist/ (via `node sync-dist.js`, qui aligne dist/package.json tout seul). Le manuel PDF n'est PAS régénéré, et c'est volontaire : `build-manuel.py` porte une version majeure.mineure, comme le pied de page de l'interface, et cette version n'apporte ni réglage ni fonction. Vérifié en interrogeant un vrai serveur : `/api/ping` répond 2.0.2, le pied de page affiche « CASCADE v2.0 ». La section `[Non publié]` du CHANGELOG est datée, et complétée par ce qui n'y figurait pas encore : - le lot « tests » — 32 tests d'interface qui ne tournaient pas, un test qui ne mesurait rien, un test de redémarrage qui ne redémarrait rien, et le filet d'`afterEach` du champ 3D (garantie structurelle, pas correction : la cascade d'échecs a été cherchée sans être reproduite, c'est dit tel quel) ; - le lot « documentation » — les fichiers de reprise annonçaient la 1.6.0, et deux défauts présentés comme ouverts étaient corrigés depuis la 2.0.1 ; - les deux mesures MadMapper consignées depuis : le pivot de `output/rot`, et les motifs OSC que MadMapper ne développe pas. `tools/notes-version.js 2.0.2` extrait bien la section : la description de la release GitHub viendra donc du CHANGELOG, pas d'un texte recopié à côté. Au passage, le tableau de téléchargement du README annonçait encore les exécutables **1.6.0** — deux versions majeures en retard, sur la seule page que lit quelqu'un qui vient télécharger. Il est rendu insensible à la version (`Cascade-<version>-…`) plutôt que corrigé une fois de plus. ⚠ Le tag `v2.0.2` n'est PAS posé : il déclenche la construction des quatre exécutables et la publication de la release, et il attend que cette branche soit fusionnée et que Pym le décide. 262 tests, zéro échec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Une rangée de boutons qui se replie selon la largeur de la fenêtre : en régie, dans le noir, on vise une case dont on croit connaître la place et elle a bougé. C'est une grille — 8 colonnes sur poste, 4 sur tablette — et chaque pavé porte trois choses qu'il fallait deviner jusqu'ici. **L'empreinte** : une case par barre que le preset pilote, teintée comme la couche qui la pilote. C'est une COUVERTURE, pas une image du motif — un chase n'allume qu'une barre à la fois, une photo serait presque vide et changerait à chaque image. Elle passe par `resolveBars`, donc elle ment exactement comme mentirait le moteur : un groupe vidé la ramène sur toutes les barres, ici comme en scène. Une vignette qui corrigerait le moteur serait pire qu'inutile. **Le pavé qui joue**, et celui d'où l'on vient pendant un fondu. `presetActif` est posé dans `recallPreset` et pas dans la route : le rappel arrive aussi par OSC et par MIDI, et le poser au seul endroit que tous traversent évite d'en oublier un. **« Ce preset ne pilote plus aucune barre »**, en bord pointillé. C'est l'information la plus utile avant de tirer un pavé ; la masquer laisserait croire à une panne au moment du rappel. Le coût du poll était le vrai sujet. `/api/state` part HUIT FOIS PAR SECONDE : y loger 16 empreintes aurait coûté une demi-kilo-octet à chaque tour pour une donnée qui ne change qu'à l'enregistrement. Elles sont donc servies par une route dédiée, tirée seulement quand un compteur de révision bouge, et `/api/state` ne gagne que trois entiers. Un rappel n'incrémente PAS ce compteur : la banque n'a pas changé, et recharger les empreintes à ce moment-là ferait une requête de plus au pire moment. Un test vérifie qu'aucune empreinte n'est réapparue dans `/api/state`. Le calcul n'instancie aucun moteur, ne touche à aucun état d'exécution et n'émet aucun OSC : appelable en plein spectacle. Si la route échoue, les pavés s'affichent sans bande et sans un bruit — c'est testé. Corrigé au passage, trouvé en lisant ce chemin : `sanitizePresets` coupait les noms à 12 là où l'enregistrement et le renommage coupent à 16. Le rabotage n'arrivait ni à la saisie ni au rappel, mais au RECHARGEMENT du fichier, quand plus personne ne regarde — « Refrain final 2 » revenait « Refrain fin ». Une piste de la conception n'a PAS été suivie : elle signalait que la signature de rendu confondait ['AB', null] et ['A', 'B']. Vérifié dans le source — le `join` utilise un séparateur \x01 invisible, il ne confond rien. Le code existant est resté tel quel. Neuf tests ajoutés (271 au total), dont cinq dans un vrai navigateur : la grille et son empreinte, le pavé qui joue et le pavé sortant, le preset muet, un nom hostile qui n'injecte rien, et la dégradation quand la route échoue. Deux anti-mutants gardent un chemin qui n'en avait aucun ; les deux ont été vérifiés tués, pas seulement ajoutés. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Deux lignes, mais c'est précisément le fichier qui vient d'être remis d'aplomb pour avoir menti sur sa version. Le laisser diverger dès le commit suivant aurait été une leçon mal retenue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Le micro se branche là où l'oscillateur l'était déjà. Un modulateur — de couche ou global — gagne un réglage Source : « Oscillateur (boucle) » ou « Micro ». Rien d'autre ne change, donc le micro pilote tout ce qu'un modulateur pilotait : niveau, largeur, netteté, profondeur, crossfader, master. C'est le choix qui rend le reste possible. Router l'enveloppe par `midiApply` aurait ÉCRIT le niveau dans `state.layers[].level` seize fois par seconde : il aurait été sauvegardé, exporté, figé dans les presets — mot pour mot le défaut que le commentaire de `appliquerLFO` dit avoir évité. Ici la valeur vit le temps d'une image et hérite gratuitement du non-écrasement de l'état, du voyage dans les presets, et du relâchement à STOP (`tick` sort avant les modulateurs). DEUX LIMITES, dites dans l'aide, dans le panneau et au CHANGELOG : - il lit une ÉNERGIE, pas un tempo. Décision de Pym : on livre l'énergie, il l'essaie en salle, et on décide ensuite de la détection de battement — ~150 lignes de DSP dont la justesse dépend du répertoire, avec l'erreur d'octave comme échec structurel, invalidable hors d'une vraie salle ; - le micro n'est lisible que sur la machine hôte : les navigateurs l'interdisent hors origine sûre. Assumé et écrit, comme pour Web MIDI. Pas de HTTPS auto-signé (certificat à générer, openssl absent de Windows, écran d'avertissement à chaque connexion — impossible à demander à un régisseur). Sans micro la page ne pousse rien : un iPad ne peut pas écraser l'hôte. LA PROPRIÉTÉ DE SÉCURITÉ, et elle a son anti-mutant : la péremption REND LA MAIN. Sans niveau frais depuis 700 ms — onglet fermé, micro débranché, machine en veille — le réglage du régisseur reprend, avec un fondu. Retomber sur `min` aurait été le noir en plein show sur un master à min 0 ; rester figé aurait cloué le spectacle sur ce qu'entendait un onglet fermé. Aucun conflit avec Ableton Link, et c'est mieux que de bien l'arbitrer : le suiveur n'écrit jamais `stepMs`, donc `applyLinkBpm` n'a rien à écraser. La calibration vit dans `state.settings` et NON dans le localStorage — écart assumé avec le plan de conception : la promesse du projet est que la régie tienne sur une clé USB, et un réglage laissé dans le navigateur resterait sur la machine. Vérifiable sans micro, ce qui a décidé du choix : la moitié serveur se teste en poussant un niveau en query, et la DSP est isolée en fonction PURE nourrie d'un spectre fabriqué à la main. Le chemin nominal s'exécute quand même pour de vrai — Chromium est lancé avec un faux périphérique audio, donc getUserMedia, AudioContext et la boucle d'analyse tournent réellement en test. Deux pièges rencontrés en écrivant les tests, tous deux du genre que ce dépôt a déjà payé : le cache d'envoi ne réémet pas une valeur inchangée, donc quatre assertions passaient sur un tableau VIDE sans rien mesurer — elles lisent maintenant les niveaux calculés, avec une garde qui refuse un relevé vide ; et le test STOP pouvait passer même si le moteur n'émettait jamais rien, il a désormais un témoin positif. 19 tests ajoutés (290 au total), dont 6 dans un vrai navigateur. Le nouvel anti-mutant a été vérifié tué, pas seulement ajouté. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Une trentaine de réglages à plat, dont beaucoup ne se touchent qu'au montage. Deux replis les rangent — « Symétries » (les deux miroirs et leurs axes) et « Mélange & espace » (fusion, profondeur, décalage réparti, jeu du crossfader). Rien d'inventé : c'est le motif de « Groove & découpe », déjà en place depuis la 1.3. La CSS des replis était d'ailleurs écrite au PLURIEL et non préfixée, donc prévue pour être réutilisée : les deux nouveaux `details` héritent du style sans une ligne de CSS de plus. Chacun porte une pastille qui compte ET nomme ce qui est hors de sa valeur neutre. Sans elle, un miroir ou une fusion oubliés derrière un repli fermé deviennent un mystère — c'est exactement ce que la pastille de Groove évite déjà. Le repli s'ouvre tout seul en ARRIVANT sur une couche qui a des réglages actifs, et pas à chaque rendu. C'est le piège documenté de Groove : rouvrir en continu empêche l'utilisateur de refermer quoi que ce soit. Un test le verrouille — il simule une vraie arrivée en effaçant le marqueur de couche, puis referme et vérifie que ça reste fermé sur cinq cycles de rendu. Écrire ce test a d'ailleurs confirmé le mécanisme : ma première version le croyait cassé, alors qu'elle réutilisait une couche déjà visitée par le test précédent. Le comportement était juste, c'est le test qui mesurait mal. 292 tests, dont 45 dans un vrai navigateur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Cascade s'ouvre depuis un iPad, donc depuis le Wi-Fi de la salle. Un code facultatif empêche un curieux de prendre la main sur la lumière en plein show. QUATRE CHIFFRES, C'EST 10 000 COMBINAISONS. Sans limitation de tentatives, ça se casse en quelques secondes et le code ne protège RIEN : la limitation n'est donc pas un raffinement, c'est la fonction elle-même. Cinq essais ratés bloquent l'appareil une minute, et pendant ce blocage même le bon code est refusé. Ma première version avait exactement ce défaut, et la mesure l'a montrée : « encore 4 essais » indéfiniment. `jusqua <= maintenant` est vrai aussi pour `jusqua = 0`, c'est-à-dire quand il n'y a jamais eu de blocage, donc le compteur repartait de zéro à chaque tentative. Corrigé, et un anti-mutant reproduit la forme fautive à l'identique — vérifié tué. Trois choix de périmètre, assumés et écrits dans l'aide : - le code n'est JAMAIS demandé sur la machine hôte. C'est ce qui garantit qu'on ne peut pas s'enfermer dehors, et c'est le bon périmètre : la menace est le Wi-Fi de la salle, pas la machine qu'on a physiquement sous la main ; - la PAGE reste servie sans le code — sinon il serait impossible d'afficher la demande. C'est l'API qui est fermée, pas l'interface ; - l'OSC et le MIDI ne sont pas concernés : câble ou console sur le réseau de production, autre domaine de confiance, et les fermer casserait les installations existantes. Le secret ne fuit pas. On stocke un haché salé, et ce haché ne sort JAMAIS du serveur — trouvé en le testant : il partait dans `/api/state` avec le reste des réglages, et un haché de 4 chiffres se casse hors ligne instantanément, donc l'envoyer revenait à envoyer le code. Retiré aussi de `/api/settings`. L'export de projet, lui, ne portait déjà pas `settings`. Le jeton de session voyage en cookie HttpOnly + SameSite=Strict : la page ne le lit jamais, donc un nom de fixture piégé ne peut pas le faire fuir. Détail qui compte en régie : une page qui affiche la demande de code compte quand même comme une interface ouverte. Sans ça, l'arrêt automatique pouvait couper Cascade pendant que le régisseur tape son code. Le pire qu'un curieux puisse faire est donc de garder le serveur allumé, sans rien piloter. Le portillon ne se voit que depuis une adresse NON locale, donc ses tests cherchent une vraie interface réseau — et, comme pour le navigateur, la CI exige désormais qu'il y en ait une : un « ignoré » ne doit jamais passer pour un « réussi ». C'est la troisième fois que ce mode de panne se présente dans ce dépôt. Corrigé en passant : le fichier de configuration écrivait la clé `scene` deux fois dans le même objet. 303 tests, 31 mutations sur 31. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
…ait pas assez Les fonctions des commits précédents ont été relues avec une seule consigne : essayer de les casser. Cinq trous réels, tous corrigés, tous couverts par un test vérifié en échec sans son correctif. ⚠ LE PLUS SÉRIEUX — LE SUIVEUR AUDIO PLONGEAIT VERS LE NOIR EN DÉCROCHANT, et ma propre documentation affirmait le contraire. Le fondu de péremption faisait décroître le NIVEAU vers 0. Or `min + (max−min) × niveau` envoie un niveau nul sur `min` : le paramètre visait donc sa borne basse pendant 450 ms avant de rendre la main d'un coup. Sur le master avec min à 0, c'est le noir en plein spectacle — exactement ce que le commentaire de la fonction, le CHANGELOG et l'anti-mutant promettaient d'éviter. La fraîcheur dose maintenant le RETOUR À LA MAIN et non le niveau : la valeur glisse du micro vers le réglage posé à la main, sans jamais viser `min`. Et le test de sécurité qui gardait cette propriété ne la gardait qu'à moitié : il ne mesurait qu'APRÈS la péremption complète, donc il ratait le creux au milieu. Vérifié : en réintroduisant le défaut, l'ancien test passe, le nouveau tombe. C'est la quatrième fois dans cette branche qu'un test mesure moins qu'il ne promet — le motif est constant, et c'est toujours la mesure qui le révèle. LA LIMITATION DU CODE D'ACCÈS S'EFFONDRAIT FACE À PLUSIEURS ADRESSES. Le blocage par IP arrête un curieux, mais un attaquant qui fait varier sa source — trivial en IPv6, où il tient tout un /64 — retrouvait les 10 000 combinaisons. Le commit précédent affirmait « sans limitation, le code ne protège RIEN » ; la limitation existait mais se contournait, donc l'affirmation était fausse. Un plafond GLOBAL, indépendant de l'IP, s'ajoute : 30 échecs par minute tous clients confondus et tout le réseau est refusé. La machine hôte n'est jamais prise dedans — vérifié. Au passage `accesEssais` grandissait sans borne : oubli des adresses inactives, balayage des jetons expirés, plafond dur. CSRF SUR LA MACHINE HÔTE. L'exemption locale laisse passer localhost sans jeton, c'est voulu. Mais rien n'empêchait une page piégée ouverte dans le navigateur de l'hôte de poster un formulaire vers `/api/new` (efface le projet), `/api/quit` ou `/api/blackout`. Les POST exigent désormais `Content-Type: application/json` : un formulaire ne peut pas poser ce type. Vérifié d'abord que toute l'UI l'envoie déjà et que les lanceurs ne font que du GET — le garde ne casse rien. L'EMPREINTE D'UN PRESET POUVAIT MENTIR. Elle passe par `resolveBars`, qui résout les groupes VIVANTS — un preset n'en mémorise pas. Modifier le contenu d'un groupe changeait donc ce qu'un preset piloterait au rappel, sans que la vignette bouge. Deux routes rafraîchissent le compteur de révision. J'ai vérifié que la géométrie, elle, n'entre pas dans l'empreinte : trois autres routes soupçonnées sont hors de cause, elles ne sont pas touchées. DOUBLE CLIC SUR « ÉCOUTER » : DEUX MICROS OUVERTS. `suiveur.actif` n'était posé qu'après `getUserMedia` ET `resume()` — or la demande d'autorisation est le moment le plus long, et celui où l'on reclique. Le second démarrage écrasait le flux et le contexte du premier, qui restaient orphelins : micro jamais relâché, AudioContext fuité, deux boucles d'analyse. Garde synchrone avant tout `await`. Ajouté aussi : un test de caractérisation sur la SEULE voie encore vivante du défaut v1 — vider un groupe (au lieu de le supprimer) rend la couche à tout le plateau. Il ne corrige rien, il verrouille un choix documenté pour qu'un mutant ne le change pas en silence. C'était la piste laissée ouverte dans `CLAUDE.md`. Le garde-fou anti-mutant a fait son travail pendant ce commit : mon refactor a changé `Date.now()` en `now` dans la ligne surveillée, la forme saine a disparu, la suite est tombée. C'est précisément ce pour quoi il existe. README complété (suiveur audio, grille de presets) et la note « il n'y a pas de mot de passe » corrigée — elle était devenue fausse. 309 tests, 31 mutations sur 31. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Le PDF en était resté à la 2.0 : il ne parlait ni du suiveur audio, ni de la grille de presets, ni des replis, ni du code d'accès. Pire, il affirmait encore « il n'y a pas de mot de passe : toute personne sur le même réseau peut piloter le show » — devenu faux, sur la page même qui explique comment connecter un iPad. Quatre sections ajoutées, écrites pour un régisseur, pas pour un développeur : - 3.3 Le code d'accès — ce qu'il protège, pourquoi il n'est jamais demandé sur la machine hôte, pourquoi l'OSC et le MIDI n'en dépendent pas, et la limite assumée : il ne protège pas contre quelqu'un qui a le fichier de configuration. - 4.7 Les replis — et surtout la règle qui compte : la pastille garantit qu'un réglage caché ne l'est jamais silencieusement. - 8.8 Le suiveur audio — avec les deux limites en note, énergie et pas tempo, micro seulement sur la machine hôte, plus le comportement de sécurité quand le son décroche. - 10 La grille de presets — en insistant sur ce que la bande de couleur EST : une couverture, pas une image du motif. C'est la question qu'un régisseur se pose devant un pavé (« qu'est-ce que ça va allumer ? »), et une vignette prise pour une photo tromperait. Vérifié plutôt que supposé : le manuel passe de 15 à 17 pages, et le contrôle de glyphes du générateur reste vert (29 caractères non-ASCII, tous dessinables) — c'est lui qui empêche les carrés vides du piège n°4. PDF recopié à la racine ET dans dist/, comme l'impose la règle n°1. ⚠ Note ajoutée à CLAUDE.md : la prochaine version est une MINEURE (2.1.0, pas 2.0.3). La section non publiée contient des fonctions nouvelles, donc le versionnage sémantique l'impose — et il faudra passer `VERSION = "2.1"` dans `build-manuel.py`, qui porte majeure.mineure. Au passage, une dépendance système manquait pour générer le manuel sur une machine nue : `fonts-dejavu-extra` (l'italique). Le générateur le disait correctement — « aucune police utilisable » — mais sans nommer le paquet. 309 tests, zéro échec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Sur une machine nue, `build-manuel.py` s'arrêtait sur « Aucune police utilisable trouvée » alors que DejaVu était installé. La cause : il exigeait les QUATRE fichiers d'une famille, or l'oblique vit dans un paquet séparé (`fonts-dejavu-extra`) rarement présent, et le repli reportlab échouait de la même façon (`VeraMono.ttf` absent). Condamner tout le manuel pour une italique manquante est disproportionné : une note en romain reste lisible, pas de manuel du tout ne l'est pas. Le régulier suffit désormais à s'en sortir : chaque fichier manquant est remplacé par lui, et le script DIT lequel et quel paquet l'apporte. Le contrôle de glyphes, lui, reste strict — c'est le garde-fou du piège n°4 (carrés vides), et il n'a rien à voir avec la disponibilité d'un fichier de police. Le message d'échec ultime nomme maintenant les commandes d'installation par plateforme, au lieu d'une liste de noms de familles qui n'aide personne. Vérifié dans les deux sens : avec toutes les polices, et en retirant réellement l'oblique du système — le manuel se génère dans les deux cas, avec l'avertissement dans le second. Consigne d'installation ajoutée à CLAUDE.md, y compris le piège où j'ai perdu du temps : le dossier de sortie doit exister, sinon reportlab échoue à la toute fin, après avoir tout mis en page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Bloquer tout le réseau après 30 échecs arrête un attaquant multi-adresses, mais laisse quelqu'un empêcher volontairement les AUTRES appareils d'entrer pendant une minute. C'est un déni de service, et il ne faut pas le découvrir en salle. Le compromis est assumé et noté à côté du code : c'est le moins cher des deux. Le régisseur garde sa machine, jamais bloquée ; l'alternative — laisser passer — rendrait le code cassable. Une gêne d'une minute contre la prise de contrôle du spectacle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Le « 31 mutations sur 31 détectées » de la documentation n'avait jamais été vérifié d'un bloc — le chiffre était additionné à la main, mutant par mutant, au fil des commits. Il est maintenant mesuré : campagne complète, 31 cassages volontaires, 31 détectés, zéro aveugle, et `server.js` restauré identique à la fin (le contrôle que fait l'outil lui-même). Elle a tourné dans une COPIE du dépôt : `tests/mutation.js` modifie `server.js` en place, donc la lancer dans l'arbre de travail interdit d'y toucher pendant une heure — et un commit parti à ce moment-là emporterait un mutant. C'est exactement l'accident du 28/07. Le fichier de reprise gagne ce qui lui manquait sur les fonctions récentes : - les deux routes ajoutées (`/api/presets-info` et `/api/acces`) et POURQUOI la première est hors de `/api/state` ; - les DEUX gardes qui pèsent désormais sur toutes les routes `/api/` — le portillon du code d'accès, et l'exigence de `Content-Type: application/json` qui ferme la CSRF locale. Quelqu'un qui ajoute une route doit savoir qu'elles existent avant de se demander pourquoi son `curl` reçoit un 415 ; - le haché du code d'accès, avec l'avertissement qu'il ne sort JAMAIS du serveur mais qu'il est en clair dans `cascade-config.json` — limite assumée ; - la calibration audio dans `settings` et non dans le `localStorage`, avec sa raison (la régie tient sur une clé USB) ; - `src` sur les deux modulateurs, et son repli sur `'lfo'` qui garantit qu'une vieille configuration se comporte comme avant. L'en-tête dit maintenant qu'il y a quatre fonctions non publiées et que la prochaine version est une mineure — pour qu'on ne tague pas une 2.0.3 par réflexe. 309 tests, 31 mutations sur 31. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Le risque de cette mécanique était le blocage perpétuel : si le compteur d'échecs ne repartait pas de zéro à la levée, le plafond se redéclencherait aussitôt et plus aucun iPad n'entrerait jamais. Raisonner ne suffisait pas. Mesuré, fenêtre ramenée à 2 s pour l'observer : six échecs sur six adresses distinctes bloquent une adresse jamais vue ; après la fenêtre, un échec rend à nouveau 401 et le bon code passe. Le blocage se lève. La raison est notée avec la mesure, parce qu'elle est fragile : la durée du blocage vaut celle de la fenêtre, donc au moment où il se lève la fenêtre est forcément expirée et le compteur repart. Raccourcir le blocage sous la fenêtre casserait cette propriété — et personne ne le devinerait en relisant. Ce n'est pas testable dans la suite sans l'immobiliser une minute, d'où la mesure à la main et sa trace ici. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Faire relire les correctifs de la relecture précédente était le bon réflexe : deux d'entre eux étaient contournables ou nuisibles, et les deux ont été mesurés contre un vrai serveur, pas déduits. ⛔ LE GARDE CSRF SE CONTOURNAIT EN UNE LIGNE. Il testait une SOUS-CHAÎNE. `Content-Type: multipart/form-data; boundary=application/json` la contient — et la règle CORS ne regarde que l'ESSENCE du type, en ignorant les paramètres. Ce type est donc safelisté : un `fetch` en `no-cors` le pose sans pré-vol. Mesuré : `/api/quit` tuait le serveur, `/api/acces` posait le code de l'installation. Le commentaire que j'avais écrit affirmait « un formulaire ne peut pas poser ce type » — c'était vrai, et à côté de la question : la brèche était un fetch. On compare maintenant l'essence. ⛔ LE DIALOGUE RÉGLAGES RETIRAIT LE CODE D'ACCÈS À CHAQUE ENREGISTREMENT. Le champ est vidé à chaque ouverture, puisque le serveur ne renvoie jamais le code — et un champ vide déclenchait un retrait. Ouvrir Réglages pour changer le port MadMapper et cliquer Enregistrer supprimait donc le code en silence, sans même un message, et déconnectait toutes les tablettes. Mon propre commentaire disait « laisser vide veut dire ne rien changer » : le code faisait l'inverse. Un champ vide ne touche plus à rien, et le retrait est passé sur un bouton dédié avec confirmation — un geste destructeur doit être un geste, pas un effet de bord. Trois autres, moins graves. Une balise `<img src="…/api/state?a=1">` sur une page piégée pouvait clouer un modulateur audio en butée (le niveau n'est plus accepté que d'une requête de script). Un rappel de preset qui remplace le plateau change l'empreinte des presets sans disposition propre — les projets importés en ont. Et changer d'entrée son pile pendant le démarrage du micro pouvait avaler le redémarrage. ⚠ UN ANTI-MUTANT AJOUTÉ AU PREMIER JET SURVIVAIT : aucun test n'envoyait l'en-tête qu'il surveillait. Un garde-fou que rien ne tue est un faux garde-fou, et il est pire qu'aucun puisqu'il rassure. Vérifié un par un cette fois : les 33 mutants sont tueurs. Le compte de la session : trois passes de relecture, dix défauts réels, dont quatre bloquants — et la moitié dans du code écrit pour en corriger d'autres. 313 tests, 33 mutations sur 33. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
…te pour Pym Campagne complète repassée après les derniers correctifs : 33 cassages volontaires, 33 détectés, zéro aveugle, aucun motif absent, et `server.js` restauré identique. Les deux anti-mutants ajoutés hier — le garde CSRF et le niveau audio poussé par une balise image — sont bien tueurs dans la campagne entière, pas seulement quand on les vérifie isolément. La consigne d'usage est notée avec le chiffre : lancer la campagne dans une COPIE du dépôt. Elle modifie `server.js` en place pendant une heure, et un commit parti à ce moment-là emporterait un mutant — c'est exactement l'accident du 28/07. ⚠ UNE LIMITE RESTE OUVERTE, ET JE NE LA CORRIGE PAS TOUT SEUL. Rien ne vérifie l'en-tête `Host` : une page hébergée sur un domaine qui résout vers 127.0.0.1 devient même origine aux yeux du navigateur, et tous les gardes CSRF tombent avec elle (« DNS rebinding »). Le correctif tient en cinq lignes — refuser un `Host` qui n'est ni localhost ni une adresse du réseau local. Mais il casserait l'accès par nom d'hôte, `http://mac-de-pym.local:3333`, qui fonctionne aujourd'hui. C'est un échange : une attaque sophistiquée contre une façon de se connecter qui marche. Ça se décide avec celui qui s'en sert, pas depuis une session. La limite est donc écrite à côté du code, avec le correctif possible et ce qu'il coûte. 313 tests, 33 mutations sur 33. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Décidé avec Pym : il accède à la régie par le QR code ou par l'adresse IP, donc on peut fermer la faille sans rien lui casser. Sans ce garde, tous les autres tombaient. Une page sur `mechant.com` dont le DNS se ré-résout vers 127.0.0.1 devient « même origine » pour le navigateur : elle pose le Content-Type qu'elle veut, le cookie SameSite ne protège plus, et Cascade l'exempte du code d'accès puisque la requête vient de localhost. Contrôle complet de la lumière et lecture du projet, depuis n'importe quelle page web, sans même être sur le réseau. Le garde CSRF et le code d'accès ne servaient à rien contre ça — c'est la raison d'être de ce commit. Une requête rebindée porte un `Host` étranger, et c'est là qu'on la coupe. Sont acceptés : `localhost`, les adresses IP littérales (ce que donne le QR code), et les noms en `.local`. Ce dernier point mérite d'être compris avant d'être touché : `.local` est réservé au mDNS par la RFC 6762. Personne ne peut en posséder un sur Internet, et le système ne le résout que sur le réseau local — un attaquant ne peut donc pas s'en servir pour un rebinding. C'est ce qui permet de garder `mac-de-pym.local:3333` sans rouvrir la faille. Un domaine qui s'en approche, lui, est bien refusé : `evil.local.attaquant.com` ne se termine pas par `.local`. Une requête sans en-tête `Host` passe : un navigateur en envoie toujours un, donc son absence n'est jamais une attaque par rebinding — seulement du HTTP/1.0 ou un `curl` nu. La PAGE est refusée elle aussi, pas seulement l'API. La servir puis laisser chacun de ses appels échouer ressemblerait à une panne ; le refus, lui, dit quoi utiliser à la place. Restent refusés, assumé : les noms de machine Windows sans suffixe et les alias du fichier `hosts`. Vérifié contre un vrai serveur, dans les deux sens, et l'anti-mutant est tueur. 314 tests, 34 mutations sur 34. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
…ws seulement La demande n°1 de Pym, arbitrée avec lui : point vert quand le show tourne, rouge à l'arrêt, clic droit pour ouvrir l'interface, démarrer, arrêter, quitter. Node n'a AUCUNE API de zone de notification — cette icône demande du code natif, et c'est pour ça que la demande dormait. Sous Windows, PowerShell donne accès à `System.Windows.Forms.NotifyIcon`, présent d'origine : zéro installation, zéro dépendance npm. La règle n°2 tient, et c'est ce qui a décidé du choix. ⚠ WINDOWS SEULEMENT, et c'est un choix, pas un oubli. macOS n'a pas d'équivalent scriptable — un élément de barre de menus demande une application compilée ou un outil tiers. Plutôt qu'une fonction bancale partout, une fonction franche là où se trouve la régie. Hors Windows, la case est estompée et l'écran le dit. ⚠ ÉTEINTE PAR DÉFAUT, ET JAMAIS EXÉCUTÉE. Ce code PowerShell a été écrit depuis une machine Linux : je n'ai pas pu le lancer une seule fois. Le prétendre fonctionnel serait malhonnête. Tout est donc enveloppé — plateforme, réglage, try/catch, gestionnaires d'erreur sur le processus — de sorte qu'un échec soit silencieux et ne touche jamais le serveur. Un test le vérifie : on active le réglage sous Linux, où PowerShell n'existe pas, et Cascade doit continuer de répondre. Le premier essai sur une vraie machine Windows reste à faire. Le script est EMBARQUÉ dans server.js, pas posé à côté : le distribuable est un exécutable unique, et un `.ps1` externe n'existerait pas près de lui. Il est écrit dans le dossier temporaire au démarrage. Le dialogue passe par l'API HTTP de Cascade plutôt que par un tuyau maison — le script sait déjà parler à localhost, et ça évite d'inventer un protocole. Il respecte au passage les deux gardes ajoutés hier : `Content-Type: application/json` et un `Host` en 127.0.0.1. Limite honnête du montage, écrite dans le code : quand le serveur s'arrête, l'icône DISPARAÎT au lieu de passer au rouge. Une fois Cascade parti, il ne reste rien pour la dessiner. Faire survivre l'icône au serveur demanderait que le lanceur en soit le propriétaire — un autre montage, plus lourd. Note de méthode : la suite a échoué une fois pendant ce travail, sur les tests d'interface. Trois passes complètes ensuite, toutes vertes. C'était donc l'aléa de charge que le projet documente — pas une régression. Je le note plutôt que de le taire : la tentation, dans ces cas-là, est d'élargir une fenêtre au hasard, et c'est précisément ce qui a déjà fait passer une suite de 1 à 19 échecs. 315 tests, 34 mutations sur 34. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Le manuel passe à 18 pages. La section dit ce qui compte vraiment : Windows uniquement (macOS n'a pas d'équivalent scriptable sans dépendance), et l'icône DISPARAÎT à l'arrêt au lieu de rougir — une fois le programme parti, plus rien ne la dessine. Icône présente = Cascade tourne. Rappelle aussi que fermer la fenêtre pendant un show ne coupe déjà rien : l'icône sert à le voir, pas à le garantir. PDF regénéré à la racine et dans dist/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
…tion docs/ETAT-DU-PROJET.md est le fichier qu'on relit avant de toucher au code. Il n'y avait ni le contrôle du Host ni le systray — donc deux choses qu'une prochaine session aurait redécouvertes, ou pire, « réparées ». Le garde CSRF gagne aussi la raison pour laquelle il compare l'essence du type et pas une sous-chaîne : c'est un contournement mesuré, pas une précaution théorique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Relecture adversariale du systray — code jamais exécuté, écrit depuis Linux. La trouvaille principale n'est pas dans le PowerShell : elle est dans ce qu'il sondait. Le script interrogeait /api/state toutes les 1,5 s. Or /api/state remet lastUiPollAt à jour — c'est comme ça que Cascade sait qu'une interface est ouverte. L'arrêt automatique exige 8 s de silence : la condition n'était donc PLUS JAMAIS vraie. Case cochée, on ferme son navigateur, Cascade ne se ferme plus tout seul. Le blocage était mutuel : le script ne sort que quand le serveur meurt, le serveur ne mourait plus parce que le script sondait. Avec le mode app (pas de terminal) et une icône que Windows range d'office dans le tiroir caché, il ne restait que le Gestionnaire des tâches. Une fonction d'agrément cassait une fonction livrée, en silence. Le sondage passe par /api/ping, qui n'y touche pas — et qui ne renvoie pas fixtures, couches, scène, vues et niveaux pour lire un booléen. running et systray n'en sortent que pour la machine hôte : la route est hors du portillon, donc publique. Mesuré pour de vrai, pas déduit : instance à part, veilleur armé, délai de grâce à 400 ms (CASCADE_UI_GONE_MS), sondée uniquement par ping — et on regarde si le processus est mort. Avec témoin qui poll et doit survivre. Deux mutants rejouent le défaut, tous deux détectés. Sept autres correctifs de la même relecture : trois échecs avant de partir (un pic de charge faisait disparaître l'icône pour de bon) · -ErrorAction Stop (sans lui, rien ne garantissait que le catch parte, et powershell.exe survivait à Cascade) · un seul chemin de sortie qui efface l'icône (kill() est un TerminateProcess : pas de NIM_DELETE, pastille morte à l'écran) · journalisation du code de sortie (trois bâillons superposés rendaient tout diagnostic impossible) · course sur la référence au processus · BOM sur le .ps1 · la case se grisait d'après navigator.platform, donc d'après la machine qui REGARDE, alors que l'icône se pose sur l'hôte. Et six assertions qui pouvaient passer à vide : .every() est vrai sur un tableau vide, une boucle sur rien n'assure rien. Le plancher de la liste de mutations, resté à 27 pour 34 mutants, remonte à 34. 328 tests, aucun ignoré. Manuel à 18 pages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Il observait 1,2 s pour un délai de grâce de 400 ms, en sondant toutes les 100 ms : une seule requête locale qui traîne au-delà de 400 ms sur un runner chargé, et le serveur s'arrête alors que le test attend qu'il vive. Une flakiness attendait son runner. Grâce à 600 ms, fenêtre à 2 s, pas de sondage à 50 ms — 12× de marge, et une fenêtre 3× plus longue que la grâce, sans quoi le témoin ne prouverait rien : un serveur qui survit moins longtemps que sa propre grâce survivrait même sans être sollicité. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
La relecture du systray a nommé l'angle mort : la CI ne tourne que sur Ubuntu, donc AUCUNE ligne de ce PowerShell n'est jamais vérifiée par autre chose qu'une lecture de source. Une parenthèse manquante survivrait jusqu'à la machine de régie. L'analyseur de PowerShell est une classe .NET ordinaire : il ne demande pas de session graphique. On ne prétend donc pas tester l'icône — on ferme le seul angle mort qu'un runner peut fermer, et le commentaire le dit. Le passage Windows lance aussi la suite : le commentaire en tête du fichier promettait déjà d'attraper « un chemin qui n'existe que sous Windows », ce qu'un runner Ubuntu ne peut pas faire. La promesse devient vraie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
En PowerShell, $null.Count vaut 0. Si l'analyseur ne rendait rien du tout, « zéro erreur d'analyse » aurait été vrai et l'étape serait passée au vert sans avoir lu une ligne — exactement le défaut que cette session passe son temps à débusquer ailleurs. Deux témoins : on exige d'abord d'avoir vraiment obtenu des jetons, puis on redonne le même script avec une accolade en trop et on vérifie que l'analyseur le REFUSE. Une étape incapable de dire non ne mesure rien. Le premier passage Windows a lancé la suite entière : 328 tests, 0 échec, 0 ignoré — les 46 tests de navigateur compris. La promesse du commentaire en tête du fichier (« un chemin qui n'existe que sous Windows ») est tenue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Il promettait déjà d'attraper « un chemin qui n'existe que sous Windows », ce qu'un runner Ubuntu ne peut pas faire. C'est vrai depuis aujourd'hui : la suite tourne sur les deux, et le premier passage Windows est vert — 328 tests, 0 ignoré. Et la note sur l'icône disait « la CI ne dira jamais rien de cette fonction ». À corriger dans les deux sens : elle en dit maintenant quelque chose (le script s'analyse, et la suite tourne sous Windows), mais elle ne dira jamais si l'icône se pose — NotifyIcon demande un bureau. L'essai en vrai reste à faire. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
Zéro aveugle, zéro motif absent, server.js restauré à l'identique dans la copie. Les deux mutants de l'icône de notification n'étaient jusqu'ici vérifiés qu'à part : le chiffre n'est plus additionné à la main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
…ne durée Le passage Windows a rendu rouge : 2 tests sur 328. Deux vrais défauts de test, pas un incident de runner. 1. Le faux périphérique de Chromium a mis plus de 900 ms à s'ouvrir sur un runner chargé. L'attente était FIXE. Elle attend maintenant la CONDITION (suiveur.actif), avec une échéance large. Ce n'est pas élargir une fenêtre pour faire passer un test : la condition mesure exactement la même chose, et échoue toujours si le micro ne s'ouvre jamais — le test « micro refusé » le prouve, il emprunte le même chemin. 2. Le second échec était COLLATÉRAL, et c'est le plus intéressant. Ces tests se passaient l'état du suiveur les uns aux autres sans jamais le dire. Le premier ayant laissé le micro ouvert, le clic du suivant l'a ARRÊTÉ au lieu de le démarrer — et ce test-là a rapporté « un seul getUserMedia doit partir, vu 0 », c'est-à-dire une panne qui n'existait pas. Un test qui se trompe de diagnostic coûte plus cher qu'un test qui tombe : on part chercher un défaut ailleurs. Les quatre tests du micro partent désormais d'un suiveur explicitement fermé. 46 tests d'interface verts deux fois de suite, 328 sur la suite complète. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH
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.
Travail hors-ligne, sans MadMapper ni matériel. Tout ce qui suit est mesuré, pas supposé.
328 tests · 0 échec · 0 ignoré · 36 mutants sur 36 détectés · CI verte, sur Ubuntu ET sur Windows.
Ce qui est ajouté
Suiveur audio — le micro devient une source de modulateur
Un modulateur (par couche ou global) accepte maintenant
src: 'lfo' | 'audio'. L'analyse Web Audio se fait dans la page et l'énergie remonte en query sur le poll.⚠ Deux limites assumées, écrites et décidées avec Pym — ne pas les « corriger » :
Quand le flux se périme, le modulateur ne retombe pas vers
min— il fond vers le réglage de l'opérateur (doserMod). La première version fondait versmin: un master à zéro en plein show. Le test de sécurité d'alors ne mesurait qu'après la péremption et ratait le creux.Les 16 presets deviennent une grille
Chaque case dit ce qu'elle pilote : une empreinte de couverture (quelles barres, pas une photo du motif), servie à la demande par
GET /api/presets-info. Volontairement hors de/api/state, qui part 8 fois par seconde.Code d'accès facultatif à 4 chiffres
Haché salé, comparé en temps constant, cookie
HttpOnly+SameSite=Strict. Cinq essais ratés bloquent l'appareil une minute, plus un plafond général contre l'essaimage sur plusieurs adresses.⚠ La machine hôte en est exemptée exprès — on ne peut pas s'enfermer dehors. L'OSC et le MIDI ne sont pas couverts : autre domaine de confiance, les fermer casserait les installations existantes. Décidé avec Pym.
Le panneau Couches se replie
advMiroirsetadvMelange, avec pastilles de réglages actifs, commeadvGroove.Icône de zone de notification (Windows)
PowerShell +
NotifyIcon: zéro installation, zéro dépendance npm. Vert = ça tourne, rouge = à l'arrêt ; clic droit pour piloter.⚠ Windows seulement, et c'est un choix — macOS n'a pas d'équivalent scriptable sans application compilée. ⚠ Éteinte par défaut et jamais exécutée : écrite depuis Linux.
Ce que la CI en dit maintenant, et ce qu'elle ne dira jamais. Un passage
windows-latesta été ajouté : il analyseSYSTRAY_PS1avec[Parser]::ParseInput— aucune session graphique requise — et lance la suite entière. Une parenthèse manquante ne partira donc plus en régie. Mais aucun runner ne posera jamais l'icône :NotifyIcondemande un bureau.tests/systray.test.jslit le source en plus : c'est mieux que rien, et ce n'est que ça. L'essai en vrai reste à faire.⚠ Avant ce premier essai : Windows range toute nouvelle icône dans le tiroir caché (chevron
^). Sans cette note — désormais dans les Réglages et le manuel — l'essai n'aurait rien prouvé : le script tourne, l'icône existe, personne ne la voit.Ce qui est corrigé
L'icône de notification désactivait l'arrêt automatique
Trouvé en relecture adversariale du systray. La trouvaille n'est pas dans le PowerShell : elle est dans ce qu'il sondait.
Le script interrogeait
/api/statetoutes les 1,5 s. Or/api/stateremetlastUiPollAtà jour — c'est comme ça que Cascade sait qu'une interface est ouverte. L'arrêt automatique exige 8 s de silence : la condition n'était plus jamais vraie. Case cochée, on ferme son navigateur, Cascade ne se ferme plus tout seul. Le blocage était mutuel : le script ne sort que quand le serveur meurt, le serveur ne mourait plus parce que le script sondait. Avec le mode app (pas de terminal) et une icône rangée dans le tiroir caché, il ne restait que le Gestionnaire des tâches.Une fonction d'agrément cassait une fonction livrée, en silence.
Le sondage passe par
/api/ping, qui n'y touche pas — et qui ne renvoie pas fixtures, couches, scène, vues et niveaux pour lire un booléen.runningetsystrayn'en sortent que pour la machine hôte : la route est hors du portillon, donc publique.Mesuré, pas déduit : instance à part, veilleur armé, grâce à 400 ms (
CASCADE_UI_GONE_MS), sondée uniquement par ping — et on regarde si le processus meurt. Avec un témoin qui poll et doit survivre. Deux mutants rejouent le défaut.Sept autres correctifs de la même relecture : trois échecs avant de partir (un pic de charge faisait disparaître l'icône pour de bon) ·
-ErrorAction Stop(sans lui rien ne garantissait que lecatchparte, etpowershell.exesurvivait à Cascade) · un seul chemin de sortie qui efface l'icône (kill()est unTerminateProcess: pas deNIM_DELETE, pastille morte à l'écran) · journalisation du code de sortie (trois bâillons superposés rendaient tout diagnostic impossible) · course sur la référence au processus · BOM sur le.ps1· la case se grisait d'aprèsnavigator.platform, donc d'après la machine qui regarde, alors que l'icône se pose sur l'hôte.Cascade ne répond plus qu'à son propre nom (DNS rebinding)
Sans ce garde, tous les autres tombaient. Une page sur
mechant.comqui se ré-résout vers127.0.0.1est « chez elle » pour le navigateur : elle pose leContent-Typequ'elle veut,SameSitene protège plus, et Cascade l'exempte du code puisque ça vient de localhost. Contrôle complet de la lumière depuis n'importe quelle page web.Seuls
localhost, un littéral IP et*.localsont désormais acceptés enHost. Conséquence assumée : on se connecte par IP ou par le QR code.Le garde CSRF était contournable — mesuré
.includes('application/json')se contourne en une ligne :multipart/form-data; boundary=application/jsoncontient la sous-chaîne et la règle CORS ne regarde que l'essence, donc ce type est safelisté et passe sans pré-vol. Mesuré contre un vrai serveur :/api/quittuait le processus. On coupe au premier;.Le dialogue Réglages retirait le code en silence
Le champ était vidé à l'ouverture et « vide = retrait ». Enregistrer un réglage sans rapport désarmait la protection. Maintenant un champ vide ne change rien, et le retrait a son bouton confirmé.
Le compteur d'essais ne comptait pas
e.jusqua <= Date.now()est aussi vrai pourjusqua = 0: le blocage se levait à chaque tentative. « restants : 4 » indéfiniment.Le haché salé partait dans
/api/statesansCode()le retire de/api/stateet/api/settings.Le manuel refusait de se générer faute d'italique
fonts-dejavu-extramanquant faisait tout échouer. Le romain substitue et le script le dit. Le manuel passe de 15 à 18 pages.Ce qui est réparé dans la mesure elle-même
C'est le fil rouge : sept fois, un test a mesuré moins — ou autre chose — que ce qu'il promettait. À chaque fois c'est la mesure qui l'a révélé, jamais la relecture du test.
findBrowser()ne regardait que les chemins système. La CI échoue désormais sans navigateur, et sans adresse réseau : le trou « ignoré = vert » est fermé côté CI aussi.runtime.levels, avec un helper qui exige un tableau non vide, et un témoin positif ajouté au test STOP.min.sec-fetch-dest. Le test manquant est écrit..every()pouvaient passer à vide —.every()est vrai sur un tableau vide, une boucle sur rien n'assure rien. Le pire : « une seule fixture : pas de division par zéro » serait passé au vert avec un moteur totalement muet, c'est-à-dire exactement la panne cherchée.getUserMediadoit partir, vu 0 » : une panne qui n'existait pas. Un test qui se trompe de diagnostic coûte plus cher qu'un test qui tombe.Deux garde-fous qui ne gardaient plus rien, aussi : le plancher de la liste de mutations était resté à 27 pour 34 mutants — on pouvait en supprimer sept sans que rien ne le dise (remonté à 34) ; et ma propre étape d'analyse PowerShell pouvait passer sans rien analyser, parce qu'en PowerShell
$null.Countvaut 0. Elle exige maintenant d'avoir obtenu des jetons, et vérifie qu'un script volontairement cassé est bien refusé.Quatre relectures adversariales ont trouvé dix-huit défauts réels, dont cinq bloquants — dont la moitié dans du code écrit pour corriger d'autres défauts.
Documentation
CHANGELOG.md,README.md,CLAUDE.md,docs/ETAT-DU-PROJET.md,docs/PLAN-V2.md,docs/V2-AXES-PISTES.md,docs/V2-TESTS-MADMAPPER.md, et le manuel PDF (racine etdist/).docs/V1-V2.mddevient une pierre tombale : il décrivait une migration terminée.Une limite reste ouverte et signalée à Pym, non corrigée :
resolveBars()se replie sur toutes les barres actives quand le groupe d'une couche est vide. Un test de caractérisation la fige — elle ne changera plus par accident.Vérifications
npm test→ 328 / 328, aucun ignoré, sur Ubuntu et sur Windowsserver.jsrestauré à l'identiquenode sync-dist.js --check→dist/synchrone (règle n°1)🤖 Generated with Claude Code
https://claude.ai/code/session_014AygLFhdJYfSvYLNR2Y2kH