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

  1. Vector path is built-but-unactivated. Route search_semantic_roots through SemanticGraphIngester.search(query) (or search_similar(embedder.embed([query])[0])) instead of the raw CONTAINS loop.
  2. 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).