Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 42 additions & 10 deletions scripts/check_unaddressed_nits.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,22 @@
# La forme verdict-gras (`**BLOCAGE ...**` en tete de corps) est couverte par
# _block_emitted (2e branche), pas par ce pattern. La prose descriptive peut
# suivre sur la meme ligne. Alias anglais `[BLOCK] lane` pour les reviews
# ecrites en anglais.
BLOCAGE_LANE = re.compile(r"(?m)^\s*\[(?:BLOCAGE|BLOCK)\]\s+lane\s+\S+")
# ecrites en anglais. `[HOLD] lane` (#13779) : HOLD est le verbe que
# variation-protocol.md donne au coordinateur (« HOLD sans remplacement =
# echec coordinateur ») — entre crochets et en tete de ligne, il est aussi
# peu ambigu que les deux autres.
BLOCAGE_LANE = re.compile(r"(?m)^\s*\[(?:BLOCAGE|BLOCK|HOLD)\]\s+lane\s+\S+")

# #13779 — la forme VERDICT du HOLD coordinateur : le corps COMMENCE par HOLD,
# modulo l'emphase markdown (`**HOLD ...**`) et le titre (`## HOLD ...`). Deux
# resserrements par rapport a la pose de BLOCAGE (n'importe ou dans les 60
# premiers chars, casse indifferente), parce que « hold » est un mot ordinaire
# la ou « blocage » est deja presque toujours un verdict :
# - POSITION : tete de corps, pas tete de fenetre — « je ne mets pas de hold
# sur cette PR » ne pose rien ;
# - CASSE : HOLD majuscule, la forme dans laquelle le protocole et les posts
# du coordinateur l'ecrivent — « hold on, je regarde » ne pose rien.
HOLD_HEAD = re.compile(r"^[\s*_#]*HOLD\b")

# Marqueurs de reserve d'un reviewer bot (le verdict est dans le body, pas l'etat).
# #12311 — `REQUEST_CHANGES` (verbe, e.g. « [Hermes] Review — REQUEST_CHANGES »)
Expand Down Expand Up @@ -1105,6 +1119,21 @@ def has_live_marker(body: str, markers: tuple[str, ...]) -> bool:
return False


def _lift_participle_after(head: str, end: int) -> bool:
"""Le mot qui SUIT l'occurrence est-il un participe de levee ?

Miroir post-fenetre de `_is_cited` : « BLOCAGE leve », « HOLD lifted »
NOMMENT le blocage pour le clore, ils ne l'emettent pas (mention #11636).
"""
tail = head[end:end + 10].lstrip(" \t:;,.)('\"-—*")
word = ""
for ch in tail:
if not ch.isalpha():
break
word += ch
return word.lower() in ("leve", "levee", "lifted")


def _block_emitted(body: str) -> bool:
"""Le coordinateur POSE-t-il un blocage (verdict, jamais une citation) ?

Expand All @@ -1121,7 +1150,13 @@ def _block_emitted(body: str) -> bool:
la position du verdict, pas une substring de milieu. La narration
« pas un blocage » (fixture #11190) vit en section ; l'emission
aussi. Les negations immediates (« pas de blocage ») restent citees
via `_is_cited`.
via `_is_cited` ;
(c) le verdict `**HOLD ...**` en TETE de corps (#13779). HOLD est le
verbe que variation-protocol.md §3 donne au coordinateur (« HOLD
sans remplacement = echec coordinateur ») — un gate de merge qui
ignore le verbe de l'instrument qui le pilote est aveugle a son
propre pilote. Pose plus stricte que (b) — debut de corps ET
majuscule, cf HOLD_HEAD — parce que « hold » est un mot ordinaire.

La levée d'un blocage passe par les formes canoniques — LIFT_MARKER
reconnu (donc classify -> None dans la branche levee, cf `classify`) ou
Expand Down Expand Up @@ -1154,6 +1189,9 @@ def _block_emitted(body: str) -> bool:
uhead = head.upper()
if head.lstrip(" \t*_").upper().startswith("[OVERRIDE]"):
return False
m_hold = HOLD_HEAD.match(head)
if m_hold and not _lift_participle_after(head, m_hold.end()):
return True
for marker in ("BLOCAGE", "BLOCK"):
pos = 0
while (i := uhead.find(marker, pos)) != -1:
Expand All @@ -1162,13 +1200,7 @@ def _block_emitted(body: str) -> bool:
if before.isalnum() or before == "_" or before == "[" or after.isalnum() or after == "_":
pos = i + len(marker)
continue
tail = head[i + len(marker):i + len(marker) + 10].lstrip(" \t:;,.)('\"-—*")
word = ""
for ch in tail:
if not ch.isalpha():
break
word += ch
if word.lower() in ("leve", "levee", "lifted"):
if _lift_participle_after(head, i + len(marker)):
pos = i + len(marker)
continue
if not _is_cited(normalised[max(0, i - 30):i]):
Expand Down
135 changes: 135 additions & 0 deletions scripts/tests/test_check_unaddressed_nits_hold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Tests #13779 facette B — le gate B.0 et le verbe HOLD du coordinateur.

Defaut mesure : `.claude/rules/variation-protocol.md` §3 donne au coordinateur
UN verbe pour retenir une PR — **HOLD** (« HOLD sans remplacement = echec
coordinateur », « HOLD : citer la sortie de variation_light_cap.py »). Le gate,
lui, ne connaissait que BLOCAGE / BLOCK. Le post reel de myia-ai-01 sur #13712
(2026-08-30T22:19:57Z) — « **HOLD coordinateur — un point du preflight reste
ouvert, et le gate ne le voit pas.** » — rendait donc `classify -> None` : un
gate de merge aveugle au verbe de l'instrument qui le pilote.

Les tests sont ecrits par leurs FAUX NEGATIFS (un jeu de motifs se valide par
les formes qu'il doit attraper, jamais par ses hits, cf anti-regression.md) :
les cinq premiers ECHOUENT sur le code d'avant, les suivants sont des controles
qui passent des deux cotes et bornent l'elargissement.

La pose de HOLD est PLUS STRICTE que celle de BLOCAGE, sur deux axes, parce que
« hold » est un mot ordinaire la ou « blocage » est deja presque toujours un
verdict : tete de CORPS (pas n'importe ou dans les 60 premiers chars) et
MAJUSCULE (la forme du protocole). Les controles ci-dessous fixent ces deux
bornes — sans elles, « je ne mets pas de hold sur cette PR » bloquerait.
"""

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)

