Documentation

Faire tourner une session de recherche sans surveillance

C’est le programme d’exploitation d’une session de recherche qui tourne sans humain qui regarde : une session de nuit, ou une longue passation pendant la journée. Il remplace les prompts par nuit (les programmes round-6 et night-3, conservés à l’étiquette git research-archive-2026-09-24 sous docs/prompts/) et y intègre ce qui a mal tourné ces nuits-là. Les règles de recherche qu’il applique sont dans PLAN.md, « Standing rules for every round » ; les commandes sont dans AGENTS.md.

Une session grimpe par ascension et confirme sous des règles enregistrées, et laisse un compte rendu sur lequel Jared peut agir. Elle ne publie pas.

1. Démarrage

Passe les 30 premières minutes à lire, pas à lancer.

  1. AGENTS.md, en entier (commandes, suites gelées, emplacements canoniques, réglages Modal).
  2. PLAN.md : où nous en sommes, ce que nous avons appris, les règles permanentes, la politique de données et Next. Pour un résultat sur lequel tu veux t’appuyer, lis ses preuves dans l’archive : git show research-archive-2026-09-24:PLAN.md.
  3. Les compétences dans .agents/skills/ : kev-modal-study (lancer, surveiller et récupérer le travail GPU ; lis ses Gotchas), kev-verify (prouver qu’un changement de code n’a pas de régression), kev-pr-description (avant toute PR), thermonuclear-code-review.
  4. kev/rounds.py (sa docstring est le schéma de spec), la spec passée la plus proche dans experiments/rounds/, et kev/autoresearch.py (session).
  5. La passation elle-même : autorisation (dollars Modal, dollars AI Gateway), ce qui est dans le périmètre, ce qui a besoin de Jared.

Puis mets en place :

  • Travaille dans un worktree sur une branche de recherche (git worktree add -b research/<session> /tmp/kev-<session> origin/main). Pousse après chaque commit pour que rien ne soit perdu si la machine s’endort. Le code destiné à main passe par sa propre PR relue.
  • Lis uv run modal billing summary --json et consigne metered_cost comme référence dans le fichier d’état (section 6).
  • Si une session précédente a laissé un fichier d’état, lis-le d’abord et reprends à partir de lui ; les jobs Modal détachés continuent de tourner sans toi.

2. Budgets et la règle de dépense

  • L’autorisation est le total de la session, en comptant tout ce qui tourne encore. Avant chaque lancement, relis le coût mesuré et ne lance pas si (metered_now - baseline) + sum(admission bounds of everything still running) >= authorization.
  • La borne d’admission d’une étude est imprimée au lancement et enregistrée dans runs/<study>.spawn.json ; la borne d’un appel de benchmark est compute_bound(gpu, timeout, trials) (kev/budget.py). Le budget d’étude d’une spec doit être au moins sa borne (kev.rounds validate le vérifie ; modal_app.admit_study refuse une étude au-dessus de son budget avant que quoi que ce soit tourne, et une étude est plafonnée à $250 et 28 800 s).
  • Garde une réserve (environ 10 % de l’autorisation) dans laquelle aucune phase ne planifie : les relevés de facturation sont en retard et révisés, et les bornes d’admission surestiment fortement les lectures (un lot de lectures porte le timeout de son job le plus lent).
  • Consigne chaque relevé avec son heure UTC dans le fichier d’état. La dépense AI Gateway (lectures de référence Jev, juges d’étiquettes) a son propre plafond, appliqué par le script qui la dépense, et est journalisée dans runs/<name>/usage.json.
  • La limite de dépense de l’espace de travail Modal ne peut être relevée que depuis le tableau de bord ; l’atteindre tue les conteneurs en cours au milieu de l’entraînement.

3. Enregistrer une ronde

Une ronde est une section de PLAN.md plus une spec, commitées ensemble avant tout entraînement ou lecture.

  1. Écris la section de PLAN.md : pourquoi (l’écart mesuré et ses preuves), les données (gelées d’abord, avec manifestes), les bras, la règle (primaire, gardes avec seuils dimensionnés pour chaque suite, rang), les étapes de confirmation, et le budget. Utilise les règles permanentes ; n’invente pas une nouvelle statistique pour une seule ronde.
  2. Écris experiments/rounds/r<N>.json en copiant la spec passée la plus proche (r15 pour un delta joint, r17 pour un 27B, r10 pour une ronde de compétences, r20 pour des bras post-hoc sans entraînement : un pool de températures, des checkpoints interpolés ; r23 pour des mélanges vers un autre checkpoint, dont les bras nomment trained_on pour l’entraînement des deux extrémités). Omets "archive" : cette clé marque les rondes enregistrées 5-18. Chaque fichier de plan que la spec nomme, et chaque lecture parente dont sa règle a besoin, doit exister dans ce checkout ; si un parent manque d’une lecture, launch-reads <spec> --parents la crée. Retire chaque lecture d’une suite retirée (kev.suite.REMOVED_SUITES, avec la raison) : evals/external/scienthoon-v1 a été retirée le 2026-09-27, donc à partir de la ronde 23 la lecture, le panneau et la garde scienthoon disparaissent ; evals/external/wanli-v2 et typesafe-v1 ont été retirées le 2026-09-30, donc à partir de la ronde 27 leurs lectures disparaissent aussi, et SemIf est la seule lecture externe restante (rapport seulement). Les externes mis en pool ne sont pas une garde : la règle auditée de la ronde 24, que les rondes 23-26 suivent, rapportait SemIf, WANLI-v2 et TypeSafe comme panneaux optionnels. validate et launch refusent une ronde après la dernière ronde de la suite qui la nomme encore.
  3. Une nouvelle donnée est un nouveau répertoire sous evals/ avec un manifest.json (sha256 par fichier, hachages des entrées). Sous la politique de données SFT (PLAN.md), les corpus privés ne gardent que le manifeste dans git, avec une entrée "mirror" pointant vers le jeu de données privé.
  4. OBLIGATOIRE : toute température servie ou livrée vient d’un pool de jeux de données tenus à l’écart, jamais d’une partition du corpus d’entraînement. Une ronde qui lit la calibration (ECE, Brier, erreurs confiantes, couverture) enregistre un pool temperature pour ses bras (copie r20 : les huit sources publiques tenues à l’écart de la partition de calibration transfer-r3 + transfer-v9 MMLU-Pro), et une publication livre la température que scripts/calibrate_checkpoint.py ajuste sur le même pool. Les éléments tenus à l’écart des sources d’entraînement (les partitions calibration / development d’une suite d’entraînement) sont en distribution : la ronde 19 a servi ses bras SFT à T 0.955 ajusté sur des lignes de développement sft-v1 et a échoué à chaque critère de calibration (breadth-v1 ECE 0.059) ; le pool de jeux de données tenus à l’écart de la ronde 20 a donné 0.0085 sur le même checkpoint. Ce qui l’impose :
    • À partir de la ronde 21, kev.rounds validate et launch refusent une ronde dont la règle ou la confirmation a un critère que la température déplace (ECE, Brier, NLL, erreurs confiantes, couverture ; tout sauf l’exactitude) et aucun pool temperature. Les rondes <= 20 n’impriment qu’un !!! warning par bras entraîné sur un corpus d’entraînement, donc leurs specs enregistrées valident encore.
    • kev.rounds validate refuse une lecture de pool qui (a) est une suite d’entraînement d’un bras, un composant de celle-ci (inputs.components de sft-v1) ou la suite data de son plan, (b) met en pool une source sur laquelle un bras s’est entraîné, ou (c) lit la partition calibration ou development d’un corpus d’entraînement ; il refuse aussi un pool qu’il ne peut pas vérifier (un bras dont l’entraînement est inconnu, une suite sans manifeste ni sources listées). Les bras checkpoint sans essai peuvent nommer trained_on.
    • Le read-out consigne le temperature_source de chaque bras ; le tableau imprime !!! pour un bras servi aux lignes de développement d’un corpus d’entraînement de son essai (les rondes 5-19 l’ont tous été ; désormais une telle température ne sert qu’au criblage).
    • scripts/calibrate_checkpoint.py refuse les mêmes ensembles d’ajustement (vérifiés contre la suite d’entraînement de head.pt’s) ; --allow-in-distribution ne sert qu’à reproduire un ancien ajustement, et il est consigné dans head.pt["temperature_fit"].
    • La température en essai d’un essai (result.json calibration_fit) dit role: in-trial screening ... not a served or shipped temperature.
    • Les parents sont servis à la température ajustée sur les lignes de développement de leur essai (pour Kev-27B c’est son 1.38 livré, ajusté sur les mêmes lignes) ; le read-out consigne cela et leur T head.pt livré (parent_temperature_source), et validate avertit quand les deux diffèrent de plus de 0.05 sur les lignes d’un corpus d’entraînement.
    • Le contrôle de disjonction se fait par NOM de source (nominal, pas sémantique) : deux suites portant le même jeu de données sous des noms différents le passent. Un pool doit donc utiliser des sources qui sont en évaluation seule dans Kev par construction, comme les huit sources publiques tenues à l’écart de transfer-r3 et le MMLU-Pro de transfer-v9. L’allowlist sources d’une lecture de pool doit nommer des sources que sa suite liste (une coquille est un problème), et un entraînement que le vérificateur ne peut pas lister (un fichier data hors evals/, un manifeste sans sources) est un problème pour une nouvelle ronde.
    • calibrate_checkpoint.py --temperature T (une valeur manuelle, rien d’ajusté) a besoin de --reason, consigné dans head.pt["temperature_fit"] (par ex. « copied from the pool fit of runs/r20-readout »).
  5. uv run python -m kev.rounds validate experiments/rounds/r<N>.json (ajoute --partitions pour vérifier les partitions) jusqu’à ce qu’il imprime ok. Commite la section PLAN et la spec en un seul commit, pousse. Cette heure de commit est l’heure d’enregistrement.

4. Le faire tourner de bout en bout

KEV_GPU=H200 uv run modal deploy modal_app.py                     # after any change to kev/*.py or any new file under evals/
uv run python -m kev.rounds launch experiments/rounds/r<N>.json   # one ::study per study, 60 s apart, logs in runs/<study>.log
caffeinate -i nohup uv run python -m kev.rounds watch experiments/rounds/r<N>.json > runs/r<N>.watch.log 2>&1 &
  • Dans les cinq premières minutes de chaque étude, compte les pas d’optimiseur par minute dans modal container logs <id> et projette le temps réel contre le timeout (ep0 step N/M : M porte sur toutes les époques). Un conteneur arrivé à timeout ne sauve rien ; annule (FunctionCall.from_id(cid).cancel()) et relance sous un nouveau nom d’étude avec moins d’enregistrements ou un timeout plus long.
  • watch interroge les essais lancés, récupère chaque étude finie (une récupération d’étude à la fois), lance une fois les lectures de ce bras (un appel ::benchmarks groupé par bras, à 60 s d’intervalle), les attend et écrit runs/r<N>-readout/round<N>.json et un tableau. Il est redémarrable : l’état est dans runs/<study>.watch.json et l’intention de lancement dans runs/r<N>-reads-<arm>.json. À la main : launch-reads <spec> [--arms a,b] [--parents] [--dry-run], readout <spec>.
  • Écris le read-out dans la section PLAN : chaque bras, chaque critère avec son intervalle, le verdict et ce qui a échoué.
  • La confirmation est délibérée, jamais automatique. Pour le candidat que le read-out nomme, écris le choix dans PLAN.md et commite-le, puis par étape : launch-reads <spec> --stage <stage> --arm <arm>, puis confirm <spec> --stage <stage> --arm <arm> (→ runs/r<N>-verdict/<size>-<stage>.json). Teste les panneaux avant la lecture verrouillée. Une lecture chacun, sans exception.
  • Plusieurs rondes enregistrées à la suite sous un plafond : uv run python -m kev.autoresearch session experiments/rounds/r19.json [...] --spend-start <baseline> --spend-cap <authorization>. Il valide, lance et surveille chaque ronde jusqu’à son read-out, s’arrête avant une ronde dont les budgets dépasseraient le plafond, ajoute à runs/autoresearch-sessions.jsonl et imprime les commandes de confirmation ; il ne les exécute jamais. kev.autoresearch leaderboard rafraîchit runs/leaderboard.{jsonl,md} (non commité), compare apparie les essais contre une référence sur l’exactitude de transfert, release-check --study <name> filtre chaque config de cette étude (une config ne passe que si toutes ses graines passent leurs gardes).

5. Ce qu’une session peut toucher et ne peut pas toucher

Peut : écrire des specs, des plans et des sections de PLAN.md ; construire de nouvelles données gelées sous de nouveaux répertoires ; lancer des études et des lectures via modal_app.py ; changer les scripts et les constantes d’infrastructure de modal_app.py ; ouvrir des PR pour du code qui appartient à main.

