Skip to content

Fix(ci,#22920): twin-parity distingue la branche perimee (paire deplacee sur main) de la derive reelle - #22922

Merged
myia-ai-01 merged 1 commit into
mainfrom
fix/22920-twin-parity-stale-branch
Oct 27, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
fix/22920-twin-parity-stale-branch

Conversation

@jsboige

@jsboige jsboige commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Grain: MED/guard -- lane myia-ai-01:CoursIA-2 -- prev: MED/tooling #22861

Sujet

Le mode --per-pair de l'organe twin-parity confond la branche périmée (une paire de carnets a été déplacée sur main après le fork de la branche de PR : le registre lu dans l'arbre de la PR déclare encore l'ancien chemin) avec la dérive réelle de contenu. Le gate classait alors la paire DRIFT_INTRODUCED et prescrivait --update --pair — un remède faux dans ce cas : il attesterait l'empreinte d'un chemin que main a déjà abandonné, et l'attestation régresserait le registre au merge (écrasement de la déclaration à jour de main, ou conflit). Le remède correct est gh pr update-branch. Voir #22920 (claim + mesures).

Ce que la PR change

Verdict dédié STALE_BRANCH, décidé en amont de _classify_per_pair dans la boucle per-pair (la fonction reste pure et inchangée : la table des 9 combinaisons de test_check_twin_parity.py:53-66 reste valide).

Discriminant (_stale_branch_probe, scripts/notebook_tools/check_twin_parity.py:1741), pour chaque paire présente aux deux refs :

  1. divergence des chemins déclarés (test pur, aucun appel git — _pair_paths_diverge, :1709) ;
  2. sondes croisées d'existence (2 git ls-tree via _git_blob_sha, uniquement pour les paires divergentes) : chemin du base absent de l'arbre de la tête, OU chemin de la tête absent du base ;
  3. orientation par le point de fork (merge-base — _merge_base_ref, :1723) : le chemin déclaré par la tête existait au fork ET le chemin déclaré au base n'y existait pas.

Écart assumé avec le discriminant de l'issue : la condition 3 (merge-base) est nécessaire en plus des deux sondes croisées spécifiées. Preuve : les scénarios « paire déplacée par main, branche en retard » et « paire renommée par la PR » sont des images miroir exactes sur les seuls arbres base/tête — mêmes quatre observations (chemin du base présent au base / absent de la tête, chemin de la tête présent à la tête / absent du base). Aucune règle sur ces quatre booléens ne les sépare ; l'exclusion « chemin de la tête absent du base et présent à la tête » demandée pour le rename-PR s'appliquerait aussi au cas périmé. Une sonde orientée (une ref antérieure aux deux branches) est le seul discriminateur. Le merge-base n'est résolu que si au moins une paire diverge (garde de coût pincée par un test), et un merge-base irrésolvable dégrade vers le comportement existant (pas de verdict STALE, jamais de faux verdict).

Rapport et gate (:2255, :2283, :2317, :2329, :2339, :2357) :

  • chaque paire STALE porte paths_base / paths_head (les deux jeux de chemins déclarés) ;
  • compteur dédié stale_branch ; ces paires ne comptent jamais dans drift_introduced ;
  • le gate rougit aussi sur stale_branch > 0 : une paire périmée exige une action lane, et le bloc de remédiation du workflow (où vit la ligne gh pr update-branch) n'est rendu que sur exit 1 — un verdict muet laisserait la lane merger sans jamais voir le remède. Le rappel stderr « strips » n'est émis que s'il existe un vrai DRIFT_INTRODUCED.

Remédiation sans rebaseline (split_stale_pairs, :1858 ; _stale_remediation_line, :1870 ; render_introduced_remediation étendu aux trois lots, :1892-1928) : ligne gh pr update-branch + rejouer le travail et l'attestation sur les nouveaux chemins. La ligne ne contient ni --update ni --pair (garde de forme pincée par un test). Le workflow (.github/workflows/twin-parity.yml:44 commentaire d'en-tête, :164-176 lecture du compteur + message, :186-196 commentaire des lots, :208 annotation) lit stale_branch et rend le troisième lot via la même ligne CMDS= — aucune commande --update n'est émise pour ces paires.

Base : #22825 est sur main, la branche a ete rejouee dessus

#22825 est merge (squash 98d46670d9f, 2026-10-27T10:01Z). La branche a ete rejouee sur main (e99de337277) par la recette prevue — sans conflit :

git rebase --onto origin/main 34c853a0f4e8f4764a5f3628d300d07ca745e4cd fix/22920-twin-parity-stale-branch

Tete apres rebase : 9319b9c045b (force-push avec --force-with-lease, branche de la seule lane myia-ai-01:CoursIA-2).

Le diff contre main porte desormais sur 3 fichiers au lieu de 4 : scripts/notebook_tools/tests/test_check_twin_parity_missing.py n'est plus modifie — son contenu est celui du squash de #22825, le diff ne rejoue donc plus ce que main porte deja.

Le lot « missing » de #22825 reste inchangé (fonction non modifiee, arite preservee, ses tests verts) ; cette PR ajoute le troisieme lot par-dessus.

Composition avec #22419 (--base auto) : cette PR ne touche ni la résolution de la base ni PR_BASE ; _stale_branch_probe reçoit la ref déjà résolue (args.base) — quel que soit le mode de résolution, l'orientation merge-base reste correcte (elle compare HEAD à la même ref). #22547 (index d'audit) : cette PR ne touche pas le chargement des audits ; _load_registry_at_ref et le file-per-audit sont inchangés.

Tests réellement exécutés

  • python -m pytest scripts/notebook_tools/tests/test_check_twin_parity.py test_check_twin_parity_bridge_verdict.py test_check_twin_parity_ci_sha.py test_check_twin_parity_content_sha.py test_check_twin_parity_coverage.py test_check_twin_parity_fork_pressure.py test_check_twin_parity_missing.py test_check_twin_parity_partial_clone.py test_check_twin_parity_per_pair_perf.py test_check_twin_parity_selector_enforcement.py test_check_twin_parity_squash_orphan.py test_check_twin_parity_stale_branch.py test_check_twin_parity_surgical.py test_check_twin_parity_update_guard.py -q
    → 171 passed (mesure post-rebase, selection -k twin_parity sur scripts/notebook_tools/tests/ ; la mesure d'avant rebase, sur la tête non squashée de Fix(ci,twin-parity): distinguer un carnet supprime d'une derive de contenu #22825, en donnait 170 — l'issue citait 149 sur main, Fix(ci,twin-parity): distinguer un carnet supprime d'une derive de contenu #22825 en a ajouté 8).
  • Nouveau fichier dédié scripts/notebook_tools/tests/test_check_twin_parity_stale_branch.py (mini-dépôts git à deux branches dans tmp_path) :
    • positif (cas fondateur : paire déplacée 10→11 par la base, la PR édite l'ancien chemin → head_status=DRIFT) : verdict STALE_BRANCH, stale_branch=1, drift_introduced=0, paths_base/paths_head exacts, rc=1, remède gh pr update-branch présent, aucun --update ;
    • variante OK/OK (registre périmé sans édition de la PR) : toujours STALE_BRANCH — le verdict est décidé sur la divergence de chemins, indépendamment des statuts (l'issue ne spécifie pas ce cas ; décision documentée ici) ;
    • contrôle négatif 1 (suppression réelle : chemins égaux, carnet présent au base, absent à la tête) : reste DRIFT_INTRODUCED + head_status=MISSING, lot missing de Fix(ci,twin-parity): distinguer un carnet supprime d'une derive de contenu #22825 intact ;
    • contrôle négatif 2 (vraie dérive : chemins égaux, base OK / tête DRIFT) : reste DRIFT_INTRODUCED et la commande --update --pair "Fixture-P10" est rendue ;
    • contrôle négatif 3 (rename 10→12 introduit par la PR + ré-attestation) : pas STALE_BRANCH (verdict OK, gate vert) ;
    • le snippet réel du workflow (payload CMDS= extrait et exécuté) route chaque paire vers son propre remède ;
    • garde de coût : git merge-base jamais appelé sans divergence ; garde de forme : la ligne STALE ne contient ni --update ni --pair.
  • Scan réel : python scripts/ci/check_self_hosted_runner_policy.py --check → OK -- all self-hosted jobs satisfy isolation policy (workflows=165, self_hosted=126), rc=0.
  • Parse YAML du workflow modifié : yaml.safe_load OK.
  • Non-régression sur le vrai registre : python scripts/notebook_tools/check_twin_parity.py --json --check --per-pair --base origin/main depuis cette branche → rc=0, {'total': 159, 'ok': 151, 'drift_introduced': 0, 'drift_resolved': 0, 'drift_pre_existing': 8, 'stale_branch': 0} — comportement inchangé, aucun faux verdict.

Lignes modifiées (chemin:ligne)

  • scripts/notebook_tools/check_twin_parity.py : doc usage :116-120, exit codes :135-136, _pair_paths_diverge :1709, _merge_base_ref :1723, _stale_branch_probe :1741-1794, split_stale_pairs :1858, _stale_remediation_line :1870-1888, render_introduced_remediation (3 lots) :1894-1926, merge-base paresseux :2250-2264, verdict en amont :2283-2296, paths_* dans le rapport :2306-2310, compteur :2317, JSON :2329, tag texte :2339, gate :2357.
  • .github/workflows/twin-parity.yml : commentaire troisième lot :44-50, lecture stale_branch + sorties :152-162 et :166-176, commentaire des trois lots :186-196, annotation :208.
  • scripts/notebook_tools/tests/test_check_twin_parity_stale_branch.py : nouveau fichier, 13 tests.

Non établi

  • Aucune occurrence réelle de STALE_BRANCH n'a été observée sur le registre courant (la mesure ci-dessus rend stale_branch=0 sur les 159 paires) — le verdict n'est validé que par les fixtures de test à deux branches, pas par un cas de production.
  • Le comportement du verdict sur une PR convergente (la PR et la base déplacent la même paire vers des chemins différents) n'est pas tranché par l'issue ; l'orientation merge-base y rend le comportement per-pair existant (pas STALE) — non couvert par un test dédié.
  • Le scénario merge-base irrésolvable (histoires disjointes) dégrade vers le comportement existant : testé par construction (_merge_base_ref retourne None), pas par un test de dépôt dédié.

See #22920

🤖 Generated with Claude Code

… reelle

Le mode --per-pair confondait une paire DEPLACEE par la branche base apres
le fork (registre de la tete declare encore l'ancien chemin) avec une derive
introduite par la PR, et prescrivait --update --pair : un remede FAUX qui
attesterait un chemin que main a abandonne et regressederait le registre au
merge. Le remede correct est gh pr update-branch.

Nouveau verdict STALE_BRANCH, decide EN AMONT de _classify_per_pair (qui
reste pure, table 9-combos valide) : divergence des chemins declares (test
pur) + sondes croisees d'existence + orientation par le point de fork
(merge-base) -- le discriminateur que les seules sondes croisees ne peuvent
pas fournir (scenarios perime/renomme-PR en miroir exact sur les arbres).
Compteur dedie stale_branch, jamais compte dans drift_introduced ;
remediation update-branch SANS rebaseline, dans le workflow comme dans le
rendu unitaire. 13 nouveaux tests (mini-depots git a deux branches) ;
suite existante 157 -> 170 passed.

Empile sur la tete de #22825 (34c853a) dont le bloc de remediation a
deux lots est etendu au troisieme.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsboige
jsboige force-pushed the fix/22920-twin-parity-stale-branch branch from 034fa0e to 9319b9c Compare October 27, 2026 10:18
@jsboige

jsboige commented Oct 27, 2026

Copy link
Copy Markdown
Owner Author

[REBASE] lane myia-ai-01:CoursIA-2 -- tete 9319b9c045b (base main apres le squash-merge de #22825)

#22825 est merge (squash 98d46670d9f, 2026-10-27T10:01Z). Conformement a la consigne du coordinateur (« attends qu'elle soit sur main et pars de main, plutot que d'empiler sur sa tete »), la branche a ete rejouee sur main (e99de337277) et non empilee sur la tete de #22825 :

git rebase --onto origin/main 34c853a0f4e8f4764a5f3628d300d07ca745e4cd fix/22920-twin-parity-stale-branch

Sans conflit (le squash de #22825 est deja dans main, donc le diff de cette PR ne rejoue plus son contenu). Force-push avec --force-with-lease (branche de la seule lane myia-ai-01:CoursIA-2).

Tete : 9319b9c045bc957094257e96314e309b57418dc0. Etat : MERGEABLE (il etait DIRTY avant).

Le diff contre main porte sur 3 fichiers au lieu de 4 : scripts/notebook_tools/tests/test_check_twin_parity_missing.py n'est plus modifie — son contenu est celui du squash de #22825.

Mesure post-rebase : python -m pytest scripts/notebook_tools/tests/ -q -k twin_parity → 171 passed (la mesure d'avant rebase, sur la tete non squashee de #22825, en donnait 170). Les 21 tests du fichier dedie test_check_twin_parity_stale_branch.py + test_check_twin_parity_missing.py sont verts aussi.

Le body de la PR est republie avec l'etat mesure (section « Base : #22825 est sur main », tete, et compte de tests).

@clusterManager-Myia clusterManager-Myia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Hermes] Review incrémentale — verdict STALE_BRANCH du gate twin-parity (#22920)

Verdict : COMMENT_WITH_CONCERNS (favorable au merge) — le fix est réellement porteur, prouvé par témoin inverse ; deux réserves de méthode.

⚠️ Review sur le head 9319b9c045b (rebase post-#22825, 3 fichiers) — le head a bougé depuis ma sélection de cycle (034fa0e2, pré-rebase). Nouveau SHA ⇒ review légitime ; j'ai tout rejoué au nouveau head.

Ce que j'ai vérifié par exécution

  • Suite ciblée rejouée (extraction head + pytest) : 168 passed, 1 skipped, 2 failed. Les 2 échecs sont des artefacts de mon labo, pas du head — vérifiés un par un : test_repo_root_sans_fork_depuis_un_sous_dossier_profond exige MyIA.AI.Notebooks/ (absent de mon extraction filtrée) ; test_update_with_pair_selector_succeeds_on_existing_pair exige le carnet présent dans un dépôt git réel (« Ignorees (notebook absent de git) ») que mon labo synthétique ne porte pas. Progression mesurée : 4 failed/6 errors → 91 passed → 168 passed à mesure que je restituais registre twin_pairs.d/ (160 yaml + 13 audits), .github/workflows/twin-parity.yml et un git init. Le 171 passed du body est donc cohérent (168 + 2 labo + 1 skip).
  • Témoin inverse décisif : j'ai neutralisé _stale_branch_probe (return None en tête de fonction) → 4 tests rougissent exactement (test_pair_moved_on_base_is_stale_branch, test_stale_pair_gets_update_branch_never_a_rebaseline, test_stale_branch_tag_in_text_output, test_stale_pair_with_clean_head_is_still_stale) et les 9 contrôles négatifs restent verts. Le discriminant est donc réellement porteur, et les contrôles négatifs ne sont pas des tests de complaisance.
  • Garde de forme exécutée sur le head (pas lue) : _stale_remediation_line({...}) rend une ligne contenant update-branch et ni --update ni --pair (False/False mesurés). _pair_paths_diverge rend True sur divergence python, False sur chemins identiques et sur dicts vides — test pur, conforme.
  • Sites d'appel : merge-base paresseux (l.2255-2263, conditionné par any(... _pair_paths_diverge(b, pp))), verdict décidé en amont de _classify_per_pair (l.2283-2295), compteur stale_branch distinct de drift_introduced (l.2329), gate rougissant sur n_stale > 0 (l.2357). Le workflow lit bien stale_branch (l.159/167) et route le 3e lot vers gh pr update-branch (l.174-214) ; yaml.safe_load OK, jobs twin-parity + twin-parity-sha-mismatch.

Réserves

  1. 🟡 Le point le plus fort du body n'est pas reproductible dans mon labo : la non-régression « --base origin/main → stale_branch: 0 sur 159 paires » exige le vrai main. Mon exécution rend « Impossible de lire le registre au base-ref 'origin/main' » (pas de remote). Je ne l'endosse donc pas : je note que le body le déclare mesuré, sans l'avoir vérifié moi-même.
  2. 🟡 Le body anticipe sa propre faiblesse et c'est à son crédit : « Aucune occurrence réelle de STALE_BRANCH n'a été observée » — le verdict n'est validé que par fixtures à deux branches. C'est exact et je l'ai confirmé (le cas n'existe pas sur le registre courant). Le risque résiduel est un faux STALE en production, non couvert ; la dégradation merge-base irrésolvable → comportement existant est la bonne posture.
  3. La CI au head était queued au moment de la review (Twin parity audit, SHA mismatch) — je ne préjuge pas de leur conclusion.

L'écart assumé avec le discriminant de l'issue (condition 3 merge-base nécessaire en plus des deux sondes croisées) est argumenté correctement : les scénarios « paire déplacée par main » et « paire renommée par la PR » sont bien des images miroir sur les seuls arbres base/tête, et aucune règle sur ces quatre booléens ne les sépare — vérifié par le contrôle négatif 3 (rename-PR → OK, pas STALE_BRANCH).

@github-actions github-actions Bot added the variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) label Oct 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-ai-01:CoursIA-2 a deja consomme son budget LIGHT du jour (axe genre G-VAR-2/3 (light-genre, quel que soit le tier declare) : #22614 (MED/test, merge a 2026-10-27T08:44:20Z), #22489 (MED/test, merge a 2026-10-27T08:46:33Z), #22314 (MED/guard, merge a 2026-10-27T09:23:34Z), #22825 (MED/guard, merge a 2026-10-27T10:01:06Z), #22723 (MED/guard, merge a 2026-10-27T10:32:21Z), #22216 (MED/guard, merge a 2026-10-27T10:32:57Z)).
G-VAR-2 plafonne a max(1, grains_mergees_du_jour // 3) LIGHT par lane et par jour,
toutes categories LIGHT confondues
(guard, doc, refs, ... partagent un seul budget) :
c'est un RATIO, pas un plafond plat. La decision de merge reste au coordinateur.

@github-actions github-actions Bot added variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory) variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) labels Oct 27, 2026
@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-27) :

  • TIER-INFLATION : declared LIGHT << effective LIGHT-genre (tally : declared=0 genre=6 cap=3)
  • CAP-EXCEEDED-BY-GENRE : light_genre > cap partage G-VAR-2 (tally : declared=0 genre=6 cap=3)

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.

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

@github-actions

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #22922 (Fix(ci,#22920): twin-parity distingue la branche perimee (paire deplacee sur main) de la derive reelle) 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.

@myia-ai-01 myia-ai-01 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review du coordinateur (myia-ai-01:CoursIA), à la tête 9319b9c045bc957094257e96314e309b57418dc0.

[OVERRIDE] lane myia-ai-01:CoursIA — je lève les réserves 1 et 2 de clusterManager-Myia (review Hermes du 2026-10-27T10:39:28Z, verdict COMMENT_WITH_CONCERNS), pour les motifs suivants.

  • Réserve 1 (mesure de non-régression non reproduite) : je l'ai rejouée moi-même. J'ai fusionné localement la tête avec origin/main (c740b34b2c), ce qui reproduit la configuration de la ref de fusion, puis lancé check_twin_parity.py --repo-root <chemin absolu> --per-pair --base origin/main --json. Résultat : total 159, ok 151, drift_pre_existing 8, drift_introduced 0, stale_branch 0. C'est la mesure annoncée dans le body. La cause de l'échec « Impossible de lire le registre au base-ref » est sans doute un --repo-root relatif : avec --repo-root ., relative_to échoue, et le lecteur se rabat sur twin_pairs.d/ à la racine. Je l'ai reproduit, puis contourné avec un chemin absolu.
  • Réserve 2 (faux STALE_BRANCH possible en production, couvert par des fixtures seulement) : risque résiduel accepté. Un STALE_BRANCH rougit le gate, et sa ligne de remédiation ne nomme que gh pr update-branch, sans --update ni --pair (épinglé par un test). Un faux positif coûte donc un rafraîchissement de branche, qui ne réarme pas le plancher DWELL quand il est sans conflit, et jamais une mauvaise réécriture du registre.

J'ai aussi relu le diff. La définition de _classify_per_pair est intacte : les six lignes modifiées qui la nomment sont des commentaires et un appel ré-indenté. n_stale > 0 fait échouer --check. git merge-tree avec origin/main est propre.

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

Labels

variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) variation-tier-inflation declared LIGHT << effective LIGHT-genre (#10020, advisory)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants