diff --git a/scripts/check_unaddressed_nits.py b/scripts/check_unaddressed_nits.py index 9624b15468..98879f0d5c 100644 --- a/scripts/check_unaddressed_nits.py +++ b/scripts/check_unaddressed_nits.py @@ -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: @@ -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: diff --git a/scripts/tests/test_check_unaddressed_nits_unevaluated.py b/scripts/tests/test_check_unaddressed_nits_unevaluated.py index 0b24df5d8e..b10a06762a 100644 --- a/scripts/tests/test_check_unaddressed_nits_unevaluated.py +++ b/scripts/tests/test_check_unaddressed_nits_unevaluated.py @@ -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()