diff --git a/.claude/rules/merge-cycle-contract.md b/.claude/rules/merge-cycle-contract.md new file mode 100644 index 0000000000..bf8fef7ac4 --- /dev/null +++ b/.claude/rules/merge-cycle-contract.md @@ -0,0 +1,62 @@ +# Contrat du cycle de merge — ce qui se rend, dans quel ordre, sous quel nom + +S'applique aux **trois roles** qui produisent les merges du depot : le coordinateur `myia-ai-01:CoursIA`, le titulaire `myia-po-2025:CoursIA-2`, le secretaire `myia-po-2026:CoursIA-3`. Source : mandat user 2026-09-22 — « on a souvent des regressions car les bonnes pratiques ne sont pas cristallisees avant d'etre oubliees, c'est peut-etre le bon moment de mettre les choses en dur ». + +Les **mecanismes** du gate (codes de sortie, empreinte, `[ADJOINT PREFLIGHT]`) vivent dans les skills `coordinate` / `coordinate-adjoint` et dans [coordinator-discipline.md](coordinator-discipline.md). Cette regle ne les repete pas : elle fixe les **quatre gestes** dont l'oubli a un cout mesure, et que rien n'empechait d'oublier. + +## Regle 1 — un lot se rend NOMINATIVEMENT, jamais en compte (HARD) + +Un lot de PRs pre-machees se rend en **citant chaque PR**. Un cumul (« 56 OK B.0 frais », « la file est prete ») n'est pas une livraison : c'est une affirmation que le destinataire doit re-etablir a l'aveugle, et cette re-decouverte **perime** les dossiers qu'elle traverse. + +Mesure du 2026-09-22, trois lots reels sur le meme cycle : + +| Format rendu | Conversion en merge | +|---|---:| +| liste **nominative** (PRs citees une a une) | **10/10 — 100 %** | +| **cumul** non nomme | **15/53 — 28 %** | +| auto-tire par le coordinateur | **2/54 — 4 %** | + +Le travail sous-jacent etait **le meme** dans les trois cas. L'ecart vient entierement du format du rendu. **C'est le levier de debit le plus rentable mesure a ce jour**, avant toute optimisation de quota ou de cout d'appel. + +Corollaire : un lot se rend **au fil de l'eau**, jamais en barriere. Chaque dossier pret remonte seul ; attendre d'avoir le lot complet ajoute de la peremption sans ajouter d'information. + +## Regle 2 — le dossier se pose EN DERNIER (HARD) + +Toute ecriture tierce sur les surfaces de discussion **posterieure** au dossier le perime. Auditer une PR, y repondre, y poster un constat : chacun de ces gestes invalide une attestation deja ecrite — **y compris la sienne propre**. + +L'ordre est donc : lire, ecrire tout ce qu'on a a ecrire, **puis** attester. Mesure du 2026-09-22 : **12 des 32** refus `rc=1` du cycle portaient `discussion changed after dossier` — un dossier juste, tue par une ecriture qui a suivi. + +La piece jumelle vit dans [git-workflow.md](git-workflow.md) : **la branche est gelee entre le dossier et le merge**. Un dossier a besoin d'une branche silencieuse, sinon le travail de prevalidation est detruit par le travail de reparation, indefiniment. + +## Regle 3 — `BLOCKED` vaut autant que `READY`, et ne se maquille jamais (HARD) + +Un dossier `verdict: BLOCKED` est une **livraison complete**, pas un echec : il laisse le coordinateur dispatcher depuis un motif atteste **sans ouvrir les surfaces**, donc sans les perimer. + +**N'ecrire jamais `READY` pour rendre son travail visible.** Un `b0: clear` faux a deja ete mesure sur une PR portant 3 findings HIGH ouverts. Le verdict decrit l'etat de la PR, jamais l'effort fourni. + +Symetrique cote coordinateur : un `BLOCKED` se dispatche **depuis son motif**, il ne se re-audite pas. Et quand la cause est d'**infrastructure** (parc de runners, `main` rouge, minuteur `DWELL`), l'attester en la **nommant** — renvoyer la PR a son auteur pour reparation lui demande de reparer ce qui n'est pas chez lui. + +## Regle 4 — un levier se rend AVEC sa portee (HARD) + +Toute amelioration proposee ou livree nomme, dans la **meme phrase**, ce qu'elle **ne** traite **pas**. Un levier rendu sans sa portee laisse croire que le probleme est traite, et ferme l'enquete sur la cause dominante. + +Cas fondateur : le double fetch des check-runs du gate (#17390) reduit le cout **par appel**. La cause dominante mesuree de la consommation est la **peremption** — des dossiers produits puis jamais consommes. Rendre le premier sans nommer la seconde aurait fait passer un gain marginal pour une solution. + +Corollaire, porte par [[a-correct-fix-can-perime-the-whole-fleet-at-once]] : un changement **juste** de l'ensemble des champs qui composent une **empreinte** perime **toutes** les attestations en vol d'un coup. Son livrable est une **issue** avec sa fenetre et son controle positif, pas un commit de passage. + +## Partition des emetteurs — pas d'auto-attestation + +Le gate compare la lane du dossier au tag `Grain:` de la PR : une lane ne peut pas attester ce qu'elle porte. La partition d'un gisement se fait donc **par lane porteuse**, et se declare au moment du dispatch. + +Le login GitHub etant partage, le champ `lane` est une **declaration fail-closed**, pas une preuve d'identite. Ce que le gate exige est que la prevalidation soit **tierce**, pas qu'elle vienne d'une lane nommee. + +## Mesures et incidents + +Chiffres, distribution des refus par motif, et le detail du cycle qui fonde ces quatre regles : [docs/reference/merge-cycle-measures.md](../../docs/reference/merge-cycle-measures.md). + +## Voir aussi + +- [coordinator-discipline.md](coordinator-discipline.md) — mecanique du gate et des roles +- [git-workflow.md](git-workflow.md) — gel de branche, `update-branch` et peremption du dossier +- [pr-review-discipline.md](pr-review-discipline.md) — §B.0, emission des marqueurs de reserve +- [harness-hygiene.md](harness-hygiene.md) — les 3 tiers : pourquoi le detail est en `docs/` diff --git a/docs/reference/merge-cycle-measures.md b/docs/reference/merge-cycle-measures.md new file mode 100644 index 0000000000..fe67797d80 --- /dev/null +++ b/docs/reference/merge-cycle-measures.md @@ -0,0 +1,90 @@ +# Cycle de merge — mesures qui fondent le contrat + +Detail de [`.claude/rules/merge-cycle-contract.md`](../../.claude/rules/merge-cycle-contract.md). Le harnais porte les quatre regles ; ce fichier porte les chiffres, pour que le harnais reste succinct ([harness-hygiene](../../.claude/rules/harness-hygiene.md)). + +## Le cycle du 2026-09-22 — 30 merges, et ou sont partis les refus + +30 PRs mergees sur la fenetre, mesure firsthand (`search/issues is:pr is:merged merged:>=` = 30), corroborant exactement la somme des trois passes du cycle (2 + 10 + 18). Le mandat user de ~20 merges par cycle est tenu. + +Sur **80 candidates** passees au gate d'entree : + +| Issue | Compte | Ce que ca dit | +|---|---:|---| +| mergees | 18 | — | +| **pas de dossier digne de confiance** (`rc=1`/`rc=2`) | **32** | la production de dossiers plafonne | +| **rouge CI** | **27** | cause d'infrastructure, pas un defaut de PR | +| `BLOCKED` atteste (`rc=3`) | 3 | dispatchable depuis le motif, sans ouvrir les surfaces | + +**Aucune** des 59 refusees ne l'a ete pour un defaut d'elle-meme. C'est le constat qui fonde la regle 3 : renvoyer ces PRs a leurs auteurs leur demande de reparer ce qui n'est pas chez eux. + +### Les 32 sans dossier, par motif + +| Motif rendu par le gate | Compte | +|---|---:| +| surfaces changees ou non entierement attestees, dont **12** explicitement « changed after dossier » | ~20 | +| `no [ADJOINT PREFLIGHT] dossier comment found` | 11 | +| `rc=2` — snapshot bouge pendant la lecture | 1 | + +Le motif dominant n'est pas l'absence de travail : c'est du travail **fait puis perime**. D'ou la regle 2. + +## La mesure de conversion — pourquoi le format du rendu est le levier + +Trois lots, meme cycle, meme gate, meme coordinateur : + +| Lot | Format | Converti | Taux | +|---|---|---:|---:| +| titulaire | liste **nominative** | 10 / 10 | **100 %** | +| secretaire | **cumul** (« 56 OK B.0 frais ») | 15 / 53 | **28 %** | +| coordinateur | auto-tire sur les plus vieilles | 2 / 54 | **4 %** | + +Le travail sous-jacent du deuxieme lot etait reel et de qualite : 96 PRs attestees, 56 fraiches. Il a converti a 28 % parce qu'il est arrive en **compte**, forcant une re-decouverte a l'aveugle sur 80 PRs — re-decouverte qui a elle-meme perime une partie des attestations qu'elle traversait. + +**Le format de rendu pese davantage que le cout unitaire d'un appel API.** C'est la reponse structurelle a la question du plafond de quota, et elle est gratuite. + +## Le cas fondateur de la regle 4 — #17390 + +Le gate (`scripts/check_adjoint_prevalidation.py`) recupere l'etat des checks **deux fois** par PR : + +| Source | Ligne | Transport | +|---|---|---| +| `statusCheckRollup` dans `_pr_metadata` | `:732` | **GraphQL**, le champ le plus cher : il deplie chaque check-run | +| `_head_check_runs(headRefOid)` | `:763` via `:706` | **REST**, et deja la source de verite declaree du projet (`latest_wins_check_runs`, `:396`) | + +Le retrait du champ est **juste** et n'a pas ete livre en code : `statusCheckRollup` entre dans `_metadata_identity`, donc dans l'empreinte, donc le retirer perime **toutes** les attestations en vol d'un coup (~92 au moment de la mesure). Le livrable est l'issue #17390, avec sa fenetre et son controle positif obligatoire — un test qui **echoue** si un check conclut pendant la lecture du snapshot. + +Et la portee se declare : ce levier reduit le cout **par appel**, pas la peremption, qui est la cause dominante. + +## Capacite CI — l'arbitrage user du 2026-09-22 + +Q34 du registre demandait s'il fallait plafonner la concurrence des runners sur ai-01. **Arbitrage user : non — on ne coupe pas la capacite, on l'equilibre entre GitHub, po-2024 et ai-01, et on monte po-2026 si besoin.** + +Le cadrage binaire de la question etait fautif, et la mesure du vivant l'a montre. Sur ai-01 : + +``` +coursia-runner : 10 slots x COURSIA_RUNNER_CPUS=2 = 20 vCPU +coursia-waiters : 16 slots x WAITER_CPUS=0.25 = 4 vCPU + total = 24 vCPU +COURSIA_RUNNER_CPU_BUDGET = 24 +coursia-ci.slice CPUQuota = 24 vCPU (nproc = 32) +``` + +Le dimensionnement est **coherent et delibere** : 24 demandes contre 24 accordes, au vCPU pres. Il n'y avait ni derive a corriger ni sur-souscription a reduire. + +Le defaut est **l'absence de marge**. Un budget exactement sature throttle en permanence des que la charge monte, et chaque slot de calcul lance `pytest -n 4` — jusqu'a 40 workers xdist forkant du `git` sur 20 vCPU de quota. De la sortent les `BlockingIOError: [Errno 11]` sur `_fork_exec` (EAGAIN sur `fork()`) et les runners morts sans logs (`conclusion=null` + `BlobNotFound`). + +Repartition du pool `coursia-linux`, cible de **129 des 173** declarations `runs-on` du depot : + +| Hote | slots online | busy a la mesure | +|---|---:|---:| +| `myia-ai-01` | 10 | 8 | +| `myia-po-2024` | 8 | 7 | +| `myia-po-2026` | 2 | 2 | +| **total** | **20** | **17** | + +Le levier retenu est donc d'**ajouter** des slots sur po-2026 (cible 8, parite avec po-2024), ce qui soulage ai-01 par le tirage de file sans toucher a sa configuration. + +**Non etabli, et pas devine** : `docker ps` rend 0 conteneur runner sur ai-01 alors que 28 `Runner.Listener` tournent. La cause n'est pas mesuree. De meme, la corruption d'etiquette `cour sia-linux` signalee par po-2026 n'est pas mesurable depuis ai-01 (`actions/runners` rend 403 sous `myia-ai-01`, readlink sur `/proc//cwd` refuse). + +### Piege de provisionnement a ne pas rejouer + +Deployer une unite systemd declarant N slots **sans** son drop-in de sizing fait retomber `supervise.sh` sur `MEMORY=4g` par defaut ; le garde de budget refuse, `Restart=always` reboucle toutes les 30 s, et le pool ne monte **jamais**. Les deux fichiers partent ensemble ou aucun. Et `assert_cpu_budget()` **sort immediatement sans rien imprimer quand le budget vaut 0** : son silence n'est pas un feu vert, c'est une absence de mesure. Detail : [`persist/README.md`](../../scripts/ci/docker/linux-runner/persist/README.md).