Ne peut pas, sans l’accord explicite de Jared :

  • publier ou changer quoi que ce soit sur le Hub (kev.publish, hf upload, hf repos tag, scripts/publish_space.sh, un head.pt publié), rendre public un dépôt privé, ou déployer un point de terminaison public ;
  • commiter sur main, force-push, ou fusionner une PR (le code atteint main par des PR relues, fusionnées en squash, avec une CI verte) ;
  • modifier quoi que ce soit qui existe sous evals/ (gelé), ou l’évaluateur : kev/experiment.py: EVALUATOR_FILES, les gardes, kev/metrics.py, la lecture appariée de kev/rounds.py. Un changement d’évaluateur nécessaire fait sa propre PR, vérifiée avec kev-verify et tests/test_rounds.py, avant qu’une ronde n’en dépende ;
  • passer --allow-test ou exécuter locked_test hors d’une étape de confirmation enregistrée ;
  • mettre une sortie de Jev, ou une génération de modèle fermé, dans les données d’entraînement ;
  • entraîner en local (un Mac 32 GB ne peut pas contenir ces modèles) ni faire tourner deux processus d’entraînement sur une même machine ;
  • supprimer un checkpoint ou un instantané du volume runs (modal volume rm, shutil.rmtree dans un conteneur), ou désactiver les instantanés d’un essai à poids complets ("snapshot_fractions": "none") dans une spec enregistrée. Les essais à poids complets gardent des instantanés à 0.25, 0.5 et 0.75 de leurs pas (kev.experiment.SNAPSHOT_FRACTIONS) pour qu’une lecture puisse trouver le meilleur point d’une exécution après sa fin : la ronde 19 ne l’a pas pu, parce que le seul état en cours d’exécution était un point de reprise, supprimé à la fin de l’exécution, et le meilleur checkpoint d’AutoJev était à 0.7 époque. Les instantanés d’un 27B font ~154 GB de volume par essai ; cet espace relève de Jared, pas de la session. Les instantanés vivent sur le volume runs (principal) ; un miroir Hub privé (snapshot_hub_repo dans un plan, ou modal_app.py::mirror_snapshots) est un stockage à long terme pour un checkpoint qui vaut la peine d’être conservé, pas un remplacement : mettre en miroir des checkpoints 27B (~51 GB chacun, dans un dépôt privé comme jaredpalmer/kev-snapshots) relève aussi de Jared, et jamais d’un dépôt public.

Si un bras est bloqué (authentification, limite de dépense, un déploiement qui ne marchera pas en 30 minutes), note ce qui s’est passé et passe au bras suivant. N’attends pas un humain.

6. Résilience

  • Fichier d’état runs/<session>-state.json (runs/ est dans gitignore ; git add -f sur la branche de recherche) : référence et autorisation, relevés de dépense avec heures UTC, chaque étude avec ses ids de spawn, sa borne et son statut, les lectures lancées et récupérées, les candidats, les PR, les décisions en attente. Mets-le à jour après chaque lancement, récupération et lecture, et commite-le avec la section PLAN.
  • Jobs détachés. Les études se lancent sur l’app déployée et survivent au client local ; une erreur locale après study peut quand même avoir lancé des essais, donc exécute modal container list avant de relancer, et ne relance jamais sous le même nom d’étude. Les sondes et benchmarks tournent avec --detach.
  • Les surveillants sont des processus locaux et meurent avec la machine ou le réseau. Lance-les sous nohup et caffeinate ; redémarre watch après toute interruption (il reprend depuis son état). Il réessaie lui-même les erreurs de DNS et de connexion ; l’exception propre d’un essai est un échec et est signalée.
  • Les essais à poids complets arrivés à timeout sont poursuivis par le surveillant, pas par Modal. Les essais se lancent avec les réessais de Modal désactivés ; quand l’appel d’un essai à poids complets se termine par son timeout, watch exécute modal_app.py::resume --trial <label>, qui lance la tentative suivante (elle continue depuis le dernier point de reprise commité) avec le GPU et le timeout pour lesquels l’étude a été admise et le consigne dans runs/<study>.spawn.json (attempts, au plus 1 + kev.budget.FULL_FT_RETRIES par essai, le compte avec lequel la borne d’admission a été calculée ; un essai dont l’appel courant tourne encore n’est jamais poursuivi). Tant que le surveillant est arrêté, rien n’est poursuivi : redémarre-le et il reprend le timeout. Pourquoi : Modal facturait chaque tentative arrivée à timeout deux fois (le timeout, puis la tâche qu’il tuait 30 s plus tard), donc Retries(2) donnait à l’essai de la ronde 22 deux de ses trois tentatives, et le réessai du kill peut démarrer à côté d’une tentative en cours (scripts/modal_retry_probe.py). Une étude lancée avant le registre n’a pas de compte : resume --trial <label> --beyond-bound la poursuit à la main, hors de toute borne, et le dit. Deux tentatives ne partagent jamais un essai : chacune est consignée en attente avant son spawn, et chacune détient un bail sur le volume kev-leases (battement de cœur chaque minute) ; une nouvelle tentative refuse tant que le bail d’une autre est frais, et une continuation attend jusqu’à kev.budget.LEASE_STALE (15 min) après le dernier battement de cœur d’une tentative tuée avant de se lancer.
  • Les coupures réseau tuent les clients locaux, pas le travail distant : une lecture dont le client est mort a généralement fini sur Modal ; récupère son répertoire depuis le volume (modal volume get kev-runs /<name> runs/<name>) au lieu de la relancer.
  • Un benchmark ou une sonde qui échoue laisse son répertoire sur le volume ; réessaie sous un nouveau nom.

