From 0e4ab461e4ee8d202cd84289a92e391d8d26b071 Mon Sep 17 00:00:00 2001 From: citarf <8773429+citarf@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:56:15 +0400 Subject: [PATCH] =?UTF-8?q?code=5Findex:=20d=C3=A9-polluer=20code=5Fchunks?= =?UTF-8?q?=20de=20la=20gouvernance=20data-platform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit search_code remontait la prose FR de data-platform (catalog/contracts/inventory/ lineage…) AVANT le vrai code applicatif : pour « où est fait X dans le code », le fichier attendu était absent du top-10, enterré sous des .md/.yaml de gouvernance (déjà indexés, eux, dans docs_chunks/search_docs). Vérifié : en excluant cette prose, 2/3 des questions code remontent rang 1. - walk.iter_files : support `exclude_per_repo` (motifs relatifs, par repo) ; - manifest : exclure de data-platform, côté CODE uniquement, catalog/contracts/ inventory/lineage/audits/adr + *.md (gardés dans docs_chunks). Le vrai code (tools/*.py) reste indexé ; - index.py : reconstruire le FTS aussi quand il n'y a QUE des suppressions (sinon BM25 incohérent après retrait de lignes). docs_chunks (search_docs) totalement inchangé. 74 tests verts. --- tools/code_index/index.py | 12 +++++++----- tools/code_index/manifest.yaml | 15 +++++++++++++++ tools/code_index/walk.py | 7 ++++++- tools/tests/test_code_index_walk.py | 19 +++++++++++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/tools/code_index/index.py b/tools/code_index/index.py index 77482d2..9798ee7 100644 --- a/tools/code_index/index.py +++ b/tools/code_index/index.py @@ -220,11 +220,13 @@ def _rows_for(batch_files: list[FileBlob]) -> tuple[list[dict], list[str]]: total_rows += len(rows_b) print(f" {min(fstart + BATCH_FILES, len(to_index))}/{len(to_index)} fichiers " f"indexés, {total_rows} chunks écrits…", flush=True, file=sys.stderr) - if cfg.hybrid: - # Compacte en mono-fragment PUIS construit le BM25 : le builder FTS natif de - # LanceDB deadlock sur une table multi-fragment (cf. store.rebuild_with_fts). - print(" compaction + index FTS BM25…", flush=True, file=sys.stderr) - store.rebuild_with_fts(db, meta=sidecar, table=cfg.table) + + # FTS reconstruit dès qu'il y a eu un changement — y compris des SUPPRESSIONS seules + # (sans ré-embedding) : retirer des lignes sans recompacter laisserait un index BM25 + # incohérent (et multi-fragment). rebuild_with_fts n'appelle pas l'API. + if cfg.hybrid and (to_index or gone): + print(" compaction + index FTS BM25…", flush=True, file=sys.stderr) + store.rebuild_with_fts(db, meta=sidecar, table=cfg.table) print(f"Indexé : {len(to_index)} fichiers (ré)indexés, {total_rows} chunks, " f"{len(gone)} fichiers retirés.") diff --git a/tools/code_index/manifest.yaml b/tools/code_index/manifest.yaml index 5618b35..b6eb858 100644 --- a/tools/code_index/manifest.yaml +++ b/tools/code_index/manifest.yaml @@ -78,6 +78,21 @@ exclude_globs: - "uv.lock" - "poetry.lock" +# Exclusions PAR REPO (motifs relatifs au repo, fnmatch). La gouvernance data-platform +# (catalog/contracts/inventory/lineage/audits/adr + prose .md) est DÉJÀ indexée dans le +# corpus « docs » (table docs_chunks, search_docs). L'indexer AUSSI en CODE polluait +# search_code : sa prose FR matchait les requêtes et enterrait le vrai code applicatif. +# Côté code, on ne garde de data-platform que le VRAI code (tools/*.py, scripts). +exclude_per_repo: + data-platform: + - "catalog/*" + - "contracts/*" + - "inventory/*" + - "lineage/*" + - "audits/*" + - "adr/*" + - "*.md" + # ── Corpus « docs » : gouvernance data-platform (table docs_chunks, modèle texte) ── # Chemins RELATIFS au repo data-platform (résolus sous base_dir/data-platform). Chunking par # entrée (cf. chunk_structured) ; CSV de volumétrie et *.sql.gz exclus (servis par les outils diff --git a/tools/code_index/walk.py b/tools/code_index/walk.py index 7042a44..ac21798 100644 --- a/tools/code_index/walk.py +++ b/tools/code_index/walk.py @@ -57,6 +57,10 @@ def iter_files(manifest: dict, base_dir: Path, *, repos: list[str] | None = None include_ext = {e.lower() for e in manifest.get("include_ext", [])} exclude_dirs = set(manifest.get("exclude_dirs", [])) exclude_globs = list(manifest.get("exclude_globs", [])) + # Exclusions PAR REPO (motifs relatifs au repo) : ex. retirer la gouvernance data-platform + # (catalog/contracts/inventory/lineage/…) du corpus CODE — elle est déjà dans docs_chunks, + # et sa prose enterrait le vrai code applicatif dans search_code. + per_repo = manifest.get("exclude_per_repo", {}) or {} wanted = set(repos) if repos else None for repo in manifest.get("repos", []): @@ -65,11 +69,12 @@ def iter_files(manifest: dict, base_dir: Path, *, repos: list[str] | None = None root = (base_dir / repo).resolve() if not root.is_dir(): continue + repo_globs = exclude_globs + list(per_repo.get(repo, [])) for abspath in sorted(root.rglob("*")): if not abspath.is_file(): continue rel = abspath.relative_to(root).as_posix() - if is_excluded(rel, exclude_dirs, exclude_globs): + if is_excluded(rel, exclude_dirs, repo_globs): continue if abspath.suffix.lower() not in include_ext: continue diff --git a/tools/tests/test_code_index_walk.py b/tools/tests/test_code_index_walk.py index b010573..d69913e 100644 --- a/tools/tests/test_code_index_walk.py +++ b/tools/tests/test_code_index_walk.py @@ -65,3 +65,22 @@ def test_max_file_bytes(tmp_path: Path) -> None: _touch(tmp_path / "repoA" / "big.py", b"#" * 5000) found = list(iter_files(MANIFEST, tmp_path, repos=["repoA"], max_file_bytes=1000)) assert found == [] + + +def test_exclude_per_repo(tmp_path: Path) -> None: + """Exclusion ciblée par repo : la gouvernance de repoA est retirée, mais le même + chemin dans repoB (non visé) est conservé, et le code de repoA reste.""" + manifest = { + "repos": ["repoA", "repoB"], + "include_ext": [".py", ".yaml"], + "exclude_per_repo": {"repoA": ["catalog/*", "contracts/*"]}, + } + _touch(tmp_path / "repoA" / "catalog" / "x.yaml") + _touch(tmp_path / "repoA" / "contracts" / "c.yaml") + _touch(tmp_path / "repoA" / "tools" / "real.py") + _touch(tmp_path / "repoB" / "catalog" / "keep.yaml") # repoB non visé → gardé + keys = {f.key for f in iter_files(manifest, tmp_path)} + assert "repoA/tools/real.py" in keys + assert "repoB/catalog/keep.yaml" in keys + assert "repoA/catalog/x.yaml" not in keys + assert "repoA/contracts/c.yaml" not in keys