diff --git a/scripts/check_adjoint_prevalidation.py b/scripts/check_adjoint_prevalidation.py index 4e0dc13c32..57d1cd74bd 100644 --- a/scripts/check_adjoint_prevalidation.py +++ b/scripts/check_adjoint_prevalidation.py @@ -156,8 +156,21 @@ "myia-po-2027:CoursIA-2", }) # `Grain: -- lane ` in the pull request body names the -# lane that carries the work. Same grammar as scripts/check_lane_claim.py. -GRAIN_LANE_RE = re.compile(r"Grain:[^\n]*?\blane\s+([A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+)") +# lane that carries the work. The canonical reader is `grain_tag.extract_lane`, +# the one `scripts/check_lane_claim.py` uses; the patterns below are `findall` +# variants of it, kept because the consumers need EVERY marker of a body, not +# just the first. +# +# #15864 (4th variant of the self-block class) -- a sentence period is admitted +# by the token class (hostnames need it), so `lane myia-po-2023:CoursIA-2.` used +# to be captured as the lane `myia-po-2023:CoursIA-2.` -- a lane no cluster lane +# matches. A token can no longer END on a period; periods INSIDE it survive +# (`Foo.Bar:CoursIA-2` is unchanged), so the constraint only removes prose +# punctuation. Measured on #14549, 2026-10-09: `delivering_lanes` returned +# `myia-po-2023:CoursIA-2.`, and a lane named ONLY under the punctuated form is +# absent from the set -- its own self-attesting dossier then passes the gate. +LANE_TOKEN = r"[A-Za-z0-9_.-]*[A-Za-z0-9_-]:[A-Za-z0-9_.-]*[A-Za-z0-9_-]" +GRAIN_LANE_RE = re.compile(r"Grain:[^\n]*?\blane\s+(" + LANE_TOKEN + r")") SHARED_GITHUB_LOGIN = "jsboige" # The gate's only consumer. Every worker lane signs SHARED_GITHUB_LOGIN, so this # login is the one surface author the coordinator can recognise as itself. diff --git a/scripts/check_closure_dossier.py b/scripts/check_closure_dossier.py index d1749d2231..00f30766d7 100644 --- a/scripts/check_closure_dossier.py +++ b/scripts/check_closure_dossier.py @@ -78,13 +78,14 @@ try: import gh_identity - from check_adjoint_prevalidation import QUALIFYING_LANES, GRAIN_LANE_RE + from check_adjoint_prevalidation import QUALIFYING_LANES, GRAIN_LANE_RE, LANE_TOKEN except ImportError: # charge depuis scripts/ en invocation directe sys.path.insert(0, dirname(abspath(__file__))) import gh_identity # type: ignore[no-redef] from check_adjoint_prevalidation import ( # type: ignore[no-redef] QUALIFYING_LANES, GRAIN_LANE_RE, + LANE_TOKEN, ) REPO = "jsboige/CoursIA" @@ -121,9 +122,13 @@ _RESIDUE_FOLLOWUP_RE = re.compile(r"^followup\s+#(\d+)$") _RESIDUE_WAIVER_RE = re.compile(r"^waiver:\s+\S.+") -#: ``[DELIVERED] lane ...`` -- protocole lane-claim. +#: ``[DELIVERED] lane ...`` -- protocole lane-claim. Le +#: token reprend ``LANE_TOKEN`` (voir check_adjoint_prevalidation) : un point +#: final de phrase n'entre plus dans la lane capturee (#15864, mesure #14549) -- +#: sinon une lane marquee uniquement sous forme ponctuee echappe a +#: ``delivering_lanes`` et son dossier d'auto-attestation passe le gate. _DELIVERED_LANE_RE = re.compile( - r"\[DELIVERED\][^\n]*?\blane\s+([A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+)" + r"\[DELIVERED\][^\n]*?\blane\s+(" + LANE_TOKEN + r")" ) #: Un item d'acceptance : `` -> ``. diff --git a/scripts/check_unaddressed_nits.py b/scripts/check_unaddressed_nits.py index 07623ae0d7..f56572ab81 100644 --- a/scripts/check_unaddressed_nits.py +++ b/scripts/check_unaddressed_nits.py @@ -4392,12 +4392,19 @@ def _names_author(body: str, author: str) -> bool: # lift est signe par la lane de la PR SI ET SEULEMENT SI l'extraction lit # la meme chaine canonique dans les deux sources (le format `CoursIA` vs # `CoursIA-2` discriminne -- la lane ne peut signer QUE la sienne). +# #15864 (mesure #14549) : un point final de phrase ne peut plus terminer le +# token de lane -- `lane myia-po-2023:CoursIA-2.` etait capture comme une lane +# qui ne matche rien. Les points internes survivent (hostnames). Le meme +# fragment vit dans check_adjoint_prevalidation.LANE_TOKEN ; copie locale pour +# ne pas coupler le gate B.0 au gate d'adjoint. Voie (a) ci-dessous hors +# perimetre : sa lane est delimitee par des crochets, pas suivie de prose. +_LANE_TOKEN = r"[A-Za-z0-9_.-]*[A-Za-z0-9_-]:[A-Za-z0-9_.-]*[A-Za-z0-9_-]" _PR_LANE_FROM_BODY_RE = re.compile( # `Grain: ... -- lane :` (corps PR) ; on capture # la lane canonique ; un prefixe optionnel avant n'est pas admis (la # balise est en tete de ligne apres decoration markdown). r"(?m)^[#>*+\-\s]*Grain:[^\n]*?\blane\s+" - r"([A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+)") + r"(" + _LANE_TOKEN + r")") _LIFT_LANE_SIGNATURE_RE = re.compile( # Voie (a) -- `[machine:workspace]` en tete de paragraphe. r"(?m)^[#>*+\-\s]*\[\s*([A-Za-z][\w.\-]*:[A-Za-z][\w.\-]*(?:\s+[^\]]+)?)\]" @@ -4407,8 +4414,8 @@ def _names_author(body: str, author: str) -> bool: # commentaire. Deux formes observees : `-- lane myia-po-XXXX:...` # (declaration finale separee par `--`) et `lane myia-po-XXXX:... # -- ...` (dans la meme ligne que le commentaire precedent). - r"(?m)(?:^|\s)--\s*lane\s+([A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+)\s*$" - r"|^lane\s+([A-Za-z0-9_.-]+:[A-Za-z0-9_.-]+)(?:\s*--|\s*$)") + r"(?m)(?:^|\s)--\s*lane\s+(" + _LANE_TOKEN + r")\.?\s*$" + r"|^lane\s+(" + _LANE_TOKEN + r")\.?(?:\s*--|\s*$)") def _lift_signed_by_pr_lane(lift_body: str, pr_body: str) -> bool: diff --git a/scripts/tests/test_check_adjoint_prevalidation.py b/scripts/tests/test_check_adjoint_prevalidation.py index c47726aeb2..8b9c32e5fe 100644 --- a/scripts/tests/test_check_adjoint_prevalidation.py +++ b/scripts/tests/test_check_adjoint_prevalidation.py @@ -2941,3 +2941,19 @@ def test_review_threads_single_pr_is_still_one_operation(monkeypatch): assert len(calls) == 1 assert [t["id"] for t in threads] == ["t9-1"] + + +def test_grain_lane_re_refuse_le_point_final_de_phrase(): + """#15864 (mesure #14549) -- la classe de token admet le point (hostnames), + donc `lane myia-po-2023:CoursIA-2.` etait capture comme une lane fantome. + Un token ne peut plus se TERMINER par un point ; les points internes + survivent.""" + dotted = "Grain: DEEP/genai — lane myia-po-2023:CoursIA-2. Enonce reecrit." + assert mod.GRAIN_LANE_RE.findall(dotted) == ["myia-po-2023:CoursIA-2"] + clean = "Grain: DEEP/genai — lane myia-po-2023:CoursIA-2" + assert mod.GRAIN_LANE_RE.findall(clean) == ["myia-po-2023:CoursIA-2"] + host = "Grain: MED/docs — lane foo.bar.baz:CoursIA-2." + assert mod.GRAIN_LANE_RE.findall(host) == ["foo.bar.baz:CoursIA-2"] + # Aucune lane ne se reduit a un point : machine et workspace requis. + assert mod.GRAIN_LANE_RE.findall("Grain: MED/docs — lane :CoursIA-2") == [] + assert mod.GRAIN_LANE_RE.findall("Grain: MED/docs — lane myia-po-2023:") == [] diff --git a/scripts/tests/test_check_closure_dossier.py b/scripts/tests/test_check_closure_dossier.py index dc20492e95..59688e36ba 100644 --- a/scripts/tests/test_check_closure_dossier.py +++ b/scripts/tests/test_check_closure_dossier.py @@ -697,3 +697,23 @@ def fake_gh_json(args): out = _merged_referring_prs("o/r", 15578) assert out == [] + + +def test_delivering_lanes_refuse_le_point_final_de_phrase(): + """#15864 (mesure #14549, 2026-10-09) -- un marqueur ponctue doit reduire a + la lane REELLE : sinon une lane nommee uniquement sous forme ponctuee est + absente de l'ensemble d'exclusion et son dossier d'auto-attestation passe + le gate. Instance mesuree : `myia-po-2023:CoursIA-2.` dans delivering_lanes.""" + snap = _snapshot( + comments=[_comment("[DELIVERED] lane myia-po-2023:CoursIA-2. " + "Tranche livree, voir PR.")], + merged_prs=[ + {"number": 17901, "merged_at": "2026-09-19T10:00:00Z", + "body": "Grain: DEEP/notebook-python — lane myia-po-2024:CoursIA. " + "-- See #17900."}, + ], + ) + lanes = ccd.delivering_lanes(snap) + assert "myia-po-2023:CoursIA-2" in lanes + assert "myia-po-2024:CoursIA" in lanes + assert not any(ln.endswith(".") for ln in lanes), sorted(lanes) diff --git a/scripts/tests/test_check_unaddressed_nits_18149.py b/scripts/tests/test_check_unaddressed_nits_18149.py index 78ef38949c..211d282543 100644 --- a/scripts/tests/test_check_unaddressed_nits_18149.py +++ b/scripts/tests/test_check_unaddressed_nits_18149.py @@ -181,3 +181,24 @@ def test_18149_garde_unitaire_voxel_marker_lane(): # PR sans tag `Grain:` assert mod._lift_signed_by_pr_lane( lift_body, "SYNTHESE sans tag Grain.") is False + + +def test_18149_point_final_de_phrase_ne_forme_pas_la_lane(): + """#15864 (mesure #14549) -- un pied de signature ponctue reste la lane + qui signe : `-- lane myia-po-2024:CoursIA.` doit etre lu comme + `myia-po-2024:CoursIA`, pas comme une lane qui ne matche rien.""" + pr_body = "Grain: MED/notebook-python -- lane myia-po-2024:CoursIA" + # Voie (b), forme `-- lane X.` : le point final est de la prose. + lift_footer = "Leve au commit abc123. -- lane myia-po-2024:CoursIA." + assert mod._lift_signed_by_pr_lane(lift_footer, pr_body) is True + # Voie (b), forme `lane X. -- ...` dans la meme ligne. + lift_inline = "lane myia-po-2024:CoursIA. -- Leve au commit abc123." + assert mod._lift_signed_by_pr_lane(lift_inline, pr_body) is True + # Le tag Grain du PR lui-meme ponctue : la lane carrier reste lisible. + pr_dotted = "Grain: MED/notebook-python -- lane myia-po-2024:CoursIA." + assert mod._PR_LANE_FROM_BODY_RE.search(pr_dotted).group(1) \ + == "myia-po-2024:CoursIA" + # Un point INTERNE (hostname) survit : ce n'est pas de la prose. + pr_host = "Grain: MED/docs -- lane foo.bar:CoursIA-2" + assert mod._PR_LANE_FROM_BODY_RE.search(pr_host).group(1) \ + == "foo.bar:CoursIA-2"