Part of #170 (source↔sink reachability API).
Goal
Given a (source, sink) pair that are connected, return the actual path(s) through the call graph that connect them — the witness/trace that explains why they match. Forward/reverse reachability (#171, #172) answer whether; this answers how, which is what makes a match auditable and triageable.
Proposed API
def get_reachability_paths(
source: str,
sink: str,
*,
shortest_only: bool = True, # one/all shortest paths vs. enumerate simple paths
max_paths: int | None = None, # cap when enumerating
max_depth: int | None = None, # path-length bound (cutoff)
) -> List[List[str]]: # each path = ordered list of signatures source..sink
...
Each path is the ordered list of node signatures from source to sink; an optional richer return could include the per-edge metadata (provenance, dispatch tag) so a trace is fully explainable.
Semantics
Directed paths source -CALLS*-> sink. shortest_only=True returns the shortest path(s); otherwise enumerate simple (acyclic) paths up to max_depth / max_paths.
Backing implementations
- Neo4j:
- shortest:
MATCH p = shortestPath((s {signature:$src})-[:CALLS*..$depth]->(t {signature:$sink})) RETURN [n IN nodes(p) | n.signature]
- all shortest:
allShortestPaths(...)
- bounded simple paths / k-paths: APOC (
apoc.path.expandConfig, apoc.algo.allSimplePaths)
- In-memory (NetworkX):
nx.shortest_path, nx.all_shortest_paths, nx.all_simple_paths(G, source, sink, cutoff=max_depth).
Notes
- Simple-path enumeration can explode combinatorially; default to shortest, require explicit opt-in for exhaustive enumeration, and always surface when results were truncated by
max_paths / max_depth.
- Returning
[] cleanly distinguishes "not reachable" from a single empty path.
Acceptance criteria
Part of #170 (source↔sink reachability API).
Goal
Given a
(source, sink)pair that are connected, return the actual path(s) through the call graph that connect them — the witness/trace that explains why they match. Forward/reverse reachability (#171, #172) answer whether; this answers how, which is what makes a match auditable and triageable.Proposed API
Each path is the ordered list of node signatures from
sourcetosink; an optional richer return could include the per-edge metadata (provenance, dispatch tag) so a trace is fully explainable.Semantics
Directed paths
source -CALLS*-> sink.shortest_only=Truereturns the shortest path(s); otherwise enumerate simple (acyclic) paths up tomax_depth/max_paths.Backing implementations
MATCH p = shortestPath((s {signature:$src})-[:CALLS*..$depth]->(t {signature:$sink})) RETURN [n IN nodes(p) | n.signature]allShortestPaths(...)apoc.path.expandConfig,apoc.algo.allSimplePaths)nx.shortest_path,nx.all_shortest_paths,nx.all_simple_paths(G, source, sink, cutoff=max_depth).Notes
max_paths/max_depth.[]cleanly distinguishes "not reachable" from a single empty path.Acceptance criteria
max_paths/max_depthhonored and truncation observable.sourcetosink(every consecutive pair is a realCALLSedge) and matches the in-memory backend's result.(source, sink)returns[].