diff --git a/scripts/audit/detect_organ_duplication.py b/scripts/audit/detect_organ_duplication.py index 54d1e4af79a..a31ae48ba05 100644 --- a/scripts/audit/detect_organ_duplication.py +++ b/scripts/audit/detect_organ_duplication.py @@ -23,6 +23,12 @@ (la porte de sortie vit dans le body). Les jumeaux ``X.lean`` / ``X_en.lean`` d'un meme lake partagent leur serie proprietaire : ils ne se signalent pas. +Sous-modules (#13902, item 4) : une collision avec un organe de la section +``submodules`` (les cinq depots sous-modules) exige la section « ce que cette +extraction rend a l'amont » (ou « ne rend rien ») -- la copie declaree seule +ne blanchit pas : extraire l'ame d'un sous-module sans nommer le contrat de +retour est l'affaiblissement que l'EPIC interdit. + Sortie : une ligne par collision au format COLLISION () -> () --check : exit 1 si au moins une collision non declaree. @@ -60,6 +66,17 @@ r"\bcopie\s+(?:pedagogique\s+)?(?:declaree|fidele)\b", re.IGNORECASE ) +# Contrat de retour amont (#13902, item 4) : une collision avec un organe +# SUBMODULE (les cinq depots sous-modules de .gitmodules, geste de perennite) +# n'est pas blanchie par la seule copie declaree -- le body doit dire ce que +# l'extraction rend a l'amont, ou declarer explicitement ne rien rendre. +PAYBACK_RE = re.compile( + r"rend\w*\s+a\s+l['’]\s*amont" + r"|cette\s+extraction\s+rend" + r"|ne\s+rend\s+rien", + re.IGNORECASE, +) + # --- Lean (#20210) ---------------------------------------------------------- # Declaration en tete de ligne de CODE (commentaires deja retires par mask_lean). @@ -79,6 +96,14 @@ LEAN_TREE_DIR = "MyIA.AI.Notebooks" LEAN_PATHSPECS = (":(glob)MyIA.AI.Notebooks/**/*.lean", ":(glob)MyIA.AI.Notebooks/**/lakefile.*") +# Fixtures du detecteur lui-meme : ses propres tests ajoutent des lignes +# « class » DANS des chaines de patch de test -- ce ne sont pas des +# extractions, et l'organe qui se flaggerait lui-meme rendrait son advisory +# inutilisable sur ses propres PRs. Exemption bornee a ce fichier seul. +SELF_FIXTURE_FILES = frozenset({ + "scripts/tests/test_detect_organ_duplication.py", +}) + def strip_accents(text: str) -> str: import unicodedata @@ -92,7 +117,16 @@ def is_declared(body: str) -> bool: return bool(DECLARATION_RE.search(strip_accents(body))) +def is_payback(body: str) -> bool: + if not body: + return False + return bool(PAYBACK_RE.search(strip_accents(body))) + + def load_index(path: Path): + """(owners, symbol_owners, submodule_names) : la section ``submodules`` + (#13902) charge les memes champs que ``series`` -- ses organes exigent le + contrat de retour amont au lieu de la copie declaree.""" data = yaml.safe_load(path.read_text(encoding="utf-8")) series = data.get("series") or {} if not isinstance(series, dict) or not series: @@ -104,7 +138,17 @@ def load_index(path: Path): owners[name] = owner_path for sym in entry.get("symbols") or []: symbol_owners.setdefault(str(sym), name) - return owners, symbol_owners + submodule_names = set() + submodules = data.get("submodules") or {} + for name, entry in submodules.items(): + owner_path = str(entry.get("owner_path", "")).rstrip("/") + owners[name] = owner_path + submodule_names.add(name) + for sym in entry.get("symbols") or []: + # setdefault : un symbole deja proprietaire cote series reste a + # son premier proprietaire -- la curation exclut les doublons. + symbol_owners.setdefault(str(sym), name) + return owners, symbol_owners, submodule_names def resolve_series(path: str, owners: dict): @@ -133,7 +177,9 @@ def parse_patch(patch_text: str, owners: dict, symbol_owners: dict): if m: current_file = m.group(2) continue - if current_file is None or not line.startswith("+") or line.startswith("+++"): + if current_file is None or current_file in SELF_FIXTURE_FILES: + continue + if not line.startswith("+") or line.startswith("+++"): continue content = line[1:].lstrip() if content.startswith("#"): @@ -490,6 +536,7 @@ def git_diff(base: str, head: str, cwd: Path) -> str: result = subprocess.run( ["git", "diff", f"{base}...{head}", "--unified=0"], capture_output=True, text=True, cwd=str(cwd), check=True, + encoding="utf-8", errors="replace", ) return result.stdout @@ -506,6 +553,7 @@ def git_show(ref: str, path: str, cwd: Path): def git_merge_base(base: str, head: str, cwd: Path) -> str: result = subprocess.run( ["git", "merge-base", base, head], capture_output=True, text=True, cwd=str(cwd), + encoding="utf-8", errors="replace", ) found = result.stdout.strip() return found if result.returncode == 0 and found else base @@ -551,7 +599,7 @@ def main(argv=None) -> int: if not args.patch and not args.base: args.base = "origin/main" - owners, symbol_owners = load_index(args.index) + owners, symbol_owners, submodule_names = load_index(args.index) cwd = args.cwd or Path.cwd() if args.patch: @@ -566,6 +614,7 @@ def main(argv=None) -> int: if args.body_file else "" ) declared = is_declared(body) + payback = is_payback(body) collisions = parse_patch(patch_text, owners, symbol_owners) blocks = parse_added(patch_text) @@ -580,17 +629,27 @@ def main(argv=None) -> int: undeclared = [] for file, file_series, owner, sym in collisions: origin = file_series if file_series else "?" - if declared: + if owner in submodule_names: + # Contrat #13902 : la copie declaree ne suffit pas, seule la + # section de retour amont blanchit une extraction de sous-module. + if payback: + print(f"EXEMPTED {origin} ({file}) -> {owner} ({sym}) [retour amont declare]") + else: + print(f"COLLISION {origin} ({file}) -> {owner} ({sym}) " + f"[retour amont manquant -- #13902]") + undeclared.append((file, owner, sym)) + elif declared: print(f"EXEMPTED {origin} ({file}) -> {owner} ({sym}) [copie declaree]") else: print(f"COLLISION {origin} ({file}) -> {owner} ({sym})") undeclared.append((file, owner, sym)) - status = "EXEMPTED" if declared and collisions else ( + status = "EXEMPTED" if collisions and not undeclared else ( "BLOCKED" if undeclared else "CLEAN" ) print(f"{status} {len(collisions)} collision(s), {len(undeclared)} non declaree(s), " - f"{len(owners)} series dans l'index, {len(lean_owners)} lakes Lean") + f"{len(owners) - len(submodule_names)} series + {len(submodule_names)} submodules " + f"dans l'index, {len(lean_owners)} lakes Lean") if args.check and undeclared: return 1 diff --git a/scripts/audit/organ_api_index.yaml b/scripts/audit/organ_api_index.yaml index f5173b7fea2..87651c546fe 100644 --- a/scripts/audit/organ_api_index.yaml +++ b/scripts/audit/organ_api_index.yaml @@ -187,3 +187,109 @@ series: - add_score_claim - slugify # Mesure : 36 notebooks, 19 symboles .ipynb. + +submodules: + # Contrat de retour amont (#13902, item 4) : une collision avec un organe de + # cette section n'est PAS blanchie par la seule « copie declaree » -- le body + # doit porter la section « ce que cette extraction rend a l'amont » (ou + # declarer explicitement ne rien rendre). Ces depots sont des sous-modules = + # geste de perennite (mandat user 2026-09-01) : extraire leur ame sans nommer + # le contrat de retour est le mecanisme d'affaiblissement que l'EPIC interdit. + # + # Source des symboles : MESURE au 2026-10-23 sur les arbres GitHub des cinq + # depots AUX GITLINKS EPINGLES d'origin/main (git ls-tree), convention C# une + # classe publique par fichier, spot-checks Fallacy.cs (« public class + # Fallacy ») et VettingLevel.cs (« public enum VettingLevel »). Curation : + # noms distinctifs seulement -- les noms d'algorithmes standards (Differential + # Evolution, Simulated Annealing...) sont exclus : reimplementer un algorithme + # de manuel n'est pas extraire l'organe. Borne connue : DEF_RE ne voit que + # def/class -- un « record », « enum » ou « type » F# ajoute hors du + # sous-module n'est pas attrape. + semantic-fleet: + owner_path: MyIA.AI.Notebooks/GenAI/SemanticKernel/semantic-fleet + remote: MyIntelligenceAgency/semantic-fleet + pin: 2a9d48b4a899 + organ: "Connectors.AI.MultiConnector (fork SK v0.34.3) -- vetting, analyse multi-completion, couts par appel" + symbols: + - MultiCompletionAnalysis + - MultiCompletionAnalysisSettings + - ConnectorPromptEvaluation + - VettingLevel + - CallRequestCostCreditor + - CompletionJob + - MultiConnectorConfiguration + - MultiOpenAICompatibleConnectorConfiguration + + Argumentum: + owner_path: MyIA.AI.Notebooks/SymbolicAI/Argument_Analysis/Argumentum + remote: ArgumentumGames/Argumentum + pin: e62787eaaecd + organ: "AssetConverter + entites du corpus (fallacies, vertus, scenarios) -- la surface C# mesuree au pin" + symbols: + - Fallacy + - Virtue + - ArgumentVirtue + - Scenario + - SexyContentData + - DatasetUpdater + - DatasetUpdaterRootConfig + - RecordsUpdater + - AssetConverterConfig + - BatchImageConverterConfig + - PromptExample + + MetaGeneticSharp: + owner_path: MyIA.AI.Notebooks/Search/MetaGeneticSharp + remote: jsboige/MetaGeneticSharp + pin: 3a7d91e57b63 + organ: "meta-genetique : MetaGeneticAlgorithm + crossovers geometriques (Domain)" + symbols: + - MetaGeneticAlgorithm + - EukaryoteChromosome + - GeometricCrossover + - GeometricConverter + - TypedGeometricConverter + - KendallTauEmbedding + - OrderedEmbedding + - InsertionEmbedding + - EdgeEmbedding + - IdentityEmbedding + - KnownCompoundMetaheuristics + - SubEvolutionContext + + Z3.Linq: + owner_path: MyIA.AI.Notebooks/SymbolicAI/SMT/Z3.Linq + remote: MyIntelligenceAgency/Z3.Linq + pin: 8a2562dba761 + organ: "binding LINQ->Z3 : Theorem, rewriters d'attributs, exemples Sudoku/RiverCrossing" + symbols: + - Theorem + - SudokuTheorem + - SudokuGridTheorem + - SudokuTheoremRewriter + - TheoremGlobalRewriterAttribute + - TheoremPredicateRewriterAttribute + - BitVecWidthAttribute + - SolverKind + - MissionariesAndCannibals + + Automata: + owner_path: MyIA.AI.Notebooks/SymbolicAI/SMT/Automata + remote: MyIntelligenceAgency/Automata + pin: b1335eebafac + organ: "automates symboliques (Microsoft) : CharSetSolver, tree algebra, BDD algebras" + symbols: + - Automaton + - CharSetSolver + - TreeTheory + - TreeRule + - TreeTransducer + - BooleanAlgebraZ3 + - Z3Provider + - RankedAlphabet + - ValueTerm + - BREX + - BREXManager + - BDDAlgebra + - ConditionalDerivative + - BasicCountingSet diff --git a/scripts/tests/test_detect_organ_duplication.py b/scripts/tests/test_detect_organ_duplication.py index 7581d4d4cff..23aab26c4c4 100644 --- a/scripts/tests/test_detect_organ_duplication.py +++ b/scripts/tests/test_detect_organ_duplication.py @@ -72,10 +72,16 @@ def _write(tmp_path: Path, name: str, text: str) -> Path: def test_index_loads_measured_series(): mod = _load_detector() - owners, symbol_owners = mod.load_index(INDEX) - assert len(owners) >= 6, "acceptance: >= 6 series mesurees" + owners, symbol_owners, submodule_names = mod.load_index(INDEX) + assert len(owners) - len(submodule_names) >= 6, "acceptance: >= 6 series mesurees" for sym in ("Operateur", "ops_de_base", "ops_avec_radeau", "prop", "successeurs"): assert symbol_owners[sym] == "Planners" + assert submodule_names == { + "semantic-fleet", "Argumentum", "MetaGeneticSharp", "Z3.Linq", "Automata", + }, "contrat #13902 : les cinq sous-modules dans l'index" + assert symbol_owners["VettingLevel"] == "semantic-fleet" + assert symbol_owners["Fallacy"] == "Argumentum" + assert symbol_owners["CharSetSolver"] == "Automata" def test_greffe2_flagged_without_body(tmp_path, capsys): @@ -204,6 +210,128 @@ def test_accent_tolerant_declaration_forms(): assert not mod.is_declared("") +# --- Sous-modules (#13902, item 4) : contrat de retour amont ------------------- + +# Forme de l'extraction fondatrice : un fichier CoursIA (hors sous-module) qui +# redistille la physique du MultiConnector de semantic-fleet. +SF_EXTRACTION_PATCH = """diff --git a/MyIA.AI.Notebooks/GenAI/Texte/mc_local.py b/MyIA.AI.Notebooks/GenAI/Texte/mc_local.py +--- a/MyIA.AI.Notebooks/GenAI/Texte/mc_local.py ++++ b/MyIA.AI.Notebooks/GenAI/Texte/mc_local.py +@@ -0,0 +1,3 @@ ++class VettingLevel: ++ pass ++class MultiCompletionAnalysis: +""" + +BODY_PAYBACK = ( + "## Retour amont\n\nCe que cette extraction rend à l'amont : une PR " + "semantic-fleet consommant le banc de vetting (extension tracée #13902)." +) +BODY_PAYBACK_NOTHING = ( + "Elle ne rend rien : copie de 30 lignes pour la séance 3, motif pédagogique." +) + + +def test_submodule_collision_requires_payback(tmp_path, capsys): + """Extraction d'un symbole de sous-module sans section de retour : bloquée, + même marqueur #13902 -- la copie déclarée seule ne blanchit pas.""" + mod = _load_detector() + patch = _write(tmp_path, "sf.diff", SF_EXTRACTION_PATCH) + rc = mod.main(["--patch", str(patch), "--index", str(INDEX), "--check"]) + out = capsys.readouterr().out + assert rc == 1 + assert out.count("COLLISION") == 2 + assert "-> semantic-fleet (VettingLevel)" in out + assert "[retour amont manquant -- #13902]" in out + assert "BLOCKED" in out + + body = _write(tmp_path, "body.md", BODY_DECLARED_CANONIQUE) + rc = mod.main(["--patch", str(patch), "--body-file", str(body), + "--index", str(INDEX), "--check"]) + out = capsys.readouterr().out + assert rc == 1, "la copie déclarée seule ne satisfait pas le contrat #13902" + assert out.count("COLLISION") == 2 + + +def test_submodule_whitened_by_payback(tmp_path, capsys): + mod = _load_detector() + patch = _write(tmp_path, "sf.diff", SF_EXTRACTION_PATCH) + body = _write(tmp_path, "body.md", BODY_PAYBACK) + rc = mod.main(["--patch", str(patch), "--body-file", str(body), + "--index", str(INDEX), "--check"]) + out = capsys.readouterr().out + assert rc == 0 + assert out.count("[retour amont declare]") == 2 + assert "0 non declaree" in out + + body_nothing = _write(tmp_path, "body2.md", BODY_PAYBACK_NOTHING) + rc = mod.main(["--patch", str(patch), "--body-file", str(body_nothing), + "--index", str(INDEX), "--check"]) + assert rc == 0, "« ne rend rien » explicite satisfait le contrat" + assert "[retour amont declare]" in capsys.readouterr().out + + +def test_submodule_owner_path_is_maintenance_not_extraction(tmp_path, capsys): + """Éditer DANS le sous-module (maintenance) ne collisionne jamais.""" + mod = _load_detector() + patch = _write(tmp_path, "inside.diff", """diff --git a/MyIA.AI.Notebooks/GenAI/SemanticKernel/semantic-fleet/dotnet/src/Connectors/Connectors.AI.MultiConnector/Analysis/VettingLevel.cs b/MyIA.AI.Notebooks/GenAI/SemanticKernel/semantic-fleet/dotnet/src/Connectors/Connectors.AI.MultiConnector/Analysis/VettingLevel.cs +--- a/MyIA.AI.Notebooks/GenAI/SemanticKernel/semantic-fleet/dotnet/src/Connectors/Connectors.AI.MultiConnector/Analysis/VettingLevel.cs ++++ b/MyIA.AI.Notebooks/GenAI/SemanticKernel/semantic-fleet/dotnet/src/Connectors/Connectors.AI.MultiConnector/Analysis/VettingLevel.cs +@@ -1,2 +1,4 @@ ++public class VettingLevel ++{ ++} +""") + rc = mod.main(["--patch", str(patch), "--index", str(INDEX), "--check"]) + out = capsys.readouterr().out + assert rc == 0 + assert "CLEAN" in out and "0 collision" in out + + +def test_payback_does_not_whiten_series_collision(tmp_path, capsys): + """Symétrie : la section de retour amont ne blanchit PAS une collision de + série (Planners exige toujours la copie déclarée).""" + mod = _load_detector() + patch = _write(tmp_path, "greffe2.diff", GREFFE2_PATCH) + body = _write(tmp_path, "body.md", BODY_PAYBACK) + rc = mod.main(["--patch", str(patch), "--body-file", str(body), + "--index", str(INDEX), "--check"]) + out = capsys.readouterr().out + assert rc == 1 + assert out.count("COLLISION") == 5 + assert "[copie declaree]" not in out + + +def test_is_payback_forms(): + mod = _load_detector() + assert mod.is_payback("Ce que cette extraction rend à l'amont : une extension.") + assert mod.is_payback("l'extension rend à l'amont sous forme de PR") + assert mod.is_payback("rend a l'amont") # sans accents + assert mod.is_payback("l’extension rendra à l’amont") # apostrophe typographique + assert mod.is_payback("Elle ne rend rien : pédagogie pure.") + assert not mod.is_payback("on a copié le style du module") + assert not mod.is_payback(BODY_DECLARED_CANONIQUE), "copie déclarée ≠ retour amont" + assert not mod.is_payback("") + + +def test_detector_fixture_file_does_not_self_flag(tmp_path, capsys): + """La PR qui étend le détecteur ajoute « class VettingLevel » DANS ses + chaînes de patch de test : l'organe ne doit pas se flagger lui-même.""" + mod = _load_detector() + patch = _write(tmp_path, "self.diff", """diff --git a/scripts/tests/test_detect_organ_duplication.py b/scripts/tests/test_detect_organ_duplication.py +--- a/scripts/tests/test_detect_organ_duplication.py ++++ b/scripts/tests/test_detect_organ_duplication.py +@@ -1,1 +1,3 @@ ++SF_EXTRACTION_PATCH = "class VettingLevel:" ++class VettingLevel: ++ pass +""") + rc = mod.main(["--patch", str(patch), "--index", str(INDEX), "--check"]) + out = capsys.readouterr().out + assert rc == 0 + assert "CLEAN" in out and "0 collision" in out + + # --- Lean (#20210) : index derive d'un faux arbre, citations, jumeaux _en --- HECKE_PROBE = "MyIA.AI.Notebooks/SymbolicAI/Lean/hecke_lean/Hecke/Probe.lean"