1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Add semantic flow and convergence web views

This commit is contained in:
Andraxion 2026-07-25 17:34:58 -04:00
parent f9f7105983
commit 6609edc804
14 changed files with 616 additions and 128 deletions

View file

@ -34,10 +34,39 @@ from .errors import DocForgeError
from .index import APPLICATION_ID, INDEX_SCHEMA_VERSION, ProjectIndex, re_tokenize
from .project import project_root_fingerprint
VISUALIZATION_TEMPLATE = "graph-browser@13"
VISUALIZATION_TEMPLATE = "graph-browser@14"
DEFAULT_EDGE_LIMIT = 100
MAX_EDGE_LIMIT = 400
MAX_LINEAGE_EDGE_LIMIT = 1_000
FLOW_REVERSED_RELATIONS = frozenset(
{
"defined_in",
"inherits",
"imports",
"depends_on",
"reads",
"tested_by",
}
)
FLOW_CONTEXT_RELATIONS = frozenset(
{
"documents",
"governs",
"relates_to",
}
)
WEB_ROOT_ADJACENT_RELATIONS = frozenset(
{
"activates",
"calls",
"contains",
"defines",
"dispatches_to",
"implemented_by",
"launches",
"writes",
}
)
DEFAULT_INITIAL_GRACE_SECONDS = 120.0
DEFAULT_LEASE_SECONDS = 180.0
LEASE_MONITOR_INTERVAL_SECONDS = 1.0
@ -138,6 +167,7 @@ class VisualizationIndexSnapshot:
statuses=_facet_rows(connection, "nodes", "status"),
relations=_facet_rows(connection, "edges", "relation"),
max_results=self.max_results,
max_depth=self.max_depth,
snapshot=True,
)
@ -367,11 +397,12 @@ class VisualizationIndexSnapshot:
)
def lineage(self, node_id: str, *, limit: int) -> dict[str, object]:
"""Return every bounded, directed ancestry path terminating at ``node_id``.
"""Return bounded semantic flow paths terminating at ``node_id``.
A lineage follows stored edge direction only: ``source -> target``. This keeps
Flow literal and auditable. It does not reinterpret relationship meanings or
reverse dependency/data edges as the old client-side Flow view did.
Structural and execution edges retain their stored direction. Dependency,
import, data-read, inheritance, and ``tested_by`` edges are reversed so their
prerequisites flow into the consumer. Context-only documentation edges remain
available in Web but do not clutter Flow.
"""
if type(limit) is not int or limit < 1 or limit > MAX_LINEAGE_EDGE_LIMIT:
raise DocForgeError(
@ -391,31 +422,37 @@ class VisualizationIndexSnapshot:
)
visited = {node_id}
frontier = {node_id}
selected: list[dict[str, str]] = []
hops = {node_id: 0}
selected: list[dict[str, object]] = []
selected_keys: set[tuple[str, str, str]] = set()
truncated = False
while frontier and len(selected) < limit:
placeholders = ",".join("?" for _ in frontier)
remaining = limit - len(selected)
rows = connection.execute(
"SELECT source_id, relation, target_id FROM edges "
f"WHERE target_id IN ({placeholders}) "
"ORDER BY source_id, relation, target_id LIMIT ?",
(*sorted(frontier), remaining + 1),
f"WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders}) "
"ORDER BY source_id, relation, target_id",
(*sorted(frontier), *sorted(frontier)),
).fetchall()
if len(rows) > remaining:
rows = rows[:remaining]
truncated = True
next_frontier: set[str] = set()
for row in rows:
edge = {
"source_id": row["source_id"],
"relation": row["relation"],
"target_id": row["target_id"],
}
key = (row["source_id"], row["relation"], row["target_id"])
if key in selected_keys or row["relation"] in FLOW_CONTEXT_RELATIONS:
continue
reversed_edge = row["relation"] in FLOW_REVERSED_RELATIONS
source_id = row["target_id"] if reversed_edge else row["source_id"]
target_id = row["source_id"] if reversed_edge else row["target_id"]
if target_id not in frontier:
continue
if len(selected) >= limit:
truncated = True
break
selected_keys.add(key)
edge = _visualization_edge(row, reversed_edge=reversed_edge)
selected.append(edge)
source_id = edge["source_id"]
if source_id not in visited:
visited.add(source_id)
hops[source_id] = hops[target_id] + 1
next_frontier.add(source_id)
frontier = next_frontier
if frontier and len(selected) >= limit:
@ -430,6 +467,113 @@ class VisualizationIndexSnapshot:
lineage=True,
edge_limit=limit,
truncated=truncated,
hops=hops,
node=_node_dict(root_row),
nodes=[_node_dict(row, include_content=False) for row in node_rows],
edges=selected,
snapshot=True,
)
def web(self, node_id: str, *, depth: int, limit: int) -> dict[str, object]:
"""Return a bounded convergence web centered on ``node_id``.
Web follows every semantic contributor path toward the focus, including the
context relationships omitted from Flow. It also reverses direct focus-owned
members and execution dependencies into adjacent contributor branches. Later
traversal continues only toward those branches, so entering a package or class
cannot fan out through unrelated siblings.
"""
if type(depth) is not int or depth < 1 or depth > self.max_depth:
raise DocForgeError("invalid_depth", "Traversal depth is outside the configured limit")
if type(limit) is not int or limit < 1 or limit > MAX_LINEAGE_EDGE_LIMIT:
raise DocForgeError(
"invalid_limit",
"Visualization web limit exceeds the fixed safety boundary",
maximum=MAX_LINEAGE_EDGE_LIMIT,
)
with self._connection() as connection:
root_row = connection.execute(
"SELECT * FROM nodes WHERE node_id = ?", (node_id,)
).fetchone()
if root_row is None:
raise DocForgeError(
"missing_node",
"No node has the requested stable ID",
node_id=node_id,
)
visited = {node_id}
frontier = {node_id}
hops = {node_id: 0}
selected: list[dict[str, object]] = []
selected_keys: set[tuple[str, str, str, bool]] = set()
truncated = False
for hop in range(1, depth + 1):
if not frontier or len(selected) >= limit:
break
placeholders = ",".join("?" for _ in frontier)
values = tuple(sorted(frontier))
rows = connection.execute(
"SELECT source_id, relation, target_id FROM edges "
f"WHERE source_id IN ({placeholders}) OR target_id IN ({placeholders}) "
"ORDER BY source_id, relation, target_id",
(*values, *values),
).fetchall()
next_frontier: set[str] = set()
for row in rows:
semantic_reversed = row["relation"] in FLOW_REVERSED_RELATIONS
semantic_source = row["target_id"] if semantic_reversed else row["source_id"]
semantic_target = row["source_id"] if semantic_reversed else row["target_id"]
candidates: list[tuple[str, bool]] = []
if semantic_target in frontier:
candidates.append((semantic_source, semantic_reversed))
if (
semantic_source == node_id
and row["relation"] in WEB_ROOT_ADJACENT_RELATIONS
):
candidates.append((semantic_target, not semantic_reversed))
for source_id, reversed_edge in candidates:
key = (
row["source_id"],
row["relation"],
row["target_id"],
reversed_edge,
)
if key in selected_keys:
continue
existing_hop = hops.get(source_id)
if existing_hop is not None and existing_hop < hop:
continue
if len(selected) >= limit:
truncated = True
break
selected_keys.add(key)
selected.append(_visualization_edge(row, reversed_edge=reversed_edge))
if source_id not in visited:
visited.add(source_id)
hops[source_id] = hop
next_frontier.add(source_id)
if truncated:
break
frontier = next_frontier
if frontier and len(selected) >= limit:
truncated = True
convergent_ids = {node_id}
for edge in selected:
convergent_ids.add(cast(str, edge["source_id"]))
convergent_ids.add(cast(str, edge["target_id"]))
placeholders = ",".join("?" for _ in convergent_ids)
node_rows = connection.execute(
f"SELECT * FROM nodes WHERE node_id IN ({placeholders}) ORDER BY node_id",
tuple(sorted(convergent_ids)),
).fetchall()
return self._result(
root=node_id,
web=True,
depth=depth,
edge_limit=limit,
truncated=truncated,
hops={node: hops[node] for node in sorted(convergent_ids)},
node=_node_dict(root_row),
nodes=[_node_dict(row, include_content=False) for row in node_rows],
edges=selected,
@ -794,6 +938,9 @@ class VisualizationRunner:
elif parsed.path == f"{prefix}/api/lineage":
self._touch_lease()
payload = self._lineage(reader, params)
elif parsed.path == f"{prefix}/api/web":
self._touch_lease()
payload = self._web(reader, params)
else:
self._respond_error(
handler,
@ -891,6 +1038,24 @@ class VisualizationRunner:
)
return reader.lineage(node_id, limit=limit)
def _web(
self,
reader: VisualizationIndexSnapshot,
params: dict[str, list[str]],
) -> dict[str, object]:
node_id = _one(params, "id").strip()
if not node_id:
raise DocForgeError("missing_node", "One exact node ID is required")
depth = _integer(_one(params, "depth") or str(reader.max_depth))
limit = _integer(_one(params, "limit") or str(MAX_LINEAGE_EDGE_LIMIT))
if limit > MAX_LINEAGE_EDGE_LIMIT:
raise DocForgeError(
"invalid_limit",
"Visualization web limit exceeds the fixed safety boundary",
maximum=MAX_LINEAGE_EDGE_LIMIT,
)
return reader.web(node_id, depth=depth, limit=limit)
def _filter(
self,
reader: VisualizationIndexSnapshot,
@ -1346,6 +1511,23 @@ def _integer(value: str) -> int:
return int(value)
def _visualization_edge(
row: sqlite3.Row,
*,
reversed_edge: bool,
) -> dict[str, object]:
stored_source_id = cast(str, row["source_id"])
stored_target_id = cast(str, row["target_id"])
return {
"source_id": stored_target_id if reversed_edge else stored_source_id,
"relation": row["relation"],
"target_id": stored_source_id if reversed_edge else stored_target_id,
"stored_source_id": stored_source_id,
"stored_target_id": stored_target_id,
"reversed": reversed_edge,
}
def _node_dict(row: sqlite3.Row, *, include_content: bool = True) -> dict[str, object]:
result = {
"node_id": row["node_id"],