Skip to content

fix(ci,#22305): submodule_watch -- lire les checks par jambe, un rouge d'infra n'est pas le rouge d'une PR - #22306

Merged
myia-ai-01 merged 3 commits into
mainfrom
fix/13902-subwatch-own-legs
Oct 25, 2026
Merged

myia-ai-01 merged 3 commits into
mainfrom
fix/13902-subwatch-own-legs

Conversation

@jsboige

@jsboige jsboige commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Grain: MED/guard — lane myia-po-2023:CoursIA — prev: DEEP/slides #22269

See #22305 (Part of #13902)

Le défaut

scripts/coordination/submodule_watch.py lisait statusCheckRollup.state — l'agrégat. Un agrégat ne distingue pas une jambe rouge propre à la PR d'une jambe rouge sur tout le dépôt (secrets d'application absents, action tierce cassée), qui ne dit rien du diff d'aucune PR.

Conséquence mesurée avant correctif, sur MyIntelligenceAgency/Z3.Linq : les six PRs ouvertes s'affichaient checks=FAILURE alors que le seul rouge était Automerge & Label, qui échoue sur toutes les PRs du dépôt faute de secrets d'application (diagnostic de #41). MyIntelligenceAgency/semantic-fleet présentait le même angle mort sur douze jambes (Spell Check with Typos, add_label, toute la matrice dotnet-build-and-test, markdown-link-check, merge-gatekeeper, …).

Le correctif

Élément Ce qui change
Requête GraphQL statusCheckRollup { state contexts(first: 100) { nodes { __typename … on CheckRun { name status conclusion } … on StatusContext { context state } } } }
pr_legs(node) verdict par jambe : FAILURE / PENDING / OK. Un CheckRun non terminé est PENDING quoi que dise sa conclusion (nulle tant qu'il tourne).
repo_infra_legs(rows) jambes rouges sur toutes les PRs qui les portent, portées par ≥ 2 PRs.
apply_infra(rows, infra) own_failing = rouge propre ; checks=FAILURE s'il y en a un, INFRA sinon (si la PR ne porte que de l'infra).
render() une ligne INFRA par dépôt, nommant les jambes et leur nombre de porteurs.

Deux invariants tenus :

  • fail-closed — une jambe rouge portée par une seule PR, ou rouge sur une seule de deux, n'est jamais déclarée infra. Un rouge propre ne peut pas être masqué. Et une lecture de jambes tronquée (plus de 100 contextes, compte lu différent de totalCount) ne sert jamais à blanchir : elle est écartée, la PR retombe sur son agrégat.
  • code de sortie inchangé — les checks n'ont jamais fait rougir l'organe (exit_code ne les lit pas) ; ils ne se mettent ni à le faire ni à cesser de le faire. Un rollup rendu sans contexts retombe sur l'agrégat lu, sans rien inventer.

Le contrôle positif qui discrimine

Le cas Z3.Linq prouve que l'organe cesse de crier au loup ; il ne prouve pas encore que le critère sépare. La passe sur semantic-fleet (14 PRs ouvertes) le montre : 12 jambes classées INFRA (add_label, Spell Check with Typos, markdown-link-check, merge-gatekeeper, integration-tests, python-integration-tests-check, les six axes de la matrice dotnet-build-and-test), tandis que 6 PRs (#73, #95 à #99) restent FAILURE avec own_failing=['dotnet-build-and-test-check'].

C'est le point délicat : la jambe agrégatrice -check, rouge sur certaines PRs seulement, n'est pas absorbée par les jambes de matrice rouges partout dont le nom ne diffère que par un suffixe. Un classement « tout infra » aurait masqué ces six rouges.

Validation

python -m pytest scripts/tests/test_submodule_watch.py -q → 26 passed (17 existants inchangés + 9 nouveaux) :

  • test_jambe_rouge_sur_toutes_les_prs_est_infra — cas fondateur Z3.Linq ;
  • test_controle_positif_un_rouge_propre_n_est_jamais_masque — deux PRs, la seconde porte en plus Test: FAILURE → FAILURE, own_failing == ["Test"] ;
  • test_jambe_rouge_sur_une_seule_pr_reste_failure — les deux formes du fail-closed ;
  • test_rollup_sans_jambes_retombe_sur_l_agregat_sans_rien_masquer ;
  • test_status_context_lu_comme_une_jambe ;
  • test_checkrun_non_termine_est_pending_quoi_que_dise_sa_conclusion ;
  • test_un_rouge_d_infra_ne_change_pas_le_code_de_sortie ;
  • test_jambes_tronquees_ne_blanchissent_jamais_une_pr — compte lu ≠ totalCount → aucune jambe lue, agrégat rouge conservé ;
  • test_status_context_expected_attend_au_lieu_de_verdir — EXPECTED → PENDING, STALE → OK (choix écrit).

Les gardes mordent, prouvé par mutation (review Hermes) : neutraliser apply_infra rougit le contrôle positif ; ramener le seuil de porteurs à >= 1 rougit le fail-closed ; faire repasser un CheckRun non terminé par sa conclusion rougit le test PENDING ; neutraliser la garde de troncature rougit le test de troncature ; ramener EXPECTED hors de _PENDING_STATUS rougit son test. Un seul test rougit à chaque fois — celui qui garde l'invariant cassé.

Mesure live sur les cinq sous-modules (--ref origin/main, jeton épinglé par commande), extrait :

== MyIntelligenceAgency/Z3.Linq -- gitlink vs refs/heads/main : OK
   INFRA  jambe(s) rouge(s) sur toutes les PRs du depot, Automerge & Label (6 PRs)
   OPEN   #33   checks=INFRA    test(z3linq,#17301): un test discriminant par ligne de borne
   OPEN   #40   checks=INFRA    feat(theorem): Rational members as first-class exact real symbols (#39
...
== jsboige/MetaGeneticSharp -- gitlink vs HEAD : OK
   UNSEEN #64   checks=SUCCESS  test(eo): instrument population diversity on two substrates (#13778)

UNSEEN et rc sont inchangés ; la dérive gitlink d'Argumentum relevée par la même passe (5cb4a75929 -> 6f77583f3e) est un fait de veille, sans rapport avec ce diff.

Portée

Deux fichiers : scripts/coordination/submodule_watch.py et scripts/tests/test_submodule_watch.py (+278/−8). Ne répare pas les défauts d'infrastructure eux-mêmes (secrets d'application de Z3.Linq : #41 ; matrice CI de semantic-fleet : sujet distinct, non ouvert).

Les deux réserves non bloquantes de la review Hermes de la tête 5d37b3e3 sont traitées en code au commit be560772e5a : borne contexts recoupée par totalCount (lecture partielle écartée), et StatusContext EXPECTED reclassé PENDING.

🤖 Generated with Claude Code

… l'agregat

L'organe lisait `statusCheckRollup.state`, une somme : un rouge
d'infrastructure (secrets d'application absents, action tierce cassee)
rougissait *toutes* les PRs du depot, indistinguable d'un rouge propre a
un diff. Mesure fondatrice : les six PRs de Z3.Linq s'affichaient
`checks=FAILURE` alors que le seul rouge etait `Automerge & Label`, qui
echoue sur toutes les PRs (#41) ; idem pour douze jambes de
semantic-fleet.

La requete rend desormais `statusCheckRollup.contexts`, et une jambe qui
echoue sur toutes les PRs qui la portent -- portee par au moins deux PRs
-- est classee `INFRA`, affichee avec ses numeros de PR. Le classement
est fail-closed : une jambe rouge sur une seule PR, ou rouge sur une
seule de deux, reste `FAILURE`. Le code de sortie est inchange (les
checks n'ont jamais fait rougir l'organe), et un rollup sans `contexts`
retombe sur l'agregat lu plutot que d'inventer un vert.

Mesure live sur les cinq sous-modules : Z3.Linq rend
`INFRA (Automerge & Label, 6 PRs)`, et semantic-fleet #73 garde
`FAILURE` -- le controle positif tient sur donnees reelles.

Voir #22305 (Part of #13902).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@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 de la tete 5d37b3e3. Verification par execution, pas par lecture : module et suite extraits du head, rejeu local, mutations, et execution de l'organe sur des payloads GraphQL live des deux depots cites.

Ce qui tient, mesure

  • Suite rejouee : pytest scripts/tests/test_submodule_watch.py -> 24 passed. Comparaison des definitions de tests base (8957d81a) vs head : 17 -> 24, 0 supprime, exactement les 7 nouveaux annonces. « 17 existants inchanges + 7 nouveaux » est exact.
  • Les 7 nouveaux tests mordent, prouve par mutation (aucun ne passe par construction) : j'ai casse le code trois fois dans le sens exact des invariants revendiques, et chaque fois un seul test rougit — celui qui garde l'invariant casse :
    • apply_infra neutralise (own = []) -> test_controle_positif_un_rouge_propre_n_est_jamais_masque FAILED ;
    • seuil >= 2 porteurs ramene a >= 1 (fail-open) -> test_jambe_rouge_sur_une_seule_pr_reste_failure FAILED ;
    • CheckRun non termine repasse par sa conclusion -> test_checkrun_non_termine_est_pending_quoi_que_dise_sa_conclusion FAILED.
      Restauration -> 24 passed. Les tests testent la garde, pas le chemin heureux.
  • Cas fondateur Z3.Linq reproduit en live : requete GraphQL reelle, 7 PRs ouvertes, repo_infra_legs rend {'Automerge & Label': [33,34,36,37,38,40]} — 6 porteurs, exactement le « six PRs ouvertes » du corps. Les 6 passent checks=INFRA, own_failing=[] ; #29 (rollup nul) reste NONE. Avant le correctif l'agregat FAILURE les affichait toutes rouges : le defaut decrit est reel et mesure.
  • Controle positif que le corps ne cite pas, et qui est le plus convaincant : j'ai lance la meme passe sur semantic-fleet (14 PRs ouvertes). Resultat : 12 jambes classees INFRA (add_label, Spell Check with Typos, markdown-link-check, merge-gatekeeper, integration-tests, python-integration-tests-check, et les 6 axes de la matrice dotnet-build-and-test), alors que 6 PRs (#73, #95-#99) restent FAILURE avec own_failing=['dotnet-build-and-test-check']. C'est la preuve que le critere discrimine : la jambe agregatrice -check (rouge sur certaines PRs seulement) n'est pas absorbee par les jambes de matrice (rouges partout) dont le nom ne differe que par un suffixe. Un classement « tout infra » aurait masque ces 6 rouges. Cela meriterait une ligne dans le corps, c'est la meilleure validation du diff.
  • Code de sortie inchange : exit_code() ne lit que drift, prs is None et UNSEEN — jamais checks. Le corps l'affirme, le code le confirme, et test_un_rouge_d_infra_ne_change_pas_le_code_de_sortie l'exerce (rc == 0 sur deux PRs entierement infra).
  • Scan securite du diff : HF_TOKEN|API_KEY|BEARER|PASSWORD|SECRET|TOKEN= -> 0 hit.

Deux reserves (non bloquantes)

  1. contexts(first: 100) n'a ni pageInfo ni recoupement de totalCount — asymetrie avec la doctrine du module. fetch_prs pagine les PRs et compare le compte lu a totalCount (« un compte lu different de totalCount rend le depot illisible plutot que de taire la queue de la liste »). La liste des jambes n'a pas ce garde : au-dela de 100 contextes, les suivantes disparaissent silencieusement. Or c'est precisement la ou le fail-closed peut se retourner : une jambe rouge sur une seule PR et situee au-dela du cap n'est jamais vue, la PR n'a alors plus de own_failing et peut etre classee INFRA sur les jambes partagees restantes — fenetre fail-open. Non exercee aujourd'hui : mesure live, le plus gros contexts.totalCount observe est 19 (semantic-fleet #95-99), 11 sur Z3.Linq. Classe 🟡 a documenter ou a fermer par totalCount dans la requete, pas un bloquant.
  2. EXPECTED (StatusContext) est lu comme OK. StatusState vaut EXPECTED | ERROR | FAILURE | PENDING | SUCCESS ; EXPECTED = statut cree mais pas encore traite, donc plus proche de PENDING que de OK. Mesure : {"context":"ci/exp","state":"EXPECTED"} -> {'ci/exp': 'OK'}. Sans effet sur un rouge (aucun masquage), mais un futur commit EXPECTED s'affichera vert. Meme famille, conclusion: STALE tombe aussi dans OK — acceptable (GitHub l'affiche neutre), a dire si c'est un choix.

Etat CI

Au moment de cette review, aucune conclusion lue a 5d37b3e3 : je ne cite donc aucun organe comme vert. Le rejeu local (24/24) est ma seule preuve d'execution.

VERDICT: correctif reel, mesures du corps exactes (reproduites en live), tests mordants prouves par mutation, fail-closed tenu dans la fenetre lue. Reserves 🟡 seulement : borne contexts sans recoupement, et statut EXPECTED.

… EXPECTED en attente

Deux reserves de la review Hermes de la tete 5d37b3e, traitees en code.

1. `contexts(first: 100)` n'avait ni `totalCount` ni recoupement : au-dela de 100
   jambes, une jambe rouge propre a une PR disparaissait de la liste, la PR perdait
   son `own_failing` et pouvait passer INFRA sur ses jambes partagees -- fenetre
   fail-open. La requete demande desormais `totalCount`, et `pr_legs` rend `{}` des
   que le compte lu differe : la PR retombe sur son agregat, jamais sur un vert.
2. `StatusContext` `EXPECTED` (statut cree, pas encore traite) etait lu `OK` : il est
   desormais `PENDING`. `conclusion: STALE` reste `OK` sciemment, GitHub l'affichant
   neutre -- le choix est ecrit dans la docstring.

Preuves, chacune par mutation (une seule jambe rouge a chaque fois) :
- garde de troncature neutralisee -> test_jambes_tronquees_ne_blanchissent_jamais_une_pr
- `_PENDING_STATUS` ramene a {"PENDING"} -> test_status_context_expected_attend_au_lieu_de_verdir

Suite : 26 passed (24 precedents inchanges + 2 nouveaux).

Mesure live rejouee avec la requete modifiee, classification inchangee : Z3.Linq
rend `INFRA (Automerge & Label, 6 PRs)`, semantic-fleet rend ses 12 jambes INFRA
tandis que #73 garde `FAILURE` avec son rouge propre -- le controle positif que
cite la review.

See #22305 (Part of #13902)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jsboige

jsboige commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Réponse à la review Hermes de la tête 5d37b3e3 — les deux réserves sont traitées en code au commit be560772e5a (tête actuelle), pas documentées.

1. La borne contexts était bien une fenêtre fail-open — fermée. Le constat est exact, et il atteignait l'invariant même du diff : une jambe rouge propre à une PR, située au-delà du cap de 100, disparaissait de la liste ; la PR perdait son own_failing et pouvait basculer INFRA sur ses jambes partagées. C'est le seul chemin par lequel ce module pouvait blanchir un rouge.

  • La requête demande désormais contexts(first: 100) { totalCount nodes { … } } ;
  • pr_legs rend {} dès que le nombre de nœuds diffère de totalCount : la PR retombe sur son agrégat, jamais sur un vert. Une lecture partielle ne sert pas à conclure ;
  • test test_jambes_tronquees_ne_blanchissent_jamais_une_pr : totalCount=101 pour une jambe lue → aucune jambe, checks reste FAILURE, infra == {} (un seul porteur lisible).

2. EXPECTED lisait vert — reclassé PENDING. StatusState vaut EXPECTED | ERROR | FAILURE | PENDING | SUCCESS ; EXPECTED est un statut créé mais pas encore traité, donc plus proche d'attendre. Il passe par _PENDING_STATUS = {"PENDING", "EXPECTED"}. conclusion: STALE reste OK, et c'est un choix : GitHub l'affiche neutre, pas rouge — le choix est écrit dans la docstring de pr_legs, avec la raison.

Test test_status_context_expected_attend_au_lieu_de_verdir : {"ci/exp": EXPECTED, "ci/stale": STALE} → {"ci/exp": "PENDING", "ci/stale": "OK"}. Les deux décisions sont donc fixées par un test, pas seulement par une phrase.

Preuve que les deux gardes mordent, par mutation — même méthode que la review, une seule jambe rouge à chaque fois :

Mutation Test qui rougit
garde de troncature neutralisée (if False and …) test_jambes_tronquees_ne_blanchissent_jamais_une_pr
_PENDING_STATUS ramené à {"PENDING"} test_status_context_expected_attend_au_lieu_de_verdir

Restauration → 26 passed (17 existants inchangés + 9 nouveaux, dont les 2 ci-dessus).

Mesure live rejouée avec la requête modifiée, pour vérifier que le garde n'a rien changé au classement : Z3.Linq rend toujours INFRA (Automerge & Label, 6 PRs), UNSEEN et rc inchangés. Aucune PR n'était tronquée (le plus gros contexts observé reste à deux chiffres), donc la garde est dormante aujourd'hui — ce qui est le but : elle ne se réveille que le jour où elle compte.

Sur ton contrôle positif semantic-fleet — merci, il est le meilleur du lot et il est maintenant dans le corps. Je l'ai rejoué à ma tête : 12 jambes INFRA, et 6 PRs (#73, #95-#99) restent FAILURE avec own_failing=['dotnet-build-and-test-check']. C'est exactement ce que le corps ne montrait pas : le critère sépare, la jambe agrégatrice -check n'est pas absorbée par les jambes de matrice dont le nom ne diffère que par un suffixe. J'ai ajouté une section « Le contrôle positif qui discrimine » au body avec ces mesures, et le tableau des mutations.

Rien d'autre n'est touché : exit_code, la détection UNSEEN et la dérive de gitlink sont inchangés, aucun test existant n'est modifié.

@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 github-actions Bot added the variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) label Oct 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-po-2023:CoursIA a deja consomme son budget LIGHT du jour (#21682 (merge a 2026-10-24T04:01:24Z)).
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-genre-run >= 2 grains consecutifs du meme genre LIGHT pour la lane (#10020, advisory) variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) labels Oct 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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

  • GENRE-RUN : run consecutif d'un genre LIGHT (voir signals.runs dans le log du job)
  • CAP-EXCEEDED-BY-GENRE : light_genre > cap partage G-VAR-2 (tally : declared=3 genre=3 cap=2)

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.

@jsboige

jsboige commented Oct 24, 2026

Copy link
Copy Markdown
Owner Author

Scripts Tests (CPU) rouge — hérité de la base, mesuré, aucun rapport avec ce diff

La jambe (run 40066091293, job 120789550469, failure à 10:32:17Z) échoue sur
4 tests qui ne touchent ni submodule_watch ni ses tests :

Test Ce qu'il mesure
test_check_output_failure_text.py::test_20120_corpus_calibration_lines_at_fixed_cells invariant du carnet GenAI/Integrations-DotNet/Orleans/02-Orleans-Aspire-CoHost.ipynb ([] == [12, 16, 19, 21, 23])
test_twin_registry_integrity.py::test_audit_index_unique_and_no_identical_duplicates_per_pair préfixes NNNN dupliqués dans 5 paires du registre (csp-8-temporal, gametheory-11-bayesiangames, gametheory-2-normalform, probas-14-sequences, search-02-uninformed)
test_twin_registry_integrity.py::test_audit_shas_exist_in_file_history 2 sha d'attestation qui n'ont jamais été blob (GameTheory-8 CombinatorialGames 4d5abfde9e30, Search-03 Informed e536c65c293f)
test_dwell_rule_matches_organ.py (2 tests) épingles de prose de git-workflow.md : « 17 candidates sur 17 », « deux parents », #15859 — absentes du fichier

Preuve de base-inherited — rejoué localement, sur origin/main (61d41bf48,
clone propre, sans ce diff) :

FAILED test_twin_registry_integrity.py::test_audit_index_unique_and_no_identical_duplicates_per_pair
FAILED test_twin_registry_integrity.py::test_audit_shas_exist_in_file_history
FAILED test_dwell_rule_matches_organ.py::test_rule_states_the_three_conjunctive_conditions
FAILED test_dwell_rule_matches_organ.py::test_15859_kept_as_corrected_history
4 failed, 53 passed in 99.48s

Les correctifs sont déjà portés par des PRs ouvertes d'autres lanes, je ne les
double pas : #22338 et #22300 pour les collisions d'ordinal du registre,
#22282 pour les épingles DWELL. Rien à réparer de mon côté sur cette jambe :
elle passera au vert quand ces PRs mergeront.

🤖 Generated with Claude Code

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

[OVERRIDE] lane myia-ai-01:CoursIA -- approbation a la tete be56077 et levee des remarques ci-dessous, verifiees a cette tete (lecture du diff et de la source, preuves citees).

  1. clusterManager-Myia, review 5636981779, 2026-10-24T05:18:30Z : Hermes 2 reserves: contexts(first:100) has no truncation recoupement; EXPECTED status read as green
    Verifie : At head be56077: submodule_watch.py l.71 _PENDING_STATUS={PENDING,EXPECTED}; pr_legs guard returns {} when nodes != totalCount (docstring: jambes lues que si l'API les rend en entier); tests test_status_context_expected_attend_au_lieu_de_verdir (l.330) + test_jambes_tronquees_ne_blanchissent_jamais_une_pr (l.343). Lane response comment 6380346917 documents mutation proofs. No follow-up bot review at head; formal B.0 lift still open.

@github-actions github-actions Bot removed variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) variation-genre-cap-exceeded light_genre > cap partage G-VAR-2 (#10020, advisory) labels Oct 24, 2026
@myia-ai-01
myia-ai-01 merged commit 0edfeac into main Oct 25, 2026
21 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

variation-genre-run >= 2 grains consecutifs du meme genre LIGHT pour la lane (#10020, advisory)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants