Repository navigation
fix(gate,#21818): l'auteur d'un dossier normalise une seule fois (GraphQL rend le login d'un Bot sans le suffixe [bot]) - #21819
Conversation
…(GraphQL vs REST) Le gate de fermeture lisait l'auteur d'un commentaire en GraphQL, le gate de prevalidation adjointe le lisait en REST. Les deux surfaces ne rendent pas le MÊME login pour un acteur de type `Bot` -- mesure firsthand sur ai-01 (2026-10-21, commentaire `github-actions` sur #21390) : GraphQL `author { __typename login }` : login = "github-actions" (SANS suffixe) REST `user` : login = "github-actions[bot]" (AVEC suffixe) `ACCEPTED_DOSSIER_AUTHORS` est ecrit en orthographe REST : le gate de fermeture refusait donc une App de lane (`comment author 'coursia-lane-po-2026' is not an accepted dossier author`, signale par la lane secretaire), alors que le gate adjoint l'acceptait. La normalisation est portE par un point de passage UNIQUE, `gh_identity.normalize_author_login`, qui remet l'auteur en orthographe REST. La reconstruction du suffixe exige le TYPE de l'acteur (`__typename == "Bot"` en GraphQL, `type == "Bot"` en REST) : un compte UTILISATEUR nomme comme le slug de l'App reste refuse. Un auteur de type inconnu n'est pas un bot -- le doute profite au refus. Le gate de fermeture lit maintenant ses commentaires en REST, qui porte deja l'orthographe et le type ; le gate adjoint passe par la meme normalisation (idempotente sur du REST). Effet de bord corrige au passage : le test `login.endswith("[bot]")` qui ignore les commentaires de bots ne pouvait jamais reconnaitre un bot sous l'orthographe GraphQL. Tests : 49 verts dans `test_check_closure_dossier.py` (dont 6 nouveaux -- controle positif App, controle negatif du compte utilisateur homonyme, non-regression des comptes humains, chemin de lecture reel), 260 verts dans `test_check_adjoint_prevalidation.py` + `test_gh_identity.py`. Verification sur issues reelles : le gate rend KEEP sur #19969 et #18941 via le nouveau chemin REST. Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com>
|
No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml). Detector: |
|
G-VAR-2 light cap reached (advisory, non bloquant). |
|
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 |
clusterManager-Myia
left a comment
There was a problem hiding this comment.
Grain: MED/guard — lane myia-ai-01:CoursIA-2 — prev: DEEP/lean #21812
Fix(gate,#21818): l'auteur d'un dossier est normalisé une seule fois (GraphQL vs REST)
Closes #21818.
Constat (mesuré firsthand le 2026-10-21 sur ai-01)
Les deux gates qui lisent l'auteur d'un dossier ne lisaient pas la même surface :
| Gate | Surface | login rendu pour un acteur Bot |
Type disponible |
|---|---|---|---|
check_closure_dossier.py |
GraphQL (gh issue view --json comments) |
github-actions (sans suffixe) |
aucun (gh --json n'expose que login) |
check_adjoint_prevalidation.py |
REST (gh api repos/…/issues/…/comments) |
github-actions[bot] (avec suffixe) |
user.type |
Mesure directe (commentaire github-actions sur #21390, jeton épinglé jsboige) :
GraphQL : author { __typename login } -> __typename=Bot login=github-actions
REST : user -> type=Bot login=github-actions[bot]
ACCEPTED_DOSSIER_AUTHORS (scripts/gh_identity.py) est écrit en orthographe REST (coursia-lane-po-2026[bot]). Le gate de fermeture comparait donc l'orthographe GraphQL à l'orthographe REST et refusait une App de lane — symptôme rapporté par la lane secrétaire (myia-po-2028:CoursIA-3) :
comment author 'coursia-lane-po-2026' is not an accepted dossier author
alors que le gate adjoint, qui lit en REST, acceptait le même dossier. Une App de lane reste inutilisable pour la fermeture, alors que son bucket REST est intact — c'est exactement l'enjeu de #17437 (le bucket REST du login partagé tombe à 0 en moins de 60 s après chaque reset horaire).
Effet de bord de la même divergence : _is_neutral_comment ignore les commentaires de bots via login.endswith("[bot]"). Sous l'orthographe GraphQL, ce test ne reconnaît jamais un bot : un commentaire de bot posté après le dossier pouvait être lu comme une péremption du dossier.
Correctif
1. Une normalisation, un seul point de passage — scripts/gh_identity.py :
normalize_author_login(author): le login d'un acteur de typeBotest rendu en orthographe REST (login[bot]). Idempotent sur une lecture REST (login déjà suffixe), correctif sur une lecture GraphQL.author_is_bot(author): accepte les deux formes rencontrées —__typename(GraphQL) ettype(REST). Un auteur dont le type est inconnu n'est pas un bot : le doute profite au refus.is_accepted_dossier_author(author): la comparaison se fait sur le login normalisé.
2. La reconstruction du suffixe exige le TYPE de l'acteur, jamais la forme du login — sans cette exigence, un compte utilisateur nommé comme le slug de l'App (coursia-lane-po-2026, sans suffixe) passerait le gate. C'est le contrôle négatif du correctif.
3. Le gate de fermeture lit ses commentaires en REST (gh api repos/{repo}/issues/{n}/comments --paginate, comme le gate adjoint) : REST porte déjà l'orthographe ET le type de l'acteur, en une seule surface, sans requête GraphQL sur mesure ni pagination à écrire. Le gate adjoint passe par la même normalisation (idempotente sur du REST) : les deux gates comparent désormais la même forme par construction, au lieu de dépendre de la surface qui a lu l'auteur.
Tests
scripts/tests/test_check_closure_dossier.py — 49 verts, dont 6 nouveaux :
| Test | Ce qu'il verrouille |
|---|---|
test_normalisation_rend_une_app_graphql_en_orthographe_rest |
App GraphQL → …[bot] ; REST → inchangé ; humain → inchangé ; type inconnu → pas un bot |
test_un_compte_utilisateur_homonyme_est_refuse |
contrôle négatif : __typename=User sur le slug de l'App → refus ; __typename=Bot → acceptation |
test_comptes_humains_non_regresses |
jsboige accepté, un humain quelconque refusé, une App de lane inconnue refusée |
test_le_gate_accepte_un_dossier_signe_par_une_app_lue_en_rest |
contrôle positif : chemin de lecture réel (_issue_comments REST) → dossier d'App accepté, verdict CLOSE |
test_le_gate_refuse_un_compte_utilisateur_homonyme |
même chemin, acteur User → refus |
test_les_deux_gates_partagent_le_meme_point_de_normalisation |
les deux gates partagent le même objet gh_identity (pas de copie de la liste) |
scripts/tests/test_check_adjoint_prevalidation.py + scripts/tests/test_gh_identity.py : 260 verts (non-régression du gate adjoint).
Vérification sur issues réelles (le gate rend son verdict via le nouveau chemin REST) :
$ python scripts/check_closure_dossier.py 19969
KEEP -- issue #19969 carries an intact dossier attesting it must stay open.
$ python scripts/check_closure_dossier.py 18941
KEEP -- issue #18941 carries an intact dossier attesting it must stay open.
Critères d'acceptation de #21818
| # | Critère | État |
|---|---|---|
| 1 | un dossier posté sous une App de lane est accepté | couvert — contrôle positif (test du chemin de lecture) + is_accepted_dossier_author |
| 2 | un compte utilisateur homonyme est refusé | couvert — contrôle négatif : le suffixe n'est reconstruit que si le type est Bot |
| 3 | non-régression des comptes humains | couvert — jsboige accepté, humain quelconque refusé |
| 4 | la normalisation est partagée par les deux gates | couvert — point de passage unique dans gh_identity, testé |
Fichiers
scripts/gh_identity.py:author_is_bot,normalize_author_login,is_accepted_dossier_author+ la mesure consignée.scripts/check_closure_dossier.py: lecture des commentaires en REST + normalisation ; le reste du gate est inchangé.scripts/check_adjoint_prevalidation.py: les auteurs lus en REST passent par la normalisation (idempotente) et portent leur type.scripts/tests/test_check_closure_dossier.py: +6 tests.
Conséquences assumées
- Le gate de fermeture consomme un appel REST de plus par snapshot (les commentaires ne viennent plus du
gh issue view --json). Le gate adjoint en consomme déjà un de ce type ; le bucket REST reste le point sensible relevé dans #17437, et c'est précisément l'App qui doit y respirer. - Un auteur de type inconnu n'est plus reconstruit : un dossier signé sous une App lue par une surface qui ne rend pas le type serait refusé. C'est le choix fail-closed, cohérent avec « le doute profite au refus ».
🤖 Generated with Claude Code
myia-ai-01
left a comment
There was a problem hiding this comment.
APPROVE a la tete 15da88bd36 (organe de gate, lu par le coordinateur).
Lu : diff de gh_identity.py, check_closure_dossier.py, check_adjoint_prevalidation.py.
- Un seul point de passage (
normalize_author_login) pour les deux gates : la forme canonique est definie une fois. - La reconstruction du suffixe
[bot]s'appuie sur le TYPE de l'acteur (__typenameoutype), jamais sur la forme du login, et un type inconnu profite au refus : un compte utilisateur nomme comme le slug d'une App ne passe pas. C'est la propriete qui compte, et elle est tenue. - Le gate de fermeture lit desormais les commentaires en REST pagine (
--paginate), plus viagh issue view --json comments.
Mesure a cette tete (worktree isole) : pytest scripts/tests/test_check_closure_dossier.py scripts/tests/test_check_adjoint_prevalidation.py -> 293 passed.
Le rouge Scripts Tests (CPU) de 14:09Z est le rouge herite de main (registre twin), repare par #21809 ; la branche sera rafraichie apres ce merge.
Path-collision (organ #13359/#13615)Cette PR #21819 (
|
Grain: MED/guard — lane myia-ai-01:CoursIA-2 — prev: DEEP/lean #21812
Fix(gate,#21818): l'auteur d'un dossier est normalisé une seule fois (GraphQL vs REST)
Closes #21818.
Constat (mesuré firsthand le 2026-10-21 sur ai-01)
Les deux gates qui lisent l'auteur d'un dossier ne lisaient pas la même surface :
loginrendu pour un acteurBotcheck_closure_dossier.pygh issue view --json comments)github-actions(sans suffixe)gh --jsonn'expose quelogin)check_adjoint_prevalidation.pygh api repos/…/issues/…/comments)github-actions[bot](avec suffixe)user.typeMesure directe (commentaire
github-actionssur #21390, jeton épingléjsboige) :ACCEPTED_DOSSIER_AUTHORS(scripts/gh_identity.py) est écrit en orthographe REST (coursia-lane-po-2026[bot]). Le gate de fermeture comparait donc l'orthographe GraphQL à l'orthographe REST et refusait une App de lane — symptôme rapporté par la lane secrétaire (myia-po-2028:CoursIA-3) :alors que le gate adjoint, qui lit en REST, acceptait le même dossier. Une App de lane reste inutilisable pour la fermeture, alors que son bucket REST est intact — c'est exactement l'enjeu de #17437 (le bucket REST du login partagé tombe à 0 en moins de 60 s après chaque reset horaire).
Effet de bord de la même divergence :
_is_neutral_commentignore les commentaires de bots vialogin.endswith("[bot]"). Sous l'orthographe GraphQL, ce test ne reconnaît jamais un bot : un commentaire de bot posté après le dossier pouvait être lu comme une péremption du dossier.Correctif
1. Une normalisation, un seul point de passage —
scripts/gh_identity.py:normalize_author_login(author): le login d'un acteur de typeBotest rendu en orthographe REST (login[bot]). Idempotent sur une lecture REST (login déjà suffixe), correctif sur une lecture GraphQL.author_is_bot(author): accepte les deux formes rencontrées —__typename(GraphQL) ettype(REST). Un auteur dont le type est inconnu n'est pas un bot : le doute profite au refus.is_accepted_dossier_author(author): la comparaison se fait sur le login normalisé.2. La reconstruction du suffixe exige le TYPE de l'acteur, jamais la forme du login — sans cette exigence, un compte utilisateur nommé comme le slug de l'App (
coursia-lane-po-2026, sans suffixe) passerait le gate. C'est le contrôle négatif du correctif.3. Le gate de fermeture lit ses commentaires en REST (
gh api repos/{repo}/issues/{n}/comments --paginate, comme le gate adjoint) : REST porte déjà l'orthographe ET le type de l'acteur, en une seule surface, sans requête GraphQL sur mesure ni pagination à écrire. Le gate adjoint passe par la même normalisation (idempotente sur du REST) : les deux gates comparent désormais la même forme par construction, au lieu de dépendre de la surface qui a lu l'auteur.Tests
scripts/tests/test_check_closure_dossier.py— 49 verts, dont 6 nouveaux :test_normalisation_rend_une_app_graphql_en_orthographe_rest…[bot]; REST → inchangé ; humain → inchangé ; type inconnu → pas un bottest_un_compte_utilisateur_homonyme_est_refuse__typename=Usersur le slug de l'App → refus ;__typename=Bot→ acceptationtest_comptes_humains_non_regressesjsboigeaccepté, un humain quelconque refusé, une App de lane inconnue refuséetest_le_gate_accepte_un_dossier_signe_par_une_app_lue_en_rest_issue_commentsREST) → dossier d'App accepté, verdictCLOSEtest_le_gate_refuse_un_compte_utilisateur_homonymeUser→ refustest_les_deux_gates_partagent_le_meme_point_de_normalisationgh_identity(pas de copie de la liste)scripts/tests/test_check_adjoint_prevalidation.py+scripts/tests/test_gh_identity.py: 260 verts (non-régression du gate adjoint).Vérification sur issues réelles (le gate rend son verdict via le nouveau chemin REST) :
Critères d'acceptation de #21818
is_accepted_dossier_authorBotjsboigeaccepté, humain quelconque refuségh_identity, testéFichiers
scripts/gh_identity.py:author_is_bot,normalize_author_login,is_accepted_dossier_author+ la mesure consignée.scripts/check_closure_dossier.py: lecture des commentaires en REST + normalisation ; le reste du gate est inchangé.scripts/check_adjoint_prevalidation.py: les auteurs lus en REST passent par la normalisation (idempotente) et portent leur type.scripts/tests/test_check_closure_dossier.py: +6 tests.Conséquences assumées
gh issue view --json). Le gate adjoint en consomme déjà un de ce type ; le bucket REST reste le point sensible relevé dans GitHub Apps par lane : les comptes machine sont bridés au tarif anonyme (60 req/h, GraphQL 0) et hors-limite CGU #17437, et c'est précisément l'App qui doit y respirer.🤖 Generated with Claude Code