# Audit du 7 septembre 2026 — corrections et suite État : sources basées sur `375f0d2` (0.12.8), corrections regroupées pour 0.12.9. Les quatre bugs de la première passe et les trois bugs supplémentaires décrits dans les deux premières sections sont **corrigés localement**. Une nouvelle passe, détaillée en fin de document, a reproduit **cinq autres bugs, également corrigés localement sur demande**. Cet audit n'est pas une preuve d'absence de bugs dans le reste du dépôt. ## Quatre corrections vérifiées 1. **Source partiellement indisponible** (`merge_daily._executer`) : une journalière ne remplace plus sa version complète par les seuls clips encore présents. Le jour et son état sont conservés ; le passage retourne 1 et explique le report. Si une journée sélectionnée est concernée, ses agrégats hebdomadaires/mensuels ne sont pas reconstruits à partir d'un jeu incomplet. Ce report des agrégats est conservateur et global au passage. Une exclusion volontaire reste différente : elle autorise la reconstruction. 2. **Dates d'exclusion** (`blink_registre._exclusion_a_retenir`) : deux copies portant le même booléen ne font plus reculer la date de décision. Une ancienne sauvegarde ne peut donc plus rendre prioritaire une réintégration périmée, ni une exclusion périmée dans le cas inverse. 3. **Réglages des agrégats après sélection** : écarter/réintégrer un clip respecte désormais `merge_semaine` et `merge_mois` indépendamment. 4. **Fuseau des fusions manuelles** : Actualiser transmet le fuseau configuré. Le traitement d'une sélection calcule aussi son filtre de jour dans ce même fuseau. `runtime.options_fusion()` centralise les options partagées par ces deux chemins et la boucle automatique. Tests ajoutés : `test_blink_registre_exclusion_date.py`, `test_merge_daily_source_partielle.py`, cas supplémentaires dans `test_download_progression.py` et `test_serve_appliquer_selection_reglages.py`. Les reproductions originales `build-win7/audit_20260907.py` montrent maintenant la journalière intacte avec code 1, la dernière exclusion conservée, le fuseau New York effectif et les deux agrégats désactivés. Validation finale, sept corrections incluses : **526 tests réussis sous CPython 3.12 et CPython 3.8.10** (exécutés sur l'hôte, pas dans la VM Win7). `python tests.py` (intégration vidéo/interface/processus) et `docs.py --check` réussissent également. Tests locaux uniquement, avec données temporaires ; aucun réveil de caméra, aucune suppression distante, aucun arrêt d'une instance utilisateur. Des ResourceWarning préexistants restent émis par certains tests (boucle asyncio, icône tray), sans échec. Le TLS du relais Blink n'a pas été modifié. ## Trois bugs supplémentaires reproduits puis corrigés ### P1 — La mise à jour continue après un arrêt échoué `maj.finaliser()` ignore le code de sortie de `stop`. Après vingt vérifications où l'ancienne instance est toujours vivante, la fonction poursuit néanmoins la permutation et la relance. Selon les protections de fichiers de l'OS, cela peut mener à un mélange d'anciens processus et de nouveaux fichiers, ou à une relance concurrente, au lieu de refuser proprement l'installation. Reproduction : `build-win7/audit-root-update-stop.py`. Arrêt simulé avec code 1, instance toujours vivante ; `_permuter` et `_relancer` sont tout de même appelés, et `finaliser` retourne 0 si la permutation réussit. Tous les processus et remplacements sont simulés : aucune installation réelle touchée. Correction appliquée : code d'arrêt non nul ou instance encore active après le délai interdisent désormais permutation et relance. Les trois cas (échec, délai épuisé, arrêt confirmé après attente) sont couverts par `test_maj_finaliser_arret.py`. ### P2 — Une sélection sur un homonyme reconstruit la mauvaise caméra `serve.Handler.do_POST`, branche `/api/appliquer-selection`, transmet le nom brut de l'entrée à `merge --camera`. Or `merge_daily` distingue les caméras de même nom sur deux réseaux par des clés comme `jardin` et `jardin (2)`. Reproduction : deux entrées `jardin`, réseaux 111 et 222 ; exclusion du clip du réseau 222. L'exclusion vise le bon fichier, mais la commande de reconstruction contient `--camera jardin` au lieu de `jardin (2)`. L'ancienne journalière du second appareil peut ainsi conserver le clip exclu jusqu'au prochain passage global. Reproduction isolée dans `build-win7/audit-followup-20260907.py`. Correction : la sélection utilise les mêmes clés de collision que la fusion. Le test supplémentaire dans `test_serve_appliquer_selection_reglages.py` vérifie l'exclusion puis la réintégration du second homonyme, sans modifier le statut du premier. ### P2 — PMT tronquée : exception non contrôlée du démultiplexeur `blink_ts_demux.DemuxeurTSVideo._pmt()` accède aux octets 10 et 11 alors que `_section()` n'exige que trois octets. Un paquet TS complet de 188 octets, avec champ d'adaptation et section PMT tronquée à trois octets, déclenche `IndexError`. Dans la piste WebRTC, cette exception interrompt la lecture du flux au lieu d'ignorer la table invalide et d'attendre la suivante. Même diagnostic `build-win7/audit-followup-20260907.py`. Résultat obtenu sur un flux synthétique malformé, **pas observé sur les caméras réelles**. Correction : validation du type de table, de sa longueur complète et des limites des descripteurs avant de retenir un PID vidéo. Les tests de `test_blink_ts_demux.py` couvrent toutes les troncatures d'une PMT minimale avec H.264, des longueurs invalides et la reprise jusqu'à l'émission d'une NAL après réception de la table valide suivante. Robustesse liée : le tampon élémentaire n'est pas plafonné tant qu'aucun séparateur H.264 n'est reçu. Sur 1 880 000 octets TS synthétiques invalides, 1 840 000 octets restent retenus. C'est une croissance reproduite, pas une preuve d'épuisement mémoire en exploitation ; le volume réel dépend aussi de la durée maximale de session et des données reçues. ## Périmètre de cette continuation Revue ciblée du registre, des fusions/jours/périodes, des appels manuels serveur, des options de runtime, du démultiplexage WebRTC et du remplacement de version ; lectures complémentaires des boucles de téléchargement et de surveillance. Les douze corrections sont regroupées dans la version 0.12.9. Le risque de croissance du tampon sur flux sans séparateur reste une piste de durcissement distincte, non corrigée par cette passe. ## Nouvelle passe demandée — cinq bugs corrigés après reproduction L'audit initial était sans modification applicative. Reproductions réunies dans `build-win7/audit-additional-20260907.py` (diagnostic local, dossier ignoré par Git), exécutées avec succès sous Python 3.12 et 3.8.10 sur l'hôte. Ce ne sont pas des essais dans la VM Win7 ni sur les caméras. Les appels réseau, remplacements et démarrages/arrêts réels sont neutralisés ; les fichiers d'état sont exclusivement temporaires. Les scénarios et sorties ci-dessous décrivent les bugs avant correction ; les tests de non-régression ajoutés ensuite vérifient le comportement corrigé. ### P1 — Mise à jour : les processus deviennent invisibles avec un stockage redirigé Sources : `maj.py:904`, `runtime.py:689`. Une installation sans `BLINK_HOME`, dont le stockage est redirigé par `blink_home.txt`, garde ses fiches `.blink_run` dans le dossier du programme. Mais `maj.installer()` injecte `BLINK_HOME=` dans l'environnement du finaliseur. `_dossier_controle()` respecte aussi cette variable : finaliseur et commande `stop` cherchent alors les fiches dans les données au lieu de l'installation. Reproduction avec les vraies fonctions `installer()`, `finaliser()` et `_arreter_instances()`, leurs opérations externes simulées : une instance visible avant la préparation, zéro dans l'environnement réellement généré pour le finaliseur ; `stop` conclut que rien ne tourne, `finaliser` demande la permutation et retourne 0. L'ancienne fiche reste présente et son processus est toujours simulé vivant. C'est un chemin distinct de l'arrêt échoué corrigé plus haut : cette fois le code d'arrêt vaut 0 parce que la recherche porte sur la mauvaise racine. Les nouveaux contrôles ne suffisent donc pas dans ce cas. Selon l'OS, remplacement de fichiers encore utilisés ou échec sur fichiers verrouillés, puis risque de relance concurrente. Concerne le chemin de mise à jour des bundles standard ; la mise à jour automatique de l'édition Win7 est désactivée. Correction : `BLINK_CONTROL_HOME` transmet séparément la racine de contrôle. Un `BLINK_HOME` explicitement fourni par l'utilisateur reste respecté ; celui ajouté temporairement par la mise à jour est retiré à la relance pour suivre à nouveau le pointeur de stockage. L'ancienne commande `stop`, qui ignore la nouvelle variable, reçoit le contrôle via son `BLINK_HOME`. Pour la première mise à jour depuis un ancien lanceur, le finaliseur retrouve les fiches à partir de l'installation et du dossier transmis ; il refuse proprement une ambiguïté entre deux racines, sans arrêter ni remplacer. L'attente tient aussi compte des enfants/travailleurs survivant au superviseur. ### P1 — Deux changements de suppression automatique peuvent annuler une désactivation Source : `serve.py:3671` (lecture), `serve.py:3676` (écriture). La route `/api/suppression-auto` fait un lecture-modification-écriture sans verrou. L'écriture atomique évite un JSON tronqué, pas la perte d'une modification concurrente. L'interface ne désactive que la case manipulée : les cases de deux caméras peuvent envoyer des requêtes simultanées. Reproduction avec deux threads exécutant les vrais handlers : Salon est initialement actif. Désactiver Salon et activer Jardin lisent tous deux cet état. La première requête écrit l'ensemble vide ; la seconde réécrit Salon et Jardin. Les deux répondent HTTP 200 avec `ok: true`, alors que Salon est réactivé à tort. Cas confirmé sur 20/20 répétitions supplémentaires avec ordonnancement imposé ; cela démontre la fenêtre de concurrence, pas sa fréquence en utilisation réelle. Conséquence : les prochaines copies réussies peuvent de nouveau déclencher la suppression à la source malgré la désactivation demandée. Aucune suppression distante n'a été effectuée pendant le diagnostic. Correction : toute la transaction prend le verrou interprocessus de configuration, également employé lors du changement de stockage. Un délai épuisé répond 409 ; une erreur disque répond 500, sans faux succès. ### P1 — Changer de stockage peut rétablir une ancienne autorisation de suppression Source : `runtime.py:608`. Le transfert de configuration ne copie que les réglages généraux et la session. `blink_suppression_auto.json` reste sur place. Passer d'un dossier A à un dossier B neuf perd les choix ; désactiver explicitement la suppression dans B puis revenir à A réactive les anciens choix de A. Le même défaut de transfert remet aussi la langue à sa valeur par défaut dans B, mais l'effet sur la suppression est le risque important. Reproduction avec les fonctions réelles d'écriture/lecture et de changement de stockage : suppression active dans A, vide dans B, désactivée explicitement dans B, puis de nouveau active au retour à A. La clé employée est celle réellement calculée par `camera_setting_key_from_entry()`. Correction : langue et suppression automatique sont copiées avant de publier le pointeur. Une préférence absente impose aussi son défaut (français, aucune suppression), au lieu d'adopter d'anciens choix présents à destination. Un échec de copie laisse la racine active et le pointeur inchangés. ### P2 — La mise à jour ne restaure que la première instance Source : `maj.py:759`. `finaliser()` mémorise seulement `fiches[0].verbes`, alors que `stop` arrête toutes les instances. Le logiciel permet pourtant plusieurs lancements séparés, par exemple une interface et une boucle de téléchargement. Reproduction : deux fiches, `serve --port 8765` et `download --loop 1`, arrêt réussi et plus aucun survivant. Le finaliseur retourne 0 mais la seule composition transmise à `_relancer` contient `serve --port 8765`. La boucle de téléchargement n'est pas rétablie. Correction : toutes les compositions distinctes sont mémorisées et relancées, aussi en cas d'échec de permutation avec retour à l'ancienne version. Les doublons exacts ne sont pas relancés deux fois ; sans fiche, le repli `start` est conservé. ### P3 — Le nettoyage de progression peut effacer une publication toute neuve Source : `runtime.py:1869`. `_etats_travail()` décide de supprimer une fiche d'après son contenu lu, puis fait `unlink` sans synchronisation avec le worker qui écrit au même chemin. Un état terminé expiré peut être remplacé entre ces deux étapes par la progression du travail suivant du même processus persistant. Reproduction : lecture d'une ancienne fin expirée, publication réelle de `nouveau, 0/100` pendant cette fenêtre, puis nettoyage. La fiche neuve a disparu et `travail_en_cours()` renvoie `{}`. L'interface peut perdre la barre et croire momentanément le traitement terminé, jusqu'au prochain tick. Ce cas ne démontre ni perte de vidéo ni lancement effectif d'une fusion. Correction : publication et purge partagent un verrou OS léger. La purge relit la fiche sous verrou et vérifie qu'elle correspond encore au contenu qu'elle voulait retirer. Elle n'attend pas si le verrou est pris ; une publication attend au maximum 250 ms. Les erreurs restent non bloquantes pour le téléchargement. Le verrou est libéré par l'OS si son détenteur meurt. ### Tests de cette correction Ajouts : `test_maj_instances_stockage.py`, `test_suppression_auto_transactions.py`, `test_runtime_progression_concurrence.py`, cas de processus enfants dans `test_maj_finaliser_arret.py`. Le test historique du pointeur dans `test_blink2video_audit.py` neutralise désormais le `BLINK_HOME` de sa fixture pour bien isoler le cas annoncé, distinct du stockage forcé testé séparément. La concurrence de progression est exercée avec des threads et un processus de test Windows, tous sur un dossier temporaire ; la branche POSIX est simulée, sans prétendre à un essai Linux. Aucun accès aux caméras ni arrêt d'instance utilisateur, TLS Blink inchangé. Validation finale : **553 tests réussis sous CPython 3.12 et CPython 3.8.10**, sur l'hôte Windows. `python tests.py` (intégration vidéo/interface/processus), `python docs.py --check` et `git diff --check` réussissent également. Les ResourceWarning préexistants de la suite n'empêchent pas ces validations. ### Point ancien confirmé, non compté comme nouveau Le générateur d'autostart macOS émet un plist XML invalide si un chemin contient `&` (reproduction : `plistlib.loads()` lève `ExpatError`). Ce manque d'échappement était déjà consigné dans `AUDIT-2026-08-13.md`, section 28.59 ; il n'est donc pas inclus dans les cinq nouveaux bugs. Pas de test launchd réel sur macOS pendant cette passe.