# Corps EXACT du commentaire myia-ai-01 du 2026-08-30T22:19:57Z sur la PR #13712
# (tete seule : la suite est le detail du preflight, sans effet sur la pose).
FIXTURE_13712_HOLD = (
"**HOLD coordinateur — un point du preflight reste ouvert, "
"et le gate ne le voit pas.**"
)


# --- Les cinq formes que l'organe ratait (echouent sur le code d'avant) ---

def test_13779_hold_reel_de_13712_est_une_emission():
assert mod._block_emitted(FIXTURE_13712_HOLD) is True


def test_13779_hold_reel_de_13712_est_classe_block():
assert mod.classify("myia-ai-01", FIXTURE_13712_HOLD) == "BLOCK"


def test_13779_marqueur_hold_lane_en_tete_de_ligne():
body = """Preflight en cours, deux points a verifier.

[HOLD] lane myia-po-2025:CoursIA — budget LIGHT epuise (3 pour 6 grains).
"""
assert mod._block_emitted(body) is True


def test_13779_hold_en_titre_markdown():
body = """## HOLD coordinateur

Le plancher G-VAR-1 n'est pas tenu : le genre est META.
"""
assert mod._block_emitted(body) is True


def test_13779_hold_nu_sans_emphase():
body = "HOLD — je tiens cette PR le temps que la lane nomme son remplacant."
assert mod._block_emitted(body) is True


# --- Controles : les bornes de l'elargissement (passent des deux cotes) ---

def test_13779_hold_minuscule_en_prose_ne_pose_rien():
# Sans la borne de CASSE, ce corps bloquerait la PR.
body = "hold on, je regarde le rapport d'echec avant de me prononcer."
assert mod._block_emitted(body) is False


def test_13779_hold_nomme_en_milieu_de_phrase_ne_pose_rien():
# Sans la borne de POSITION, ce corps — qui dit l'exact contraire —
# bloquerait la PR : le mot tombe dans les 60 premiers caracteres.
body = "Je ne mets pas de HOLD sur cette PR, elle est saine. Je merge."
assert mod._block_emitted(body) is False


def test_13779_hold_leve_est_une_levee_pas_une_emission():
body = "**HOLD leve** — la lane a nomme son grain de contenu, je merge."
assert mod._block_emitted(body) is False


def test_13779_hold_lifted_anglais_est_une_levee():
body = "HOLD lifted — the coordinator named the replacement grain."
assert mod._block_emitted(body) is False


def test_13779_override_en_tete_reste_exempte():
# L'override est l'etage superieur du protocole (#11639) : un arbitrage qui
# NOMME le hold qu'il leve ne doit pas se re-bloquer lui-meme.
body = "[OVERRIDE] lane myia-po-2025:CoursIA — HOLD coordinateur leve, budget recompte."
assert mod._block_emitted(body) is False


def test_13779_hold_cite_en_blockquote_ne_pose_rien():
body = """> **HOLD coordinateur — budget LIGHT epuise.**

C'est traite : le grain est requalifie MED/notebook-python, cf commit 24799875f.
"""
assert mod._block_emitted(body) is False


def test_13779_marqueur_hold_lane_en_backticks_ne_pose_rien():
body = "Le protocole ecrit `[HOLD] lane <machine:workspace>` — je documente la forme."
assert mod._block_emitted(body) is False


def test_13779_frontiere_de_mot_holder_ne_pose_rien():
body = "HOLDER de la lane, je confirme que le grain est pris."
assert mod._block_emitted(body) is False


# --- Regression : les deux formes historiques sont intactes ---

def test_13779_blocage_verdict_en_tete_emet_toujours():
assert mod._block_emitted("**BLOCAGE — le preflight n'est pas passe.**") is True


def test_13779_blocage_lane_emet_toujours():
assert mod._block_emitted("[BLOCAGE] lane myia-po-2023:CoursIA — scope non tenu.") is True


def test_13779_blocage_leve_ne_pose_toujours_rien():
assert mod._block_emitted("**BLOCAGE leve** — le point est traite, je merge.") is False
Loading