7. Rapport

À la fin de la session (et dans le fichier d’état au fil de l’eau) :

  • La section PLAN.md de chaque ronde porte son enregistrement, son tableau de read-out, ses résultats de confirmation et son verdict, négatif ou non, avec les chemins de rapport.
  • Mets à jour « Where we stand » de PLAN.md (candidats publiés et confirmés, jobs en cours, dépense) et « What we have learned » si un résultat a changé ; ajoute chaque ronde au tableau Record.
  • Un résumé de session dans PLAN.md : dépense (référence, relevé final, bornes en cours), ce qui est en attente sur Modal avec les commandes exactes pour le finir, les incidents, et au plus trois prochaines étapes avec leurs preuves.
  • Chaque chiffre porte checkpoint, suite et partition, n et chemin de rapport ; commite les read-outs et verdicts dont les chiffres proviennent (.gitignore garde les rapports, pas les dumps de prédiction ; ajoute une règle pour les nouveaux répertoires de read-out).
  • Horodatages : les heures d’enregistrement et de résultat sont des heures de commit. N’écris pas une heure dans un titre avant qu’elle n’arrive ; le brouillon de la nuit 3 l’a fait, et ses horodatages n’ont pas pu être utilisés.

8. Pièges connus

  • Limite de débit de création d’app Modal. Plus d’environ trois modal run détachés en une minute échouent avec « App create rate limit exceeded » et rien ne tourne. kev.rounds échelonne les lancements à 60 s d’intervalle et groupe les lectures d’un bras en un appel ; fais de même à la main.
  • repo@sha dans les jobs de benchmark décalait auparavant chaque champ de run@suite@name@flags ; modal_app.parse_jobs analyse désormais depuis la droite, donc les révisions Hub épinglées sont sûres. Les suites et les noms ne doivent pas contenir @ ni ,.
  • Une récupération par étude. Des récupérations simultanées de la même étude supprimaient mutuellement leurs répertoires d’essai ; pull_study détient désormais un verrou par étude. Une récupération pendant que des essais tournent encore est sûre et ne rafraîchit que les essais inachevés.
  • Les récupérations laissent les poids complets sur le volume. ::pull (et watch) saute les shards à poids complets (model*.safetensors, ~51 GB par checkpoint ou instantané 27B) et les points de reprise ; tout le reste descend (résultats, lignes, head.pt, configs). Lis un checkpoint ou un instantané sur le volume : ::benchmarks --jobs "/runs/<study>/<trial>/snapshots/step-<N>/checkpoint@<suite>@<name>". ::pull --weights copie les shards quand quelque chose en local en a vraiment besoin.
  • Déployer après les données. L’image copie evals/ ; le lanceur ne vérifie que les hachages de kev/*.py, donc un essai dont le fichier de données a été ajouté après le déploiement échoue dans le conteneur. --gpu H200 sur study a besoin d’une app déployée avec KEV_GPU=H200.
  • 27B. H200 uniquement (backbone bf16, 55 GB résidents) ; timeouts d’étude jusqu’à 28 800 s (un delta de compétences de 1 époque à lr 2e-5 a tourné environ 8.8 s par pas d’optimiseur) ; les lectures fp32 environ trois fois celles d’un 9B (spec read_timeout: {"27b": 14400}) ; la lecture verrouillée a besoin de --timeout 14400 --memory-mb 131072 (spec locked_args) sur un H200 (le GPU vient du gpu de la spec / de l’app déployée, ou de --gpu H200 à la main). Chaque essai à poids bf16 échoue à la garde isolation_and_packing en essai (une vérification fp32) ; lis les résultats depuis les lignes et mesure l’isolation servie en bf16 séparément.
  • Nommage de locked_test. Quand une garde de criblage en essai a échoué, l’outil exige le suffixe -ungated (kev-4b-r8-ungated) ; le verdict suit quand même la règle enregistrée.
  • Timeouts de lecture par suite. modal_app.READ_TIMEOUTS fixe les panneaux à états longs à 7 200 s, les documents à 5 400 s, transfer-v9 à 3 600 s, sinon 1 800 s. Un --timeout global gonfle la borne d’admission de chaque job du lot.
  • Admission de budget. Un lancement au-dessus de son --budget sort avant que quoi que ce soit tourne ; relance avec un budget au moins égal à la borne imprimée.
  • Les serveurs externes sont à vol unique. Le serveur d’AutoJev répondait à une requête à la fois (HTTP 529 quand occupé) ; sonde un point de terminaison étranger avant un long kev.benchmark --remote et règle --remote-concurrency sur ce qu’il peut prendre. Compte les requêtes qu’il rejette (par exemple un 422 au-delà de son contexte) comme de la couverture, ne les abandonne jamais en silence.
  • Température : livrée vs en essai. Le result.json d’un essai et son résumé verrouillé sont notés à l’ajustement en essai ; une publication livre la T que scripts/calibrate_checkpoint.py a écrite dans head.pt. Le premier face-à-face AutoJev servait Kev-27B à la valeur en essai 1.19 au lieu du 1.38 livré et a dû être corrigé. Dis quelle T chaque chiffre utilise, et où elle a été ajustée (section 3, règle 4 : jeux de données tenus à l’écart, jamais les partitions propres du corpus d’entraînement).
  • Calibration à contexte long. Un panneau avec "by_length": true rapporte exactitude, ECE, Brier et erreurs confiantes par bucket de jetons d’état (sous 8k jusqu’à 64k+, et les queues 8k+/16k+/32k+), et un critère peut en garder un (long.ece_16k_plus.candidate <= 0.05). Les jetons sont comptés depuis les enregistrements de suite des lectures, donc les deux côtés partagent les buckets.
  • Corrections de suite sans nouvelle version de suite. Un panneau peut retirer des sources, des tâches ou une liste (privée, enregistrée par hachage) d’ids ou de sources des deux côtés (exclude_sources, exclude_tasks, exclude_file), et un panneau en rapport seulement est marqué "optional": true pour qu’une lecture de rapport manquante ne rende jamais un candidat incomplet. Choisis les exclusions sur la validité des étiquettes avant tout read-out en dessous (la ronde 24 les a prises de l’audit du 2026-09-27), et ne commite jamais une liste privée.
  • Capacité de l’espace de travail. L’espace de travail a fait tourner au plus une dizaine de conteneurs GPU à la fois ; des conteneurs en attente sont de la capacité, pas un bug, donc ne les relance pas.
  • Petites suites. Une garde sur 89 ou 144 questions ne peut pas résoudre un plancher de 2-3 pp ; garde-les via un panneau mis en pool.
  • Les lectures Jev échouent en cours d’exécution sur des 503 de passerelle ; relance toute la lecture sous un nouveau nom plutôt que de recoudre des lignes partielles.
  • Données à cible souple. Quand tu écris un constructeur, vérifie une poignée d’enregistrements à l’œil : target somme à 1 et la masse de l’étiquette est d’au moins 0.5 sauf si l’enregistrement est inconnaissable (kev.data.none_pair a un jour entraîné une masse nulle sur des cibles souples, corrigé dans #60).
  • Redirige la sortie de modal run ...::study vers un fichier de log ; un filtre peut masquer le SystemExit qui explique pourquoi rien n’a été lancé.