Source code for tg_model.analysis.impact
"""Value-graph propagation (Phase 5): upstream / downstream **value slots** only.
This is **dependency reachability** on one compiled :class:`~tg_model.execution.dependency_graph.DependencyGraph`.
It does **not** aggregate requirements, hazards, behavior, interfaces, or other program semantics —
do not treat it as a full “engineering impact” or FMEA surface without layering your own model.
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from tg_model.execution.dependency_graph import DependencyGraph
from tg_model.execution.value_slots import ValueSlot
def _value_node_id(slot: ValueSlot) -> str:
return f"val:{slot.path_string}"
def _slot_ids_for_nodes(graph: DependencyGraph, node_ids: set[str]) -> frozenset[str]:
ids: set[str] = set()
for nid in node_ids:
sid = graph.get_node(nid).slot_id
if sid is not None:
ids.add(sid)
return frozenset(ids)
[docs]
@dataclass(frozen=True)
class ImpactReport:
"""Value-graph reachability summary from a set of changed slots."""
changed_paths: tuple[str, ...]
upstream_slot_ids: frozenset[str]
downstream_slot_ids: frozenset[str]
[docs]
def dependency_impact(
graph: DependencyGraph,
changed: Sequence[ValueSlot],
*,
upstream: bool = True,
downstream: bool = True,
) -> ImpactReport:
"""Return other value slots reachable from ``changed`` on the value graph.
Parameters
----------
graph : DependencyGraph
Compiled graph for the configuration under study.
changed : sequence of ValueSlot
Slots whose perturbation you want to analyze.
upstream, downstream : bool, default True
Include reachability in each direction.
Returns
-------
ImpactReport
Excludes the changed slots' own ``stable_id`` values from the sets.
Raises
------
ValueError
If a changed slot does not map to a ``val:<path>`` node in ``graph``.
Notes
-----
This is **dependency reachability only**, not full engineering impact (see module docstring).
"""
if not changed:
return ImpactReport((), frozenset(), frozenset())
seeds = [_value_node_id(s) for s in changed]
for vid in seeds:
if vid not in graph.nodes:
raise ValueError(
f"changed slot maps to unknown graph node {vid!r} "
f"(is this graph compiled for the same configured model?)"
)
seed_ids = {s.stable_id for s in changed}
up_ids: frozenset[str] = frozenset()
down_ids: frozenset[str] = frozenset()
if upstream:
up_ids = frozenset(
sid for sid in _slot_ids_for_nodes(graph, graph.dependency_closure(seeds)) if sid not in seed_ids
)
if downstream:
down_ids = frozenset(
sid for sid in _slot_ids_for_nodes(graph, graph.dependent_closure(seeds)) if sid not in seed_ids
)
paths = tuple(s.path_string for s in changed)
return ImpactReport(paths, up_ids, down_ids)
# Explicit alias: prefer this name when “impact” would oversell scope.
value_graph_propagation = dependency_impact