RAG Context-Injection Retrieval & Scoping Investigation
Investigation of the orchestrata-ultimate semantic-graph (RAG) subsystem and its one production consumer, the context injector. Findings verified against source via the AST toolkit + orchgraph (no grep, no raw source scanning).
1. What the RAG stores that the AST/graph does not
Chunk (src/orchestrata_ultimate/semantic_graph/models.py:46-57):
id: str
content: str
embedding: list[float] | None # <- the genuinely-new datum
metadata: dict[str, Any] # module, file_path, project_id, language, line ranges
One line: the graph stores edges between named symbols (Function—CALLS→Method); the RAG stores a vector (embedding: list[float]) per 50-line text chunk, so retrieval is by meaning rather than by name/structure.
Example: _retry_with_backoff in core/.../bolt_sync.py and an unrelated retry loop in semantic_graph/.../memgraph.py share zero symbols/edges — the graph cannot link them, but their embeddings are distance-close because both mean "retry with exponential backoff."
2. The retrieval stack is split-brained
Stack A — full hybrid (built + tested, NOT wired to production). SemanticGraphIngester.search() (ingestion.py:315-432): vector pass → keyword pass → weighted RRF fusion → module boost → prominence boost → rerank. Callers: test_ingestion.py + 3 scripts. Zero production callers (refs SemanticGraphIngester; every production .search() is re.search).
Stack B — context-injection bridge (wired to production, keyword-only). ContextInjector._query_semantic_bridge (context_injector.py:1211) → search_semantic_roots (semantic_bridge.py:145) → _query_memgraph_chunks (semantic_bridge.py:63):
MATCH (c:CodestralChunk) WHERE toLower(c.content) CONTAINS $term
Never touches the embedder, the vector index, search_similar, or RRF. Opens its own raw Bolt driver and runs a bare CONTAINS loop.
Reconciliation: the RAG's data (chunk text) is retrieved in production; the RAG's intelligence (embeddings + vector index + hybrid + rerank) is not.
3. Context injection is built, with a "semantic" section
ContextInjector → _query_context() (7 steps) → GraphContext.to_prompt() → payload['graph_context'] (~4000 tokens). Eight sections: files, relationships, tests, recent_changes, similar_patterns (structural KG) · semantic_files (RAG bridge, keyword-only) · requirements, decisions (KG TRACES_TO / APPLIES_TO).
_query_semantic_context (L1254) is the KG requirements/decisions layer, not the RAG. The bridge runs only if query: where query = payload.get("text") or payload.get("intent") or "".
4. Scoping: project and module
Chunks carry project_id (chunk_file metadata ingestion.py:183, persisted via add_chunks c += $props, read back via _node_to_chunk). Project-scoped retrieval is implementable today — but no retrieval path filters on it:
| Retrieval path | Project filter? | Module filter? |
|---|---|---|
search_similar (vector, memgraph.py:472) |
no | no |
search_keyword (memgraph.py:710) |
no | no |
_query_memgraph_chunks (bridge, semantic_bridge.py:63) |
no | no |
SemanticGraphIngester.search (hybrid) |
no | boost only (+0.02) |
The only project scoping is a post-filter in _query_neo4j_files (semantic_bridge.py:95-122), applied after global retrieval and only when project_id is not None. SemanticGraphConfig has no project_id field (its database field is test-worker multi-tenancy). project_id defaults to None, so the bridge is fully unscoped unless configured.
5. The poisoning verdict
Cross-project — real, but content-bounded and conditional. What leaks is path + one line (SemanticRoot.snippet(), ≤120 chars), formatted as ## Semantically related files → {path}: {snippet}. The header says "…ALREADY in the codebase. You MUST integrate with them" — a wrong-project path told to "integrate with" actively misleads. Fully cross-project when project_id=None; otherwise leaks via the shared-path edge case (dedup keyed by f.path, not f.id).
Cross-module — not poisoning, it is the feature (but unranked). Cross-module semantic retrieval is the intent; Stack B has no ranking ("relevant" = "contains the word", first-5-per-term).
6. The gap and fix
- Vector path is built-but-unactivated. Route
search_semantic_rootsthroughSemanticGraphIngester.search(query)(orsearch_similar(embedder.embed([query])[0])) instead of the rawCONTAINSloop. - Project scoping is missing at retrieval. Add
AND c.project_id = $project_id(and a vector-path filter) so retrieval is scoped before the File mapping.
7. Ruled out
"RAG fully orphaned" (split-brained, not orphaned) · "embedding is the only difference" (the only non-redundant datum) · "context injection has no semantic layer" (keyword, not vector) · "_query_semantic_context is the RAG" (it is the KG why-layer) · "project scoping impossible" (project_id is persisted, just never filtered) · "module is a scope" (only a boost).
Related Pages
- Semantic Graph — the
semantic_graphpackage page this investigation drills into - Semantic Graph (concept)