2026-10-02 19:32:46 +02:00
|
|
|
"""Text chunking for the skill router (`skills.py`).
|
|
|
|
|
|
|
|
|
|
The vector index that once lived beside the chunker (`VectorIndex`, `build_vector_index`, `query_vector_index`,
|
|
|
|
|
`build_retrieval_package`) and the `ousast index` subcommand that fed it were removed on 2026-10-02: nothing ever
|
|
|
|
|
read the `chunks.json` they wrote. The decision engine's embeddings live in `learn/embeddings.py`.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-06-17 21:13:51 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import hashlib
|
2026-10-02 19:32:46 +02:00
|
|
|
from dataclasses import dataclass
|
2026-06-17 21:13:51 +02:00
|
|
|
|
|
|
|
|
VALID_NAMESPACES = {"repo_code", "docs", "static_findings", "mechanisms", "skills", "traces"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class CodeChunk:
|
|
|
|
|
chunk_id: str
|
|
|
|
|
namespace: str
|
|
|
|
|
path: str
|
|
|
|
|
language: str
|
|
|
|
|
start_line: int
|
|
|
|
|
end_line: int
|
|
|
|
|
text: str
|
|
|
|
|
metadata: dict[str, str | int | bool]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def chunk_text_namespace(
|
|
|
|
|
*,
|
|
|
|
|
namespace: str,
|
|
|
|
|
path: str,
|
|
|
|
|
text: str,
|
|
|
|
|
metadata: dict[str, str | int | bool] | None = None,
|
|
|
|
|
max_lines: int = 80,
|
|
|
|
|
) -> list[CodeChunk]:
|
|
|
|
|
if namespace not in VALID_NAMESPACES:
|
|
|
|
|
raise ValueError(f"unsupported namespace: {namespace}")
|
|
|
|
|
if max_lines < 1:
|
|
|
|
|
raise ValueError("max_lines must be at least 1")
|
|
|
|
|
lines = text.splitlines()
|
|
|
|
|
chunks: list[CodeChunk] = []
|
|
|
|
|
for start in range(0, len(lines), max_lines):
|
|
|
|
|
chunk_lines = lines[start : start + max_lines]
|
|
|
|
|
if not any(line.strip() for line in chunk_lines):
|
|
|
|
|
continue
|
|
|
|
|
start_line = start + 1
|
|
|
|
|
end_line = start + len(chunk_lines)
|
|
|
|
|
chunk_text = "\n".join(chunk_lines)
|
2026-06-18 10:06:21 +02:00
|
|
|
chunk_metadata: dict[str, str | int | bool] = {
|
|
|
|
|
"path": path,
|
|
|
|
|
"namespace": namespace,
|
|
|
|
|
"start_line": start_line,
|
|
|
|
|
"end_line": end_line,
|
|
|
|
|
}
|
2026-06-17 21:13:51 +02:00
|
|
|
if metadata:
|
2026-06-18 10:06:21 +02:00
|
|
|
chunk_metadata.update({key: value for key, value in metadata.items() if isinstance(value, str | int | bool)})
|
2026-06-17 21:13:51 +02:00
|
|
|
chunks.append(
|
|
|
|
|
CodeChunk(
|
|
|
|
|
chunk_id=_chunk_id(f"{namespace}:{path}", start_line, end_line, chunk_text),
|
|
|
|
|
namespace=namespace,
|
|
|
|
|
path=path,
|
|
|
|
|
language=str(chunk_metadata.get("language", "text")),
|
|
|
|
|
start_line=start_line,
|
|
|
|
|
end_line=end_line,
|
|
|
|
|
text=chunk_text,
|
|
|
|
|
metadata=chunk_metadata,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return chunks
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _chunk_id(path: str, start_line: int, end_line: int, text: str) -> str:
|
|
|
|
|
digest = hashlib.sha256(f"{path}:{start_line}:{end_line}:{text}".encode()).hexdigest()[:16]
|
|
|
|
|
return f"{path}:{start_line}-{end_line}:{digest}"
|