From f092e799e7a3974fd65ee601ca0b76f9577def29 Mon Sep 17 00:00:00 2001 From: jsboige Date: Sun, 30 Aug 2026 00:51:02 +0200 Subject: [PATCH] fix(translation,#10038): le compte de paires ne depend plus de l arbre local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extrait de #13543 sa moitie non destructive. #13543 melait la suppression de deux notebooks _en (collision frontale avec #13542, qui les complete) et ce correctif de perimetre ; seul le second est ici. discover_pairs traversait les copies du depot : une machine portant des worktrees rend un compte different de la CI, et EXPECTED_PAIR_COUNT — une egalite — devient non maintenable. La version de #13543 sautait une LISTE DE NOMS. Mesure du 30/08 : un worktree nomme `.wt-parity-split` faisait toujours rendre 2 paires inexistantes hors de la copie, filtre actif. Le commentaire promettait l independance a l environnement, la condition ne filtrait que sept noms. Ce qui caracterise une copie n est pas son nom mais la presence d un `.git`. La garde teste cela. Controles : racine polluee -> 0 paire ; worktree reel -> 2 paires (pas de sur-filtrage) ; 31 tests passent. Ne touche PAS EXPECTED_PAIR_COUNT : c est le cliquet de #13542. Co-Authored-By: Claude-Code --- .../translation/check_translation_parity.py | 47 +++++++++++++++++++ .../tests/test_check_translation_parity.py | 42 +++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/scripts/translation/check_translation_parity.py b/scripts/translation/check_translation_parity.py index a1c1136aa7..fbc54135c6 100644 --- a/scripts/translation/check_translation_parity.py +++ b/scripts/translation/check_translation_parity.py @@ -153,6 +153,51 @@ def load_cells(nb_path: Path) -> Tuple[List[CellRecord], Optional[str]]: _SUFFIX_RE = re.compile(r"^(?P.+)_(?P" + "|".join(TARGET_LANGS) + r")$") +# Repertoires qui contiennent des COPIES du depot ou des dependances +# vendorisees. Les traverser fait dependre le compte de paires de +# l environnement local : une machine portant deux worktrees rend 6 la ou la +# CI rend 2, et `EXPECTED_PAIR_COUNT` — une egalite — devient non +# maintenable (le dev « corrige » vers son chiffre local et casse la CI). +# Mesure du 2026-08-29 sur cette machine : 6 paires vues, dont 4 venaient de +# `.worktrees/`, aucune n existant sur `main`. +_SKIP_DIRS = frozenset({ + ".worktrees", # copies du depot (git worktree) + ".git", + ".venv", + "venv", + "node_modules", + ".lake", # paquets Lean vendorises + ".ipynb_checkpoints", +}) + + +def _is_scannable(path: Path, repo_root: Path) -> bool: + """Vrai si ``path`` est dans l arbre source, hors copies et vendorise. + + Teste les composants RELATIFS a ``repo_root`` : un depot situe sous un + chemin contenant lui-meme un nom de la liste (par ex. un checkout dans + ``/home/x/venv/CoursIA``) ne doit pas devenir invisible en entier. + """ + try: + rel = path.relative_to(repo_root) + except ValueError: + return True + if any(part in _SKIP_DIRS for part in rel.parts): + return False + # La liste de noms ci-dessus ne couvre que les conventions connues. Un + # worktree nomme autrement contamine quand meme le compte : mesure du + # 2026-08-30, un worktree `.wt-parity-split` a la racine faisait rendre + # 2 paires qui n existaient que dans la copie, filtre actif. Ce qui + # caracterise une copie n est pas son nom mais la presence d un `.git` + # (repertoire pour un clone, fichier pour un worktree ou un submodule). + probe = repo_root + for part in rel.parts[:-1]: + probe = probe / part + if (probe / ".git").exists(): + return False + return True + + def discover_pairs( repo_root: Path, langs: List[str] ) -> List[Tuple[Path, Path, str, str]]: @@ -181,6 +226,8 @@ def discover_pairs( pairs: List[Tuple[Path, Path, str, str]] = [] seen: Set[Tuple[str, str]] = set() for nb_path in sorted(repo_root.rglob("*.ipynb")): + if not _is_scannable(nb_path, repo_root): + continue stem = nb_path.stem suffix_match = _SUFFIX_RE.match(stem) if suffix_match and suffix_match.group("lang") in langs: diff --git a/scripts/translation/tests/test_check_translation_parity.py b/scripts/translation/tests/test_check_translation_parity.py index d6d1f7e395..3fe0470b42 100644 --- a/scripts/translation/tests/test_check_translation_parity.py +++ b/scripts/translation/tests/test_check_translation_parity.py @@ -642,6 +642,48 @@ def test_cli_repo_root_missing_returns_two(tmp_path, capsys, monkeypatch): assert rc == 2 +def test_discover_pairs_skips_repo_copies_and_vendored_trees(tmp_path): + """Une paire dans `.worktrees/` (copie du depot) ne doit PAS etre comptee. + + Controle POSITIF d abord : la meme paire, placee dans l arbre source, EST + vue. Sans cette moitie, le test passerait aussi avec un walk qui ne trouve + jamais rien — c est la seule facon de distinguer « exclu » de « aveugle ». + """ + src = tmp_path / "serie" / "N-1.ipynb" + src.parent.mkdir(parents=True) + src.write_text("{}", encoding="utf-8") + src.with_name("N-1_en.ipynb").write_text("{}", encoding="utf-8") + + visible = p.discover_pairs(tmp_path, ["en"]) + assert len(visible) == 1, "controle positif : une paire de l arbre source doit etre vue" + + for skipped in (".worktrees", ".venv", "node_modules", ".lake"): + copy = tmp_path / skipped / "clone" / "serie" / "N-2.ipynb" + copy.parent.mkdir(parents=True) + copy.write_text("{}", encoding="utf-8") + copy.with_name("N-2_en.ipynb").write_text("{}", encoding="utf-8") + + after = p.discover_pairs(tmp_path, ["en"]) + assert len(after) == 1, ( + f"4 paires ajoutees dans des repertoires exclus ne doivent rien changer, " + f"vu {len(after)}" + ) + + +def test_is_scannable_matches_on_relative_parts_only(tmp_path): + """Un depot dont le CHEMIN PARENT porte un nom exclu reste scannable. + + Regression : un test sur le chemin absolu rendrait invisible tout depot + situe sous, par ex., `/home/x/venv/CoursIA`. + """ + root = tmp_path / "venv" / "CoursIA" + (root / "serie").mkdir(parents=True) + inside = root / "serie" / "N.ipynb" + inside.write_text("{}", encoding="utf-8") + assert p._is_scannable(inside, root) is True + assert p._is_scannable(root / ".venv" / "x" / "N.ipynb", root) is False + + # --------------------------------------------------------------------------- # Reference — live state (manual) # ---------------------------------------------------------------------------