Skip to content

feat(ci,#17290): exposer le motif atteste du dossier adjoint dans --json - #17291

Merged
myia-ai-01 merged 3 commits into
mainfrom
feat/17290-adjoint-motif
Sep 23, 2026
Merged

myia-ai-01 merged 3 commits into
mainfrom
feat/17290-adjoint-motif

Conversation

@jsboige

@jsboige jsboige commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Grain: MED/guard -- lane myia-po-2026:CoursIA -- prev: MED/docs #17289

Quoi: sur exit 3, le dossier est intact — c'est ce que le code signifie — donc errors vaut [] par construction. La règle attachée à ce code demande pourtant de « dispatcher depuis le motif attesté par le dossier », et le gate ne le publiait nulle part : la seule voie était de re-télécharger les commentaires et de re-parser à la main les champs que le gate venait de lire.
Preuve: 58 tests passent ; 3 mutations → 3 fois le BON test tombe, restauration byte-identique vérifiée par sha256 ; sur les PRs live nommées par l'acceptance, blocking_fields discrimine les motifs (["checks"] vs ["b0"]).
Perimetre: 2 fichiers — scripts/check_adjoint_prevalidation.py (+104/−17), scripts/tests/test_check_adjoint_prevalidation.py (+130). Aucun notebook, aucun workflow, aucun .github/. Le contrat de dossier est inchangé côté émission.

Closes #17290

Le défaut, tel qu'il se lit dans le code

verdict == BLOCKED ⇒ return dossier.fields.get("verdict", ""), [] : la liste d'erreurs est vide parce que le dossier est intègre. Le seul champ qui aurait pu porter le motif était donc structurellement vide, et le result émis ne portait que pr, head, ready, verdict, errors.

L'organe se contredisait lui-même dans sa propre sortie :

BLOCKED-WITH-SUBSTANCE -- PR #16219 has an intact adjoint dossier at 292badb7… attesting it is NOT mergeable.
  Do not open its surfaces: dispatch from the dossier's stated reason.

La phrase désignait un motif que la commande ne rendait pas.

Ce que le gate publie maintenant

Ajout Rôle
BLOCKING_FIELDS les 4 champs du contrat qui doivent être à leur valeur READY — exactement ceux que validate_dossier contraint quand le dossier clame READY
blocking_fields(dossier) leur complément : le motif. Ordre du contrat, donc deux dossiers bloqués pour la même raison rendent identiquement → dispatch groupable
dossier_payload(dossier) tous les champs lus + provenance (author, created_at, comment_index)
build_result() construction pure, donc testable sans réseau (c'est ce qui rend l'acceptance vérifiable)
evaluate_with_dossier() rend le dossier ; evaluate() garde sa forme à 2 éléments — aucun appelant ne bouge sous lui

Aucun champ nouveau n'est requis à l'émission : on publie ce que parse_dossier parsait déjà. Le contrat de dossier est inchangé.

Le cas que je refuse de maquiller

Un dossier BLOCKED peut déclarer les 4 champs au vert : validate_dossier ne les contraint que sous verdict: READY, donc un dossier bloqué honnête peut porter sa raison dans sa prose. blocking_fields rend alors [] — et c'est la vraie réponse. Inventer un champ pour combler le silence fabriquerait précisément le motif que cette issue existe pour publier. Le cas est pinné par test_a_blocked_dossier_can_name_no_blocking_field.

Sur les PRs live (lecture seule, aucune surface ouverte)

#16219  rc=3  checks=BLOCKED  b0=clear              -> blocking_fields: ["checks"]
#16352  rc=3  b0=blocked      checks=latest-wins-green -> blocking_fields: ["b0"]
#17285  rc=1  (aucun dossier)                        -> aucun bloc `dossier`

Les deux premiers sont exactement les contrôles positifs nommés par l'acceptance, et ils discriminent : un checks mort part à l'adjoint pour un --template, un b0 réel part à la lane porteuse pour une phrase de levée. Sans motif exposé, ces deux dispatches étaient indiscernables.

Sortie humaine sur le même rc=3 :

  Do not open its surfaces: dispatch from the dossier's stated reason.
  attested reason: checks=BLOCKED, b0=clear, scope=pass, domain=not-applicable (blocking: checks)

Falsification (une épingle qui passe toujours est un test vide)

Sauvegarde par cp dans un arbre jetable — jamais git checkout --, qui restaure depuis l'INDEX.

Mutation Résultat attendu Observé
blocking_fields retiré du résultat émis le test d'exposition tombe 1 failed — test_blocked_result_publishes_the_dossier_and_its_blocker
le bloc dossier publié même sur dossier refusé le contrôle négatif tombe 1 failed — test_a_refused_dossier_is_not_published_at_all
blocking_fields nomme les 4 champs en permanence le contrôle positif tombe 1 failed — test_blocking_fields_name_checks_on_a_checks_blocked_dossier
aucun changement 58 passed 58 passed

sha256 de la source identique avant/après les 3 mutations.

Acceptance

Critère État Preuve
--json sur rc=3 expose dossier + blocking_fields non vide fait live #16219 et #16352 ; pinné par test_blocked_result_publishes_the_dossier_and_its_blocker
--json sur rc=0 expose le même bloc, blocking_fields: [] fait pinné par test_ready_result_publishes_the_dossier_with_no_blocker (unitaire : aucune PR live à dossier READY trouvée au moment du commit)
contrôle positif : #16219 → ["checks"], #16352 → ["b0"] fait live, verbatim ci-dessus
contrôle négatif : empreinte cassée → rc=1 sans bloc dossier fait pinné par test_a_refused_dossier_is_not_published_at_all (mutation B) ; variante « dossier absent » vérifiée live sur #17285
aucun champ nouveau requis à l'émission fait dossier_payload = dict(dossier.fields) + provenance ; contrat inchangé
tests dans scripts/tests/test_check_adjoint_prevalidation.py fait 8 tests ajoutés (58 au total)

Deux lignes de ce tableau sont prouvées par test unitaire et non par une PR live (rc=0 : aucune PR à dossier READY disponible à l'instant du commit). Je le dis plutôt que de laisser croire à six preuves live.

Hors scope, respecté

Le contenu du contrat de dossier (aucun champ ajouté côté émission) ; check_unaddressed_nits.py ; la péremption de b0 (#17271).

🤖 Generated with Claude Code

Sur exit 3 le dossier est INTACT -- c'est ce que le code signifie -- donc
`errors` vaut [] par construction. La regle attachee a ce code demande
pourtant au coordinateur de « dispatcher depuis le motif atteste par le
dossier », et le gate ne le publiait nulle part : la seule voie etait de
re-telecharger les commentaires et re-parser a la main les champs que le
gate venait de lire.

- `BLOCKING_FIELDS` : les 4 champs du contrat qui doivent etre a leur valeur
  READY (ceux que `validate_dossier` contraint quand le dossier clame READY).
- `blocking_fields(dossier)` : leur complement -- le motif. Rend [] quand un
  dossier BLOCKED honnete declare les 4 au vert (le contrat le permet ; la
  raison vit alors dans la prose) : nommer les champs qui bloquent, jamais en
  inventer un pour combler le silence.
- `dossier_payload(dossier)` : tous les champs lus + provenance. Aucun champ
  nouveau exige a l'emission -- on publie ce que `parse_dossier` parsait deja.
- `build_result()` pur, donc testable sans reseau. Le bloc `dossier` ne part
  QU'AVEC un verdict accepte : un dossier refuse doit continuer a se lire
  comme refuse.
- `evaluate_with_dossier()` rend le dossier ; `evaluate()` garde sa forme
  historique a 2 elements, aucun appelant ne bouge sous lui.
- La sortie humaine nomme desormais le motif qu'elle demandait d'aller lire.

Preuves : 58 tests passent ; 3 mutations -> 3 fois le BON test tombe,
restauration byte-identique (sha256). Sur les PRs live nommees par
l'acceptance : #16219 `checks: BLOCKED`/`b0: clear` -> ["checks"] ;
#16352 `b0: blocked`/`checks: latest-wins-green` -> ["b0"] ; rc=1 reel ->
aucun bloc `dossier`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #17291 (feat(ci,#17290): exposer le motif atteste du dossier adjoint dans --json) 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.

@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] APPROVE — preuve-vive rejouée firsthand dans un worktree au head b9c0d90.

Rejeu intégral :

  • Tests : pytest scripts/tests/test_check_adjoint_prevalidation.py → 58/58 PASS (1.7 s), dont les 8 neufs ; le cas b0 a son test dédié (test_blocking_fields_name_b0_on_a_b0_blocked_dossier) et evaluate() garde sa forme à 2 éléments (test_evaluate_keeps_its_two_tuple_shape).
  • Contrôle positif live rejeu : python scripts/check_adjoint_prevalidation.py 16219 --json → rc=3, bloc dossier complet (lane, provenance comment_index: 10), blocking_fields: ["checks"] — verbatim conforme au body. Sortie humaine : attested reason: checks=BLOCKED, b0=clear, scope=pass, domain=not-applicable (blocking: checks).
  • Contrôle négatif live rejeu : #17285 (aucun dossier) → rc=1, aucun bloc dossier — conforme.
  • #16352 diverge aujourd'hui : rc=1 NO_DOSSIER (discussion changed) — une levée de réserve Hermes a été postée à 19:44Z, après le dossier du 10:57Z que le body citait (b0: blocked). C'est un dérivé du live, pas un défaut de la PR : la discrimination b0 était vraie au moment du body et reste pinnée par test unitaire. Le dossier redeviendra contraignant dès qu'un preflight frais sera émis.
  • Sec-scan : 0 match sur les 2 fichiers. build_result() pur = l'acceptance est vérifiable sans réseau ; aucun champ requis à l'émission (contrat inchangé).

L'honnêteté du tableau d'acceptance (2 lignes prouvées par test unitaire, non live, et dites comme telles) est appréciée.

@github-actions github-actions Bot added the variation-tag-missing PR sans tag Grain: <TIER>/<GENRE> (variation-protocol) label Sep 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Grain tag obligatoire (#10045, bloquant).

Grain tag absent (no Grain: / in body).

Pour passer ce gate, le body doit porter en tete une ligne de la forme :

Grain: <DEEP|MED|LIGHT>/<genre> -- lane <machine:workspace> -- prev: <TIER>/<GENRE> #<PR>

Le <genre> doit figurer dans l'enumeration §1 de variation-protocol.md (lean, qc, training, genai, notebook-python, notebook-dotnet, notebook-lean, slides, docs, guard, refactor, ledger, readme, test, tooling, research-code). Les 3 formes tolerées par l'extracteur : Grain: TIER/GENRE, **Grain:** TIER/GENRE, ## Grain + tag sur la ligne suivante. La lane doit suivre le format <machine>:<workspace> (cf. lane-claim-protocol.md).

@jsboige

jsboige commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2026:CoursIA-3
pr: 17291
head: deae559
complete: true
body: read
comments-reviewed: 3
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: a75aa54656f6bae034b2de63aa5bfed429f3cbbfd3b861e24d9dad2b879d1a06
diff-files: 2
diff-additions: 234
diff-deletions: 17
checks: BLOCKED
b0: blocked
scope: pass
domain: pass
verdict: BLOCKED
[/ADJOINT PREFLIGHT]

[Tell c.81 - BLOCKED-WITH-SUBSTANCE] Always-on guards failure + PR gate FAIL (rate-limit). Cycle 15 hub secretaire.

# Conflicts:
#	scripts/tests/test_check_adjoint_prevalidation.py
@github-actions github-actions Bot added variation-light-cap-reached Lane ayant deja merge une LIGHT aujourd'hui (cap G-VAR-2 atteint) and removed variation-tag-missing PR sans tag Grain: <TIER>/<GENRE> (variation-protocol) labels Sep 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

G-VAR-2 light cap reached (advisory, non bloquant).
La lane myia-po-2026:CoursIA a deja consomme son budget LIGHT du jour (axe genre G-VAR-2/3 (light-genre, quel que soit le tier declare) : #17275 (MED/guard, merge a 2026-09-23T00:03:57Z), #17272 (MED/guard, merge a 2026-09-23T00:11:26Z), #17432 (MED/guard, merge a 2026-09-23T00:23:00Z), #17384 (LIGHT/notebook, merge a 2026-09-23T00:59:13Z), #17414 (MED/guard, merge a 2026-09-23T01:15:48Z), #17247 (LIGHT/test, merge a 2026-09-23T01:24:30Z), #17382 (LIGHT/notebook, merge a 2026-09-23T04:12:48Z), #17285 (MED/guard, merge a 2026-09-23T06:30:29Z)).
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

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

Copy link
Copy Markdown
Contributor

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

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

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 Sep 23, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2026:CoursIA-3
pr: 17291
head: e100d40
complete: true
body: read
comments-reviewed: 6
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: 591106d35511f11863d6447c7ef2200b1dfce3e12609cc7a50c098a6590dd5dd
diff-files: 2
diff-additions: 234
diff-deletions: 17
checks: latest-wins-green
b0: clear
scope: pass
domain: not-applicable
verdict: READY
[/ADJOINT PREFLIGHT]

@jsboige

jsboige commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 17291
head: e100d40
complete: true
body: read
comments-reviewed: 7
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: b21ffe73c237669f9040fc144a59c208081fc3066455bdf02da03485d01930ea
diff-files: 2
diff-additions: 234
diff-deletions: 17
checks: latest-wins-green
b0: pass
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

Note : merge e100d40 résolu à la main sur scripts/tests/test_check_adjoint_prevalidation.py ; la résolution est l'union des deux blocs de tests (seuls les marqueurs retirés, diff auto-merge→tête = 1+/3−). Arbre fusionné avec origin/main : pytest 72 passed ; le gate fusionné, rejoué sur #17497, rend blocking_fields ['checks'] et errors []. La review Hermes 5271616602 date de deae559 : elle précède cette résolution.

@jsboige

jsboige commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 17291
head: e100d40
complete: true
body: read
comments-reviewed: 8
reviews-reviewed: 1
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: b846f072999ead49107f5a0eaebee3df0b93f6a73fbc6aa8a07145abbfd570e3
diff-files: 2
diff-additions: 234
diff-deletions: 17
checks: latest-wins-green
b0: clear
scope: pass
domain: pass
verdict: READY
[/ADJOINT PREFLIGHT]

Note : merge e100d40 résolu à la main sur scripts/tests/test_check_adjoint_prevalidation.py ; la résolution est l'union des deux blocs de tests (seuls les marqueurs retirés, diff auto-merge→tête = 1+/3−). Arbre fusionné avec origin/main : pytest 72 passed ; le gate fusionné, rejoué sur #17497, rend blocking_fields ['checks'] et errors []. La review Hermes 5271616602 date de deae559 : elle précède cette résolution. Le dossier 5794400580 juste au-dessus porte une valeur b0 invalide (refusé par le gate) : celui-ci le remplace.

@myia-ai-01
myia-ai-01 merged commit 1fe8107 into main Sep 23, 2026
19 of 21 checks passed
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