orchestrata_ultimate.knowledge_graph.context_injector
Reference › orchestrata_ultimate › knowledge_graph › context_injector
ContextInjector enriches CXP packets with graph context so a volk knows the existing code structure before it changes or integrates with it (Spec 008 REQ-008-013, Spec 106 REQ-106-007). The context assembler and the kernel's pipeline_kg import it. It reads the operational graph (orchkg on orchgraph) through a synchronous repository and, via semantic_bridge, can add file hits from the shared semantic graph (memgraph).
There are two layers. The packet layer: should_inject checks whether a subject (implement, fix issues, review and test, design review) triggers injection, and inject extracts target files (_extract_targets), builds a GraphContext with _query_context and attaches it to the payload. Query steps are exact file lookup by id and path, a multi-hop traversal capped at 50 entities and roughly 2000 tokens, recent changes, similar patterns (files sharing base classes), the semantic bridge, and the semantic layer of requirements and prior decisions the code traces to (resolved with requirement_resolution). GraphContext.to_prompt renders the sections within a token budget. Called without a task type, inject keeps its original behaviour.
The task-type layer was merged in from a former separate injector: TaskType (build, refactor, debug, document, test, review) drives inject_for_task, which returns a VolkContext of ContextNode entries with to_dict and to_markdown. That path runs the deterministic pipeline in context_pack, and _apply_context_pack adapts the packed result into the node-oriented shape. ContextExpansion carries the broader parameters that context_validator_loop requests on a retry. Project ids come from project_resolver.
At a glance
| Source | src/orchestrata_ultimate/knowledge_graph/context_injector.py |
| Lines of code | 1568 |
| Classes | 6 |
| Functions | 2 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.graph, orchestrata_ultimate.knowledge_graph, orchestrata_ultimate.knowledge_graph.canonical_schema, orchestrata_ultimate.knowledge_graph.context_pack, orchestrata_ultimate.knowledge_graph.context_pack.cache, orchestrata_ultimate.knowledge_graph.context_pack.relevance, orchestrata_ultimate.knowledge_graph.requirement_resolution, orchestrata_ultimate.knowledge_graph.semantic_bridge, orchestrata_ultimate.memory.token_counter, orchestrata_ultimate.semantic_graph.config |
| Imported by | orchestrata_ultimate.context.assembler, orchestrata_ultimate.kernel.pipeline_kg, orchestrata_ultimate.knowledge_graph.context_pack.cache, orchestrata_ultimate.knowledge_graph.context_pack.frame, orchestrata_ultimate.knowledge_graph.context_pack.selector, orchestrata_ultimate.knowledge_graph.context_validator_loop, scripts.benchmark_context_pack, scripts.context_pack |
| Tests that import it | tests/context/test_assembler.py, tests/generated_recovery/test_knowledge_graph_context_injector.py, tests/kernel/test_pipeline_kg.py, tests/knowledge_graph/context_pack/test_cache.py, tests/knowledge_graph/context_pack/test_frame.py, tests/knowledge_graph/context_pack/test_packer.py, tests/knowledge_graph/context_pack/test_relevance.py, tests/knowledge_graph/context_pack/test_selector.py, tests/knowledge_graph/test_artifact_provenance_sync.py, tests/knowledge_graph/test_context_injector_coverage.py, tests/knowledge_graph/test_context_injector_coverage_wave2.py, tests/knowledge_graph/test_context_injector_label_regression.py, tests/knowledge_graph/test_context_injector_selector_wiring.py, tests/knowledge_graph/test_context_injector_task_types.py, tests/knowledge_graph/test_context_pack_performance.py, tests/knowledge_graph/test_context_validator_loop.py, tests/knowledge_graph/test_graph_context_injection.py, tests/knowledge_graph/test_knowledge_signatures.py, tests/knowledge_graph/test_runtime_signals.py, tests/knowledge_graph/test_semantic_bridge.py, tests/session/test_context_window.py |
Class diagram
Dependency diagram
API
Constants
| Name | Type | Value |
|---|---|---|
SUBJECT_TASK_TYPE |
dict[str, TaskType] |
{'implement': TaskType.BUILD, 'fix_issues': TaskType.DEBUG, 'review_and_test': … |
Classes
TaskType (enum)
Bases: Enum · Line 87
Types of tasks a Volk can perform (drives the task-type query layer).
| Member | Value |
|---|---|
BUILD |
'build' |
REFACTOR |
'refactor' |
DEBUG |
'debug' |
DOCUMENT |
'document' |
TEST |
'test' |
REVIEW |
'review' |
ContextExpansion (dataclass)
Decorators: traces(spec='008', story='US-003', reqs=['REQ-008-122']), dataclass · Line 100
Parameters for expanding KG context on a validator-loop retry.
| Attribute | Type | Default |
|---|---|---|
extra_hops |
int |
0 |
include_tested_by |
bool |
False |
include_requirement_closure |
bool |
False |
limit_multiplier |
int |
1 |
GraphContext (dataclass)
Decorators: traces(spec='008', story='US-008-001', reqs=['REQ-008-102']), dataclass · Line 174
Context extracted from the knowledge graph for a task.
| Attribute | Type | Default |
|---|---|---|
files |
list[dict[str, Any]] |
field(default_factory=list) |
relationships |
list[dict[str, Any]] |
field(default_factory=list) |
tests |
list[str] |
field(default_factory=list) |
recent_changes |
list[dict[str, Any]] |
field(default_factory=list) |
similar_patterns |
list[dict[str, Any]] |
field(default_factory=list) |
semantic_files |
list[dict[str, Any]] |
field(default_factory=list) |
requirements |
list[dict[str, Any]] |
field(default_factory=list) |
decisions |
list[dict[str, Any]] |
field(default_factory=list) |
| Method | Signature | Summary |
|---|---|---|
to_prompt |
to_prompt(max_context_tokens: int = 4000) -> str |
Format context as a text block for volk prompts. |
_format_files_section |
_format_files_section() -> list[str] |
Format the existing-code-structure section (files → classes/funcs). |
_format_relationships_section |
_format_relationships_section() -> list[str] |
Format relationships grouped by type (inherits/calls/imports/...). |
_format_tests_section |
_format_tests_section() -> list[str] |
Format the covering-tests section. |
_format_recent_changes_section |
_format_recent_changes_section() -> list[str] |
Format the recent-changes section. |
_format_similar_patterns_section |
_format_similar_patterns_section() -> list[str] |
Format the similar-patterns section (files sharing bases/protocols). |
_format_semantic_files_section |
_format_semantic_files_section() -> list[str] |
Format the semantic-search files section. |
_format_requirements_section |
_format_requirements_section() -> list[str] |
Format the requirements the target code traces to (the 'why'). |
_format_decisions_section |
_format_decisions_section() -> list[str] |
Format prior decisions that apply to the target code (respect them). |
ContextNode (dataclass)
Decorators: dataclass · Line 393
A node injected into Volk context (task-type-aware layer).
| Attribute | Type | Default |
|---|---|---|
node_type |
str |
|
label |
str |
|
properties |
dict[str, Any] |
|
relationships |
list[dict[str, str]] |
field(default_factory=list) |
source_file |
str | None |
None |
line_number |
int | None |
None |
VolkContext (dataclass)
Decorators: traces(spec='106', reqs=['REQ-106-042', 'REQ-106-043']), dataclass · Line 406
Complete task-type-aware context for a Volk task.
| Attribute | Type | Default |
|---|---|---|
volk_alias |
str |
|
task_type |
TaskType |
|
nodes |
list[ContextNode] |
field(default_factory=list) |
files |
list[str] |
field(default_factory=list) |
specs |
list[str] |
field(default_factory=list) |
requirements |
list[str] |
field(default_factory=list) |
relationships |
list[str] |
field(default_factory=list) |
summary |
str |
'' |
metadata |
dict[str, Any] |
field(default_factory=dict) |
| Method | Signature | Summary |
|---|---|---|
to_dict |
to_dict() -> dict[str, Any] |
Convert to dictionary for injection. |
to_markdown |
to_markdown() -> str |
Convert to markdown for human-readable context. |
_markdown_bullet_section staticmethod |
_markdown_bullet_section(heading: str, items: list[str], code_quoted: bool = False) -> list[str] |
Render a simple bulleted markdown section, or nothing if empty. |
_markdown_nodes_section |
_markdown_nodes_section() -> list[str] |
Render the Nodes section with per-node properties and relationships. |
ContextInjector
Decorators: traces(spec='008', story='US-003', reqs=['REQ-008-013']), traces(spec='106', reqs=['REQ-106-007']) · Line 501
Queries the graph and injects graph context into CXP packets.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(repo: SyncGraphRepository | None, *, relevance: 'KgRelevanceRetriever | None' = None, project_id: str | None = None, pack_cache: 'ContextPackCache | None' = None, semantic_config: 'SemanticGraphConfig | None' = None) -> None |
Initialize with a graph repository. |
_connected_repo property |
_connected_repo() -> SyncGraphRepository |
Type-narrowing accessor for the query helpers below. |
should_inject |
should_inject(subject: str) -> bool |
Check if a CXP packet subject triggers injection. |
inject |
inject(payload: dict[str, Any], *, task_type: TaskType | None = None, task_params: dict[str, Any] | None = None, volk_alias: str = '', expansion: ContextExpansion | None = None) -> dict[str, Any] |
Query the graph and add context to a CXP packet payload. |
_extract_targets |
_extract_targets(payload: dict[str, Any]) -> list[str] |
Extract target file paths from a packet payload. |
_query_context |
_query_context(target_files: list[str], query: str = '', expansion: ContextExpansion | None = None) -> GraphContext |
Query the graph for context around target files. |
_query_exact_files |
_query_exact_files(target_files: list[str], context: GraphContext, limit_mult: int = 1) -> set[str] |
Query File nodes by canonical f.id and exact f.path. |
_append_file_record |
_append_file_record(context: GraphContext, record: dict[str, Any]) -> None |
Append a single Cypher file record to context.files. |
_query_recent_changes |
_query_recent_changes(file_paths: list[str], context: GraphContext) -> None |
Populate context.recent_changes for target files and neighbours. |
_append_recent_changes |
_append_recent_changes(records: list[dict[str, Any]], context: GraphContext) -> None |
Append non-empty recent-change records to context. |
_query_similar_patterns |
_query_similar_patterns(file_paths: list[str], context: GraphContext) -> None |
Populate context.similar_patterns with files whose classes share base classes or implemented protocols with the target files. |
_query_semantic_bridge |
_query_semantic_bridge(file_paths: list[str], query: str, context: GraphContext) -> None |
Populate context.semantic_files via the semantic-search bridge. |
_query_semantic_context |
_query_semantic_context(file_paths: list[str], context: GraphContext, max_depth: int = 2) -> None |
Add the KG semantic layer to context: requirements the target code traces to ((sym)-[:TRACES_TO]->(:RequirementItem) — the as-built label the KG sync writer actually MERGEs, per… |
inject_for_task |
inject_for_task(volk_alias: str, task_type: TaskType, task_params: dict[str, Any]) -> VolkContext |
Build task-type-aware context for a Volk task. |
_apply_context_pack |
_apply_context_pack(context: VolkContext, subgraph: Any, packed: Any) -> None |
Adapt a packed ContextPack into context's node-oriented shape. |
Functions
| Function | Signature | Summary |
|---|---|---|
_format_change_ts traces |
_format_change_ts(ts: Any) -> str |
Normalize a Neo4j timestamp value to an ISO-ish string. |
_task_params_text |
_task_params_text(task_params: dict[str, Any]) -> str |
Flatten task_params values into one entity-linking text blob. |
Source
# mypy: ignore-errors
"""Knowledge Graph Context Injector — enriches CXP packets with graph context.
Queries the knowledge graph for entities related to the target files
in a CXP packet and attaches them as graph_context in the payload. Volks
use this context for awareness of existing code structure when modifying
or integrating with existing code.
Triggers on: implement, fix_issues, review_and_test, design_review subjects.
Query strategy: multi-hop traversal capped at 50 entities and ~2000 tokens.
W4.4: also hosts the task-type-aware query layer (BUILD/REFACTOR/DEBUG/
DOCUMENT/TEST/REVIEW), merged in from the formerly-orphaned
``volk_context_injector.VolkContextInjector`` (ruled D9,
plans/decisions-2026-07-06.md — wire the handlers into the live path rather
than leave them as dead unit-tested code). ``ContextInjector.inject()``
keeps its original behavior byte-for-byte when called without ``task_type``;
passing ``task_type`` additionally runs :meth:`ContextInjector.inject_for_task`
and adds a ``task_context`` key to the payload.
W8.5 (SPEC-106 FR-005, REQ-106-040): :meth:`ContextInjector.inject_for_task`
no longer dispatches to the six ad-hoc ``_inject_*_context`` handlers
(``_inject_build_context`` .. ``_inject_review_context``) or their
``_build_summary`` helper — it now builds a ``RequestFrame``
(``context_pack.analyze_request``), selects a ranked subgraph
(``context_pack.select``), and greedily packs it under a token budget
(``context_pack.pack``), then adapts the result into the same
:class:`VolkContext` shape this method has always returned. Those six
handlers had zero remaining references anywhere in the codebase (confirmed
via ``serialize_codebase_ast.py refs`` before removal) and were deleted
outright rather than kept as dead code (session 2026-07-09).
"""
import logging
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any
from orchestrata_ultimate.core.base.traces import traces
# Runtime import (not TYPE_CHECKING): the ``repo:`` annotations must be
# evaluable — PEP 649 lazy evaluation runs them when inspect.signature()
# is called (test_knowledge_signatures), and a guard-only import NameErrors.
from orchestrata_ultimate.core.graph import SyncGraphRepository
from orchestrata_ultimate.knowledge_graph import canonical_schema as cs
from orchestrata_ultimate.memory.token_counter import WordTokenCounter
if TYPE_CHECKING:
# KgRelevanceRetriever's own module imports context_pack.selector, which
# imports TaskType from THIS module — a real (non-TYPE_CHECKING) import
# here would be circular. Kept type-only; the ``relevance:`` parameter
# below uses a quoted forward reference so PEP 649's lazy annotation
# evaluation (see the ``repo:`` note above) never needs this name bound
# at runtime — inspect.signature() only reads parameter names/defaults
# in this codebase's signature tests, never .annotation.
from orchestrata_ultimate.knowledge_graph.context_pack.cache import (
ContextPackCache,
)
from orchestrata_ultimate.knowledge_graph.context_pack.relevance import (
KgRelevanceRetriever,
)
from orchestrata_ultimate.semantic_graph.config import SemanticGraphConfig
_logger = logging.getLogger(__name__)
# Subjects that trigger context injection
INJECT_SUBJECTS = frozenset(
{
"implement",
"fix_issues",
"review_and_test",
"design_review",
"scope_review",
"ux_design",
}
)
MAX_ENTITIES = 50
MAX_CONTEXT_CHARS = 8000
# Default token budget for inject_for_task's selector/packer path (SPEC-106
# FR-005, REQ-106-040). Matches GraphContext.to_prompt's existing default so
# the two context-injection surfaces stay budget-consistent.
DEFAULT_TASK_CONTEXT_BUDGET = 4000
class TaskType(Enum):
"""Types of tasks a Volk can perform (drives the task-type query layer)."""
BUILD = "build" # Build from scratch
REFACTOR = "refactor" # Refactor existing code
DEBUG = "debug" # Debug/repair
DOCUMENT = "document" # Generate documentation
TEST = "test" # Write tests
REVIEW = "review" # Code review
@traces(spec="008", story="US-003", reqs=["REQ-008-122"])
@dataclass
class ContextExpansion:
"""Parameters for expanding KG context on a validator-loop retry.
A ``None`` expansion keeps the default query shape; any non-default field
broadens the context injected into the next LLM call.
"""
extra_hops: int = 0
include_tested_by: bool = False
include_requirement_closure: bool = False
limit_multiplier: int = 1
# Maps a CXP packet ``subject`` (see INJECT_SUBJECTS) to the TaskType the
# task-type-aware layer understands. Wired into the live pipeline call site:
# ``PipelineKGMixin.inject_for_subject`` (kernel/pipeline_kg.py) passes
# ``task_type=SUBJECT_TASK_TYPE.get(subject)`` into ``inject()``, so every
# INJECT_SUBJECTS member (all of which have a TaskType entry here, kept in
# lockstep on purpose) reaches ``inject_for_task``; a subject absent from
# this table keeps the pre-existing graph_context-only path.
SUBJECT_TASK_TYPE: dict[str, TaskType] = {
"implement": TaskType.BUILD,
"fix_issues": TaskType.DEBUG,
"review_and_test": TaskType.TEST,
"design_review": TaskType.REVIEW,
"scope_review": TaskType.REVIEW,
"ux_design": TaskType.BUILD,
}
@traces(spec="008", story="US-003", reqs=["REQ-008-103"])
def _format_change_ts(ts: Any) -> str:
"""Normalize a Neo4j timestamp value to an ISO-ish string.
Handles ``datetime`` objects, numeric epochs (ms or s), and strings.
Unknown/invalid values return the empty string so the caller can decide
whether to keep the record.
"""
if ts is None:
return ""
if hasattr(ts, "isoformat"):
return ts.isoformat()
if isinstance(ts, int | float):
# Neo4j datetime is the common case; raw numbers are treated as ms.
from datetime import UTC, datetime
try:
return datetime.fromtimestamp(ts / 1000, tz=UTC).isoformat()
except ValueError, OSError, OverflowError:
return str(ts)
return str(ts)
def _task_params_text(task_params: dict[str, Any]) -> str:
"""Flatten ``task_params`` values into one entity-linking text blob.
``context_pack.analyze_request`` links free TEXT to graph entities, but
the legacy task-type handlers took structured keys instead (``specs``,
``file``, ``package``, ``feature``, ``error``, ``bug``, ...). Rather than
special-case every legacy key, every value is joined into one blob so
any embedded canonical id/qualname token is linkable regardless of which
key carried it (REQ-106-040).
"""
parts: list[str] = []
for value in task_params.values():
if isinstance(value, list | tuple | set):
parts.extend(str(v) for v in value if v)
elif value:
parts.append(str(value))
return " ".join(parts)
@traces(spec="008", story="US-008-001", reqs=["REQ-008-102"])
@dataclass
class GraphContext:
"""Context extracted from the knowledge graph for a task.
Attributes:
files: File nodes with path, classes, functions.
relationships: Import/call/inherit relationships.
tests: Test functions covering the target entities.
recent_changes: Recent modifications to target files.
similar_patterns: Files with similar interfaces.
semantic_files: Files matched by semantic chunk search.
"""
files: list[dict[str, Any]] = field(default_factory=list)
relationships: list[dict[str, Any]] = field(default_factory=list)
tests: list[str] = field(default_factory=list)
recent_changes: list[dict[str, Any]] = field(default_factory=list)
similar_patterns: list[dict[str, Any]] = field(default_factory=list)
semantic_files: list[dict[str, Any]] = field(default_factory=list)
# Semantic layer: the "why" behind the code — requirements the target code
# traces to, and decisions that apply to it. Populated from the KG's
# TRACES_TO / (:Decision)-[:APPLIES_TO] edges, not just file structure.
requirements: list[dict[str, Any]] = field(default_factory=list)
decisions: list[dict[str, Any]] = field(default_factory=list)
@traces(spec="008", story="US-008-001", reqs=["REQ-008-102"])
def to_prompt(self, max_context_tokens: int = 4000) -> str:
"""Format context as a text block for volk prompts.
The output is structured to be unambiguous and force the LLM
to acknowledge existing code before generating new code.
Returns:
Formatted context string capped at MAX_CONTEXT_CHARS.
"""
parts: list[str] = [
"# KNOWLEDGE GRAPH CONTEXT — READ BEFORE WRITING CODE\n"
"The following entities and relationships are ALREADY in the "
"codebase. You MUST integrate with them, not duplicate them. "
"Reference specific class/function names from this context "
"in your response.\n"
]
parts.extend(self._format_files_section())
parts.extend(self._format_relationships_section())
parts.extend(self._format_tests_section())
parts.extend(self._format_recent_changes_section())
parts.extend(self._format_similar_patterns_section())
parts.extend(self._format_semantic_files_section())
parts.extend(self._format_requirements_section())
parts.extend(self._format_decisions_section())
result = "\n".join(parts)
if WordTokenCounter().count(result) > max_context_tokens:
result = result[: max_context_tokens * 4] # rough char estimate
return result
@traces(spec="008", story="US-008-001", reqs=["REQ-008-102"])
def _format_files_section(self) -> list[str]:
"""Format the existing-code-structure section (files → classes/funcs)."""
if not self.files:
return []
parts = ["## Existing Code Structure"]
for f in self.files[:10]:
path = f.get("path", "?")
parts.append(f"\n### {path}")
for cls in f.get("classes", []):
methods = ", ".join(cls.get("methods", [])[:8])
bases = cls.get("bases", [])
base_str = f"({', '.join(bases)})" if bases else ""
parts.append(f" class {cls['name']}{base_str}: {methods}")
for fn in f.get("functions", []):
parts.append(f" def {fn['name']}({fn.get('args', '')})")
return parts
# Ordered render config for typed relationship groups:
# (rel_type, heading, limit, line_formatter).
_REL_GROUPS = (
(
cs.REL_INHERITS,
"\n## Inheritance (class → base class)",
10,
lambda rel: f" {rel['from']} inherits from {rel['to']}",
),
(
cs.REL_CALLS,
"\n## Call Graph (caller → callee)",
15,
lambda rel: f" {rel['from']}() calls {rel['to']}()",
),
(
cs.REL_IMPORTS,
"\n## Import Dependencies (file → file)",
10,
lambda rel: f" {rel['from']} imports from {rel['to']}",
),
(
cs.REL_DEFINES,
"\n## Class Methods (class → method)",
15,
lambda rel: f" {rel['from']}.{rel['to']}()",
),
)
@traces(spec="008", story="US-008-001", reqs=["REQ-008-102"])
def _format_relationships_section(self) -> list[str]:
"""Format relationships grouped by type (inherits/calls/imports/...)."""
if not self.relationships:
return []
known = {t for t, _, _, _ in self._REL_GROUPS}
parts: list[str] = []
for rel_type, heading, limit, fmt in self._REL_GROUPS:
group = [r for r in self.relationships if r.get("type") == rel_type]
if group:
parts.append(heading)
parts.extend(fmt(rel) for rel in group[:limit])
others = [r for r in self.relationships if r.get("type") not in known]
if others:
parts.append("\n## Other Relationships")
parts.extend(
f" {rel.get('from', '?')} "
f"--[{rel.get('type', '?')}]--> "
f"{rel.get('to', '?')}"
for rel in others[:10]
)
return parts
@traces(spec="008", story="US-008-001", reqs=["REQ-008-102"])
def _format_tests_section(self) -> list[str]:
"""Format the covering-tests section."""
if not self.tests:
return []
parts = ["\n## Covering Tests (do NOT break these)"]
parts.extend(f" {t}" for t in self.tests[:10])
return parts
@traces(spec="008", story="US-008-001", reqs=["REQ-008-102"])
def _format_recent_changes_section(self) -> list[str]:
"""Format the recent-changes section."""
if not self.recent_changes:
return []
parts = ["\n## Recent Changes"]
for ch in self.recent_changes[:5]:
ts = ch.get("timestamp") or ""
author = ch.get("author") or ""
tail = ""
if ts and author:
tail = f" ({ts} by {author})"
elif ts:
tail = f" ({ts})"
elif author:
tail = f" (by {author})"
parts.append(f" {ch.get('file', '?')}: {ch.get('change', '?')}{tail}")
return parts
@traces(spec="008", story="US-008-001", reqs=["REQ-008-102"])
def _format_similar_patterns_section(self) -> list[str]:
"""Format the similar-patterns section (files sharing bases/protocols)."""
if not self.similar_patterns:
return []
parts = ["\n## Similar Patterns (analogous code to reference)"]
for sp in self.similar_patterns[:5]:
path = sp.get("path", "?")
shared = sp.get("shared_bases", [])
if shared:
parts.append(f" {path} — shares {', '.join(shared[:3])}")
else:
parts.append(f" {path}")
return parts
@traces(spec="008", story="US-008-001", reqs=["REQ-008-102"])
def _format_semantic_files_section(self) -> list[str]:
"""Format the semantic-search files section."""
if not self.semantic_files:
return []
parts = ["\n## Semantically related files"]
for sf in self.semantic_files[:5]:
path = sf.get("path", "?")
snippet = sf.get("snippet", "")
if snippet:
parts.append(f" {path}: {snippet}")
else:
parts.append(f" {path}")
return parts
@traces(spec="008", story="US-008-001", reqs=["REQ-008-102"])
def _format_requirements_section(self) -> list[str]:
"""Format the requirements the target code traces to (the 'why')."""
if not self.requirements:
return []
parts = ["\n## Requirements this code traces to (satisfy them)"]
for r in self.requirements[:20]:
line = f" {r.get('id', '?')}: {r.get('description', '')}"
specs = r.get("specs")
if specs:
line += f" [{', '.join(specs)}]"
distance = r.get("distance")
if distance is not None and distance > 0:
line += f" (distance {distance})"
parts.append(line.rstrip())
return parts
@traces(spec="008", story="US-008-001", reqs=["REQ-008-102"])
def _format_decisions_section(self) -> list[str]:
"""Format prior decisions that apply to the target code (respect them)."""
if not self.decisions:
return []
parts = ["\n## Prior decisions affecting this code (respect them)"]
for d in self.decisions[:10]:
reqs = d.get("requirements") or []
tail = f" [{', '.join(reqs)}]" if reqs else ""
parts.append(
f" {d.get('id', '?')}: {d.get('rationale', '')}{tail}".rstrip()
)
return parts
@dataclass
class ContextNode:
"""A node injected into Volk context (task-type-aware layer)."""
node_type: str
label: str
properties: dict[str, Any]
relationships: list[dict[str, str]] = field(default_factory=list)
source_file: str | None = None
line_number: int | None = None
@traces(spec="106", reqs=["REQ-106-042", "REQ-106-043"])
@dataclass
class VolkContext:
"""Complete task-type-aware context for a Volk task.
Built by :meth:`ContextInjector.inject_for_task` — the node-oriented
counterpart to :class:`GraphContext` (which drives the generic
``inject()`` prompt). Distinct from GraphContext because it's keyed by
a ``task_type`` and renders per-node markdown/dict output rather than a
single prompt block.
Attributes:
metadata: SPEC-106 FR-005 selector/packer measurement + degradation
info (REQ-106-042/REQ-106-043) — e.g. ``{"budget": ..., "spent":
..., "section_count": ..., "node_ids": [...]}`` on a normal
``inject_for_task`` call, or ``{"reason": ...}`` when degraded
(e.g. the KG is unavailable). Empty when neither applies.
"""
volk_alias: str
task_type: TaskType
nodes: list[ContextNode] = field(default_factory=list)
files: list[str] = field(default_factory=list)
specs: list[str] = field(default_factory=list)
requirements: list[str] = field(default_factory=list)
relationships: list[str] = field(default_factory=list)
summary: str = ""
metadata: dict[str, Any] = field(default_factory=dict)
@traces(spec="008", story="US-003", reqs=["REQ-008-013"])
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for injection."""
return {
"volk": self.volk_alias,
"task_type": self.task_type.value,
"files": self.files,
"specs": self.specs,
"requirements": self.requirements,
"relationships": self.relationships,
"node_count": len(self.nodes),
"summary": self.summary,
"nodes": [
{
"type": n.node_type,
"label": n.label,
"properties": n.properties,
"file": n.source_file,
"line": n.line_number,
}
for n in self.nodes
],
}
def to_markdown(self) -> str:
"""Convert to markdown for human-readable context."""
lines = [
f"# Context for {self.volk_alias}",
f"## Task: {self.task_type.value}\n",
]
lines.extend(self._markdown_bullet_section("Specifications", self.specs))
lines.extend(self._markdown_bullet_section("Requirements", self.requirements))
lines.extend(
self._markdown_bullet_section("Files", self.files, code_quoted=True)
)
lines.extend(self._markdown_nodes_section())
return "\n".join(lines)
@staticmethod
def _markdown_bullet_section(
heading: str, items: list[str], code_quoted: bool = False
) -> list[str]:
"""Render a simple bulleted markdown section, or nothing if empty."""
if not items:
return []
lines = [f"## {heading}"]
for item in items:
lines.append(f"- `{item}`" if code_quoted else f"- {item}")
lines.append("")
return lines
def _markdown_nodes_section(self) -> list[str]:
"""Render the Nodes section with per-node properties and relationships."""
lines = ["## Nodes"]
for node in self.nodes:
lines.append(f"### {node.label} ({node.node_type})")
for key, value in node.properties.items():
lines.append(f"- **{key}**: `{value}`")
if node.relationships:
lines.append("**Relationships:**")
for rel in node.relationships:
lines.append(f" - `{rel['type']}` → {rel['target']}")
lines.append("")
return lines
@traces(spec="008", story="US-003", reqs=["REQ-008-013"])
@traces(spec="106", reqs=["REQ-106-007"])
class ContextInjector:
"""Queries the graph and injects graph context into CXP packets.
Used by PipelineKernel before routing escalation packets.
Queries multi-hop relationships around target files and attaches
the result as payload['graph_context'].
Also hosts the task-type-aware layer (:meth:`inject_for_task`, merged
in W4.4 from the former standalone ``VolkContextInjector``) — pass
``task_type=`` to :meth:`inject` to additionally populate
``payload['task_context']`` with BUILD/REFACTOR/DEBUG/DOCUMENT/TEST/
REVIEW-specific graph queries.
Attributes:
repo: ``SyncGraphRepository`` backing this injector (backlog #163
phase 5b — was a raw ``neo4j.Driver`` before).
"""
@traces(spec="106", reqs=["REQ-106-063"])
def __init__(
self,
repo: SyncGraphRepository | None,
*,
relevance: "KgRelevanceRetriever | None" = None, # noqa: UP037
project_id: str | None = None,
pack_cache: "ContextPackCache | None" = None, # noqa: UP037
semantic_config: "SemanticGraphConfig | None" = None, # noqa: UP037
) -> None:
"""Initialize with a graph repository.
Args:
repo: ``SyncGraphRepository`` (backlog #163). If None, injection
is disabled.
relevance: Optional SPEC-106 FR-006 symbolic relevance retriever.
When ``None`` (the default — every pre-existing call site),
:meth:`inject_for_task` is byte-identical to the FR-003-only
path (REQ-106-063). When wired, it refines the selected
subgraph's ranking with Prolog-deduced relevance tiers.
project_id: Optional project scope for canonical ``File`` lookups.
When set, :meth:`_query_context` resolves target paths against
``f.path`` exactly and filters by ``f.project_id``; when
omitted, lookups are project-agnostic.
pack_cache: Optional cache for rendered context packs. When set,
repeated calls with the same snapshot_id/task/volk/subject/
payload-fingerprint reuse the rendered markdown.
semantic_config: Optional Memgraph semantic-graph config for the
semantic-search bridge. When omitted, the bridge uses
``SemanticGraphConfig.from_env()``.
"""
self._repo = repo
self._available = repo is not None
self._relevance = relevance
self._project_id = project_id
self._pack_cache = pack_cache
self._semantic_config = semantic_config
@property
def _connected_repo(self) -> SyncGraphRepository:
"""Type-narrowing accessor for the query helpers below.
Only reached after a ``self._available`` check already confirmed
``self._repo`` is not None, so this never actually raises in
practice — it exists so pyright can see a non-Optional repository
without re-guarding every query method (mirrors
``GraphSyncer._connected_repo``).
"""
if self._repo is None:
raise RuntimeError("Not connected — no graph repository.")
return self._repo
@traces(spec="008", story="US-003", reqs=["REQ-008-013"])
def should_inject(self, subject: str) -> bool:
"""Check if a CXP packet subject triggers injection.
Args:
subject: CXP packet subject string.
Returns:
True if context is injected.
"""
return self._available and subject in INJECT_SUBJECTS
@traces(spec="008", story="US-003", reqs=["REQ-008-013"])
def inject(
self,
payload: dict[str, Any],
*,
task_type: TaskType | None = None,
task_params: dict[str, Any] | None = None,
volk_alias: str = "",
expansion: ContextExpansion | None = None,
) -> dict[str, Any]:
"""Query the graph and add context to a CXP packet payload.
Extracts target files from the payload, queries the graph for
related entities, and adds a 'graph_context' key. When ``task_type``
is supplied, ALSO runs the task-type-aware layer (see
:meth:`inject_for_task`) and adds a 'task_context' key. Callers that
never pass ``task_type`` (the pre-W4.4 default) see zero behavioral
change.
Args:
payload: The CXP packet payload dict.
task_type: Optional task category (BUILD/REFACTOR/DEBUG/
DOCUMENT/TEST/REVIEW) driving the task-type-specific query
layer. ``None`` (default) skips it entirely.
task_params: Parameters for the task-type layer (e.g. ``specs``,
``file``, ``package``, ``feature``, ``error``); defaults to
``payload`` itself when omitted.
volk_alias: The volk this task context is being built for
(cosmetic — rendered into the task context output).
expansion: Optional expansion controls for the validator feedback
loop (larger limits, extra hops, TESTED_BY neighbours).
Returns:
The payload with graph_context (and, when ``task_type`` is
given, task_context) added.
"""
if not self._available:
return payload
target_files = self._extract_targets(payload)
if target_files:
try:
query = payload.get("text") or payload.get("intent") or ""
context = self._query_context(
target_files, query=query, expansion=expansion
)
payload["graph_context"] = context.to_prompt()
_logger.debug(
"Injected graph context: %d files, %d rels, %d tests",
len(context.files),
len(context.relationships),
len(context.tests),
)
except Exception as exc:
_logger.warning("Context injection failed: %s", exc)
payload["graph_context"] = ""
elif task_type is None:
return payload
if task_type is not None:
try:
volk_context = self.inject_for_task(
volk_alias=volk_alias,
task_type=task_type,
task_params=task_params if task_params is not None else payload,
)
payload["task_context"] = volk_context.to_dict()
except Exception as exc:
_logger.warning("Task-type context injection failed: %s", exc)
return payload
@traces(spec="008", story="US-003", reqs=["REQ-008-013"])
def _extract_targets(
self,
payload: dict[str, Any],
) -> list[str]:
"""Extract target file paths from a packet payload.
Args:
payload: CXP packet payload.
Returns:
List of file path strings to query context for.
"""
targets: list[str] = []
# From files_created (QA, CodeDirector payloads)
files_created = payload.get("files_created", [])
targets.extend(files_created)
# From project_dir (scan for .py files)
project_dir = payload.get("project_dir", "")
if project_dir:
import os
for root, dirs, files in os.walk(project_dir):
dirs[:] = [d for d in dirs if d not in {".venv", "__pycache__", ".git"}]
for f in files:
if f.endswith(".py") and not f.startswith("test_"):
targets.append(
os.path.relpath(os.path.join(root, f), project_dir)
)
if len(targets) > 30:
break
# From file_tree (Architect payloads)
file_tree = payload.get("file_tree", "")
if file_tree:
import re
for m in re.finditer(r"(\S+\.py)", file_tree):
fname = m.group(1).strip("`'\"")
if fname not in targets:
targets.append(fname)
return targets[:30]
@traces(spec="008", story="US-003", reqs=["REQ-008-005"])
def _query_context(
self,
target_files: list[str],
query: str = "",
expansion: ContextExpansion | None = None,
) -> GraphContext:
"""Query the graph for context around target files.
Performs multi-hop traversal:
1. Find File nodes matching targets
2. Get classes and functions defined in those files
3. Get relationships (imports, calls, inherits)
4. Get tests covering those functions
5. Get recent changes
6. Get similar patterns
7. Get semantically related files (when ``query`` is non-empty)
Args:
target_files: File paths to query context for.
query: Optional user/intent text for semantic chunk search.
expansion: Optional expansion controls for broader context.
Returns:
GraphContext with aggregated results.
"""
context = GraphContext()
repo = self._connected_repo
expansion = expansion or ContextExpansion()
limit_mult = max(1, expansion.limit_multiplier)
# 1. File entities with classes (including methods and bases) and functions.
# Resolve by canonical File.id first (project-qualified), then by
# exact path for legacy nodes. Project scoping is applied whenever
# the injector was configured with a project_id.
candidate_paths = target_files[: 10 * limit_mult]
self._query_exact_files(candidate_paths, context, limit_mult=limit_mult)
if not context.files:
return context
# 2. Relationships (imports, calls, inherits, has_method)
file_paths = [f["path"] for f in context.files if f.get("path")]
if file_paths:
result = repo.execute_read(
"MATCH (f:File)-[:DEFINES]->(src) "
"WHERE f.path IN $paths "
"OPTIONAL MATCH (src)-[r:IMPORTS|CALLS|INHERITS|DEFINES]->(tgt) "
"WHERE r IS NOT NULL "
"RETURN src.name AS from_name, "
" type(r) AS rel_type, "
" tgt.name AS to_name, "
" labels(tgt)[0] AS to_type "
f"LIMIT {50 * limit_mult}",
{"paths": file_paths},
)
for record in result:
if record["rel_type"]:
context.relationships.append(
{
"from": record["from_name"],
"type": record["rel_type"],
"to": record["to_name"],
"to_type": record.get("to_type", ""),
}
)
# 2b. Extra relationship hops for validator-loop expansion.
if expansion.extra_hops > 0:
result = repo.execute_read(
"MATCH (f:File)-[:DEFINES]->(src) "
"WHERE f.path IN $paths "
"OPTIONAL MATCH (src)-[:IMPORTS|CALLS|INHERITS|DEFINES*1.."
f"{expansion.extra_hops + 1}]->(tgt) "
"RETURN DISTINCT src.name AS from_name, "
" 'reaches' AS rel_type, "
" tgt.name AS to_name, "
" labels(tgt)[0] AS to_type "
f"LIMIT {30 * limit_mult}",
{"paths": file_paths},
)
for record in result:
if record["to_name"] and record["to_name"] not in {
r["to"] for r in context.relationships
}:
context.relationships.append(
{
"from": record["from_name"],
"type": record["rel_type"],
"to": record["to_name"],
"to_type": record.get("to_type", ""),
}
)
# 3. Covering tests
if file_paths:
result = repo.execute_read(
"MATCH (f:File)-[:DEFINES]->(fn:Function) "
"WHERE f.path IN $paths "
"OPTIONAL MATCH (t:TestResult)-[:COVERS]->(fn) "
"WHERE t IS NOT NULL "
"RETURN t.name AS test_name "
f"LIMIT {20 * limit_mult}",
{"paths": file_paths},
)
for record in result:
if record["test_name"]:
context.tests.append(record["test_name"])
# 3b. TESTED_BY neighbours for validator-loop expansion.
if expansion.include_tested_by:
result = repo.execute_read(
"MATCH (f:File)-[:DEFINES]->(fn:Function) "
"WHERE f.path IN $paths "
"OPTIONAL MATCH (fn)<-[:TESTED_BY]-(t:Test) "
"WHERE t IS NOT NULL "
"RETURN t.name AS test_name "
f"LIMIT {20 * limit_mult}",
{"paths": file_paths},
)
for record in result:
if record["test_name"]:
context.tests.append(record["test_name"])
# 4. Recent changes — target files and direct neighbours.
self._query_recent_changes(file_paths, context)
# 5. Similar patterns — files whose classes share bases/protocols.
self._query_similar_patterns(file_paths, context)
# 6. Semantic files — Memgraph chunks mapped to operational-KG File roots.
if query:
self._query_semantic_bridge(file_paths, query, context)
# 7. Semantic layer — the "why": requirements the target code traces
# to, and decisions applying to it (not just code structure).
self._query_semantic_context(
file_paths,
context,
max_depth=3 if expansion.include_requirement_closure else 2,
)
return context
@traces(spec="008", story="US-003", reqs=["REQ-008-005"])
def _query_exact_files(
self,
target_files: list[str],
context: GraphContext,
limit_mult: int = 1,
) -> set[str]:
"""Query ``File`` nodes by canonical ``f.id`` and exact ``f.path``.
When ``project_id`` is set, the canonical id has the shape
``"{project_id}:{path}"`` (per :data:`CANONICAL_MERGE_KEYS`). The query
also falls back to exact ``f.path`` matches within the same project so
legacy File nodes remain resolvable. Returns the set of matched file
paths; found files are appended to ``context.files``.
"""
repo = self._connected_repo
or_clauses: list[str] = []
params: dict[str, Any] = {}
if self._project_id is not None:
or_clauses.append("f.id IN $ids")
params["ids"] = [f"{self._project_id}:{p}" for p in target_files]
or_clauses.append("f.path IN $paths")
params["paths"] = target_files
where_clause = "(" + " OR ".join(or_clauses) + ")"
if self._project_id is not None:
where_clause += " AND f.project_id = $project_id"
params["project_id"] = self._project_id
result = repo.execute_read(
"MATCH (f:File) "
f"WHERE {where_clause} "
"OPTIONAL MATCH (f)-[:DEFINES]->(c:Class) "
"OPTIONAL MATCH (f)-[:DEFINES]->(fn:Function) "
"WHERE NOT EXISTS { MATCH (:Class)-[:DEFINES]->(fn) } "
"RETURN f.path AS path, f.line_count AS lines, "
" collect(DISTINCT {name: c.name, methods: c.methods, "
" bases: c.base_classes}) AS classes, "
" collect(DISTINCT {name: fn.name, "
" args: fn.signature}) AS functions "
f"LIMIT {50 * limit_mult}",
params,
)
matched: set[str] = set()
for record in result:
path = record["path"]
if path:
matched.add(path)
self._append_file_record(context, record)
return matched
@traces(spec="008", story="US-003", reqs=["REQ-008-005"])
def _append_file_record(
self, context: GraphContext, record: dict[str, Any]
) -> None:
"""Append a single Cypher file record to ``context.files``.
Avoids duplicating a file already present in the context.
"""
path = record.get("path")
if any(f.get("path") == path for f in context.files):
return
context.files.append(
{
"path": path,
"lines": record["lines"],
"classes": [
{
"name": c.get("name", ""),
"methods": c.get("methods", []) or [],
"bases": c.get("bases", []) or [],
}
for c in (record["classes"] or [])
if c.get("name")
],
"functions": [
{
"name": fn.get("name", ""),
"args": fn.get("args", ""),
}
for fn in (record["functions"] or [])
if fn.get("name")
],
}
)
@traces(spec="008", story="US-003", reqs=["REQ-008-013"])
def _query_recent_changes(
self,
file_paths: list[str],
context: GraphContext,
) -> None:
"""Populate ``context.recent_changes`` for target files and neighbours.
Tries three sources in order and stops at the first one that yields
results:
1. ``(:File)-[:CHANGED_IN]->(:Change)`` — explicit change records.
2. ``(:File)-[:NEXT_VERSION]->(:File)`` — version-chain diff metadata.
3. File node properties ``updated_at`` / ``synced_at`` — last sync.
Direct neighbours (files linked via imports/calls/inherits through a
shared symbol) are included so volks see changes in code they depend
on. Project scoping is applied when the injector was configured with a
``project_id``. Empty results leave the section omitted.
"""
if not file_paths:
return
from orchestrata_ultimate.knowledge_graph.canonical_schema import (
REL_CHANGED_IN,
REL_NEXT_VERSION,
)
repo = self._connected_repo
paths = file_paths[:10]
def _project_filter(prefix: str = "f") -> tuple[str, dict[str, Any]]:
extra: dict[str, Any] = {}
if self._project_id is None:
return "", extra
return f" AND {prefix}.project_id = $project_id", {
"project_id": self._project_id
}
def _neighbour_clause() -> str:
return (
"OPTIONAL MATCH (f)-[:DEFINES]->(sym) "
"OPTIONAL MATCH (sym)-[:IMPORTS|CALLS|INHERITS]->(other) "
"OPTIONAL MATCH (other)<-[:DEFINES]-(nf:File) "
"WITH collect(DISTINCT f) + collect(DISTINCT nf) AS files "
"UNWIND files AS f "
)
def _run_query(cypher: str, params: dict[str, Any]) -> list[dict[str, Any]]:
try:
return repo.execute_read(cypher, params)
except Exception as exc:
_logger.warning("Recent-changes query failed: %s", exc)
return []
# ── Source 1: explicit Change nodes ─────────────────────────────────
pf, pf_params = _project_filter()
params = {"paths": paths, **pf_params}
result = _run_query(
"MATCH (f:File) WHERE f.path IN $paths "
+ pf
+ " "
+ _neighbour_clause()
+ f"OPTIONAL MATCH (f)-[:{REL_CHANGED_IN}]->(ch:Change) "
"WITH f, ch WHERE ch IS NOT NULL "
"RETURN f.path AS path, "
" coalesce(ch.description, ch.change_type, 'modified') AS change, "
" coalesce(ch.timestamp, ch.created_at) AS ts, "
" coalesce(ch.author, 'unknown') AS author "
"ORDER BY ts DESC LIMIT 5",
params,
)
if result:
self._append_recent_changes(result, context)
return
# ── Source 2: NEXT_VERSION chain ────────────────────────────────────
result = _run_query(
"MATCH (f:File) WHERE f.path IN $paths "
+ pf
+ " "
+ _neighbour_clause()
+ f"OPTIONAL MATCH (f)-[:{REL_NEXT_VERSION}]->(next:File) "
"WITH f, next WHERE next IS NOT NULL "
"RETURN f.path AS path, "
" coalesce(next.change_summary, 'newer version available') AS change, "
" coalesce(next.created_at, next.updated_at, next.synced_at) AS ts, "
" coalesce(next.author, 'unknown') AS author "
"ORDER BY ts DESC LIMIT 5",
dict(params),
)
if result:
self._append_recent_changes(result, context)
return
# ── Source 3: file timestamp properties ─────────────────────────────
result = _run_query(
"MATCH (f:File) WHERE f.path IN $paths "
+ pf
+ " "
+ _neighbour_clause()
+ "RETURN f.path AS path, "
" 'file updated' AS change, "
" coalesce(f.updated_at, f.synced_at) AS ts, "
" coalesce(f.author, 'unknown') AS author "
"ORDER BY ts DESC LIMIT 5",
dict(params),
)
if result:
self._append_recent_changes(result, context)
@traces(spec="008", story="US-003", reqs=["REQ-008-103"])
def _append_recent_changes(
self,
records: list[dict[str, Any]],
context: GraphContext,
) -> None:
"""Append non-empty recent-change records to ``context``.
Filters out records with no usable timestamp and deduplicates by
file path, keeping the most recent entry per file.
"""
seen: set[str] = set()
for record in records:
path = record.get("path")
ts = record.get("ts")
if not path or ts is None:
continue
if path in seen:
continue
seen.add(path)
context.recent_changes.append(
{
"file": path,
"change": record.get("change") or "modified",
"timestamp": _format_change_ts(ts),
"author": record.get("author") or "unknown",
}
)
@traces(spec="008", story="US-003", reqs=["REQ-008-013"])
def _query_similar_patterns(
self,
file_paths: list[str],
context: GraphContext,
) -> None:
"""Populate ``context.similar_patterns`` with files whose classes share
base classes or implemented protocols with the target files.
The query gathers base/protocol names from the target files' classes
(via ``base_classes`` property and ``INHERITS``/``IMPLEMENTS``
relationships), then finds other ``File`` nodes whose classes share at
least one of those names. Results are ranked by the number of shared
bases/protocols and then by path similarity, capped at 5. Target files
themselves are excluded. Project scoping is applied when configured.
"""
if not file_paths:
return
from orchestrata_ultimate.knowledge_graph.canonical_schema import (
REL_IMPLEMENTS,
REL_INHERITS,
)
repo = self._connected_repo
paths = file_paths[:10]
pf: str = ""
params: dict[str, Any] = {"paths": paths}
if self._project_id is not None:
pf = " AND f.project_id = $project_id"
params["project_id"] = self._project_id
# Collect base/protocol names from target files.
result = repo.execute_read(
"MATCH (f:File)-[:DEFINES]->(c:Class) "
"WHERE f.path IN $paths " + pf + " "
"RETURN collect(DISTINCT c.base_classes) AS base_lists, "
" collect(DISTINCT c.name) AS class_names",
dict(params),
)
if not result:
return
bases: set[str] = set()
for lst in result[0].get("base_lists", []) or []:
if lst:
bases.update(str(b) for b in lst if b)
# Also gather explicit INHERITS/IMPLEMENTS targets.
result = repo.execute_read(
"MATCH (f:File)-[:DEFINES]->(c:Class)-[r:"
+ REL_INHERITS
+ "|"
+ REL_IMPLEMENTS
+ "]->(base) "
"WHERE f.path IN $paths " + pf + " "
"RETURN collect(DISTINCT base.name) AS explicit_bases",
dict(params),
)
if result:
for name in result[0].get("explicit_bases", []) or []:
if name:
bases.add(name)
if not bases:
return
# Find other files sharing those bases/protocols via explicit edges.
sim_params = {"bases": sorted(bases), **params}
other_pf = ""
if self._project_id is not None:
other_pf = " AND other.project_id = $project_id"
edge_query = (
"MATCH (other:File)-[:DEFINES]->(c:Class)-[:"
+ REL_INHERITS
+ "|"
+ REL_IMPLEMENTS
+ "]->(base) "
"WHERE NOT other.path IN $paths " + other_pf + " AND base.name IN $bases "
"RETURN other.path AS path, collect(DISTINCT base.name) AS shared_bases, "
" count(DISTINCT base) AS shared_count "
# ORDER BY the RETURNED ALIAS, not `other.path`: after an
# aggregation orchgraph/Memgraph only keeps the projected names
# in scope and answers "Unbound variable: other" (verified live
# 2026-07-26). Neo4j tolerates either, so the alias is portable.
"ORDER BY shared_count DESC, path "
"LIMIT 5"
)
edge_result = repo.execute_read(edge_query, sim_params)
# Also include files whose Class.base_classes property overlaps.
prop_query = (
"MATCH (other:File)-[:DEFINES]->(c:Class) "
"WHERE NOT other.path IN $paths " + other_pf + " "
# coalesce, because a Class with no `base_classes` property makes
# this a `UNWIND null`: Neo4j quietly yields zero rows, orchgraph/
# Memgraph raises "Argument of UNWIND must be a list, but 'null'
# was provided" and kills the whole similar-patterns section
# (verified live 2026-07-26). An empty list is the same answer on
# both engines.
"UNWIND coalesce(c.base_classes, []) AS base "
"WITH other, base "
"WHERE base IN $bases "
"RETURN other.path AS path, collect(DISTINCT base) AS shared_bases, "
" count(DISTINCT base) AS shared_count "
# ORDER BY the RETURNED ALIAS, not `other.path`: after an
# aggregation orchgraph/Memgraph only keeps the projected names
# in scope and answers "Unbound variable: other" (verified live
# 2026-07-26). Neo4j tolerates either, so the alias is portable.
"ORDER BY shared_count DESC, path "
"LIMIT 5"
)
prop_result = repo.execute_read(prop_query, sim_params)
# Merge and dedupe by path, preferring higher shared_count.
merged: dict[str, dict[str, Any]] = {}
for record in list(edge_result) + list(prop_result):
path = record.get("path")
if not path or path in paths:
continue
shared = set(record.get("shared_bases", []) or [])
if path in merged:
merged[path]["shared_bases"].update(shared)
else:
merged[path] = {"path": path, "shared_bases": shared}
if not merged:
return
sorted_patterns = sorted(
merged.values(),
key=lambda x: (-len(x["shared_bases"]), x["path"]),
)[:5]
for sp in sorted_patterns:
context.similar_patterns.append(
{
"path": sp["path"],
"shared_bases": sorted(sp["shared_bases"])[:3],
}
)
@traces(spec="107", reqs=["REQ-107-015"])
def _query_semantic_bridge(
self,
file_paths: list[str],
query: str,
context: GraphContext,
) -> None:
"""Populate ``context.semantic_files`` via the semantic-search bridge.
Queries Memgraph for chunks matching the user intent, maps their
``file_path`` values back to operational-KG ``File`` roots, and
appends the matched files (with a snippet) to ``context.semantic_files``.
Empty results or unreachable backends leave the section omitted.
"""
if not file_paths or not query:
return
from orchestrata_ultimate.knowledge_graph.semantic_bridge import (
search_semantic_roots,
)
try:
roots = search_semantic_roots(
query=query,
target_files=file_paths,
project_id=self._project_id,
config=self._semantic_config,
graph_repo=self._repo,
max_terms=6,
limit_per_term=5,
max_chunks_per_file=1,
)
for root in roots[:5]:
context.semantic_files.append(
{
"path": root.path,
"snippet": root.snippet(),
"line_count": root.line_count,
}
)
except Exception as exc:
_logger.debug("Semantic bridge query failed: %s", exc)
@traces(spec="008", story="US-003", reqs=["REQ-008-106", "REQ-008-107"])
def _query_semantic_context(
self,
file_paths: list[str],
context: GraphContext,
max_depth: int = 2,
) -> None:
"""Add the KG semantic layer to ``context``: requirements the target
code traces to (``(sym)-[:TRACES_TO]->(:RequirementItem)`` — the
as-built label the KG sync writer actually MERGEs, per
``session/kg_sync/_stage_specs_traces.py``) and decisions that apply
to it (``(:Decision)-[:APPLIES_TO]->(file|symbol)``).
When ``self._project_id`` is set, all queries are scoped to that
project so requirements/decisions from other projects do not leak
into the volk context.
Matched by symbol *name* (robust to the Class/CodeClass label
divergence) and file path. Best-effort — divergent id/description keys
are coalesced, and an empty result simply omits the section.
Args:
max_depth: Maximum depth for requirement closure traversal.
"""
repo = self._connected_repo
symbol_names = [
name
for f in context.files
for name in (
[c.get("name") for c in f.get("classes", [])]
+ [fn.get("name") for fn in f.get("functions", [])]
)
if name
]
if symbol_names:
from orchestrata_ultimate.knowledge_graph.canonical_schema import (
REL_TRACES_TO,
)
from orchestrata_ultimate.knowledge_graph.requirement_resolution import (
closure_requirement_chain,
)
project_filter = ""
params: dict[str, Any] = {"names": symbol_names}
if self._project_id is not None:
project_filter = " AND ri.project_id = $project_id"
params["project_id"] = self._project_id
result = repo.execute_read(
f"MATCH (sym)-[:{REL_TRACES_TO}]->(ri:RequirementItem) "
"WHERE sym.name IN $names " + project_filter + " "
"RETURN collect(DISTINCT ri.id) AS item_ids",
params,
)
item_ids = (
[i for i in (result[0].get("item_ids") or []) if i] if result else []
)
for chain in closure_requirement_chain(
item_ids, repo, project_id=self._project_id, max_depth=max_depth
):
req: dict[str, Any] = {
"id": chain.id,
"description": chain.description,
"distance": chain.distance,
}
if chain.specs:
req["specs"] = chain.specs
context.requirements.append(req)
if file_paths or symbol_names:
dec_params: dict[str, Any] = {"paths": file_paths, "names": symbol_names}
dec_filter = ""
if self._project_id is not None:
# Decisions apply to files/symbols that carry the project scope.
dec_filter = " AND x.project_id = $project_id"
dec_params["project_id"] = self._project_id
result = repo.execute_read(
"MATCH (d:Decision)-[:APPLIES_TO]->(x) "
"WHERE (x.path IN $paths OR x.name IN $names) " + dec_filter + " "
"OPTIONAL MATCH (d)-[:JUSTIFIED_BY]->(req:Requirement) "
"RETURN DISTINCT coalesce(d.id, d.decision_id) AS id, "
" coalesce(d.rationale, d.summary, d.title) AS rationale, "
" collect(DISTINCT coalesce(req.req_id, req.id)) AS reqs "
"LIMIT 10",
dec_params,
)
for record in result:
if record["id"]:
context.decisions.append(
{
"id": record["id"],
"rationale": record["rationale"] or "",
"requirements": [r for r in (record["reqs"] or []) if r],
}
)
# ── Task-type-aware layer (W4.4 merge of former VolkContextInjector;
# W8.5 re-wire onto the SPEC-106 selector/packer) ──────────────────────
# The six _inject_*_context handlers below are no longer called from
# inject_for_task (superseded by the canonical selector/packer) but are
# left in place, untouched — other tests/coverage still reference them
# directly, and retiring them is a later cleanup decision, not this
# wire-up's.
@traces(
spec="106",
reqs=[
"REQ-106-040",
"REQ-106-042",
"REQ-106-043",
"REQ-106-063",
"REQ-106-065",
],
)
def inject_for_task(
self,
volk_alias: str,
task_type: TaskType,
task_params: dict[str, Any],
) -> VolkContext:
"""Build task-type-aware context for a Volk task.
SPEC-106 FR-005 (REQ-106-040): builds a deterministic ``RequestFrame``
from ``task_params`` (``context_pack.analyze_request``), selects a
ranked, task-type-templated subgraph (``context_pack.select``), and
greedily packs it under a token budget (``context_pack.pack``) — no
LLM/embedding anywhere in selection (D11). The pack is then adapted
into this method's original return shape by node label: Module/
Class/Function sections also contribute their source file to
``context.files``; Spec sections to ``context.specs``; Requirement/
RequirementItem sections to ``context.requirements``; every section
becomes a :class:`ContextNode`. The rendered pack markdown
(``context_pack.render``) becomes ``context.summary``.
Also records one measurement (REQ-106-042) — task type, budget,
spent tokens, section count, and selected node ids — as both a
structured log line and ``context.metadata``.
Args:
volk_alias: The volk this context is being built for (cosmetic —
used only in the rendered markdown/dict output and as the
frame's ``requester``).
task_type: Which of the six task categories to select for.
task_params: Task-type-specific parameters (e.g. ``specs``,
``file``, ``package``, ``feature``, ``error``) — every value
is flattened into the frame's entity-linking text
(:func:`_task_params_text`).
Returns:
VolkContext with nodes/files/specs/requirements/relationships
populated from the packed subgraph, plus the rendered pack as
``summary``. Empty (but valid), with a reason in ``metadata``,
when the graph is unavailable (REQ-106-043).
"""
from orchestrata_ultimate.knowledge_graph.context_pack import (
analyze_request,
render,
select,
)
from orchestrata_ultimate.knowledge_graph.context_pack import (
pack as pack_subgraph,
)
from orchestrata_ultimate.knowledge_graph.context_pack.relevance import (
TIER_CORE,
TIER_PERIPHERAL,
TIER_RELATED,
rescore_by_tier,
)
context = VolkContext(volk_alias=volk_alias, task_type=task_type)
if not self._available:
context.metadata = {"reason": "Graph unavailable"}
return context
repo = self._connected_repo
frame = analyze_request(
{"text": _task_params_text(task_params)},
repo=repo,
task_type=task_type,
budget=DEFAULT_TASK_CONTEXT_BUDGET,
requester=volk_alias or "agent",
)
subgraph = select(frame, repo)
# SPEC-106 FR-006 (REQ-106-063): strictly opt-in. With no retriever
# wired, this branch never runs and `subgraph` is untouched — the
# FR-003-only ranking stays byte-identical to the pre-wiring path.
relevance_tiers: dict[str, str] | None = None
if self._relevance is not None:
relevance_tiers = self._relevance.refine(subgraph)
subgraph = rescore_by_tier(subgraph, relevance_tiers)
packed = pack_subgraph(subgraph, frame)
self._apply_context_pack(context, subgraph, packed)
cached_summary = (
self._pack_cache.get(packed, task_type=task_type, volk_alias=volk_alias)
if self._pack_cache is not None
else None
)
if cached_summary is not None:
context.summary = cached_summary
_logger.debug("Context pack cache hit: %s", packed.snapshot_id)
else:
rendered = render(packed)
context.summary = rendered
if self._pack_cache is not None:
self._pack_cache.put(
rendered,
packed,
task_type=task_type,
volk_alias=volk_alias,
)
context.metadata = {
"task_type": task_type.value,
"budget": packed.budget,
"spent": packed.spent,
"section_count": len(packed.sections),
"node_ids": [section.node_id for section in packed.sections],
}
if relevance_tiers is not None:
# REQ-106-065: per-tier node counts observable alongside the
# FR-005 measurement record.
context.metadata["relevance_tiers"] = {
TIER_CORE: sum(1 for t in relevance_tiers.values() if t == TIER_CORE),
TIER_RELATED: sum(
1 for t in relevance_tiers.values() if t == TIER_RELATED
),
TIER_PERIPHERAL: sum(
1 for t in relevance_tiers.values() if t == TIER_PERIPHERAL
),
}
_logger.info(
"inject_for_task measurement: task_type=%s budget=%d spent=%d "
"sections=%d node_ids=%s",
task_type.value,
packed.budget,
packed.spent,
len(packed.sections),
context.metadata["node_ids"],
)
return context
@traces(spec="106", reqs=["REQ-106-040"])
def _apply_context_pack(
self,
context: VolkContext,
subgraph: Any,
packed: Any,
) -> None:
"""Adapt a packed ``ContextPack`` into ``context``'s node-oriented shape.
Routes each packed section into VolkContext's fields BY THE SELECTED
NODE'S CANONICAL LABEL: Module/Class/Function sections also
contribute their source file (the relpath half of the canonical
``<relpath>::<qualname>`` id) to ``context.files``; Spec sections to
``context.specs``; Requirement/RequirementItem sections to
``context.requirements``. ``context.relationships`` records the two
per-node graph SIGNALS the selector surfaces (REQ-106-022) —
TESTED_BY presence and TRACES_TO degree — since the canonical
selector (unlike the six retired ad-hoc handlers) never returns Test
nodes themselves: TESTED_BY is ranking-only, not an expansion edge.
Args:
context: The VolkContext being built; mutated in place.
subgraph: The ``RankedSubgraph`` :func:`select` produced (for the
per-node ranking signals — properties, tested_by, trace
degree — that a packed ``PackSection`` alone doesn't carry).
packed: The ``ContextPack`` :func:`pack` produced.
"""
from orchestrata_ultimate.knowledge_graph.canonical_schema import (
LABEL_CLASS,
LABEL_FUNCTION,
LABEL_MODULE,
LABEL_REQUIREMENT,
LABEL_REQUIREMENT_ITEM,
LABEL_SPEC,
parse_code_entity_id,
)
code_labels = {LABEL_MODULE, LABEL_CLASS, LABEL_FUNCTION}
ranked_by_id = {node.node_id: node for node in subgraph.nodes}
seen_files: set[str] = set()
for section in packed.sections:
ranked = ranked_by_id.get(section.node_id)
properties = dict(ranked.properties) if ranked is not None else {}
context.nodes.append(
ContextNode(
node_type=section.label,
label=section.node_id,
properties=properties,
)
)
if section.label in code_labels:
try:
relpath, _ = parse_code_entity_id(section.node_id)
except ValueError:
relpath = section.node_id
if relpath not in seen_files:
seen_files.add(relpath)
context.files.append(relpath)
elif section.label == LABEL_SPEC:
context.specs.append(section.node_id)
elif section.label in (LABEL_REQUIREMENT, LABEL_REQUIREMENT_ITEM):
context.requirements.append(section.node_id)
if ranked is not None:
if ranked.tested_by:
context.relationships.append(f"{section.node_id} TESTED_BY")
if ranked.trace_degree > 0:
context.relationships.append(
f"{section.node_id} TRACES_TO x{ranked.trace_degree}"
)