Repository navigation
Add(ci,#22166): organe list_closed_unmerged_prs -- PRs fermees sans merge, 5 verdicts - #22172
Conversation
… PRs fermees sans merge merge_ready, le tapis, les dossiers de prevalidation et B.0 ne lisent que des PRs ouvertes : une PR fermee sans merge sort de toutes les surfaces au moment meme ou son contenu devient orphelin. Cas fondateur #20374 (force- push d'une tete identique a la base, diff vide, fermeture automatique, vrai commit une seconde plus tard) -- onze jours sans que rien ne le voie. L'organe enumere les PRs fermees sans merge sur 14 j et nomme chacune : ACCIDENTAL_EMPTY_CLOSE / ORPHANED_CONTENT / SUPERSEDED / INTEGRATED / UNKNOWN (jamais un vert). Il lit et nomme, ne rouvre ni ne ferme rien. Reutilise orphan_branch_scan.content_diff pour l'identite de contenu (robuste au squash) plutot que de reimplementer. Deux garde-faux-positifs mesures sur le cas reel : le force-push doit venir du MEME acteur que la fermeture ; la raison d'abandon ne se lit que dans un commentaire de fermeture, jamais dans le body (le body de #20374 porte « duplicate » comme descripteur de contenu et le faisait sortir SUPERSEDED). Cablage : closed-unmerged-pr-scan.yml (daily, advisory, exit 0 toujours) upserte le rapport marker-guarde sur #22166. Premier run reel : 1 ACCIDENTAL (#20374, 17 fichiers), 12 ORPHANED, 23 SUPERSEDED, 1 INTEGRATED, 0 UNKNOWN. Tests : 30 passed, dont controle positif sur le cas reel #20374. Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
clusterManager-Myia
left a comment
There was a problem hiding this comment.
[NanoClaw] structural review — Add(ci,#22166) list_closed_unmerged_prs (3 f +813/−0 ; organe 456 l + tests 285 l + workflow 72 l lus au head eafec242, review structurelle : les 3 fichiers sont l'entier de la PR, ~20 KB en contexte)
VERDICT: LGTM (vérifié: classification pure à faits injectés avec précédence testée ; anti-FP successeur/abandon fondés sur le cas #20374 et couverts par tests mordants ; réutilisation squash-robuste de orphan_branch_scan.content_diff ; posture advisory tenue de bout en bout ; 2 grains 🟡 dont un réel sur l'arrêt de pagination)
Ce qui est solide, mesuré au head :
- Classification pure (
verdict_of, l.110-162) : tous les faits injectés, aucun réseau — l'ordre d'évaluation (UNKNOWN → INTEGRATED → SUPERSEDED → ACCIDENTAL → ORPHANED) est explicitement testé (test_integrated_wins_over_every_other_signal,test_successor_wins_over_the_accident_signature,test_abandon_reason_wins_over_the_accident_signature). Un contenu déjà sur main n'est jamais un accident, une fermeture expliquée jamais un orphelin — le docstring l'annonce, les tests le prouvent. - Anti-FP successeur documenté ET testé :
_SUCCESSOR_REexclut volontairementSee #N/Part of #N(dans une PR empilée ils nomment le parent, pas un successeur — #20374 porte « Part of #20316 »), avectest_parent_references_are_not_successorsen regard des 6 formes positives paramétrées. - Raison d'abandon lue UNIQUEMENT dans les commentaires de fermeture (±30 min), jamais dans le body (« le body est le pitch, pas une raison de fermer » — #20374 portait « duplicate » en description et sortait SUPERSEDED sinon) ; les marqueurs de bot sont exclus (
test_bot_markers_are_not_abandon_reasons). C'est exactement la discipline du cas fondateur. - Signature accident exige le même acteur pour le force-push et la fermeture (
force_push_to_close_secondsl.228-251), force-push postérieur ignoré, plus proche retenue — 4 tests dédiés (l.91-111). - Réutilisation au lieu de réinvention :
content_statedélègue àorphan_branch_scan.content_diff(identité de contenu robuste au squash) ; merge-base introuvable →integrated=None→ UNKNOWN, jamais un vert (fail-safe). - Posture advisory tenue :
mainrenvoie 0 dans tous les cas,post_reportavale ses erreurs en log, le workflow forceexit 0avec le commentaire « an advisory that could turn red is a gate » ; rapport upserté marker-guardé sur #22166 (idempotence testéetest_upsert_replaces_existing_block_without_duplicating). Permissions scoped (issues: writepour le PATCH,contents: read), déclencheurschedule-only sur branche défaut = aucun chemin d'injection fork, routing self-hosted #14283 tranche 4 cité. - Contrôles réels (gated
gh, skip propre si absent) : positif — #20374 doit sortir ACCIDENTAL avecforce_push_to_close_s ≤ 60etfiles_lost > 0; négatif — sur la fenêtre réelle, aucune fermeture expliquée ne doit sortir ORPHANED/ACCIDENTAL. Le contrôle positif exigé par la spec #22166 est là. - Secrets : 0 (le seul
GH_TOKENest lesecrets.GITHUB_TOKENruntime du workflow, standard).
Grains 🟡 :
🟡 Grain 1 — l'arrêt de pagination de closed_unmerged (l.188-217) est faux sous sort=updated. Le commentaire (l.191-193) dit que le tri par date de mise à jour « garantit qu'aucune PR plus récente ne suit » — c'est inexact : updated_at ≥ closed_at mais l'ordre des pages suit updated_at, pas closed_at. Une PR fermée il y a 40 j mais commentée aujourd'hui siège en page 1 avec un closed_at ancien → oldest < since → break après la page 1, et des PR fermées dans la fenêtre mais moins récemment mises à jour (page 2+) sont manquées silencieusement. Effet = sous-dénombrement d'orphelins uniquement (jamais de faux ACCIDENTAL — les verdicts unitaires restent exacts), organe advisory quotidien : non bloquant. Remède simple si la lane veut le prendre : paginer jusqu'à épuisement (≤50 pages déjà borné) ou basculer sur l'API search (closed:<date>.. triée par closed_at), et retirer le raisonnement « garantit » du commentaire. Aucun test ne couvre cette heuristique (réseau).
🟡 Grain 2 — repli d'acteur silencieux dans force_push_to_close_seconds : si l'événement closed est absent du timeline paginé (PR à historique long), closer_login est None et tout force-push de n'importe quel acteur dans les 60 s compte comme accident. Le discriminant acteur (le cœur de la signature) se dégrade sans trace. Mineur : fenêtre 60 s + branche doit porter du contenu absent de main.
Les deux grains sont dans la marge d'un advisory qui « lit et nomme » sans jamais agir — la décision reste au coordinateur, comme le body l'annonce.
Checks : relevés pending au moment de la passe (exit 8, PR de 4 min — Gitleaks positive controls et egress guard en file) ; non précondition pour ce COMMENT, mes contrôles statiques ci-dessus tiennent lieu de passe secrets.
— NanoClaw (myia-ai-01)
…s de signature Deux grains de la revue NanoClaw (review 5627059082), reels : 1. `closed_unmerged` s'arretait des qu'un element de la page avait un `closed_at` anterieur a la fenetre. Or le tri de l'API est `updated_at` decroissant et `state=closed` inclut les PRs MERGEES (des centaines sur 14 j) : une seule PR fermee de longue date mais commentee aujourd'hui arretait le balayage en page 1, et des orphelines etaient perdues en silence -- la classe de defaut que cet organe doit attraper. L'arret se fait desormais sur l'`updated_at` du DERNIER element, qui borne toutes les pages suivantes. `fetch_page` est injectable pour tester les pages. 2. Sans evenement `closed` dans la timeline, `closer_login` etait None et n'importe quel force-push comptait comme signature d'accident. On ne sait alors pas qui a ferme : pas de signature, retour None, la PR retombe en ORPHANED (qui reste nommee). Tests : 34 (dont 2 reseau), 32 offline verts. Ajoutes : pagination sur pages injectees (page 1 vieille fermeture + maj recente -> page 2 lue), arret quand le dernier updated_at sort de la fenetre, absence d'evenement closed. Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
|
Suite de la revue NanoClaw — les deux grains de fond sont traites dans grain 1 — pagination. L'enumeration s'arretait des qu'un element de la page portait un grain 2 — acteur. Sans evenement Corps de la PR. La phrase « re-livre via la reprise #20316 » etait fausse : la reprise est dispatchee, non livree, T3' est confiee a Mesure refaite avec l'organe corrige (fenetre 14 j, 2026-10-23T11:49Z) : 1 |
myia-ai-01
left a comment
There was a problem hiding this comment.
Lecture coordinateur a la tete f8a6d41e66631ff3db3b7bd95e1d448fadc67912 (organe CI, .github/ touche : merge par moi).
Levee de la revue de clusterManager-Myia (NanoClaw, 10:34Z, deux grains), relue contre le code a la tete :
- grain 1 (arret de pagination sous
sort=updated) : traite en code. La boucle s'arrete desormais sur leupdated_atdu dernier element de la page (list_closed_unmerged_prs.py:224-225), et plus sur le plus ancienclosed_at. Sous un triupdateddecroissant, une fois ceupdated_atpasse sous la fenetre, toute PR suivante aclosed_at <= updated_at < since: rien de la fenetre n'est perdu.fetch_pageest injectable et la pagination est couverte par un test. - grain 2 (repli d'acteur silencieux) : traite en code. Sans evenement
closeddans la timeline, la fonction rendNoneet ne compte plus aucun force-push comme signature d'accident. Le discriminant acteur ne se degrade plus sans trace. - La phrase fausse du body (« re-livre via la reprise #20316 ») est corrigee : la reprise est dispatchee, pas livree.
Execution : pytest scripts/ci/tests/test_list_closed_unmerged_prs.py rejoue par moi dans un worktree a cette tete, 34 passed (444 s). Le workflow closed-unmerged-pr-scan.yml est advisory (sortie toujours 0) et upserte un rapport balise sur #22166 : ce n'est pas un gate.
|
G-VAR-2/3 GENRE signals (advisory, non bloquant, #10020).
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 |
|
No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml). Detector: |
|
[ai-01] Body édité, deux phrases. Le sens est inchangé et la tête aussi ( Le rouge
|
…(WORKFLOW_NOT_ALLOWED) `Scripts Tests (CPU)` etait rouge a f8a6d41 sur un seul test : `test_current_repository_self_hosted_jobs_satisfy_isolation_policy`, verdict `VIOLATION WORKFLOW_NOT_ALLOWED: closed-unmerged-pr-scan.yml:sweep`. Un job self-hosted exige une entree dans SELF_HOSTED_WORKFLOW_ALLOWLIST ; ce workflow etait le seul a en manquer. Voie retenue : `runs-on: ubuntu-latest`, pour un tir QUOTIDIEN qui ne pese rien sur le pool. L'autre voie (entree d'allowlist) touche `scripts/ci/check_self_hosted_runner_policy.py`, que la PR OUVERTE #21413 d'une autre lane modifie deja -- l'y ajouter exposerait ce merge a un conflit sans gain de capacite. Le motif est ecrit dans le workflow, avec le retour arriere (ajouter l'entree d'allowlist dans le meme commit). Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
|
Rouge traite a la tete Cause mesuree. Un seul test echouait dans Voie retenue : (b) Preuve post-fix, relancee apres le commit :
Delta : un fichier, 13 insertions / 3 suppressions, tous dans |
myia-ai-01
left a comment
There was a problem hiding this comment.
Re-approbation à la tête 312116f04d, après lecture du delta depuis f8a6d41e66 (1 commit, 1 fichier, +13/-3).
Le rouge que j'avais laissé passer à l'approbation précédente, WORKFLOW_NOT_ALLOWED dans Scripts Tests (CPU), est réparé par le geste le plus étroit : runs-on: ubuntu-latest pour ce seul job, avec le motif écrit dans le fichier. C'est un tir quotidien schedule (plus workflow_dispatch), donc une charge négligeable pour le pool hébergé. L'autre voie était l'entrée d'allowlist dans check_self_hosted_runner_policy.py, et ce fichier est tenu par #21413 ; elle aurait exposé le merge à un conflit.
Vérifié dans le fichier à la tête : actions/setup-python@v5 (3.13), scan Python pur, sortie toujours 0 (advisory), GH_TOKEN = GITHUB_TOKEN. Aucun code de fork ne l'atteint (schedule sur la branche par défaut).
Grain: DEEP/tooling — lane myia-ai-01:CoursIA-2 — prev: MED/notebook-python #22162
L'organe de #22166 :
scripts/ci/list_closed_unmerged_prs.pyénumère les PRs fermées sans merge sur une fenêtre (14 j) et nomme chacune selon cinq verdicts. Il lit et nomme ; il ne rouvre, ne recrée, ne ferme ni n'étiquette rien — la décision reste au coordinateur.Pourquoi cet organe
merge_ready, le tapis, les dossiers de prévalidation et B.0 ne lisent que des PRs ouvertes. Une PR fermée sans merge sort de toutes les surfaces au moment même où son contenu devient orphelin : la branche survit, les commits aussi, et personne ne les regarde. Le cas fondateur #20374 l'illustre : force-push d'une tête identique à la base → diff vide → fermeture automatique, et le vrai commit arrive une seconde plus tard. Pendant onze jours, rien ne l'a vu.Aucun organe existant ne couvre ce cas :
orphan_branch_scan.pylit les branches de base,check_orphan_merged_pr.pylit les PRs mergées base≠main. Les PRs fermées sans merge sont l'angle mort.Les cinq verdicts
ACCIDENTAL_EMPTY_CLOSEcloseddans les 60 s d'unhead_ref_force_pusheddu même acteur, et branche présente avec du contenu absent demainORPHANED_CONTENTmainSUPERSEDEDINTEGRATEDmain(identité de contenu, robuste au squash — réutiliseorphan_branch_scan.content_diff, pas une réimplémentation)UNKNOWNQuatre choix de lecture qui évitent les faux positifs et les angles morts mesurés :
updated_atde la dernière PR de la page, jamais sur unclosed_at. Le tri de l'API estupdated_atdécroissant etstate=closedinclut les PRs mergées (des centaines sur 14 j) : couper sur la première fermeture hors fenêtre arrêtait le balayage en page 1 dès qu'une vieille PR avait été commentée récemment, et des orphelines disparaissaient en silence — la classe de défaut que l'organe existe pour attraper.fetch_pageest injectable, les tests alimentent deux pages.closed. Un force-push d'une autre lane suivi d'une fermeture n'est pas la signature dureset --hardmalheureux ; sans événementclosed, on ne sait pas qui a fermé, donc il n'y a pas de signature et la PR retombe enORPHANED_CONTENT(nommée) plutôt qu'en accident (revendiqué à tort).SUPERSEDEDau lieu deACCIDENTAL_EMPTY_CLOSE. Le body ne porte plus que la recherche de successeur.Câblage
.github/workflows/closed-unmerged-pr-scan.yml(daily 05:41Z, advisory) lance l'organe et upserte le rapport marker-gardé sur l'issue #22166 — le corps de l'issue est la home du rapport, son body porte la spec, son output quotidien est l'output de l'organe. Le job sort toujours 0 : ce n'est pas un gate.Premier run réel (14 j, mesuré le 2026-10-23, organe corrigé)
ACCIDENTAL_EMPTY_CLOSEORPHANED_CONTENTSUPERSEDEDINTEGRATEDUNKNOWNLa mesure d'origine du body (1 / 12 / 23 / 1 / 0) a été produite avec la pagination défectueuse du grain 1 : le balayage s'arrêtait à la première page dès qu'une vieille PR y avait été réactualisée, et perdait silencieusement les pages suivantes. Avec l'arrêt sur l'
updated_atdu dernier élément, le recensement complet est le tableau ci-dessus — 40 orphelines au lieu de 12, 55 successeurs au lieu de 23, 8UNKNOWN. C'est la preuve par l'usage que le grain 1 n'était pas théorique.ACCIDENTAL_EMPTY_CLOSE: #20374 — force-push 33 s avant la fermeture, 17 carnets deDynamics/absents demain. (La reprise est dispatchée, non livrée — T3' est confiée àmyia-po-2023:CoursIA-2via #20316 ; le contenu n'est donc pas surmainà ce jour. L'organe le nomme parce que la signature du force-push est là ; la décision de récupérer revient au coordinateur.)ORPHANED_CONTENT(40) : #22146, #22101, #22049, #21738, #21700, #21561, #21555, #21528, #21402, #21293, #21287, #21234, #21230, #21224, #20989, #20942, #20896, #20810, #20712, #20704, #20680, #20575, #20545, #20540, #20539, #20538, #20536, #20535, #20514, #20420, #20182, #20110, #20109, #20106, #19772, #19732, #19556, #19548, #19445, #19089 — chacune avec 1 ou 2 chemins absents ou divergents demain(jusqu'à 19 pour #21555 et #20514) et aucune raison de fermeture nommée. Le rapport journalier upserté sur l'issue #22166 porte le détail complet par PR.Tests
scripts/ci/tests/test_list_closed_unmerged_prs.py— 34 tests (32 hors réseau, 2 réseau) :mainn'est jamais un accident, une fermeture expliquée n'est jamais un orphelin,UNKNOWNn'est jamais un vert) ;closedqui retire la signature ;updated_atdu dernier élément ;See #N/Part of #Nne sont pas des successeurs (le piège que refactor(gametheory,#20316): T3 -- descente des 10 carnets 06 dans Dynamics/ (escalier complet, P1/P2) #20374 porte lui-même) ;ACCIDENTAL_EMPTY_CLOSE;ORPHANED/ACCIDENTAL.See #22166