diff --git a/scripts/check_adjoint_prevalidation.py b/scripts/check_adjoint_prevalidation.py index e9659b7e6f..7dc9346f0f 100644 --- a/scripts/check_adjoint_prevalidation.py +++ b/scripts/check_adjoint_prevalidation.py @@ -732,6 +732,21 @@ def carrying_lane(snapshot: dict[str, Any]) -> str | None: return match.group(1) if match else None +def is_out_of_fleet(snapshot: dict[str, Any]) -> bool: + """True when the pull request is carried by no fleet lane at all. + + Both conditions, mirroring the `tag_required` exemption (#17713/#17715, + `variation_tag_required.py`): the head branch starts with ``claude/`` AND + the body declares « Hors flotte ». Either alone is not out of fleet -- a + fleet lane renaming its branch or copying the marker stays inside the + third-party rule (#17791 acceptance 3). + """ + head_ref = snapshot.get("headRefName") or "" + return head_ref.startswith("claude/") and "Hors flotte" in ( + snapshot.get("body") or "" + ) + + def validate_dossier(dossier: Dossier, snapshot: dict[str, Any]) -> list[str]: """Validate a parsed dossier against one live PR snapshot.""" f = dossier.fields @@ -779,6 +794,13 @@ def validate_dossier(dossier: Dossier, snapshot: dict[str, Any]) -> list[str]: errors.append( f"lane must be one of the qualifying cluster lanes, got {dossier_lane!r}" ) + elif is_out_of_fleet(snapshot): + # #17791 -- a maintainer cloud session (`claude/*` + « Hors flotte ») + # carries no fleet lane: CLAUDE.md ('À qui ce fichier s'adresse') exempts + # it from the Grain tag, so the third-party rule has no carrier to + # compare against. Every qualifying lane is third-party by construction, + # and the self-attestation check below is unreachable for it. + pass else: carrier = carrying_lane(snapshot) if carrier is None: @@ -982,6 +1004,7 @@ def build_result( "ready": verdict == VERDICT_READY, "verdict": verdict or "NO_DOSSIER", "errors": errors, + "out_of_fleet": is_out_of_fleet(snapshot), } if dossier is not None and verdict: result["dossier"] = dossier_payload(dossier) @@ -1316,6 +1339,11 @@ def main() -> int: ) elif ready: print(f"READY -- PR #{args.pr} prevalidated by adjoint at {snapshot['headRefOid']}") + if is_out_of_fleet(snapshot): + print( + " note: hors-flotte PR (claude/* + 'Hors flotte'): no fleet " + "lane carries it, any qualifying lane is third-party (#17791)" + ) elif verdict == VERDICT_BLOCKED: print( f"BLOCKED-WITH-SUBSTANCE -- PR #{args.pr} has an intact adjoint dossier at " diff --git a/scripts/tests/test_check_adjoint_prevalidation.py b/scripts/tests/test_check_adjoint_prevalidation.py index 4abec23834..30ae48d6ab 100644 --- a/scripts/tests/test_check_adjoint_prevalidation.py +++ b/scripts/tests/test_check_adjoint_prevalidation.py @@ -283,6 +283,62 @@ def test_non_shared_github_author_cannot_satisfy_gate(): assert any(error.startswith("comment author must") for error in errors) +# --- #17791 : PR hors flotte (session cloud du mainteneur) ------------------- +# +# Miroir de l'exemption `tag_required` (#17713/#17715, variation_tag_required.py) : +# branche `claude/*` ET marqueur « Hors flotte » dans le body. Une telle PR n'est +# porte par AUCUNE lane de la flotte, donc tout dossier d'une lane qualifiante +# est tiers par construction -- le refus « carrying lane cannot be established » +# y est une impasse structurelle, pas une garantie. + + +def test_out_of_fleet_pr_accepts_qualifying_dossier_without_grain_tag(): + """claude/* + « Hors flotte » : un dossier de lane qualifiante est tiers.""" + snapshot = _snapshot_with_body( + "Session cloud du mainteneur. **Hors flotte**", lane="myia-po-2023:CoursIA" + ) + snapshot["headRefName"] = "claude/affectionate-mccarthy-6dvuea" + ready, errors = mod.evaluate(snapshot) + assert ready, errors + assert errors == [] + + +def test_claude_branch_without_marker_is_still_refused(): + """Le prefixe seul n'exempte pas : sans le marqueur, le refus est intact.""" + snapshot = _snapshot_with_body( + "Session cloud du mainteneur.", lane="myia-po-2023:CoursIA" + ) + snapshot["headRefName"] = "claude/affectionate-mccarthy-6dvuea" + errors = _errors(snapshot) + assert any( + error.startswith("carrying lane cannot be established") for error in errors + ) + + +def test_fleet_branch_with_marker_is_still_refused(): + """Recopier le marqueur sur une branche de flotte ne sort pas de la regle.""" + snapshot = _snapshot_with_body( + "Session cloud du mainteneur. **Hors flotte**", lane="myia-po-2023:CoursIA" + ) + snapshot["headRefName"] = "feature/renamed-to-escape" + errors = _errors(snapshot) + assert any( + error.startswith("carrying lane cannot be established") for error in errors + ) + + +def test_self_prevalidation_refusal_survives_the_exemption_next_door(): + """Une PR de flotte taguee reste refusee en self-attestation : l'exemption + hors flotte n'ouvre aucune echappatoire a cote.""" + carrier = "myia-po-2023:CoursIA" + snapshot = _snapshot_with_body( + "Grain: DEEP/lean -- lane %s -- prev: MED" % carrier, lane=carrier + ) + snapshot["headRefName"] = "feature/ordinary-fleet-branch" + errors = _errors(snapshot) + assert any(error.startswith("self-prevalidation refused") for error in errors) + + def test_blocked_preflight_is_a_valid_dossier_but_never_ready(): """An honest BLOCKED dossier must be distinguishable from an absent one.