orchestrata_ultimate.semantic_graph.spec_trace_enricher
Reference › orchestrata_ultimate › semantic_graph › spec_trace_enricher
Enriches the shared semantic graph (memgraph) with spec, story, requirement, code and test entities, so GraphRAG can answer questions such as which code implements a given requirement or what the requirements for the CXP protocol are. SpecTraceEnricher takes the parsed specs and traces from spec_trace_parser, a project id, and optionally test traces and a test-module map. Its _build step creates the entities and relationships in memory: spec, story and requirement entities, code elements (with stable ids from _code_element_id and labels from _code_element_label), relationships from code to the requirements it implements, TestCase entities with TESTS edges from traced tests, and TestModule entities with TESTS edges from a path heuristic. The entities, relationships and summary properties expose the result as Entity and Relationship models and an EnrichmentSummary of counts. enrich writes everything to a SemanticGraph backend, and the module function enrich_project_with_specs_and_traces runs the whole parse, build and write sequence for a project. The writes use MERGE, so re-running is idempotent. The enrich_semantic_graph_specs script and the package facade import it.
At a glance
| Source | src/orchestrata_ultimate/semantic_graph/spec_trace_enricher.py |
| Lines of code | 455 |
| Classes | 2 |
| Functions | 3 |
| Imports (internal) | orchestrata_ultimate.core.traceability, orchestrata_ultimate.semantic_graph.interfaces, orchestrata_ultimate.semantic_graph.models, orchestrata_ultimate.semantic_graph.spec_trace_parser |
| Imported by | orchestrata_ultimate.semantic_graph, scripts.enrich_semantic_graph_specs |
| Tests that import it | tests/semantic_graph/test_spec_trace_enrichment.py |
Class diagram
Dependency diagram
API
Classes
EnrichmentSummary (dataclass)
Decorators: dataclass · Line 29
Result of a spec/trace enrichment run.
| Attribute | Type | Default |
|---|---|---|
specs |
int |
0 |
stories |
int |
0 |
requirements |
int |
0 |
code_elements |
int |
0 |
test_cases |
int |
0 |
test_modules |
int |
0 |
relationships |
int |
0 |
| Method | Signature | Summary |
|---|---|---|
to_dict |
to_dict() -> dict[str, int] |
SpecTraceEnricher
Line 73
Build entities and relationships from specs and code traces.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(specs: list[SpecInfo], traces: list[TraceInfo], project_id: str, test_traces: list[TraceInfo] | None = None, test_module_map: dict[str, list[Path]] | None = None) -> None |
Initialize the enricher. |
_add_entity |
_add_entity(entity: Entity) -> None |
|
_add_relationship |
_add_relationship(relationship: Relationship) -> None |
|
_build |
_build() -> None |
Construct all entities and relationships. |
_build_spec_entities |
_build_spec_entities() -> None |
|
_build_story_entities |
_build_story_entities() -> None |
|
_build_requirement_entities |
_build_requirement_entities() -> None |
|
_build_code_entities |
_build_code_entities() -> None |
|
_build_trace_relationships |
_build_trace_relationships() -> None |
Link code elements to the requirements they implement. |
_build_test_trace_entities |
_build_test_trace_entities() -> None |
Build TestCase entities + TESTS edges from @traces'd tests. |
_build_test_module_entities |
_build_test_module_entities() -> None |
Build TestModule entities + TESTS edges via path heuristic. |
entities property |
entities() -> list[Entity] |
All entities built by the enricher. |
relationships property |
relationships() -> list[Relationship] |
All relationships built by the enricher. |
summary property |
summary() -> EnrichmentSummary |
Summary of the enrichment graph. |
enrich |
async enrich(graph: SemanticGraph) -> EnrichmentSummary |
Write the spec/trace graph to the backend. |
Functions
| Function | Signature | Summary |
|---|---|---|
_code_element_id |
_code_element_id(project_id: str, trace: TraceInfo) -> str |
Return a stable entity id for a code element referenced by a trace. |
_code_element_label |
_code_element_label(trace: TraceInfo) -> str |
Return a human-readable label for a code element. |
enrich_project_with_specs_and_traces |
async enrich_project_with_specs_and_traces(graph: SemanticGraph, specs_root: Path, source_root: Path, project_id: str, tests_root: Path | None = None, with_tests: bool = True) -> EnrichmentSummary |
High-level helper: parse specs/traces and write them to the graph. |
Source
"""Enrich the semantic graph with spec, story, requirement, and trace entities.
The enricher converts parsed specs and @traces annotations into graph nodes and
edges. This lets GraphRAG answer questions such as "which code implements
REQ-004-US-001-001?" or "what are the requirements for the CXP protocol?".
"""
from __future__ import annotations
import logging
from dataclasses import dataclass
from pathlib import Path
from orchestrata_ultimate.core.traceability import TraceInfo
from orchestrata_ultimate.semantic_graph.interfaces import SemanticGraph
from orchestrata_ultimate.semantic_graph.models import Entity, Relationship
from orchestrata_ultimate.semantic_graph.spec_trace_parser import (
SpecInfo,
collect_code_traces,
collect_test_traces,
map_test_modules_to_source,
parse_all_specs,
)
logger = logging.getLogger(__name__)
@dataclass
class EnrichmentSummary:
"""Result of a spec/trace enrichment run."""
specs: int = 0
stories: int = 0
requirements: int = 0
code_elements: int = 0
test_cases: int = 0
test_modules: int = 0
relationships: int = 0
def to_dict(self) -> dict[str, int]:
return {
"specs": self.specs,
"stories": self.stories,
"requirements": self.requirements,
"code_elements": self.code_elements,
"test_cases": self.test_cases,
"test_modules": self.test_modules,
"relationships": self.relationships,
}
def _code_element_id(project_id: str, trace: TraceInfo) -> str:
"""Return a stable entity id for a code element referenced by a trace."""
parts = [project_id, "code"]
if trace.module:
parts.append(trace.module)
if trace.class_name:
parts.append(trace.class_name)
if trace.method:
parts.append(trace.method)
return ":".join(parts)
def _code_element_label(trace: TraceInfo) -> str:
"""Return a human-readable label for a code element."""
if trace.class_name and trace.method:
return f"{trace.class_name}.{trace.method}"
if trace.class_name:
return trace.class_name
return trace.method or trace.module or "unknown"
class SpecTraceEnricher:
"""Build entities and relationships from specs and code traces."""
def __init__(
self,
specs: list[SpecInfo],
traces: list[TraceInfo],
project_id: str,
test_traces: list[TraceInfo] | None = None,
test_module_map: dict[str, list[Path]] | None = None,
) -> None:
"""Initialize the enricher.
Args:
specs: Parsed specifications.
traces: Collected @traces annotations (source tree).
project_id: Project tag written into every entity.
test_traces: Collected @traces annotations from the tests tree
(audit item 5 -- TESTS edges). Each becomes a ``TestCase``
entity linked to the requirements/code it traces.
test_module_map: Heuristic ``source module -> [test file]``
mapping (see
:func:`spec_trace_parser.map_test_modules_to_source`). Each
mapped test file becomes a ``TestModule`` entity linked to
the ``CodeElement``s of that source module.
"""
self._specs = specs
self._traces = traces
self._project_id = project_id
self._test_traces = test_traces or []
self._test_module_map = test_module_map or {}
self._entities: list[Entity] = []
self._relationships: list[Relationship] = []
self._req_to_code_ids: dict[str, set[str]] = {}
self._summary = EnrichmentSummary()
self._build()
def _add_entity(self, entity: Entity) -> None:
self._entities.append(entity)
def _add_relationship(self, relationship: Relationship) -> None:
self._relationships.append(relationship)
def _build(self) -> None:
"""Construct all entities and relationships."""
self._build_spec_entities()
self._build_story_entities()
self._build_requirement_entities()
self._build_code_entities()
self._build_trace_relationships()
if self._test_traces:
self._build_test_trace_entities()
if self._test_module_map:
self._build_test_module_entities()
self._summary.specs = len(self._specs)
self._summary.stories = sum(len(s.stories) for s in self._specs)
self._summary.requirements = sum(len(s.requirements) for s in self._specs)
self._summary.relationships = len(self._relationships)
def _build_spec_entities(self) -> None:
for spec in self._specs:
entity_id = f"{self._project_id}:spec:{spec.spec_id}"
self._add_entity(
Entity(
id=entity_id,
labels=["Spec"],
properties={
"project_id": self._project_id,
"spec_id": spec.spec_id,
"slug": spec.slug,
"title": spec.title,
"status": spec.status,
"priority": spec.priority,
"author": spec.author,
"version": spec.version,
"components": spec.components,
},
)
)
def _build_story_entities(self) -> None:
for spec in self._specs:
spec_entity_id = f"{self._project_id}:spec:{spec.spec_id}"
for story in spec.stories:
entity_id = f"{self._project_id}:story:{spec.spec_id}:{story.story_id}"
self._add_entity(
Entity(
id=entity_id,
labels=["Story"],
properties={
"project_id": self._project_id,
"spec_id": spec.spec_id,
"story_id": story.story_id,
"title": story.title,
"role": story.role,
"want": story.want,
"so_that": story.so_that,
},
)
)
self._add_relationship(
Relationship(
id=f"{spec_entity_id}:HAS_STORY:{entity_id}",
type="HAS_STORY",
source_id=spec_entity_id,
target_id=entity_id,
properties={"project_id": self._project_id},
)
)
def _build_requirement_entities(self) -> None:
for spec in self._specs:
for req in spec.requirements:
story_entity_id = (
f"{self._project_id}:story:{spec.spec_id}:{req.story_id}"
)
req_entity_id = f"{self._project_id}:req:{req.req_id}"
self._add_entity(
Entity(
id=req_entity_id,
labels=["Requirement"],
properties={
"project_id": self._project_id,
"spec_id": spec.spec_id,
"story_id": req.story_id,
"req_id": req.req_id,
"title": req.title,
"checked": req.checked,
},
)
)
self._add_relationship(
Relationship(
id=f"{story_entity_id}:HAS_REQUIREMENT:{req_entity_id}",
type="HAS_REQUIREMENT",
source_id=story_entity_id,
target_id=req_entity_id,
properties={"project_id": self._project_id},
)
)
def _build_code_entities(self) -> None:
seen: set[str] = set()
for trace in self._traces:
entity_id = _code_element_id(self._project_id, trace)
if entity_id in seen:
continue
seen.add(entity_id)
self._add_entity(
Entity(
id=entity_id,
labels=["CodeElement"],
properties={
"project_id": self._project_id,
"module": trace.module,
"class_name": trace.class_name,
"method": trace.method,
"file_path": trace.file_path,
"line_number": trace.line_number,
"label": _code_element_label(trace),
},
)
)
self._summary.code_elements = len(seen)
def _build_trace_relationships(self) -> None:
"""Link code elements to the requirements they implement."""
valid_reqs = {r.req_id for spec in self._specs for r in spec.requirements}
for trace in self._traces:
code_id = _code_element_id(self._project_id, trace)
for req_id in trace.reqs:
if req_id not in valid_reqs:
continue
req_entity_id = f"{self._project_id}:req:{req_id}"
self._req_to_code_ids.setdefault(req_id, set()).add(code_id)
self._add_relationship(
Relationship(
id=f"{code_id}:IMPLEMENTS:{req_entity_id}",
type="IMPLEMENTS",
source_id=code_id,
target_id=req_entity_id,
properties={
"project_id": self._project_id,
"file_path": trace.file_path,
"line_number": trace.line_number,
},
)
)
def _build_test_trace_entities(self) -> None:
"""Build ``TestCase`` entities + ``TESTS`` edges from @traces'd tests.
Each traced test function/method becomes a ``TestCase`` entity linked
by a ``TESTS`` edge to every Requirement it references, plus (since
those requirements already tell us which ``CodeElement``s implement
them) a direct ``TESTS`` edge to those same code elements.
"""
valid_reqs = {r.req_id for spec in self._specs for r in spec.requirements}
seen: set[str] = set()
for trace in self._test_traces:
name = (
f"{trace.class_name}.{trace.method}"
if trace.class_name
else trace.method
)
if not name:
continue
test_entity_id = f"{self._project_id}:test:{trace.module}:{name}"
if test_entity_id not in seen:
seen.add(test_entity_id)
self._add_entity(
Entity(
id=test_entity_id,
labels=["TestCase"],
properties={
"project_id": self._project_id,
"module": trace.module,
"name": name,
"file_path": trace.file_path,
"line_number": trace.line_number,
},
)
)
for req_id in trace.reqs:
if req_id not in valid_reqs:
continue
req_entity_id = f"{self._project_id}:req:{req_id}"
self._add_relationship(
Relationship(
id=f"{test_entity_id}:TESTS:{req_entity_id}",
type="TESTS",
source_id=test_entity_id,
target_id=req_entity_id,
properties={
"project_id": self._project_id,
"file_path": trace.file_path,
"line_number": trace.line_number,
},
)
)
for code_id in self._req_to_code_ids.get(req_id, ()):
self._add_relationship(
Relationship(
id=f"{test_entity_id}:TESTS:{code_id}",
type="TESTS",
source_id=test_entity_id,
target_id=code_id,
properties={"project_id": self._project_id},
)
)
self._summary.test_cases = len(seen)
def _build_test_module_entities(self) -> None:
"""Build ``TestModule`` entities + ``TESTS`` edges via path heuristic.
Covers test files with no @traces coverage: a ``TestModule`` entity
per heuristically-mapped test file is linked by ``TESTS`` edges to
every ``CodeElement`` already known for that source module.
"""
module_to_code_ids: dict[str, set[str]] = {}
for trace in self._traces:
code_id = _code_element_id(self._project_id, trace)
module_to_code_ids.setdefault(trace.module, set()).add(code_id)
seen: set[str] = set()
for source_module, test_files in self._test_module_map.items():
code_ids = module_to_code_ids.get(source_module)
if not code_ids:
continue
for test_file in test_files:
module_entity_id = (
f"{self._project_id}:testmodule:{test_file.as_posix()}"
)
if module_entity_id not in seen:
seen.add(module_entity_id)
self._add_entity(
Entity(
id=module_entity_id,
labels=["TestModule"],
properties={
"project_id": self._project_id,
"file_path": test_file.as_posix(),
"source_module": source_module,
},
)
)
for code_id in code_ids:
self._add_relationship(
Relationship(
id=f"{module_entity_id}:TESTS:{code_id}",
type="TESTS",
source_id=module_entity_id,
target_id=code_id,
properties={"project_id": self._project_id},
)
)
self._summary.test_modules = len(seen)
@property
def entities(self) -> list[Entity]:
"""All entities built by the enricher."""
return self._entities
@property
def relationships(self) -> list[Relationship]:
"""All relationships built by the enricher."""
return self._relationships
@property
def summary(self) -> EnrichmentSummary:
"""Summary of the enrichment graph."""
return self._summary
async def enrich(self, graph: SemanticGraph) -> EnrichmentSummary:
"""Write the spec/trace graph to the backend.
Args:
graph: An already-connected semantic graph.
Returns:
:class:`EnrichmentSummary` with counts.
"""
if self._entities:
await graph.upsert_entities(self._entities)
if self._relationships:
await graph.upsert_relationships(self._relationships)
logger.info(
"Enriched semantic graph for %s: %s",
self._project_id,
self._summary.to_dict(),
)
return self._summary
async def enrich_project_with_specs_and_traces(
graph: SemanticGraph,
specs_root: Path,
source_root: Path,
project_id: str,
tests_root: Path | None = None,
with_tests: bool = True,
) -> EnrichmentSummary:
"""High-level helper: parse specs/traces and write them to the graph.
Args:
graph: Connected semantic graph.
specs_root: Root of the specs tree.
source_root: Root of the source tree.
project_id: Project tag.
tests_root: Root of the tests tree (e.g. ``Path("tests")``). Required
for TESTS-edge enrichment (audit item 5); ignored if
``with_tests`` is ``False``.
with_tests: Whether to scan ``tests_root`` for @traces'd test
functions/classes and heuristic test-module mappings and emit
``TestCase``/``TestModule`` entities + ``TESTS`` edges. Defaults
to ``True``.
Returns:
:class:`EnrichmentSummary`.
"""
specs = parse_all_specs(specs_root)
traces = collect_code_traces(source_root)
test_traces: list[TraceInfo] = []
test_module_map: dict[str, list[Path]] = {}
if with_tests and tests_root is not None and tests_root.is_dir():
test_traces = collect_test_traces(tests_root)
known_modules = {t.module for t in traces}
test_module_map = map_test_modules_to_source(tests_root, known_modules)
enricher = SpecTraceEnricher(
specs,
traces,
project_id,
test_traces=test_traces,
test_module_map=test_module_map,
)
return await enricher.enrich(graph)