Add function-scoped Logic visualization
This commit is contained in:
parent
9fcafc290c
commit
9b4258c852
22 changed files with 1420 additions and 62 deletions
|
|
@ -17,6 +17,10 @@ from .models import (
|
|||
BuildReportingProject,
|
||||
Edge,
|
||||
IncrementalStateProject,
|
||||
LogicEdge,
|
||||
LogicNode,
|
||||
LogicProject,
|
||||
LogicProjection,
|
||||
Node,
|
||||
ProjectService,
|
||||
ProjectSnapshot,
|
||||
|
|
@ -24,7 +28,7 @@ from .models import (
|
|||
)
|
||||
from .project import project_root_fingerprint
|
||||
|
||||
INDEX_SCHEMA_VERSION = 1
|
||||
INDEX_SCHEMA_VERSION = 2
|
||||
APPLICATION_ID = 1_146_683_778
|
||||
|
||||
|
||||
|
|
@ -42,6 +46,15 @@ def _edge_hash(edges: tuple[Edge, ...]) -> str:
|
|||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def _logic_hash(projections: tuple[LogicProjection, ...]) -> str:
|
||||
payload = json.dumps(
|
||||
[projection.as_dict() for projection in projections],
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
return hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def _connect_read_only(path: Path) -> sqlite3.Connection:
|
||||
if not path.is_file():
|
||||
raise DocForgeError("missing_index", "Derived index does not exist; run build first")
|
||||
|
|
@ -65,7 +78,10 @@ def _read_connection(path: Path) -> Generator[sqlite3.Connection, None, None]:
|
|||
connection.close()
|
||||
|
||||
|
||||
def _status(snapshot: ProjectSnapshot) -> dict[str, object]:
|
||||
def _status(
|
||||
snapshot: ProjectSnapshot,
|
||||
logic: tuple[LogicProjection, ...],
|
||||
) -> dict[str, object]:
|
||||
return {
|
||||
"project_id": snapshot.descriptor.project_id,
|
||||
"project_root_fingerprint": project_root_fingerprint(snapshot.descriptor.root),
|
||||
|
|
@ -75,6 +91,10 @@ def _status(snapshot: ProjectSnapshot) -> dict[str, object]:
|
|||
"node_count": len(snapshot.nodes),
|
||||
"edge_hash": _edge_hash(snapshot.edges),
|
||||
"edge_count": len(snapshot.edges),
|
||||
"logic_hash": _logic_hash(logic),
|
||||
"logic_projection_count": len(logic),
|
||||
"logic_node_count": sum(len(projection.nodes) for projection in logic),
|
||||
"logic_edge_count": sum(len(projection.edges) for projection in logic),
|
||||
"index_schema_version": INDEX_SCHEMA_VERSION,
|
||||
"adapter": snapshot.descriptor.adapter,
|
||||
"status": "ok",
|
||||
|
|
@ -93,7 +113,8 @@ class ProjectIndex:
|
|||
|
||||
def build(self) -> dict[str, object]:
|
||||
snapshot = self.project.load()
|
||||
status = _status(snapshot)
|
||||
logic = self._logic_projections()
|
||||
status = _status(snapshot, logic)
|
||||
build_report = (
|
||||
self.project.build_report() if isinstance(self.project, BuildReportingProject) else None
|
||||
)
|
||||
|
|
@ -133,6 +154,32 @@ class ProjectIndex:
|
|||
PRIMARY KEY (source_id, relation, target_id)
|
||||
);
|
||||
CREATE INDEX edges_target ON edges(target_id, relation, source_id);
|
||||
CREATE TABLE logic_owners (
|
||||
owner_node_id TEXT PRIMARY KEY,
|
||||
source_id TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE logic_nodes (
|
||||
owner_node_id TEXT NOT NULL,
|
||||
logic_id TEXT NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
label TEXT NOT NULL,
|
||||
source_anchor TEXT,
|
||||
PRIMARY KEY (owner_node_id, logic_id)
|
||||
);
|
||||
CREATE INDEX logic_nodes_id ON logic_nodes(logic_id, owner_node_id);
|
||||
CREATE TABLE logic_edges (
|
||||
owner_node_id TEXT NOT NULL,
|
||||
source_id TEXT NOT NULL,
|
||||
relation TEXT NOT NULL,
|
||||
target_id TEXT NOT NULL,
|
||||
label TEXT,
|
||||
ordinal INTEGER NOT NULL,
|
||||
PRIMARY KEY (
|
||||
owner_node_id, source_id, ordinal, relation, target_id
|
||||
)
|
||||
);
|
||||
CREATE INDEX logic_edges_target
|
||||
ON logic_edges(owner_node_id, target_id, source_id);
|
||||
CREATE VIRTUAL TABLE node_fts USING fts5(
|
||||
node_id UNINDEXED, title, summary, content, tags
|
||||
);
|
||||
|
|
@ -167,6 +214,39 @@ class ProjectIndex:
|
|||
"INSERT INTO edges VALUES (?, ?, ?)",
|
||||
[(edge.source_id, edge.relation, edge.target_id) for edge in snapshot.edges],
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO logic_owners VALUES (?, ?)",
|
||||
[(projection.owner_node_id, projection.source_id) for projection in logic],
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO logic_nodes VALUES (?, ?, ?, ?, ?)",
|
||||
[
|
||||
(
|
||||
projection.owner_node_id,
|
||||
node.logic_id,
|
||||
node.kind,
|
||||
node.label,
|
||||
node.source_anchor,
|
||||
)
|
||||
for projection in logic
|
||||
for node in projection.nodes
|
||||
],
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO logic_edges VALUES (?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
(
|
||||
projection.owner_node_id,
|
||||
edge.source_id,
|
||||
edge.relation,
|
||||
edge.target_id,
|
||||
edge.label,
|
||||
edge.ordinal,
|
||||
)
|
||||
for projection in logic
|
||||
for edge in projection.edges
|
||||
],
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO node_fts VALUES (?, ?, ?, ?, ?)",
|
||||
[
|
||||
|
|
@ -187,7 +267,12 @@ class ProjectIndex:
|
|||
finally:
|
||||
connection.close()
|
||||
current = self.project.load()
|
||||
if current.source_hash != snapshot.source_hash or current.revision != snapshot.revision:
|
||||
current_logic = self._logic_projections()
|
||||
if (
|
||||
current.source_hash != snapshot.source_hash
|
||||
or current.revision != snapshot.revision
|
||||
or current_logic != logic
|
||||
):
|
||||
raise DocForgeError("source_changed", "Canonical source changed during index build")
|
||||
os.replace(temporary, self.path)
|
||||
except sqlite3.Error as error:
|
||||
|
|
@ -201,13 +286,19 @@ class ProjectIndex:
|
|||
result["build"] = build_report
|
||||
return result
|
||||
|
||||
def _logic_projections(self) -> tuple[LogicProjection, ...]:
|
||||
if isinstance(self.project, LogicProject):
|
||||
return self.project.logic_projections()
|
||||
return ()
|
||||
|
||||
def check(self) -> dict[str, object]:
|
||||
if isinstance(self.project, IncrementalStateProject):
|
||||
state = self.project.incremental_state()
|
||||
if state is not None:
|
||||
return self._check_incremental_state(state)
|
||||
snapshot = self.project.load()
|
||||
expected = _status(snapshot)
|
||||
logic = self._logic_projections()
|
||||
expected = _status(snapshot, logic)
|
||||
with _read_connection(self.path) as connection:
|
||||
application_id = connection.execute("PRAGMA application_id").fetchone()[0]
|
||||
schema_version = connection.execute("PRAGMA user_version").fetchone()[0]
|
||||
|
|
@ -223,6 +314,10 @@ class ProjectIndex:
|
|||
"node_count",
|
||||
"edge_hash",
|
||||
"edge_count",
|
||||
"logic_hash",
|
||||
"logic_projection_count",
|
||||
"logic_node_count",
|
||||
"logic_edge_count",
|
||||
"index_schema_version",
|
||||
"adapter",
|
||||
):
|
||||
|
|
@ -244,10 +339,12 @@ class ProjectIndex:
|
|||
"ORDER BY source_id, relation, target_id"
|
||||
)
|
||||
)
|
||||
indexed_logic = _logic_from_connection(connection)
|
||||
fts_count = connection.execute("SELECT COUNT(*) FROM node_fts").fetchone()[0]
|
||||
if (
|
||||
indexed_nodes != snapshot.nodes
|
||||
or indexed_edges != snapshot.edges
|
||||
or indexed_logic != logic
|
||||
or fts_count != len(snapshot.nodes)
|
||||
):
|
||||
raise DocForgeError("invalid_index", "Derived index rows do not match source")
|
||||
|
|
@ -290,14 +387,22 @@ class ProjectIndex:
|
|||
"ORDER BY source_id, relation, target_id"
|
||||
)
|
||||
)
|
||||
indexed_logic = _logic_from_connection(connection)
|
||||
node_hash = _node_hash(indexed_nodes)
|
||||
edge_hash = _edge_hash(indexed_edges)
|
||||
logic_hash = _logic_hash(indexed_logic)
|
||||
fts_count = connection.execute("SELECT COUNT(*) FROM node_fts").fetchone()[0]
|
||||
if (
|
||||
metadata.get("node_hash") != node_hash
|
||||
or metadata.get("edge_hash") != edge_hash
|
||||
or metadata.get("logic_hash") != logic_hash
|
||||
or metadata.get("node_count") != str(len(indexed_nodes))
|
||||
or metadata.get("edge_count") != str(len(indexed_edges))
|
||||
or metadata.get("logic_projection_count") != str(len(indexed_logic))
|
||||
or metadata.get("logic_node_count")
|
||||
!= str(sum(len(projection.nodes) for projection in indexed_logic))
|
||||
or metadata.get("logic_edge_count")
|
||||
!= str(sum(len(projection.edges) for projection in indexed_logic))
|
||||
or fts_count != len(indexed_nodes)
|
||||
):
|
||||
raise DocForgeError("invalid_index", "Derived index rows do not match metadata")
|
||||
|
|
@ -307,6 +412,10 @@ class ProjectIndex:
|
|||
"node_count": len(indexed_nodes),
|
||||
"edge_hash": edge_hash,
|
||||
"edge_count": len(indexed_edges),
|
||||
"logic_hash": logic_hash,
|
||||
"logic_projection_count": len(indexed_logic),
|
||||
"logic_node_count": sum(len(projection.nodes) for projection in indexed_logic),
|
||||
"logic_edge_count": sum(len(projection.edges) for projection in indexed_logic),
|
||||
"status": "ok",
|
||||
"database": str(self.path),
|
||||
}
|
||||
|
|
@ -321,6 +430,28 @@ class ProjectIndex:
|
|||
)
|
||||
return self._result(checked, node=_row_to_node(row).as_dict())
|
||||
|
||||
def get_logic(self, owner_node_id: str) -> dict[str, object]:
|
||||
"""Return one function-scoped control-flow projection without expanding the graph."""
|
||||
|
||||
checked = self.check()
|
||||
with _read_connection(self.path) as connection:
|
||||
owner = connection.execute(
|
||||
"SELECT * FROM nodes WHERE node_id = ?", (owner_node_id,)
|
||||
).fetchone()
|
||||
projection = _logic_projection_from_connection(connection, owner_node_id)
|
||||
if owner is None:
|
||||
raise DocForgeError(
|
||||
"missing_node",
|
||||
"No node has the requested stable ID",
|
||||
node_id=owner_node_id,
|
||||
)
|
||||
return self._result(
|
||||
checked,
|
||||
owner=_row_to_node(owner).as_dict(include_content=False),
|
||||
available=projection is not None,
|
||||
projection=projection.as_dict() if projection is not None else None,
|
||||
)
|
||||
|
||||
def search(self, query: str, *, limit: int | None = None) -> dict[str, object]:
|
||||
checked = self.check()
|
||||
limits = self.project.descriptor.limits
|
||||
|
|
@ -492,6 +623,64 @@ def _row_to_node(row: sqlite3.Row) -> Node:
|
|||
)
|
||||
|
||||
|
||||
def _logic_projection_from_connection(
|
||||
connection: sqlite3.Connection,
|
||||
owner_node_id: str,
|
||||
) -> LogicProjection | None:
|
||||
owner = connection.execute(
|
||||
"SELECT owner_node_id, source_id FROM logic_owners WHERE owner_node_id = ?",
|
||||
(owner_node_id,),
|
||||
).fetchone()
|
||||
if owner is None:
|
||||
return None
|
||||
nodes = tuple(
|
||||
LogicNode(
|
||||
logic_id=row["logic_id"],
|
||||
kind=row["kind"],
|
||||
label=row["label"],
|
||||
source_anchor=row["source_anchor"],
|
||||
)
|
||||
for row in connection.execute(
|
||||
"SELECT logic_id, kind, label, source_anchor FROM logic_nodes "
|
||||
"WHERE owner_node_id = ? ORDER BY logic_id",
|
||||
(owner_node_id,),
|
||||
)
|
||||
)
|
||||
edges = tuple(
|
||||
LogicEdge(
|
||||
source_id=row["source_id"],
|
||||
relation=row["relation"],
|
||||
target_id=row["target_id"],
|
||||
label=row["label"],
|
||||
ordinal=row["ordinal"],
|
||||
)
|
||||
for row in 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,),
|
||||
)
|
||||
)
|
||||
return LogicProjection(
|
||||
owner_node_id=owner["owner_node_id"],
|
||||
source_id=owner["source_id"],
|
||||
nodes=nodes,
|
||||
edges=edges,
|
||||
)
|
||||
|
||||
|
||||
def _logic_from_connection(
|
||||
connection: sqlite3.Connection,
|
||||
) -> tuple[LogicProjection, ...]:
|
||||
owners = connection.execute(
|
||||
"SELECT owner_node_id FROM logic_owners ORDER BY owner_node_id"
|
||||
).fetchall()
|
||||
projections = [
|
||||
_logic_projection_from_connection(connection, row["owner_node_id"]) for row in owners
|
||||
]
|
||||
return tuple(projection for projection in projections if projection is not None)
|
||||
|
||||
|
||||
def _bounded_limit(value: int | None, maximum: int, *, default: int) -> int:
|
||||
if value is None:
|
||||
return min(default, maximum)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue