Skip to content

Fix(#20251): porte « pile » -- le gate ne voyait pas les sommets de pile - #20263

Open
jsboige wants to merge 4 commits into
mainfrom
fix/20251-porte-pile
Open

jsboige wants to merge 4 commits into
mainfrom
fix/20251-porte-pile

Conversation

@jsboige

@jsboige jsboige commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Grain: DEEP/tooling — lane myia-ai-01:CoursIA-2 — prev: DEEP/notebook-python #20257

See #20251.

Le defaut, et ce qui l'a rendu possible

L'organe juge une PR contre main, a sa tete exacte. Il ne voyait pas
qu'une autre PR ouverte porte une version plus recente des memes commits :
un sommet de pile sortait READY, et le merger en premier faisait atterrir sur
main la version anterieure aux corrections que sa propre base venait de
recevoir.

Instance fondatrice, le 2026-10-10 : la pile Percolation #19548 -> #19556 -> #19567, les trois de base main. #19567 avait ete rebatie avant les
corrections poussees ensuite sur #19548 et #19556 (enonce en Phi_iso(n),
instrument R2 et verdict INCONCLUSIVE, re-execution C.2, reponse a un
REQUEST_CHANGES). Le gate a rendu READY, #19567 a ete mergee en premier,
et main a recu les versions anterieures.

La porte

Elle vit dans derive_verdict() — le point de derivation unique que
consomment --derive-verdict, --queue (via classify_snapshot) et
refute_ready_verdict. Les deux modes demandes sont donc couverts par
construction
, pas par deux ajouts paralleles.

Et c'est refute_ready_verdict qui donne a la porte sa portee reelle. Le
merge organe (merge_ready.run_gate) lance check_adjoint_prevalidation.py <PR> --json, dont le flot passe par refute_ready_verdict -> derive_verdict. La
porte ne protege donc pas seulement la lane emettrice : elle atteint le chemin
du merge — ce qui est exactement l'enjeu, puisque c'est un merge de sommet
de pile qui a fait atterrir la version anterieure sur main. Une porte qui
n'aurait couvert que l'emission aurait laisse passer l'incident fondateur. Deux
tests d'integration tiennent cette assertion (et son controle negatif).

Quatre decisions de mecanique, chacune contre un faux positif precis :

Decision Contre quoi
Les commits s'apparient par sujet, jamais par SHA un rebase reecrit les SHA ; un sommet de pile est une branche rebatiee
Silence quand la tete visee est un ancetre de l'autre le bas d'une pile lineaire se merge en premier correctement : c'est le controle negatif qui distingue la porte d'un « deux PRs se ressemblent »
Sujets de re-declenchement ignores (empty commit, regenerer, render-list) deux PRs qui ne different que par un vidage de commit ne sont pas une pile
Mesure impossible -> aucune raison, un STACK (WARN) sur stderr l'echec de mesure prive l'organe d'une porte ; il ne l'autorise pas a refuser

main est rafraichi dans le meme git fetch que les tetes de PR : la
mesure ne depend pas de la fraicheur du clone de l'appelant. Un lot dont une
ref a disparu echoue entierement, donc la boucle retire la fautive et relance
(6 passes) — une branche supprimee est ecartee, jamais un incident.

La passe ecrit une ref par PR ouverte dans le clone de l'appelant, donc elle
elague aussi les siennes : une ref hors de l'ensemble courant n'est pas une
mesure, c'est un residu d'une passe precedente. Sans cela il en resterait une
par PR jamais nettoyee, indefiniment. Cout nul quand rien n'a ferme depuis.

--stack-index (nouveau) rend lisible ce que la porte lit. C'est l'instrument
du controle positif ci-dessous, et le geste de diagnostic quand un dossier
READY se fait refuser par une pile.

Controle positif, rejoue sur l'etat reel

python scripts/check_adjoint_prevalidation.py --stack-index sur le depot
courant — 128 PR ouvertes, 12 sommets de pile identifies, 7,1 s :

#19548 : 23 commits propres, sommet de pile de [19556]
   -> stack top: shares 15 commit(s) with OPEN #19556, which carries 9 commit(s)
      absent here -- merging this one first would land the earlier version on
      main (e.g. 'Fix(percolation,#19556): TOC entry 4 -- Phi definition
      alignee (n <= |C_o| < inf)')
#19556 : 28 commits propres, sommet de pile de [19548]
   -> stack top: shares 15 commit(s) with OPEN #19548, which carries 5 commit(s)
      absent here -- merging this one first would land the earlier version on
      main (e.g. 'Fix(percolation,#19548): fenetre beta/nu positive + fit
      log|deviation| + provenance 32 seeds')

La porte reproduit l'instance fondatrice, avec les deux sujets mesures
firsthand. Les deux PR sont signalees parce que la divergence est mutuelle :
chacune porte des commits que l'autre n'a pas — aucun ordre n'est correct, il
faut un arbitrage.

Ce que le critere 3 de l'issue ne permet plus de rejouer. Il demandait le
controle positif sur l'etat #19548 / #19556 / #19567 avant le merge de
#19567. #19567 est MERGED — l'etat d'avant n'existe plus. Le controle
est donc rejoue sur la seule paire encore reproductible, #19548 / #19556, qui
porte exactement le meme defaut. Je ne peux pas prouver que la porte aurait
refuse #19567 sur l'etat d'alors : je peux prouver qu'elle refuse aujourd'hui
la paire dont #19567 etait le sommet.

Portee, dite franchement

Sur les 128 PRs ouvertes, 12 deviennent non-READY tant que leur pile n'est
pas resolue. C'est le cout assume : la porte est conservatrice, son mode
d'echec est « ne pas merger encore », et un dossier deja emis sur un sommet de
pile est refute a la prochaine evaluation. Les 12 se lisent en une commande
(--stack-index), et 5 des 12 sont des paires mutuellement divergentes
(#19548/#19556, #19599/#20143, #19830/#19834, #20234/#20245) — celles-la
exigent une decision humaine, pas un ordre de merge.

Le rafraichissement de main ajoute un git fetch de ~128 refs par processus
(~7 s a froid, mis en cache une fois par invocation : --queue ne le paie pas
par candidat).

Validation

  • 213 tests de scripts/tests/test_check_adjoint_prevalidation.py, tous
    verts
    (0,5 s) — dont 11 neufs, tous hermetiques : l'index est injecte
    et git est simule. Un garde autouse empeche tout appel reseau par la
    porte dans la suite.
  • Les 4 tests d'acceptation de l'issue : (1) sommet de pile divergé ->
    BLOCKED avec cause nommee ; (2) bas de pile lineaire -> READY inchange ;
    (3) PR sans commit partage -> inchangee ; (4) ref de tete disparue -> ecartee,
    aucun incident. Sept tests supplementaires : sujets de re-declenchement
    seuls -> inchange ; mesure impossible -> incident et non refus ; cache paye
    une fois par processus ; elagage des refs residuelles verifie dans les deux
    sens ; la porte atteint le chemin du merge organe (refute_ready_verdict)
    et son controle negatif.
  • Controle positif rejoue sur le depot reel (ci-dessus).
  • Elagage verifie de bout en bout sur le clone reel : une ref
    refs/remotes/prh/999999 posee a la main disparait apres une passe de
    --stack-index.
  • Aucun test de regression sur main : les 11 echecs de scripts/tests/
    (test_garch_baseline.py, test_check_lane_claim.py,
    test_render_oversized_nbconvert.py) sont identiques sur un worktree propre
    a origin/main
    — mesure faite, pas supposee.

Note d'environnement (hors PR)

Le quota GraphQL du jeton jsboige etait epuise pendant la session
(API rate limit already exceeded for user ID 3159389) : le controle positif a
donc ete fait par l'organe REST + git (--stack-index, build_stack_index +
stack_reasons, qui sont exactement ce que derive_verdict appelle), et non par
un --derive-verdict de bout en bout. Le gate a rendu UNKNOWN (rc 2) sur
l'appel GraphQL — le comportement attendu, et non un faux verdict. Un
--derive-verdict 19548 de bout en bout reste a repasser apres reinitialisation
du quota.

🤖 Generated with Claude Code

L'organe juge UNE PR contre `main` a sa tete exacte, et ne voyait pas qu'une
autre PR ouverte porte une version plus recente des memes commits. Instance
fondatrice du 2026-10-10 : la pile Percolation #19548 -> #19556 -> #19567, les
trois de base `main`. #19567 avait ete rebatie avant les corrections poussees
ensuite sur #19548 et #19556 ; le gate a rendu READY, #19567 a ete mergee en
premier, et main a recu les versions anterieures.

La porte vit dans derive_verdict(), le point de derivation unique consomme par
--derive-verdict, --queue et refute_ready_verdict : les deux modes demandes sont
couverts par construction. Les commits s'apparient par SUJET (un rebase reecrit
les SHA, et un sommet de pile est une branche rebatiee) ; la porte se tait quand
la tete visee est un ancetre de l'autre, pour laisser passer le BAS d'une pile
lineaire ; les sujets de re-declenchement sont ignores ; une mesure impossible
prive l'organe de la porte sans fabriquer de refus. `main` est rafraichi dans le
meme `git fetch` que les tetes, donc la mesure ne depend pas de la fraicheur du
clone de l'appelant.

Controle positif rejoue sur l'etat reel : 128 PR ouvertes, 12 sommets identifies,
dont #19548/#19556 avec les deux sujets mesures firsthand. Le critere 3 de
l'issue demandait l'etat avant le merge de #19567, qui est MERGED : le controle
est rejoue sur la seule paire encore reproductible, et la PR le dit.

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

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).

…appelant

La passe ecrit une ref par PR ouverte dans le clone de l'appelant, et rien ne
les nettoyait jamais : une ref par PR jamais fermee s'y accumulerait pour
toujours. Une ref hors de l'ensemble courant n'est pas une mesure, c'est un
residu d'une passe precedente -- elle se supprime. Cout nul quand rien n'a
ferme depuis la derniere passe.

Verifie de bout en bout sur le clone reel : une ref `refs/remotes/prh/999999`
posee a la main disparait apres une passe de `--stack-index`. 2 tests neufs
(211/211 verts, suite toujours hermetique).

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

Copy link
Copy Markdown
Contributor

G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
La lane `myia-ai-01:CoursIA-2` voit ces signaux actifs sur les mergees du jour (UTC 2026-10-10) :

G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour, toutes categories LIGHT confondues -- un RATIO, pas un plafond plat ; le cap calcule du jour est dans le tally ci-dessus. G-VAR-3 interdit deux genres LIGHT consecutifs. Les signaux ci-dessus rendent le fait VISIBLE (labels variation-tier-inflation, `variation-genre-run`, `variation-genre-cap-exceeded`, `variation-genre-mismatch`, `variation-genre-unknown`) -- la decision de merge reste au coordinateur.

…nt l'emission

L'assertion d'integration qui compte : `merge_ready.run_gate` lance
`check_adjoint_prevalidation.py <PR> --json`, dont le flot passe par
`refute_ready_verdict` -> `derive_verdict`. La porte ne protege donc pas
seulement la lane emettrice, elle atteint le chemin du merge -- ce qui est
exactement l'enjeu, puisque c'est un merge de sommet de pile qui a fait
atterrir la version anterieure sur main. Une porte qui n'aurait couvert que
l'emission aurait laisse passer l'incident fondateur.

