Skip to content

feat(lean,#15666): sous-commandes operateur dry-run et doctor dans lean_exec - #20011

Merged
myia-ai-01 merged 1 commit into
mainfrom
feature/15666-lean-exec-operator-interface
Oct 9, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
feature/15666-lean-exec-operator-interface

Conversation

@jsboige

@jsboige jsboige commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Grain: MED/tooling — lane myia-po-2027:CoursIA-2 — prev: DEEP/lean #20000

Case 8 de l'EPIC #15666, premier volet : l'interface operateur de diagnostic.
lean_exec.py n'exposait que run, status, backends — pas de doctor, pas de
dry-run (grep dry.?run|doctor : 0 occurrence dans le fichier).

Summary

  • dry-run : « que ferait un run ? » — backend qui serait choisi, ressources
    mesurees, parallelisme qui serait accorde, verdict d'admission.
  • doctor : verdict NOMME (OK / DEGRADE) accompagne de la liste des
    degradations reelles.
  • resolve_backend(dry=True) : la decision est rendue sans etre consommee
    (aucun epinglage ecrit).

Changes

  • scripts/lean/lean_exec.py — dry_run(), doctor(), resolve_backend(dry=...),
    en-tete de module, deux sous-parsers CLI.
  • scripts/lean/tests/test_lean_exec.py — 8 tests.

Deux proprietes qui portent le livrable

1. Lecture seule. L'invariant n'est pas « la sortie est jolie » mais « rien n'a
bouge sur le disque » : pas d'epinglage de backend, pas de lease d'arbre, pas
d'entree de file, pas de verrou d'admission. Seul ecrit : la creation idempotente
du state dir, que measure_resources exige pour lire l'espace disque.

Consequence de conception : resolve_backend ecrit l'epinglage premier-ecrivain.
Un dry-run qui l'appellerait tel quel laisserait le run reel avec un choix qu'il
n'a pas fait. D'ou un flag dry et un point d'ecriture unique (_record).

2. Le verdict reproduit la chaine d'admission DANS SON ORDRE — population,
telemetrie, budget, cap, cap enregistre, backend (cf run_command._attempt). Un
ordre different rendrait un verdict plausible et faux : le premier refus annonce
doit etre celui que le run reel produirait.

Ecart mesure au passage

granted se calcule sur cfg["jobs"] — le parallelisme -Kjobs=N accorde a
l'enfant — tandis que budget est le nombre de places prises dans le cap
machine-wide. Deux grandeurs distinctes, que run_command calcule separement.
Le premier jet de dry-run les confondait (il passait budget a
compute_granted) ; c'est
test_dry_run_budget_agrees_with_status_admission qui l'a attrape, en confrontant
la prediction a la mesure de status.

doctor ne balaie pas, contrairement a status : un diagnostic qui supprime les
fichiers qu'il vient de lire n'est plus un diagnostic, et ses chiffres cessent
d'etre reproductibles. Un etat simplement incomplet (aucun lake encore epingle)
est une note, pas une degradation — un diagnostic qui crie au loup sur une
installation neuve ne sert a rien.

Ce que cette PR ne fait pas

Le case 8 a deux volets. Celui-ci livre le diagnostic. Restent ouverts, et
documentes dans l'en-tete du module : l'arret d'urgence des runs possedes par
l'organe (inspecter puis arreter, sans tuer aveuglement un travail etranger) et la
procedure de recuperation apres crash du superviseur. status et doctor
exposent deja les runs vivants et les leases sur lesquels ces deux volets
s'appuieront.

Review Checklist

  • 1. Scope — deux sous-commandes et le flag dry qu'elles exigent ; aucun
    autre changement dans le fichier.
  • 2. Post-fix validation — suite complete relancee APRES le dernier commit :
    python -m pytest scripts/lean/tests/test_lean_exec.py -q →
    54 passed, 1 skipped (le skip est pre-existant : motif native_pop,
    ci(tests): test_lean_exec.py rougit Scripts Tests selon le runner (5 echecs, exit 125) depuis le 05/10 #19382).
  • 3. Pedagogical coherence — sans objet (aucun notebook).
  • 4. Real execution — sans objet (aucun notebook). Les deux sous-commandes
    sont executees pour de vrai dans les tests, pas seulement importees.
  • 5. Regression check — resolve_backend n'a qu'un autre appelant
    (run_command, appel par mot-cle, signature preservee : dry est ajoute avec
    un defaut False). grep des definitions : dry_run, doctor et _record
    apparaissent une fois chacun. Aucun autre fichier du depot n'appelle
    lean_exec.

Anti-regression

  • Aucun sorry (aucun .lean touche).
  • Aucun @pytest.skip / assert True ajoute pour contourner un echec. Le seul
    pytest.skip nouveau est un garde de mesure : il ne se declenche que si la
    population native bouge entre deux mesures de la meme machine — ce qui rend la
    comparaison non concluante — et il le dit.
  • Le chemin reel d'epinglage est preserve : dry=False reste le defaut, et
    test_resolve_backend_dry_decision_identical_without_writing verifie que le
    mode reel ecrit toujours l'epinglage.

Test plan

python -m pytest scripts/lean/tests/test_lean_exec.py -q
# 54 passed, 1 skipped

# les 8 nouveaux, seuls :
python -m pytest scripts/lean/tests/test_lean_exec.py -q \
  -k "dry_run or doctor or resolve_backend_dry"
# 8 passed

# inspection manuelle (state dir isole) :
LEAN_EXEC_FORCE_BACKENDS=native python scripts/lean/lean_exec.py doctor
LEAN_EXEC_FORCE_BACKENDS=native python scripts/lean/lean_exec.py dry-run --cwd <un lake>

Les 8 tests : dry n'ecrit rien / dry repin preserve l'epingle / CLI sans trace (ni
epinglage, ni file, ni lease, ni run) / refus sur cache orphelin sans epingler /
budget predit == budget mesure par status / doctor OK / doctor DEGRADE sans
backend disponible / differentiel doctor ne balaie pas vs status balaie.

Le dernier porte son propre controle : il verifie que status balaie bien le
fichier perime, sinon le test serait vacant — il passerait aussi si le balayage
etait casse des deux cotes.

See #15666

🤖 Generated with Claude Code

…s lean_exec

Case 8 de l'EPIC #15666 : le diagnostic lecture-seule manquait — `lean_exec.py`
n'exposait que `run`, `status`, `backends` (grep `dry.?run|doctor` : 0 occurrence).

- `dry-run` : backend qui serait choisi, ressources mesurees, parallelisme qui
  serait accorde, verdict d'admission. Reproduit la chaine de `run_command`
  (`_attempt`) DANS SON ORDRE — population, telemetrie, budget, cap, cap
  enregistre, backend — pour que le premier refus annonce soit celui que le run
  reel produirait ; un ordre different rendrait un verdict plausible et faux.
- `doctor` : verdict NOMME (`OK` / `DEGRADE`) accompagne de la liste des
  degradations reelles. Ne balaie RIEN, contrairement a `status` : un diagnostic
  qui supprime les fichiers qu'il vient de lire n'est plus un diagnostic, et ses
  chiffres cessent d'etre reproductibles. Un etat simplement incomplet (aucun
  lake encore epingle) est une note, pas une degradation.
- `resolve_backend(dry=True)` : la decision est rendue sans etre consommee —
  aucun epinglage n'est ecrit. Une previsualisation qui epinglerait laisserait le
  run reel avec un choix qu'il n'a pas fait.

Ecart mesure au passage : `granted` se calcule sur `cfg["jobs"]` (le
parallelisme `-Kjobs=N` accorde a l'enfant), tandis que `budget` est le nombre de
places prises dans le cap machine-wide — deux grandeurs distinctes que
`run_command` calcule separement, et que le premier jet de `dry-run` confondait.

Reste ouvert dans le meme case 8, documente dans l'en-tete du module : l'arret
d'urgence des runs possedes par l'organe et la procedure de recuperation apres
crash du superviseur.

Tests : 8 ajoutes — dry n'ecrit rien / dry repin preserve l'epingle / CLI sans
trace (ni epinglage, ni file, ni lease, ni run) / refus sur cache orphelin sans
epingler / budget predit == budget mesure par `status` / doctor OK / doctor
DEGRADE sans backend disponible / differentiel `doctor` ne balaie pas vs `status`
balaie (controle du balayage inclus, sinon le test serait vacant). Suite
complete : 54 passed, 1 skipped (skip pre-existant sur population native).

See #15666

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml).

Detector: python scripts/audit/detect_organ_duplication.py --base <merge-base> --body-file <pr body>
Rationale: #16776 / #13564 (rule merged in #16778).

@github-actions github-actions Bot added the variation-adjacency-deep-med Adjacence DEEP/MED hors LIGHT : §2 l'autorise si substance distincte (coordinateur) label Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #20011 (feat(lean,#15666): sous-commandes operateur dry-run et doctor dans lean_exec) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

Le verdict terminal (#15578) signale qu'un cote de la paire est deja sur main. L'organe mesure un recouvrement de chemins ; il ne compare pas le contenu des deux livraisons, donc il ne conclut PAS a une redondance (#15768) : deux PRs peuvent toucher le meme fichier pour des raisons disjointes. L'arbitrage reste a la lane ou au coordinateur.

jsboige pushed a commit that referenced this pull request Oct 9, 2026
… (case 8 volet 2b, complet)

Le confinement fait deja mourir l'arbre avec le superviseur ; ce qui
survit a un crash, c'est l'ETAT (run records, leases d'arbre, entrees de
file au pid mort). `recover` partitionne les TROIS familles
(vivant/mort/etranger/illisible), inspecte par defaut, et balaye sur
--yes en reutilisant les balayages existants (sweep_stale_runs,
sweep_stale_tree_leases, peremption de queue_enter extraite en
_sweep_stale_queue) — jamais une seconde semantique de peremption. Un
item d'un host etranger ou un record illisible n'est jamais touche. Le
rc reste 0 (contrat doctor : le verdict vit dans la sortie). Case 8
complet : diagnostic (#20011), arret d'urgence (stop), recuperation.

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
@jsboige

jsboige commented Oct 9, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 20011
head: eeb8514
complete: true
body: read
comments-reviewed: 2
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: be3a8cd1b60beeb12598ec22c1dad728089e77b329635c63237371f569f81a18
diff-files: 2
diff-additions: 576
diff-deletions: 13
checks: latest-wins-green
b0: clear
scope: pass
domain: not-applicable
verdict: READY
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 20011
organ-rc: 0
[/ADJOINT PREFLIGHT]

@myia-ai-01
myia-ai-01 merged commit 64929c9 into main Oct 9, 2026
19 of 20 checks passed
myia-ai-01 pushed a commit that referenced this pull request Oct 10, 2026
…t recuperation apres crash (case 8, complet) (#20016)

* feat(lean,#15666): sous-commandes operateur `dry-run` et `doctor` dans lean_exec

Case 8 de l'EPIC #15666 : le diagnostic lecture-seule manquait — `lean_exec.py`
n'exposait que `run`, `status`, `backends` (grep `dry.?run|doctor` : 0 occurrence).

- `dry-run` : backend qui serait choisi, ressources mesurees, parallelisme qui
  serait accorde, verdict d'admission. Reproduit la chaine de `run_command`
  (`_attempt`) DANS SON ORDRE — population, telemetrie, budget, cap, cap
  enregistre, backend — pour que le premier refus annonce soit celui que le run
  reel produirait ; un ordre different rendrait un verdict plausible et faux.
- `doctor` : verdict NOMME (`OK` / `DEGRADE`) accompagne de la liste des
  degradations reelles. Ne balaie RIEN, contrairement a `status` : un diagnostic
  qui supprime les fichiers qu'il vient de lire n'est plus un diagnostic, et ses
  chiffres cessent d'etre reproductibles. Un etat simplement incomplet (aucun
  lake encore epingle) est une note, pas une degradation.
- `resolve_backend(dry=True)` : la decision est rendue sans etre consommee —
  aucun epinglage n'est ecrit. Une previsualisation qui epinglerait laisserait le
  run reel avec un choix qu'il n'a pas fait.

Ecart mesure au passage : `granted` se calcule sur `cfg["jobs"]` (le
parallelisme `-Kjobs=N` accorde a l'enfant), tandis que `budget` est le nombre de
places prises dans le cap machine-wide — deux grandeurs distinctes que
`run_command` calcule separement, et que le premier jet de `dry-run` confondait.

Reste ouvert dans le meme case 8, documente dans l'en-tete du module : l'arret
d'urgence des runs possedes par l'organe et la procedure de recuperation apres
crash du superviseur.

Tests : 8 ajoutes — dry n'ecrit rien / dry repin preserve l'epingle / CLI sans
trace (ni epinglage, ni file, ni lease, ni run) / refus sur cache orphelin sans
epingler / budget predit == budget mesure par `status` / doctor OK / doctor
DEGRADE sans backend disponible / differentiel `doctor` ne balaie pas vs `status`
balaie (controle du balayage inclus, sinon le test serait vacant). Suite
complete : 54 passed, 1 skipped (skip pre-existant sur population native).

See #15666

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>

* feat(lean,#15666): sous-commande `stop` — arret d'urgence des runs possedes (case 8 volet 2a)

Defaut = INSPECT : lister les runs vivants de CET host sans rien toucher ;
--yes seul arrete. Le pid tue vient exclusivement du run record, jamais
d'un scan de table de processus ; un run d'un host etranger est liste et
saute, jamais tue (pids non comparables entre namespaces, meme regle que
sweep_stale_runs). Records morts retires seulement en mode action ; un
record illisible n'est jamais retire a l'aveugle. Codes stables etendus :
1 = echec d'arret, 125 = run record inconnu. Controle positif de bout en
bout : un vrai processus tue par taskkill/killpg via stop --yes.

Branche empilee sur feature/15666-lean-exec-operator-interface (#20011) :
le merge de la base en squash exigera un rebase --onto de celle-ci.

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>

* feat(lean,#15666): sous-commande `recover` — recuperation apres crash (case 8 volet 2b, complet)

Le confinement fait deja mourir l'arbre avec le superviseur ; ce qui
survit a un crash, c'est l'ETAT (run records, leases d'arbre, entrees de
file au pid mort). `recover` partitionne les TROIS familles
(vivant/mort/etranger/illisible), inspecte par defaut, et balaye sur
--yes en reutilisant les balayages existants (sweep_stale_runs,
sweep_stale_tree_leases, peremption de queue_enter extraite en
_sweep_stale_queue) — jamais une seconde semantique de peremption. Un
item d'un host etranger ou un record illisible n'est jamais touche. Le
rc reste 0 (contrat doctor : le verdict vit dans la sortie). Case 8
complet : diagnostic (#20011), arret d'urgence (stop), recuperation.

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>

* fix(lean,#15666): stop --yes ne suicide plus sous POSIX et ne ment plus sur un zombie

Trois corrections, toutes mesurees sous POSIX (WSL Ubuntu, pytest 9.1.1) et
sur Windows, sur `scripts/lean/tests/test_lean_exec.py` :

- Controle e2e `stop --yes` : le sleeper vit desormais dans sa propre session
  (`start_new_session=True`), sinon le `killpg` de `stop --yes` emporte la
  session de pytest elle-meme (reproduit : SIGKILL -9, deux fois). Miroir de
  la production, qui spawne toujours en session propre. Precondition POSIX
  explicite, pour que le controle ne redevienne pas suicidaire en silence.
- `pid_alive` : un zombie POSIX n'est pas un processus vivant. `os.kill(pid, 0)`
  REUSSIT sur un zombie (l'entree de table survit au reap par le parent), donc
  `stop --yes` rendait `failed: pid survit au kill` sur un kill REUSSI, et les
  sweeps lisaient un run mort comme vivant. Lecture du champ 3 de
  `/proc/<pid>/stat`, fail-safe vers « vivant » si procfs est illisible.
  Controle par faux positif ajoute (`test_pid_alive_rejects_reaped_pending_zombie`).
- `queue_enter` : rewire sur `_sweep_stale_queue`. L'extraction annoncee etait
  une copie, la semantique de peremption vivait en deux endroits.

Tests : 63 collectes. Windows 61 passed / 2 skipped ; POSIX 54 passed /
9 skipped ; zero echec sur les deux plateformes (avant : la jambe POSIX
mourait en SIGKILL, puis rendait `failed: pid survit au kill`).

Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

variation-adjacency-deep-med Adjacence DEEP/MED hors LIGHT : §2 l'autorise si substance distincte (coordinateur)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants