From 517317e4fd3363f61f12da2aa78432ec2bb23dc8 Mon Sep 17 00:00:00 2001 From: jsboige Date: Thu, 17 Sep 2026 02:03:12 +0200 Subject: [PATCH] fix(guard,#16442): un bloc [ADJOINT PREFLIGHT] est une attestation, pas un nit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'organe B.0 comptait le verdict: BLOCKED d'un dossier supersede comme un nit [HUMAN] non leve (#16173) -- le dossier devenait son propre bloquant, remedie jusqu'ici par PATCH manuel (pierre tombale). Le span delimité est desormais retire du corps avant classification (concern) et dans l'etage explicit_lifts : le verdict d'un dossier appartient au gate check_adjoint_prevalidation.py (#16443), pas a l'organe nits. Bornes fail-closed : bloc malforme non retire, prose hors bloc lue normalement, dossier garde en queue A RELIRE (affichage non bloquant). Tests rouges d'abord : 8 nouveaux (dont 4 controles bilateraux), suite organe 579 passed. Diff live #15791 : 3 nits -> 2 (le dossier sort du bloquant, rc=1 inchange pour la vraie raison). Co-Authored-By: Claude Sonnet 5 --- scripts/check_unaddressed_nits.py | 35 +++- ..._check_unaddressed_nits_adjoint_dossier.py | 157 ++++++++++++++++++ 2 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 scripts/tests/test_check_unaddressed_nits_adjoint_dossier.py diff --git a/scripts/check_unaddressed_nits.py b/scripts/check_unaddressed_nits.py index aedd2cd3ea..647b58ec10 100644 --- a/scripts/check_unaddressed_nits.py +++ b/scripts/check_unaddressed_nits.py @@ -3571,10 +3571,41 @@ def can_lift(comment: dict) -> bool: return True +# #16442/#16443 — un bloc [ADJOINT PREFLIGHT] est une ATTESTATION, pas une +# remarque. Defaut mesure (#16173, 2026-09-16) : l'organe comptait le +# `verdict: BLOCKED` d'un dossier SUPERSEDE comme un nit [HUMAN] non leve -- +# le dossier devenait son propre bloquant, et le seul remede etait un PATCH +# manuel du commentaire (pierre tombale + neutralisation des marqueurs ; +# reproduction partielle par le dossier interim de lane sur #16449 a +# 23:35:02Z). La separation des organes est le principe : le VERDICT d'un +# dossier appartient au gate `check_adjoint_prevalidation.py` (#16443), qui +# le fail-close a l'empreinte pres ; l'organe nits lit des REMARQUES a +# lever. Le bloc schema est retire du corps AVANT classification -- ni +# reserve, ni (cf explicit_lifts, meme strip) levee. Deux bornes +# fail-closed : un bloc MALFORME (delimiter ouvrant sans fermant) n'est pas +# retire -- c'est le gate qui refuse le dossier, pas l'organe qui le +# blanchit ; la prose HORS du bloc (tete de pierre tombale comprise) reste +# lue normalement. +_ADJOINT_DOSSIER_SPAN = re.compile( + r"^[ \t]*\[ADJOINT PREFLIGHT\][ \t]*\r?\n" + r".*?" + r"^[ \t]*\[/ADJOINT PREFLIGHT\][ \t]*\r?(?:\n|\Z)", + re.DOTALL | re.MULTILINE, +) + + +def _strip_adjoint_dossier(body: str) -> str: + """Retirer les spans d'attestation [ADJOINT PREFLIGHT] bien delimites.""" + return _ADJOINT_DOSSIER_SPAN.sub("", body) + + def classify(author: str, body: str) -> str | None: """'HUMAN' (nit user, UI web) | 'BOT-CONCERN' (reviewer avec reserves) | None.""" if author in BOT_LOGINS or not body: return None + body = _strip_adjoint_dossier(body) + if not body.strip(): + return None # attestation pure : le gate en lit le verdict, pas l'organe stripped = body.lstrip() # #12143 — Hermes severity glyphes (subordonne LIFT_MARKERS, fix concomitant # du PR #12148 fondateur) : un glyphe est une EMISSION, et une emission @@ -4156,7 +4187,7 @@ def _lift_eligible(lift_author: str, nit_author: str, if can_lift(c) # #12908 : levée VIVE exigée — le PREFLIGHT de #12798 qui demandait # « une levée explicite » était compté comme levée par le sac de mots. - and has_live_lift(c.get("body", "")) + and has_live_lift(_strip_adjoint_dossier(c.get("body", ""))) and not _lift_cancelled(_strip_quoted(c.get("body", ""))) # #12836 / #12798 : une reserve qui narre une ancienne levee reste # une reserve, pas un evenement de levee du signal precedent. @@ -4174,7 +4205,7 @@ def _lift_eligible(lift_author: str, nit_author: str, for r in (pr_data.get("reviews") or []) if r.get("state") == "COMMENTED" and can_lift(r) - and has_live_lift(r.get("body", "")) + and has_live_lift(_strip_adjoint_dossier(r.get("body", ""))) and not _lift_cancelled(_strip_quoted(r.get("body", ""))) and classify((r.get("author") or {}).get("login", ""), r.get("body", "")) is None diff --git a/scripts/tests/test_check_unaddressed_nits_adjoint_dossier.py b/scripts/tests/test_check_unaddressed_nits_adjoint_dossier.py new file mode 100644 index 0000000000..a2b8558188 --- /dev/null +++ b/scripts/tests/test_check_unaddressed_nits_adjoint_dossier.py @@ -0,0 +1,157 @@ +"""Tests #16442/#16443 — un bloc [ADJOINT PREFLIGHT] est une ATTESTATION, pas une remarque. + +Defaut mesure (2026-09-16, #16173) : l'organe B.0 attrapait le +`verdict: BLOCKED` d'un dossier adjoint SUPERSEDE comme un nit [HUMAN] non +leve -- le dossier devenait son propre bloquant, et le remede etait un +PATCH manuel du commentaire (pierre tombale + neutralisation des +marqueurs). La PR #16449 a reproduit le motif des 23:35Z (dossier interim +de lane a cheval sur la mutation de tete). + +La separation des organes est le principe du fix : le VERDICT d'un dossier +appartient au gate `check_adjoint_prevalidation.py` (#16443), qui le +fail-close a l'empreinte pres ; l'organe nits lit des REMARQUES a lever. +Un bloc schema v1 entre ses delimiters `[ADJOINT PREFLIGHT]` / +`[/ADJOINT PREFLIGHT]` est une attestation machine-lisible : ni reserve, +ni levee. + +Les tests sont ecrits par leurs FAUX POSITIFS (un jeu de motifs se valide +par les formes qu'il doit neutraliser, cf anti-regression.md) : les +premiers ECHOUENT sur le code d'avant, les controles suivants passent des +deux cotes et bornent le retrait (une phrase de reserve HORS du bloc reste +vie ; un bloc MALFORME sans delimiter fermant n'est pas retire -- +fail-closed sur la malformation, comme le gate). +""" + +import importlib.util +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +CHECK_PATH = HERE.parent / "check_unaddressed_nits.py" + +spec = importlib.util.spec_from_file_location("check_unaddressed_nits", CHECK_PATH) +mod = importlib.util.module_from_spec(spec) +sys.modules["check_unaddressed_nits"] = mod +spec.loader.exec_module(mod) + + +# Dossier BLOCKED reconstruit de la forme exacte du dossier #16173 +# (issuecomment 5705104466 avant archivage) : schema v1, verdict BLOCKED, +# note portant la phrase de recommandation qui emettait ("avant merge"). +DOSSIER_16173_BLOCKED = """[ADJOINT PREFLIGHT] +schema: 1 +lane: myia-po-2025:CoursIA-2 +pr: 16173 +head: 3ebeea7b4bd4710e399f27d3dc699582bd38058 +complete: true +body: read +comments-reviewed: 6 +reviews-reviewed: 2 +threads-reviewed: 3 +threads-unresolved: 1 +surfaces-sha256: 0000000000000000000000000000000000000000000000000000000000000000 +diff-files: 2 +diff-additions: 48 +diff-deletions: 0 +checks: latest-wins-green +b0: blocked +scope: pass +domain: pass +verdict: BLOCKED +note: thread inline #2 non leve au head -- re-review exact-head exigee avant merge +[/ADJOINT PREFLIGHT]""" + +# Dossier interim d'une lane (forme reelle #16449, 2026-09-16T23:35:02Z) : +# laneattribue, verdict hors enum, honnete sur l'attente CI. Aucun marqueur +# de reserve ne doit en sortir non plus. +DOSSIER_16449_INTERIM = """[ADJOINT PREFLIGHT] +schema: 1 +lane: myia-po-2026:CoursIA +pr: 16449 +head: c02e81fa9e3476b8f35426032b357f53abd60fe2 +complete: true +body: read +comments-reviewed: 7 +reviews-reviewed: 1 +threads-reviewed: 0 +threads-unresolved: 0 +surfaces-sha256: 48f9cbadde57ca4850f2094842822d26c51a95c41282ca8e1f4c7369356a1617 +diff-files: 2 +diff-additions: 72 +diff-deletions: 0 +checks: new-head-in-flight +b0: clear +scope: pass +domain: pass +verdict: REPAIR-APPLIED-AWAITING-CI +note: reagregation DWELL requise au nouveau head ; definition demandee au coordinateur par DM +[/ADJOINT PREFLIGHT]""" + +# Pierre tombale (remede manuel #16173) : prose d'archive + bloc neutralise. +TOMBSTONE_16173 = """[ARCHIVE 2026-09-16] Dossier supersede par la mutation de tete -- cf dossier frais en fin de fil. + +[ADJOINT PREFLIGHT] +schema: 1 +verdict: (archive) +b0: (archive) +[/ADJOINT PREFLIGHT]""" + + +# --- Les faux positifs que l'organe comptait comme nits (echouent avant fix) --- + +def test_dossier_bloque_nest_pas_un_nit(): + assert mod.classify("jsboige", DOSSIER_16173_BLOCKED) is None + + +def test_dossier_interim_nest_pas_un_nit(): + assert mod.classify("jsboige", DOSSIER_16449_INTERIM) is None + + +def test_tombstone_nest_pas_un_nit(): + assert mod.classify("jsboige", TOMBSTONE_16173) is None + + +def test_dossier_pur_nest_pas_une_levee(): + # Un dossier dont la note RACONTE une levee ("levee par Hermes") ne doit + # pas compter comme evenement de levee : l'attestation ne leve rien. + body = DOSSIER_16449_INTERIM.replace( + "note: reagregation DWELL", + "note: reserve Hermes levee par reponse ecrite a 22:10Z ; reagregation DWELL", + ) + stripped = mod._strip_adjoint_dossier(body) + assert mod.has_live_lift(stripped) is False + + +# --- Controles : ce que le retrait ne doit PAS neutraliser (passent des deux cotes) --- + +def test_reserve_hors_du_bloc_reste_vivante(): + # Meme genre de remarque, HORS du bloc : c'est une remarque ordinaire. + # (Prose sans mot de levee -- « merge. » en fin de phrase serait lu comme + # une annonce de merge par l'etage LIFT, confondant le controle.) + prose = "Le fil inline #2 reste a nuancer sur la formulation exacte." + assert mod.classify("jsboige", prose) is not None # vivante seule + body = prose + "\n\n" + DOSSIER_16173_BLOCKED + assert mod.classify("jsboige", body) is not None # vivante aussi devant un bloc + + +def test_bloc_malforme_sans_fermant_nest_pas_retire(): + # Delimiter ouvrant sans fermant : le gate (check_adjoint_prevalidation) + # refuse ce dossier ; l'organe ne doit pas non plus le blanchir. + malformed = DOSSIER_16173_BLOCKED.replace("[/ADJOINT PREFLIGHT]", "") + assert mod.classify("jsboige", malformed) is not None + + +def test_prose_de_tombstone_avec_reserve_reste_vivante(): + body = TOMBSTONE_16173.replace( + "Dossier supersede", + "Dossier supersede MAIS le thread inline #2 reste a nuancer avant merge", + ) + assert mod.classify("jsboige", body) is not None + + +def test_strip_neutralise_le_span_et_garde_le_reste(): + body = "Avant le dossier.\n\n" + DOSSIER_16173_BLOCKED + "\n\nApres le dossier." + stripped = mod._strip_adjoint_dossier(body) + assert "verdict: BLOCKED" not in stripped + assert "Avant le dossier." in stripped + assert "Apres le dossier." in stripped