diff --git a/scripts/check_pr_perimeter.py b/scripts/check_pr_perimeter.py index 34e697f1fc..74f8ed085d 100644 --- a/scripts/check_pr_perimeter.py +++ b/scripts/check_pr_perimeter.py @@ -717,10 +717,25 @@ def check_assertion( r"\b(?:editer|modifier|toucher|ouvrir|créer|creer|ajouter|changer|mettre\s+à\s+jour|mettre\s+a\s+jour|update|edit|modify|touch|open|create|add|change)\b", re.IGNORECASE, ) -# A named file as it would appear in a body: backticked path, bare basename +# A named referent as it would appear in a body: backticked path, backticked +# DOTTED SYMBOL (`pick_idle_grain.upsert_orphans_comment`), bare basename # (.py/.cs/.yml/.json/.md/.ipynb/.sh), or a known scripts/.py shape. +# +# #13610 residual (measured 2026-09-02, po-2024): the tail class was +# `[A-Za-z0-9]+`, which excludes the underscore. A dotted symbol -- the most +# common way a French technical body points at code -- was therefore NOT a +# named referent, and the FN-safety branch kept the rouge on the FOUNDING +# sentence of #13539, whose referent was named but named as a SYMBOL: +# « L'upsert vit dans `pick_idle_grain.upsert_orphans_comment` ; le +# generaliser demanderait d'editer un fichier deja porteur de deux PRs » +# The boundary this drew was arbitrary AND invisible: `pick_idle_grain.upsert` +# passed while `pick_idle_grain.upsert_orphans` rouged, on the sole strength +# of one underscore -- two spellings of the same code reference, opposite +# verdicts. Widening the tail to `\w+` removes the inversion. It does NOT +# touch the deliberate FN-safety choice of #13612: an ANONYMOUS referent +# ("editer un fichier", nothing named on the line) still keeps the rouge. _NAMED_FILE_BODY = re.compile( - r"(?:`([^`]+\.[A-Za-z0-9]+)`|" # backticked: `pick_idle_grain.py` + r"(?:`([^`]+\.\w+)`|" # backticked: `pick_idle_grain.py`, `mod.fn_name` r"\b([\w./-]+\.(?:py|cs|yml|yaml|json|md|ipynb|ts|js|sh|toml|cfg|ini))\b)" # bare basename ) # A cited threshold ("< 15 fichiers", ">= 10 fichiers") quotes a rule, it does diff --git a/scripts/tests/test_check_pr_perimeter.py b/scripts/tests/test_check_pr_perimeter.py index 048372d4d9..41369e14b0 100644 --- a/scripts/tests/test_check_pr_perimeter.py +++ b/scripts/tests/test_check_pr_perimeter.py @@ -2865,6 +2865,110 @@ def test_13610_predicate_unit_unanimous(): ) is False # named file IS in scope +# --------------------------------------------------------------------------- +# #13610 residual (2026-09-02, measured by po-2024) -- the referent is named, +# but named as a SYMBOL, not as a file. The tail class of _NAMED_FILE_BODY was +# `[A-Za-z0-9]+`, which excludes the underscore; a dotted symbol was therefore +# not recognized as a named referent, and the FN-safety branch kept the rouge +# on the FOUNDING sentence of #13539 itself. The boundary was arbitrary AND +# invisible: the same code reference passed or rouged on the sole strength of +# one underscore. The pair below is the proof -- the first test alone would be +# satisfied by simply disabling the guard, which is why the positive control +# and the anonymous-referent control are pinned alongside it. +# --------------------------------------------------------------------------- + +# The two rows of the #13610 table, verbatim in shape: same sentence, same +# referent, one underscore apart. +_SYMBOL_SHAPE = ( + "L'upsert vit dans `pick_idle_grain.{symbole}` ; le generaliser " + "demanderait d'editer un fichier deja porteur de deux PRs ouvertes " + "de la meme lane" +) + + +def test_13610_symbol_referent_without_underscore_passes(): + """Table row 1: `pick_idle_grain.upsert` -- passed even before the fix. + Pinned so a future tightening of the tail class cannot silently take it + back, which would re-open the inversion from the other side.""" + line = _SYMBOL_SHAPE.format(symbole="upsert") + problems = check_assertion(FILES_13610, line) + assert problems == [], ( + "a dotted symbol without underscore must not rouge; got: " + + repr(problems) + ) + + +def test_13610_symbol_referent_with_underscore_passes(): + """Table row 2: `pick_idle_grain.upsert_orphans` -- ROUGED before the fix, + on the sole strength of the underscore. This is the regression the + residual names.""" + line = _SYMBOL_SHAPE.format(symbole="upsert_orphans") + problems = check_assertion(FILES_13610, line) + assert problems == [], ( + "a dotted symbol with an underscore must not rouge -- the underscore " + "is not a semantic boundary; got: " + repr(problems) + ) + + +def test_13610_founding_sentence_of_13539_passes(): + """The sentence of #13539 that founded the whole thread. Its referent was + named all along -- named as a symbol. Acceptance line 1 of #13610.""" + line = _SYMBOL_SHAPE.format(symbole="upsert_orphans_comment") + problems = check_assertion(FILES_13610, line) + assert problems == [], ( + "the founding sentence of #13539 must pass the guard; got: " + + repr(problems) + ) + + +def test_13610_symbol_widening_keeps_positive_control_rouge(): + """Acceptance line 2 of #13610, stated as a PAIR with the three tests + above: widening the tail class must NOT extinguish the positive control. + A fix that made the symbol cases pass by weakening the guard would pass + those three and fail this one -- which is the whole point of pinning it + here rather than relying on the older copy elsewhere in the file.""" + problems = check_assertion(FILES_13610, "Cette PR ne touche qu'un fichier.") + assert problems, ( + "the positive control must still rouge after the widening -- " + "3 files in the PR, assertion claims 1" + ) + + +def test_13610_symbol_widening_keeps_anonymous_referent_rouge(): + """FN-safety, restated post-widening: widening WHAT counts as a named + referent must not turn an ANONYMOUS referent into a named one. #13612's + deliberate default-fail-loud on ambiguous shapes is untouched.""" + line = ( + "le generaliser demanderait d'editer un fichier deja porteur de " + "deux PRs ouvertes" + ) + problems = check_assertion(FILES_13610, line) + assert problems, ( + "an anonymous referent must keep the rouge after the widening" + ) + + +def test_13610_symbol_predicate_unit_pair(): + """Direct unit test of the predicate on the two table rows, decoupled from + check_assertion so a pipeline change cannot mask a predicate regression -- + same rationale as test_13610_predicate_unit_unanimous.""" + files = FILES_13610 + assert _word_form_is_indef_non_pr_subject( + _SYMBOL_SHAPE.format(symbole="upsert"), files + ) is True + assert _word_form_is_indef_non_pr_subject( + _SYMBOL_SHAPE.format(symbole="upsert_orphans"), files + ) is True + assert _word_form_is_indef_non_pr_subject( + _SYMBOL_SHAPE.format(symbole="upsert_orphans_comment"), files + ) is True + # The widening is about the TAIL of a dotted referent, nothing else: + # an anonymous referent stays False. + assert _word_form_is_indef_non_pr_subject( + "editer un fichier quelque part", files + ) is False + + # --------------------------------------------------------------------------- # #13637 -- carried-from-main files. GitHub's /pulls/N/files diffs base-tip -> # head, so a branch that merged main gets main's own changes attributed to it