orchestrata_ultimate.knowledge_graph.requirement_resolution
Reference › orchestrata_ultimate › knowledge_graph › requirement_resolution
Canonical requirement identity resolution for the operational graph (orchkg on orchgraph). The graph models requirements as a chain, (:Spec)-[:DEFINES_REQUIREMENT]->(:Requirement)-[:HAS_ITEM]->(:RequirementItem), and code symbols trace to RequirementItem nodes. This module resolves those items back to their parent requirement and owning spec so every consumer sees one logical record. The frozen dataclass RequirementChain holds the unified id, a description, the owning spec ids, the item ids and a graph distance. resolve_requirement_chain takes seed item ids, resolve_requirements_for_symbol starts from a symbol name, and closure_requirement_chain expands outward to a bounded depth (default 2) and result count (default 20). All three read through a synchronous graph repository and accept an optional project id; _canonical_id and _canonical_description normalise whichever record shape comes back. They only read. context_injector and some tools import them; label and relationship names come from canonical_schema.
At a glance
| Source | src/orchestrata_ultimate/knowledge_graph/requirement_resolution.py |
| Lines of code | 262 |
| Classes | 1 |
| Functions | 5 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.core.graph.interfaces, orchestrata_ultimate.knowledge_graph.canonical_schema |
| Imported by | orchestrata_ultimate.knowledge_graph.context_injector, orchestrata_ultimate.tools.context_tools |
| Tests that import it | tests/knowledge_graph/test_requirement_resolution.py, tests/knowledge_graph/test_requirement_resolution_coverage.py |
Class diagram
Dependency diagram
API
Classes
RequirementChain (dataclass)
Decorators: dataclass(frozen=True) · Line 37
Unified Requirement + RequirementItem + Spec record.
| Attribute | Type | Default |
|---|---|---|
id |
str |
|
description |
str |
|
specs |
list[str] |
field(default_factory=list) |
item_ids |
list[str] |
field(default_factory=list) |
distance |
int |
0 |
Functions
| Function | Signature | Summary |
|---|---|---|
resolve_requirement_chain traces |
resolve_requirement_chain(seed_item_ids: list[str], repo: SyncGraphRepository, project_id: str | None = None) -> list[RequirementChain] |
Resolve RequirementItem ids to unified RequirementChain records. |
resolve_requirements_for_symbol traces |
resolve_requirements_for_symbol(symbol_name: str, repo: SyncGraphRepository, project_id: str | None = None) -> list[RequirementChain] |
Resolve requirements traced to a code symbol by name. |
closure_requirement_chain traces |
closure_requirement_chain(seed_item_ids: list[str], repo: SyncGraphRepository, project_id: str | None = None, max_depth: int = 2, max_results: int = 20) -> list[RequirementChain] |
Walk the requirement hierarchy around the seed items. |
_canonical_id |
_canonical_id(record: dict, label: str) -> str | None |
Extract the canonical id from a requirement-ish record. |
_canonical_description |
_canonical_description(record: dict) -> str |
Extract the most human-readable description available. |
Source
"""Canonical requirement identity resolution.
Unifies ``Requirement`` and ``RequirementItem`` so every KG consumer treats
them as one logical chain:
(:Spec)-[:DEFINES_REQUIREMENT]->(:Requirement)-[:HAS_ITEM]->(:RequirementItem)
^ ^
| |
unified RequirementChain id item_ids
Code symbols trace to ``RequirementItem`` nodes. The helpers in this module
resolve those items to their parent ``Requirement`` (and owning ``Spec``),
returning a stable, canonical record regardless of whether the seed was an
item id or a requirement id.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.knowledge_graph.canonical_schema import (
LABEL_REQUIREMENT,
LABEL_REQUIREMENT_ITEM,
REL_DEFINES_REQUIREMENT,
REL_HAS_ITEM,
REL_REQUIRES,
REL_TRACES_TO,
)
if TYPE_CHECKING:
from orchestrata_ultimate.core.graph.interfaces import SyncGraphRepository
@dataclass(frozen=True)
class RequirementChain:
"""Unified Requirement + RequirementItem + Spec record.
Attributes:
id: Canonical requirement id. If the resolved record has a parent
``Requirement``, this is the parent's id; otherwise it falls back
to the ``RequirementItem`` id.
description: Human-readable requirement text, coalesced from
``title``, ``description``, ``text``, or ``name`` properties.
specs: List of owning Spec ids/natural ids.
item_ids: The RequirementItem ids that trace to this requirement.
distance: Hop distance from the seed (0 = direct trace target).
"""
id: str
description: str
specs: list[str] = field(default_factory=list)
item_ids: list[str] = field(default_factory=list)
distance: int = 0
@traces(spec="106", reqs=["REQ-106-001"])
def resolve_requirement_chain(
seed_item_ids: list[str],
repo: SyncGraphRepository,
project_id: str | None = None,
) -> list[RequirementChain]:
"""Resolve RequirementItem ids to unified RequirementChain records.
For each seed item id, the query walks ``(:RequirementItem)<-[:HAS_ITEM]-
(:Requirement)<-[:DEFINES_REQUIREMENT]-(:Spec)``. If no parent Requirement
exists, the item itself is returned as a single-element chain.
Args:
seed_item_ids: RequirementItem ids to resolve.
repo: Operational graph repository.
project_id: Optional project scope filter.
Returns:
A deduplicated list of :class:`RequirementChain` records, one per
canonical requirement id.
"""
if not seed_item_ids:
return []
params: dict = {"item_ids": seed_item_ids}
project_filter = ""
if project_id is not None:
project_filter = " AND ri.project_id = $project_id"
params["project_id"] = project_id
rows = repo.execute_read(
"MATCH (ri:RequirementItem) WHERE ri.id IN $item_ids " + project_filter + " "
f"OPTIONAL MATCH (ri)<-[:{REL_HAS_ITEM}]-(r:Requirement) "
f"OPTIONAL MATCH (r)<-[:{REL_DEFINES_REQUIREMENT}]-(s:Spec) "
"RETURN coalesce(r.id, ri.id) AS id, "
" coalesce(r.title, r.description, r.id, "
" ri.text, ri.description, ri.name, ri.id) AS description, "
" collect(DISTINCT coalesce(s.id, s.natural_id)) AS specs, "
" collect(DISTINCT ri.id) AS item_ids",
params,
)
# The query aggregates per canonical id (implicit GROUP BY on the
# non-aggregate RETURN expressions ``id``/``description``) and
# ``Requirement.id`` carries a UNIQUE constraint, so it emits exactly one
# row per canonical requirement id. No row-level merge is needed — and no
# merge could ever fire, since two rows sharing a canonical id would also
# share its (deterministically-derived) description and thus collapse into
# one row before reaching this loop.
seen: dict[str, RequirementChain] = {}
for row in rows:
req_id = row.get("id")
if not req_id:
continue
seen[req_id] = RequirementChain(
id=req_id,
description=row.get("description") or "",
specs=[s for s in (row.get("specs") or []) if s],
item_ids=[i for i in (row.get("item_ids") or []) if i],
)
return list(seen.values())
@traces(spec="106", reqs=["REQ-106-001"])
def resolve_requirements_for_symbol(
symbol_name: str,
repo: SyncGraphRepository,
project_id: str | None = None,
) -> list[RequirementChain]:
"""Resolve requirements traced to a code symbol by name.
Matches ``(sym)-[:TRACES_TO]->(:RequirementItem)`` and delegates to
:func:`resolve_requirement_chain` for unification with parent Requirements.
Args:
symbol_name: Class/function name to look up.
repo: Operational graph repository.
project_id: Optional project scope filter.
Returns:
Unified requirement chains for the symbol.
"""
params: dict = {"name": symbol_name}
project_filter = ""
if project_id is not None:
project_filter = " AND ri.project_id = $project_id"
params["project_id"] = project_id
# One row per traced item (no aggregation): a symbol with no TRACES_TO
# edges yields zero rows, so the ``if not rows`` guard below is the real
# no-match path — not a dead defensive branch.
rows = repo.execute_read(
f"MATCH (sym)-[:{REL_TRACES_TO}]->(ri:RequirementItem) "
"WHERE sym.name = $name " + project_filter + " "
"RETURN DISTINCT ri.id AS item_id",
params,
)
if not rows:
return []
item_ids = [str(item_id) for r in rows if (item_id := r.get("item_id"))]
return resolve_requirement_chain(item_ids, repo, project_id=project_id)
@traces(spec="106", reqs=["REQ-106-001"])
def closure_requirement_chain(
seed_item_ids: list[str],
repo: SyncGraphRepository,
project_id: str | None = None,
max_depth: int = 2,
max_results: int = 20,
) -> list[RequirementChain]:
"""Walk the requirement hierarchy around the seed items.
Starting from ``RequirementItem`` ids, the closure collects:
* distance 0 — the seed items themselves (unified to their parent
``Requirement`` when one exists).
* distance 1+ — ``Requirement`` nodes linked by ``REQUIRES`` up to
``max_depth``.
Results are deduplicated by canonical requirement id and capped at
``max_results``. The ``distance`` field records the shortest hop count
from any seed.
Args:
seed_item_ids: RequirementItem ids that are the starting points.
repo: Operational graph repository.
project_id: Optional project scope filter.
max_depth: Maximum relationship hops to walk from the seeds.
max_results: Hard cap on returned chains.
Returns:
A list of :class:`RequirementChain` records, shortest distance first.
"""
if not seed_item_ids:
return []
# Phase 1: resolve seed items to their parent Requirements (distance 0).
direct = resolve_requirement_chain(seed_item_ids, repo, project_id=project_id)
seen: dict[str, RequirementChain] = {}
for chain in direct:
seen[chain.id] = RequirementChain(
id=chain.id,
description=chain.description,
specs=chain.specs,
item_ids=chain.item_ids,
distance=0,
)
# Phase 2: walk REQUIRES edges from the direct Requirements.
if not seen:
return []
root_ids = list(seen)
params: dict = {"root_ids": root_ids}
project_filter = ""
if project_id is not None:
project_filter = " AND r.project_id = $project_id"
params["project_id"] = project_id
# Variable-length path bounds must be literals, so interpolate max_depth.
rows = repo.execute_read(
"MATCH (r:Requirement) WHERE r.id IN $root_ids " + project_filter + " "
f"OPTIONAL MATCH path = (r)-[:{REL_REQUIRES}*1..{max_depth}]->(dep:Requirement) "
"RETURN dep.id AS id, dep.title AS title, dep.description AS description, "
" length(path) AS hops",
params,
)
for row in rows:
dep_id = row.get("id")
if not dep_id or dep_id in seen:
continue
description = row.get("title") or row.get("description") or dep_id or ""
hops = row.get("hops") or 1
seen[dep_id] = RequirementChain(
id=dep_id,
description=str(description),
distance=hops,
)
return sorted(seen.values(), key=lambda c: (c.distance, c.id))[:max_results]
def _canonical_id(record: dict, label: str) -> str | None:
"""Extract the canonical id from a requirement-ish record.
Handles both the canonical ``id`` property and the legacy ``req_id`` /
``item_id`` properties that some writers still emit.
"""
if label == LABEL_REQUIREMENT:
return record.get("id") or record.get("req_id")
if label == LABEL_REQUIREMENT_ITEM:
return record.get("id") or record.get("item_id")
return record.get("id")
def _canonical_description(record: dict) -> str:
"""Extract the most human-readable description available."""
for key in ("title", "description", "text", "name", "statement"):
value = record.get(key)
if value:
return str(value)
return ""