Deux tests : la porte demote un dossier READY sur le chemin du merge, et son
controle negatif (elle reste silencieuse quand il n'y a pas de pile), sans
lequel le premier passerait aussi sur une porte qui refuse tout.

213/213 verts.

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

jsboige commented Oct 10, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 20263
head: 5a10afa
complete: true
body: read
comments-reviewed: 2
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: e891e55d44603ce6fa0859c6fca589c3fd1589d0a1c352c7fef11961f27b5dee
diff-files: 2
diff-additions: 619
diff-deletions: 3
checks: latest-wins-green
b0: clear
scope: pass
domain: fail
verdict: BLOCKED
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 20263
organ-rc: 0
[/ADJOINT PREFLIGHT]

Lecture tierce du body complet, des deux commentaires, des reviews (aucune), threads (aucun) et du diff. La tete est inchangée et la derivation mecanique verte. Le lecteur independant rapporte 213/213 tests et reproduit le controle positif sur les refs Percolation ; integration derive_verdict/refute_ready_verdict corroborée. Ces acquis ne couvrent pas la limite suivante, relue directement par le parent.

Le body annonce « Les commits s'apparient par sujet, jamais par SHA » pour couvrir les branches rebaties par rebase. Or scripts/check_adjoint_prevalidation.py:1129-1130 exige d'abord une intersection NON VIDE de SHA et saute sinon la paire. Les sujets ne mesurent ensuite que la divergence (:1137-1139). Une pile reconstruite sans SHA commun n'est donc pas examinée, meme avec des sujets communs. L'issue #20251 explique explicitement le recours aux sujets pour les rebases ; le body affirme cette couverture alors que le code ne la fournit pas. Je ne peux attester ce contrat en l'etat.

Correction attendue de la lane porteuse : soit fournir un appariement resistant a la reecriture des SHA, avec controles apparies (pile reconstruite positive, sujets fortuitement communs negatifs), soit expliciter dans le body la restriction aux historiques partageant des SHA et obtenir l'accord du coordinateur sur cette reduction de portee. Pas de correspondance par sujet seul ajoutee aveuglement : eviter les faux positifs reste necessaire. Après correction, nouvelle lecture de domaine et gabarit frais.

La reserve porte sur la couverture annoncee, pas sur les 213 tests ni sur le controle positif acquis. Aucun verdict APPROVED/CHANGES_REQUESTED, merge ou arbitrage de flotte emis par l'adjoint.

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #20263 (Fix(#20251): porte « pile » -- le gate ne voyait pas les sommets de pile) 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.

…rits

Constat adjoint (dossier c6098840404) : _stack_partners exigeait une
intersection de SHA non vide avant toute comparaison, alors que le body
promet un appariement « par sujet, jamais par SHA » -- or un rebase
reecrit precisement tous les SHA. Un sommet de pile rebati (le cas que
la porte existe pour attraper) passait silencieusement.

Repli par SUJETS partages (hors bruit) quand l'intersection de SHA est
vide ; les controles aval -- silence d'ancetre lineaire, apport reel
non-bruit -- restent appliques, le repli n'elargit que la detection du
candidat. Option (a) de l'adjoint retenue (appariement resistant a la
reecriture), temoins apparies : le sommet rebati echoue a la porte
pre-fix, 217/217 post-fix.

See #20263

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

jsboige commented Oct 10, 2026

Copy link
Copy Markdown
Owner Author

Réserve du dossier c6098840404 (appariement sujet/SHA) traitée en a84168213 : _stack_partners replie désormais sur les sujets partagés (hors bruit) quand l'intersection de SHA est vide — l'option (a) demandée, appariement résistant à la réécriture, avec témoins appariés : pile rebâtie positive (elle échouait à la porte pré-fix), sujets fortuitement communs négatifs ; les contrôles aval (ancêtre linéaire, apport réel non-bruit) restent appliqués.

Suite locale reproduite à la tête exacte : 217/217 passés (scripts/tests/test_check_adjoint_prevalidation.py, 0,44 s). Le PR gate de la tête est annulé STARVED (pool saturé) — les jambes sont en file, le balayage les rejouera ; aucun rouge propre à la PR. Lecture de domaine et gabarit frais bienvenus à a84168213.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant