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
29 changes: 29 additions & 0 deletions scripts/ci/fast_lane_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ class Guard:
statut.
needs_base Le garde compare HEAD a la base : le runner lui garantit que
`origin/<base_ref>` est joignable.
diff_bounded Le verdict du garde est un DELTA (comparaison base/head, ou
lignes ajoutees par la PR) : un rouge corrobore ne peut pas
heriter de `main`. Lu par le picker (`_diff_bounded_check_names`,
#19645) -- la declaration est EXPLICITE et distincte de
`needs_base`, parce que l'information de bornage peut manquer
a l'argv absorbe (cas `check-nav-chain`/`check-navlinks`,
suite c.1503 du 2026-10-09 : leur workflow d'origine scopait
au diff via `git diff $BASE...HEAD`, l'argv absorbe dit
seulement `--check`) et que les ~20 gardes `needs_base` ne
sont PAS un delta borne au diff par defaut.
delta_argv Present => le garde est un DELTA en trois temps : `argv` est
execute sur HEAD (sa sortie standard est capturee comme
`head.json`), puis sur l'arbre bascule a la base
Expand All @@ -76,6 +86,10 @@ class Guard:
paths: list[str] = field(default_factory=list)
blocking: bool = True
needs_base: bool = False
# #19645 suite c.1503 : verdict DELTA lu par le picker (voir la docstring
# du champ). Declare sur les gardes dont l'argv absorbe a perdu
# l'information de bornage -- une heuristique d'argv ne peut pas la voir.
diff_bounded: bool = False
delta_argv: list[str] = field(default_factory=list)
swap_paths: list[str] = field(default_factory=list)
iterates_paths: bool = False # voir `run_iter` dans fast_lane.py
Expand Down Expand Up @@ -346,6 +360,16 @@ class Guard:
],
argv=["python", "scripts/notebook_tools/check_notebook_navlinks.py",
"--check"],
# #19645 (suite c.1503) : verdict DELTA -- NEW vs baseline committe,
# le workflow d'origine scopait au diff (`git diff $BASE...HEAD` ->
# `--diff-files`). L'argv absorbe ne porte plus ce drapeau : la borne
# se declare ici, lue par le picker (`_diff_bounded_check_names`).
# `needs_base` suit l'arbitrage ai-01 du 2026-10-12 (declaration de
# coherence : mesure du meme jour, aucun consommateur operationnel ne
# lit `needs_base` dans scripts/ ou .github/ ; le canal operant de
# l'imputation reste `diff_bounded`).
needs_base=True,
diff_bounded=True,
blocking=True,
absorbed=True,
),
Expand All @@ -360,6 +384,11 @@ class Guard:
],
argv=["python", "scripts/notebook_tools/check_notebook_nav_chain.py",
"--check"],
# Meme classe que `check-navlinks` ci-dessus : verdict DELTA (NEW vs
# baseline), workflow d'origine scope au diff, borne perdue par
# l'argv absorbe -- declaration explicite (#19645 suite c.1503).
needs_base=True,
diff_bounded=True,
blocking=True,
absorbed=True,
),
Expand Down
25 changes: 19 additions & 6 deletions scripts/pick_idle_grain.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,19 +401,27 @@ def _has_delivered_marker(issue_number: int) -> bool | None:
# la lane « pas le votre », c'est-a-dire de ne rien faire, alors que la cause
# est dans le diff de la PR. La declaration de la borne est portee par le
# registre de la voie rapide (Guard.argv contient `--diff {base_ref}...HEAD`,
# ou Guard.delta_argv porte la comparaison base/head) -- pas une liste de
# noms recopiee dans le picker. C'est cette indirection qui maintient la
# liste a jour quand la voie rapide absorbe un nouveau garde.
# Guard.delta_argv porte la comparaison base/head, ou Guard.diff_bounded est
# declare explicitement quand l'argv absorbe a perdu l'information de
# bornage -- suite c.1503, nav-chain/navlinks) -- pas une liste de noms
# recopiee dans le picker. C'est cette indirection qui maintient la liste a
# jour quand la voie rapide absorbe un nouveau garde.
def _diff_bounded_check_names() -> set[str]:
"""Noms de checks declares bornes au diff dans le registre de la voie rapide.

Trois sources couvrent les trois formes d'un garde borne au diff :
Quatre sources couvrent les formes d'un garde borne au diff :
- ``argv`` contient un drapeau ``--diff {base_ref}...HEAD`` (le garde ne
lit que les lignes AJOUTEES par la PR -- cas fondateur ``prose-counts-guard``) ;
- ``delta_argv`` porte une comparaison explicite base/head (le verdict
est delta, jamais un verdict de base) ;
- ``swap_paths`` non vide (le fast-lane runner bascule un sous-arbre a
la base pour la phase 2 -- la sortie n'a de sens qu'en delta).
la base pour la phase 2 -- la sortie n'a de sens qu'en delta) ;
- ``Guard.diff_bounded`` declare au registre (source EXPLICITE, #19645
suite c.1503) : couvre les gardes dont l'argv absorbe a PERDU
l'information de bornage -- ``check-nav-chain``/``check-navlinks``
scopaient au diff dans leur workflow d'origine, l'argv absorbe dit
seulement ``--check``, et une 4e heuristique d'argv ne detecterait
rien puisque le drapeau n'y est plus.

Echec d'import (CI minimale sans la voie rapide) : on rend un set vide
fail-closed, comme avant #19645 -- l'imputation reste la voie par defaut.
Expand All @@ -437,7 +445,12 @@ def _diff_bounded_check_names() -> set[str]:
has_diff_flag = "--diff" in argv_join and "base_ref" in argv_join
has_delta = bool(getattr(guard, "delta_argv", None))
has_swap = bool(getattr(guard, "swap_paths", None))
if has_diff_flag or has_delta or has_swap:
# 4e source (#19645 suite c.1503) : declaration EXPLICITE au
# registre, pour les gardes dont l'argv absorbe a perdu le
# drapeau de bornage (nav-chain/navlinks disent `--check` alors
# que leur verdict est un delta NEW vs baseline).
has_declared = bool(getattr(guard, "diff_bounded", False))
if has_diff_flag or has_delta or has_swap or has_declared:
names.add(guard.name)
return names

Expand Down
45 changes: 45 additions & 0 deletions scripts/tests/test_pick_idle_grain.py
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,51 @@ def test_diff_bounded_none_preserve_comportement_avant_19645(monkeypatch):
assert "prose-counts-guard" in undecided


def test_nav_guards_declares_bornes_au_diff_registre_19645_c1503():
"""#19645 suite c.1503 : nav-chain/navlinks sortent du registre, sans monkeypatch.

Leur workflow d'origine scopait au diff (`git diff $BASE...HEAD`), mais
l'argv absorbe (`--check`) ne porte plus ce drapeau : aucune heuristique
d'argv ne peut les detecter (c'est la mesure c.1503). La declaration
explicite `diff_bounded` du registre est le canal -- ce test mesure le
cablage REEL de bout en bout (pas un monkeypatch de la fonction), et
echoue si la declaration disparait ou si l'import du registre casse.
"""
names = pig._diff_bounded_check_names()
assert "check-nav-chain" in names
assert "check-navlinks" in names


def test_nav_chain_corrobore_absent_de_main_sort_de_base_c1503():
"""#19645 suite c.1503 : le scenario fondateur, de bout en bout.

`check-nav-chain` rouge et corrobore par deux PR de lanes differentes
(#20044/#20062, notebooks distincts -- aucun rouge partage possible),
ABSENT du rollup de `main` (l'agregateur ne tourne que sur
`pull_request`). Avant la declaration : `undecided` + repli fail-closed
en `base` -> le tapis disait « pas le votre » et la lane ne cherchait
pas son `orphan_entry`. Apres : `undecided` SEUL -- la lane garde la
main. Controle negatif : un check non declare, meme scenario, reste
impute a la base (le filtre est un ensemble clos, il n'elargit pas la
classe des acquittes).
"""
diff_bounded = pig._diff_bounded_check_names()
assert "check-nav-chain" in diff_bounded, "cablage registre absent"
corroborated = {"check-nav-chain": [20044, 20062],
"Scripts Tests (CPU)": [20044, 20062]}
names_by_key = {"check-nav-chain": {"check-nav-chain"},
"Scripts Tests (CPU)": {"Scripts Tests (CPU)"}}
base, infra, undecided = pig.split_base_corroboration(
corroborated, names_by_key, _probe(), # rollup de main sans les nav
diff_bounded=diff_bounded)
assert "check-nav-chain" not in base
assert "check-nav-chain" in undecided
# Controle negatif : non declare -> repli fail-closed inchange.
assert "Scripts Tests (CPU)" in base
assert "Scripts Tests (CPU)" in undecided
assert infra == {}


def test_geste_infra_n_invente_pas_d_id_de_run():
"""L'id du check-run n'est pas un id de workflow run : le picker n'en donne aucun.

Expand Down
Loading