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
12 changes: 7 additions & 5 deletions tools/code_index/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
15 changes: 15 additions & 0 deletions tools/code_index/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion tools/code_index/walk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", []):
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions tools/tests/test_code_index_walk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading