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
38 changes: 34 additions & 4 deletions scripts/check_unaddressed_nits.py
Original file line number Diff line number Diff line change
Expand Up @@ -5029,12 +5029,42 @@ def _lift_eligible(lift_author: str, nit_author: str,
LIST_FIELDS = "number,title,mergedAt,url,comments,reviews,author"


def _print_unevaluated(result: dict) -> None:
"""Imprimer verbatim ce que l'organe n'a pas evalue (#13512).
def _ok_line(pr: int, result: dict) -> str:
"""La ligne de verdict, avec sa reserve DANS la ligne (#13512, #13779).

`OK -- aucun nit non leve` repond « aucune phrase de levee ne manque », et
RIEN D'AUTRE : un commentaire que `classify` n'a pas su lire n'est pas un
commentaire absent. Le dire est tout l'organe.
commentaire absent. `_print_unevaluated` le dit -- mais il le dit SOUS la
ligne, et c'est la LIGNE qui circule : un agent qui rapporte « organe OK sur
#N » cite le verdict, pas le bloc. Un `OK` cite sans sa reserve certifie
alors exactement le silence que cet organe refuse de certifier, et la
promesse de #13779 (« cesser de certifier le silence ») s'arrete a la
frontiere du stdout.

Mesure fondatrice (2026-09-21, arbitrage ai-01) : `check_unaddressed_nits.py`
rendait `rc=0` -- et sa ligne `OK` a ete citee -- sur deux PRs dont les
reserves vivaient dans le bloc « NON EVALUE(S) ». Un `rc=0` n'est pas une
dispense de lecture, mais rien ne le rappelait la ou le verdict se lit.

Le compte qui voyage est le TOTAL non evalue, jamais le sous-ensemble
affiche -- meme regle que l'en-tete de `_print_unevaluated`. Quand il n'y a
rien a relire, la ligne reste celle d'avant, octet pour octet.
"""
total = result.get("unevaluated_total") or 0
if not total:
return f"OK PR #{pr} — aucun nit non leve."
return (
f"OK PR #{pr} — aucun nit non leve parmi les commentaires evalues ; "
f"{total} commentaire(s) NON EVALUE(S) — lire le bloc A RELIRE ci-dessous."
)


def _print_unevaluated(result: dict) -> None:
"""Imprimer verbatim ce que l'organe n'a pas evalue (#13512).

La ligne de verdict porte desormais le compte (`_ok_line`) ; ce bloc reste
le detail -- le propos est le meme : ce que l'organe n'a pas su classer, il
l'imprime.
"""
rows = result.get("unevaluated") or []
if not rows:
Expand Down Expand Up @@ -5145,7 +5175,7 @@ def gate(pr: int, as_json: bool) -> int:
if as_json:
print(json.dumps(result, indent=1, ensure_ascii=False))
elif not result["blocked"]:
print(f"OK PR #{pr} — aucun nit non leve.")
print(_ok_line(pr, result))
_print_sha_notes(result)
_print_unevaluated(result)
else:
Expand Down
69 changes: 69 additions & 0 deletions scripts/tests/test_check_unaddressed_nits_unevaluated.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,72 @@ def test_13779_surfacing_never_blocks():
res = _run([_c("2026-08-30T09:30:00Z", PREFLIGHT),
_c("2026-08-30T11:00:00Z", LATER)])
assert res["blocked"] is False


# --------------------------------------------------------------------------
# La RESERVE doit voyager DANS la ligne de verdict, pas seulement sous elle.
#
# Le bloc « A RELIRE » a beau exister, c'est la ligne `OK` qui circule : elle
# est citee telle quelle dans les rapports de lane et dans les DM. Un `OK` cite
# sans son compte certifie alors precisement le silence que l'organe refuse de
# certifier -- la promesse de #13779 s'arretait a la frontiere du stdout.
# Mesure fondatrice du 21/09 : deux PRs ou l'organe rendait `OK` (et `rc=0`)
# portaient leurs reserves vivantes dans le bloc non evalue.
# --------------------------------------------------------------------------

def test_ok_line_names_the_unevaluated_count():
res = _run([_c("2026-08-30T09:30:00Z", PREFLIGHT),
_c("2026-08-30T11:00:00Z", LATER)])
line = mod._ok_line(13712, res)
assert "2 commentaire(s) NON EVALUE(S)" in line, (
"la ligne de verdict doit porter le compte non evalue : c'est elle "
"qui est citee, pas le bloc imprime en dessous")
assert "A RELIRE" in line, "la reserve doit nommer ou lire le detail"


def test_ok_line_count_is_the_total_not_the_displayed_subset():
"""Meme regle que l'en-tete : le TOTAL, jamais le sous-ensemble affiche."""
res = _run([_c("2026-08-30T09:%02d:00Z" % m, PREFLIGHT + str(m))
for m in (10, 20, 30, 40, 50)]
+ [_c("2026-08-30T11:00:00Z", LATER)])
assert res["unevaluated_total"] == 6
assert len(res["unevaluated"]) == 4
line = mod._ok_line(13712, res)
assert "6 commentaire(s) NON EVALUE(S)" in line
assert "4 commentaire(s)" not in line, (
"sous-declarer dans la ligne ce qu'on declare en entier en dessous "
"serait exactement le defaut #13779, deplace")


def test_ok_line_is_byte_identical_when_nothing_is_unevaluated():
"""Controle positif : rien a relire -> la ligne d'origine, inchangee.

Sans ce test, une reserve affichee a tort (« 0 commentaire(s) NON
EVALUE(S) ») passerait pour un progres.
"""
concern = "Une seule chose a changer avant merge : le point 2 de la section 3."
assert mod.classify("jsboige", concern) is not None, (
"precondition : sans classement, le commentaire tomberait dans "
"`unevaluated` et le test passerait pour la mauvaise raison")
res = _run([_c("2026-08-30T09:30:00Z", concern)])
assert res["unevaluated_total"] == 0
assert res["unevaluated"] == []
assert mod._ok_line(13712, res) == "OK PR #13712 — aucun nit non leve."


def test_ok_line_keeps_the_historical_prefix():
"""Le prefixe d'avant reste un prefixe : aucun consommateur ne casse."""
res = _run([_c("2026-08-30T09:30:00Z", PREFLIGHT)])
line = mod._ok_line(13712, res)
assert line.startswith("OK PR #13712 — aucun nit non leve"), (
"un lecteur (ou un grep) qui reconnaissait la ligne d'origine doit "
"toujours la reconnaitre")


def test_print_unevaluated_does_not_repeat_the_verdict_line():
"""Une seule source pour le verdict : le bloc imprime le detail, pas `OK`."""
res = _run([_c("2026-08-30T09:30:00Z", PREFLIGHT)])
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
mod._print_unevaluated(res)
assert "OK PR" not in buf.getvalue()
Loading