Add function-scoped Logic visualization
This commit is contained in:
parent
9fcafc290c
commit
9b4258c852
22 changed files with 1420 additions and 62 deletions
|
|
@ -34,7 +34,7 @@ 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@15"
|
||||
VISUALIZATION_TEMPLATE = "graph-browser@16"
|
||||
DEFAULT_EDGE_LIMIT = 100
|
||||
MAX_EDGE_LIMIT = 400
|
||||
MAX_LINEAGE_EDGE_LIMIT = 1_000
|
||||
|
|
@ -277,10 +277,69 @@ class VisualizationIndexSnapshot:
|
|||
"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,
|
||||
logic_row = connection.execute(
|
||||
"SELECT owner_node_id, logic_id, kind, label, source_anchor "
|
||||
"FROM logic_nodes WHERE logic_id = ? "
|
||||
"ORDER BY owner_node_id LIMIT 1",
|
||||
(node_id,),
|
||||
).fetchone()
|
||||
if logic_row is None:
|
||||
raise DocForgeError(
|
||||
"missing_node",
|
||||
"No node has the requested stable ID",
|
||||
node_id=node_id,
|
||||
)
|
||||
owner_row = connection.execute(
|
||||
"SELECT * FROM nodes WHERE node_id = ?",
|
||||
(logic_row["owner_node_id"],),
|
||||
).fetchone()
|
||||
if owner_row is None:
|
||||
raise DocForgeError(
|
||||
"invalid_index",
|
||||
"Logic projection owner is missing from the primary graph",
|
||||
)
|
||||
logic_nodes = connection.execute(
|
||||
"SELECT logic_id, kind, label, source_anchor FROM logic_nodes "
|
||||
"WHERE owner_node_id = ? ORDER BY logic_id",
|
||||
(logic_row["owner_node_id"],),
|
||||
).fetchall()
|
||||
logic_edges = connection.execute(
|
||||
"SELECT source_id, relation, target_id, label, ordinal "
|
||||
"FROM logic_edges WHERE owner_node_id = ? "
|
||||
"ORDER BY source_id, ordinal, relation, target_id",
|
||||
(logic_row["owner_node_id"],),
|
||||
).fetchall()
|
||||
node = _logic_node_dict(
|
||||
logic_row,
|
||||
owner_row=owner_row,
|
||||
owner_node_id=logic_row["owner_node_id"],
|
||||
)
|
||||
return self._result(
|
||||
root=node_id,
|
||||
depth=1,
|
||||
edge_limit=limit,
|
||||
truncated=False,
|
||||
node=node,
|
||||
nodes=[
|
||||
_logic_node_dict(
|
||||
row,
|
||||
owner_row=owner_row,
|
||||
owner_node_id=logic_row["owner_node_id"],
|
||||
)
|
||||
for row in logic_nodes
|
||||
],
|
||||
edges=[
|
||||
{
|
||||
"source_id": row["source_id"],
|
||||
"relation": row["relation"],
|
||||
"target_id": row["target_id"],
|
||||
"label": row["label"],
|
||||
"ordinal": row["ordinal"],
|
||||
"reversed": False,
|
||||
}
|
||||
for row in logic_edges
|
||||
],
|
||||
snapshot=True,
|
||||
)
|
||||
visited = {node_id}
|
||||
frontier = {node_id}
|
||||
|
|
@ -342,6 +401,20 @@ class VisualizationIndexSnapshot:
|
|||
"SELECT node_id, source_path, source_anchor FROM nodes WHERE node_id = ?",
|
||||
(node_id,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
row = connection.execute(
|
||||
"""
|
||||
SELECT logic.logic_id AS node_id,
|
||||
owner.source_path AS source_path,
|
||||
logic.source_anchor AS source_anchor
|
||||
FROM logic_nodes AS logic
|
||||
JOIN nodes AS owner ON owner.node_id = logic.owner_node_id
|
||||
WHERE logic.logic_id = ?
|
||||
ORDER BY logic.owner_node_id
|
||||
LIMIT 1
|
||||
""",
|
||||
(node_id,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise DocForgeError(
|
||||
"missing_node",
|
||||
|
|
@ -396,6 +469,75 @@ class VisualizationIndexSnapshot:
|
|||
snapshot=True,
|
||||
)
|
||||
|
||||
def logic(self, owner_node_id: str) -> dict[str, object]:
|
||||
"""Return one lazy function-scoped control-flow projection."""
|
||||
|
||||
with self._connection() as connection:
|
||||
owner_row = connection.execute(
|
||||
"SELECT * FROM nodes WHERE node_id = ?",
|
||||
(owner_node_id,),
|
||||
).fetchone()
|
||||
if owner_row is None:
|
||||
raise DocForgeError(
|
||||
"missing_node",
|
||||
"No node has the requested stable ID",
|
||||
node_id=owner_node_id,
|
||||
)
|
||||
owner = connection.execute(
|
||||
"SELECT source_id FROM logic_owners WHERE owner_node_id = ?",
|
||||
(owner_node_id,),
|
||||
).fetchone()
|
||||
if owner is None:
|
||||
return self._result(
|
||||
root=owner_node_id,
|
||||
logic=True,
|
||||
available=False,
|
||||
owner=_node_dict(owner_row, include_content=False),
|
||||
nodes=[],
|
||||
edges=[],
|
||||
snapshot=True,
|
||||
)
|
||||
node_rows = connection.execute(
|
||||
"SELECT logic_id, kind, label, source_anchor FROM logic_nodes "
|
||||
"WHERE owner_node_id = ? ORDER BY logic_id",
|
||||
(owner_node_id,),
|
||||
).fetchall()
|
||||
edge_rows = connection.execute(
|
||||
"SELECT source_id, relation, target_id, label, ordinal "
|
||||
"FROM logic_edges WHERE owner_node_id = ? "
|
||||
"ORDER BY source_id, ordinal, relation, target_id",
|
||||
(owner_node_id,),
|
||||
).fetchall()
|
||||
nodes = [
|
||||
_logic_node_dict(row, owner_row=owner_row, owner_node_id=owner_node_id)
|
||||
for row in node_rows
|
||||
]
|
||||
entry = next(
|
||||
(cast(str, node["node_id"]) for node in nodes if node["logic_kind"] == "entry"),
|
||||
cast(str, nodes[0]["node_id"]) if nodes else owner_node_id,
|
||||
)
|
||||
edges = [
|
||||
{
|
||||
"source_id": row["source_id"],
|
||||
"relation": row["relation"],
|
||||
"target_id": row["target_id"],
|
||||
"label": row["label"],
|
||||
"ordinal": row["ordinal"],
|
||||
"reversed": False,
|
||||
}
|
||||
for row in edge_rows
|
||||
]
|
||||
return self._result(
|
||||
root=entry,
|
||||
logic=True,
|
||||
available=True,
|
||||
source_id=owner["source_id"],
|
||||
owner=_node_dict(owner_row, include_content=False),
|
||||
nodes=nodes,
|
||||
edges=edges,
|
||||
snapshot=True,
|
||||
)
|
||||
|
||||
def lineage(self, node_id: str, *, limit: int) -> dict[str, object]:
|
||||
"""Return bounded semantic flow paths terminating at ``node_id``.
|
||||
|
||||
|
|
@ -941,6 +1083,9 @@ class VisualizationRunner:
|
|||
elif parsed.path == f"{prefix}/api/web":
|
||||
self._touch_lease()
|
||||
payload = self._web(reader, params)
|
||||
elif parsed.path == f"{prefix}/api/logic":
|
||||
self._touch_lease()
|
||||
payload = self._logic(reader, params)
|
||||
else:
|
||||
self._respond_error(
|
||||
handler,
|
||||
|
|
@ -1056,6 +1201,16 @@ class VisualizationRunner:
|
|||
)
|
||||
return reader.web(node_id, depth=depth, limit=limit)
|
||||
|
||||
@staticmethod
|
||||
def _logic(
|
||||
reader: VisualizationIndexSnapshot,
|
||||
params: dict[str, list[str]],
|
||||
) -> dict[str, object]:
|
||||
owner_node_id = _one(params, "id").strip()
|
||||
if not owner_node_id:
|
||||
raise DocForgeError("missing_node", "One exact owner node ID is required")
|
||||
return reader.logic(owner_node_id)
|
||||
|
||||
def _filter(
|
||||
self,
|
||||
reader: VisualizationIndexSnapshot,
|
||||
|
|
@ -1546,6 +1701,29 @@ def _node_dict(row: sqlite3.Row, *, include_content: bool = True) -> dict[str, o
|
|||
return result
|
||||
|
||||
|
||||
def _logic_node_dict(
|
||||
row: sqlite3.Row,
|
||||
*,
|
||||
owner_row: sqlite3.Row,
|
||||
owner_node_id: str,
|
||||
) -> dict[str, object]:
|
||||
kind = cast(str, row["kind"])
|
||||
return {
|
||||
"node_id": row["logic_id"],
|
||||
"title": row["label"],
|
||||
"family": "logic",
|
||||
"authority": "derived",
|
||||
"status": "current",
|
||||
"tags": ("logic", kind),
|
||||
"summary": f"{kind.replace('_', ' ').title()} in {owner_row['title']}.",
|
||||
"source_path": owner_row["source_path"],
|
||||
"source_anchor": row["source_anchor"],
|
||||
"content_hash": "",
|
||||
"logic_kind": kind,
|
||||
"logic_owner_id": owner_node_id,
|
||||
}
|
||||
|
||||
|
||||
def _facet_rows(connection: sqlite3.Connection, table: str, column: str) -> list[dict[str, object]]:
|
||||
allowed = {
|
||||
("nodes", "family"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue