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
91 changes: 86 additions & 5 deletions scripts/check_pr_perimeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,8 @@ def check_assertion(
pass
elif _word_form_is_measurement_result(scan_target, block):
pass
elif _word_form_is_anaphoric_reference(scan_target):
pass
else:
problems.append(
f"l'assertion pretend {word_count} fichier(s), la liste effective en compte {len(files)} : "
Expand Down Expand Up @@ -1005,6 +1007,15 @@ def check_assertion(
r"renvoient|renvoie|mentionnent|mentionne|link|links|reference|references)\b",
re.IGNORECASE,
)
# #16085 cas B, population relayee en provenance : "N fichiers de `<sha>`"
# attribue le compte a une REVISION PASSEE (backticked, 7-40 hex) -- le body
# relaye la mesure d'un autre commit, il ne revendique pas son perimetre.
# Founder #15983 (PR body, run 34758737057) : "8 fichiers de `fe04e1f37`"
# confronte 8 vs 1 et a rougi une PR saine. Meme famille que PAST_REFERENCE
# et REFERENCE_VERB_TAIL : mauvaise surface, pas mauvais compte. Controles
# FN : suffixe < 7 hex ou non-hex n'est pas un sha, et la forme d'enumeration
# de perimetre ("N fichiers : a.py, ...") ne porte jamais ce suffixe.
_PROVENANCE_SHA_TAIL = re.compile(r"^\s*de\s+`[0-9a-f]{7,40}`")
# Formes 2-4, two-word qualifier window: artifact kinds and enumeration tails
# are often compound ("fichiers audio generes", "fichiers de tests", "fichier
# test adapte") -- the closed list matches the first OR second word after the
Expand Down Expand Up @@ -1123,6 +1134,18 @@ def _count_has_incidental_qualifier(line: str, m: re.Match) -> bool:
return False


def _count_is_provenance_sha(line: str, m: re.Match) -> bool:
"""#16085 cas B: the count relays a PROVENANCE -- "8 fichiers de
`fe04e1f37`" -- a measurement borrowed from another seat (an earlier
review's tally, pinned to the tree it measured by its commit sha).
Relayee, non re-mesuree: the guard's EQUALITY confrontation can never
validate a borrowed count against this PR's own diff, and blocking on
it reds a healthy body. The sha needs >= 7 hex chars -- "de `fe04e`"
or "de `main.yml`" do not fingerprint a commit and stay blocking."""
after = PLURAL_PAREN.sub(" ", line[m.end():], count=1)
return bool(_PROVENANCE_SHA_TAIL.match(after))


def _count_is_exempt(line: str, m: re.Match, ante_context: str = "") -> bool:
"""True when the specific COUNT match `m` on `line` is exempted by the
per-count filters (zero, threshold citation, locative scan scope,
Expand Down Expand Up @@ -1160,6 +1183,8 @@ def _count_is_exempt(line: str, m: re.Match, ante_context: str = "") -> bool:
return True
if REFERENCE_VERB_TAIL.match(after):
return True
if _count_is_provenance_sha(line, m):
return True # provenance relayee: "8 fichiers de `fe04e1f37`" (#16085)
if (before.endswith("(") and after.lstrip().startswith(")")
and PAREN_ANTECEDENT_NUM.search(before[:-1])):
return True
Expand Down Expand Up @@ -1231,6 +1256,54 @@ def _word_form_is_indef_non_pr_subject(text: str, files: list[dict]) -> bool:
)


# #16085 cas A, cardinal anaphorique delimite : "dans ces deux fichiers" ou
# l'antecedent -- des fichiers NOMMES -- siege au paragraphe precedent. La
# deixse anaphorique porte la reference (les modules dont on parle), pas le
# perimetre du diff. Founder #16075 : "Les occurrences des mots sorry /
# native_decide dans ces deux fichiers sont de la prose" sur une PR a 7
# fichiers, phrase vraie, rouge bloquant. Distinct de #14384 (connecteur
# additif en prose) : ici c'est la deixse qui porte la reference. Trois
# gardes FN : demonstratif adjacent au cardinal, antecedent nomme REQUIS au
# paragraphe precedent (forme anonyme -> fail-loud), et ligne sans mot de
# scope fort ("Perimetre : dans ces deux fichiers" reste bloquant).
_ANAPHORIC_DEM_CARD = re.compile(
r"\bces\s+(?:deux|trois|quatre|cinq|six|sept|huit|neuf|dix)\s+fichiers?\b"
r"|\bthese\s+(?:two|three|four|five|six|seven|eight|nine|ten)\s+files?\b",
re.IGNORECASE,
)


def _word_form_is_anaphoric_reference(text: str) -> bool:
"""#16085: True when a word-form count is a demonstrative anaphor
("ces deux fichiers" / "these two files") whose antecedent -- NAMED
files -- sits in the preceding paragraph. The phrase is about those
named modules, not the diff's perimeter.

FN safety mirrors _word_form_is_indef_non_pr_subject's default-fail-
loud: an anonymous antecedent ("ces deux fichiers" naming nothing)
keeps the rouge, and a line carrying a strong scope word stays
blocking. Hook: called from `check_assertion` on the `word_count`
branch only -- the digit branch ("ces 2 fichiers") is a distinct
shape, out of the issue's corpus, deliberately uncovered."""
low = text.lower()
m = _ANAPHORIC_DEM_CARD.search(low)
if m is None:
return False
line_start = text.rfind("\n", 0, m.start()) + 1
line_end = text.find("\n", m.end())
if line_end < 0:
line_end = len(text)
if _has_strong_scope(text[line_start:line_end].lower()):
return False
# Bounded lookback ONLY: the antecedent paragraph sits ABOVE the blank
# line (the corpus names its files one paragraph before the anaphor),
# so cutting at "\n\n" would amputate the window of the very thing it
# looks for -- the 500-char bound is the sole horizon.
window = text[:m.start()][-500:]
named = _NAMED_FILE_BODY.findall(window)
return any(a or b for a, b in named)


def _word_form_is_measurement_object(text: str) -> bool:
"""#13791: True when a word-form count's indefinite article ("un fichier")
sits within 80 chars AFTER a discrimination verb -- the phrase names the
Expand Down Expand Up @@ -1382,11 +1455,19 @@ def _count_is_incidental(line: str, ante_context: str = "") -> bool:
if DIFFSTAT_NEIGHBORHOOD.search(line):
# A qualifier-exempt count ("N fichiers neufs : file (330 lignes)")
# overrides the diffstat guard -- the "lignes" is a per-file size and
# the qualifier marks a sub-claim, not the whole-PR perimeter. An
# antecedent-exemption (locative "sur 2 fichiers", measurement parent,
# snapshot) does NOT override: "+307 lignes / −0 sur 2 fichiers" names
# what the diffstat measured (#11935 FN control stays blocking).
if all(_count_has_incidental_qualifier(line, m) for m in matches):
# the qualifier marks a sub-claim, not the whole-PR perimeter. A
# provenance relay ("8 fichiers de `fe04e1f37`, ~793 lignes de
# contexte") overrides too (#16085 cas B): the lignes belong to the
# borrowed measurement's own context, not to this PR's diffstat. An
# antecedent-exemption (locative "sur 2 fichiers", measurement
# parent, snapshot) does NOT override: "+307 lignes / −0 sur
# 2 fichiers" names what the diffstat measured (#11935 FN control
# stays blocking).
if all(
_count_has_incidental_qualifier(line, m)
or _count_is_provenance_sha(line, m)
for m in matches
):
return True
return False
for m in matches:
Expand Down
119 changes: 119 additions & 0 deletions scripts/tests/test_check_pr_perimeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3768,6 +3768,125 @@ def fake_rc(args):
cpp._pr_diff_text(101)


# --- #16085 : cardinal anaphorique delimite + provenance relayee en SHA ------

FILES_16085_A = [
{"path": "MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Invariant.lean"},
{"path": "MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Invariant_en.lean"},
{"path": "MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/README.md"},
{"path": "MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/README.md"},
{"path": "MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/TaitColor.lean"},
{"path": "MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Matrix.lean"},
{"path": "MyIA.AI.Notebooks/SymbolicAI/Lean/knot_lean/Knots/Smooth.lean"},
]

BODY_16085_A = (
"La migration Mathlib 4.33.0 porte sur `Knots/Invariant.lean` et son "
"sibling `Knots/Invariant_en.lean` : les instances sont fournies a la "
"forme exacte du sous-but.\n"
"\n"
"Les occurrences des mots `sorry` / `native_decide` dans ces deux "
"fichiers sont de la prose de docstring, pas des trous de preuve."
)


def test_16085_cas_a_cardinal_anaphorique_ne_rouge_pas():
"""#16085 cas A (founder #16075) : « dans ces deux fichiers » ou
l'antecedent -- les deux modules NOMMES au paragraphe precedent -- porte
la reference (deixse anaphorique, pas assertion de perimetre). La PR
touche 7 fichiers, la phrase est vraie, le rouge etait un faux positif
bloquant."""
problems = check_assertion(FILES_16085_A, BODY_16085_A)
assert problems == [], repr(problems)


def test_16085_cas_a_fn_antecedent_anonyme_reste_rouge():
"""Controle FN 1 : sans antecedent nomme au paragraphe precedent, la
forme reste ambigue -- fail-loud, le rouge tient."""
body = (
"La migration porte sur les deux modules d'origine.\n"
"\n"
"Les occurrences des mots `sorry` / `native_decide` dans ces deux "
"fichiers sont de la prose."
)
problems = check_assertion(FILES_16085_A, body)
assert any("l'assertion pretend 2 fichier" in p for p in problems), repr(problems)


def test_16085_cas_a_fn_mot_de_scope_reste_rouge():
"""Controle FN 2 : une ligne porteuse d'un mot de scope fort reste
bloquante meme sous demonstratif -- le garde ne masque pas une vraie
revendication de perimetre."""
body = (
"La migration porte sur `Knots/Invariant.lean` et "
"`Knots/Invariant_en.lean`.\n"
"\n"
"Perimetre : dans ces deux fichiers uniquement."
)
problems = check_assertion(FILES_16085_A, body)
assert any("l'assertion pretend 2 fichier" in p for p in problems), repr(problems)


def test_16085_cas_a_fn_demonstratif_absent_reste_rouge():
"""Controle FN 3 : sans demonstratif, « deux fichiers » nu reste un
compte confronte -- c'est l'anaphore qui porte le masquage, pas le
cardinal."""
body = (
"La migration porte sur `Knots/Invariant.lean` et "
"`Knots/Invariant_en.lean`.\n"
"\n"
"deux fichiers sont de la prose."
)
problems = check_assertion(FILES_16085_A, body)
assert any("l'assertion pretend 2 fichier" in p for p in problems), repr(problems)


BODY_16085_B = (
"La provenance citée (19 findings → 13 après restauration, 8 fichiers "
"de `fe04e1f37`, ~793 lignes de contexte de fence) : relayée, non "
"re-mesurée depuis mon siège."
)


def test_16085_cas_b_provenance_sha_est_incidentale():
"""#16085 cas B (founder #15983, PR BODY -- le log du run 34758737057
montre le rouge body-sourced « pretend 8, liste 1 ») : « N fichiers de
`<sha>` » attribue la population a une REVISION PASSEE, relayee. Meme
famille que PAST_REFERENCE : mauvaise surface, pas mauvais compte. La
consequence passe au routage incidental #11712 (signal, pas blocage)."""
assert _is_incidental_assertion(BODY_16085_B) is True


def test_16085_cas_b_fn_sha_trop_court_reste_bloquant():
"""Controle FN : un suffixe < 7 hex n'est pas un SHA -- la forme n'est
pas une provenance, le compte reste confronte."""
line = "8 fichiers de `fe04e` dans la restauration"
assert _is_incidental_assertion(line) is False


def test_16085_cas_b_fn_non_hex_reste_bloquant():
"""Controle FN : un backtick non-hex (`main.yml`) n'est pas un SHA."""
line = "8 fichiers de `main.yml` cites pour memoire"
assert _is_incidental_assertion(line) is False


def test_16085_cas_b_fn_vrai_perimetre_reste_bloquant():
"""Controle FN : une vraie enumeration de perimetre reste bloquante --
le masque ne doit couvrir QUE la forme « de <sha> »."""
line = "8 fichiers : a.py, b.py, c.py"
assert _is_incidental_assertion(line) is False


def test_16085_cas_b_additive_ninclut_pas_la_provenance():
"""La somme additive (#12103) lit le corps comme la selection : un
compte exonere ne joint jamais la somme."""
line = (
"La provenance citée (19 findings → 13 après restauration, 8 "
"fichiers de `fe04e1f37`, ~793 lignes) : relayée."
)
assert _additive_line_sum(line) == 0


# ---------------------------------------------------------------------------
# #16162 — un compte sous negation ou portant sur une AUTRE PR n'est pas
# un perimetre. Fondateurs mesures par ai-01 sur deux PRs ouvertes :
Expand Down
Loading