Add multi-language logic exploration
This commit is contained in:
parent
9b4258c852
commit
9161889492
18 changed files with 1639 additions and 76 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@16"
|
||||
VISUALIZATION_TEMPLATE = "graph-browser@17"
|
||||
DEFAULT_EDGE_LIMIT = 100
|
||||
MAX_EDGE_LIMIT = 400
|
||||
MAX_LINEAGE_EDGE_LIMIT = 1_000
|
||||
|
|
@ -166,6 +166,8 @@ class VisualizationIndexSnapshot:
|
|||
authorities=_facet_rows(connection, "nodes", "authority"),
|
||||
statuses=_facet_rows(connection, "nodes", "status"),
|
||||
relations=_facet_rows(connection, "edges", "relation"),
|
||||
tags=_tag_facet_rows(connection),
|
||||
capabilities=_capability_facet_rows(connection),
|
||||
max_results=self.max_results,
|
||||
max_depth=self.max_depth,
|
||||
snapshot=True,
|
||||
|
|
@ -176,9 +178,18 @@ class VisualizationIndexSnapshot:
|
|||
*,
|
||||
query: str,
|
||||
family: str | None,
|
||||
kind: str | None,
|
||||
language: str | None,
|
||||
capability: str | None,
|
||||
limit: int,
|
||||
) -> dict[str, object]:
|
||||
bounded = self._bounded_limit(limit)
|
||||
clauses, filter_values = _node_filter_clauses(
|
||||
family=family,
|
||||
kind=kind,
|
||||
language=language,
|
||||
capability=capability,
|
||||
)
|
||||
with self._connection() as connection:
|
||||
if query:
|
||||
if len(query) > self.max_query_chars:
|
||||
|
|
@ -191,10 +202,8 @@ class VisualizationIndexSnapshot:
|
|||
expression = " AND ".join(
|
||||
f'"{term.replace(chr(34), chr(34) * 2)}"' for term in terms
|
||||
)
|
||||
family_clause = "AND nodes.family = ?" if family else ""
|
||||
values: tuple[object, ...] = (
|
||||
(expression, family, bounded) if family else (expression, bounded)
|
||||
)
|
||||
filter_clause = "".join(f" AND {clause}" for clause in clauses)
|
||||
values = (expression, *filter_values, bounded)
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT nodes.*, bm25(node_fts) AS rank,
|
||||
|
|
@ -202,7 +211,7 @@ class VisualizationIndexSnapshot:
|
|||
FROM node_fts JOIN nodes USING(node_id)
|
||||
WHERE node_fts MATCH ?
|
||||
"""
|
||||
+ family_clause
|
||||
+ filter_clause
|
||||
+ " ORDER BY rank, nodes.node_id LIMIT ?",
|
||||
values,
|
||||
).fetchall()
|
||||
|
|
@ -212,16 +221,19 @@ class VisualizationIndexSnapshot:
|
|||
item.update({"rank": row["rank"], "snippet": row["snippet"]})
|
||||
results.append(item)
|
||||
else:
|
||||
family_clause = "WHERE family = ?" if family else ""
|
||||
values = (family, bounded) if family else (bounded,)
|
||||
filter_clause = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
values = (*filter_values, bounded)
|
||||
rows = connection.execute(
|
||||
f"SELECT * FROM nodes {family_clause} ORDER BY node_id LIMIT ?",
|
||||
f"SELECT nodes.* FROM nodes {filter_clause} ORDER BY node_id LIMIT ?",
|
||||
values,
|
||||
).fetchall()
|
||||
results = [_node_dict(row, include_content=False) for row in rows]
|
||||
return self._result(
|
||||
query=query,
|
||||
family=family,
|
||||
kind=kind,
|
||||
language=language,
|
||||
capability=capability,
|
||||
count=len(results),
|
||||
results=results,
|
||||
snapshot=True,
|
||||
|
|
@ -865,7 +877,14 @@ class VisualizationRunner:
|
|||
if node_id is not None:
|
||||
reader.require_node(node_id)
|
||||
elif query is not None:
|
||||
reader.search(query=query, family=None, limit=1)
|
||||
reader.search(
|
||||
query=query,
|
||||
family=None,
|
||||
kind=None,
|
||||
language=None,
|
||||
capability=None,
|
||||
limit=1,
|
||||
)
|
||||
|
||||
with self._lock:
|
||||
self._reader = reader
|
||||
|
|
@ -1135,8 +1154,18 @@ class VisualizationRunner:
|
|||
) -> dict[str, object]:
|
||||
query = _one(params, "q").strip()
|
||||
family = _one(params, "family").strip() or None
|
||||
kind = _one(params, "kind").strip() or None
|
||||
language = _one(params, "language").strip() or None
|
||||
capability = _one(params, "capability").strip() or None
|
||||
limit = _integer(_one(params, "limit") or "50")
|
||||
return reader.search(query=query, family=family, limit=limit)
|
||||
return reader.search(
|
||||
query=query,
|
||||
family=family,
|
||||
kind=kind,
|
||||
language=language,
|
||||
capability=capability,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
def _node(
|
||||
self,
|
||||
|
|
@ -1523,7 +1552,14 @@ class PersistentVisualizationRunner:
|
|||
if node_id is not None:
|
||||
snapshot.require_node(node_id)
|
||||
elif query is not None:
|
||||
snapshot.search(query=query, family=None, limit=1)
|
||||
snapshot.search(
|
||||
query=query,
|
||||
family=None,
|
||||
kind=None,
|
||||
language=None,
|
||||
capability=None,
|
||||
limit=1,
|
||||
)
|
||||
|
||||
with self._locked_registry():
|
||||
existing = self._read_registry()
|
||||
|
|
@ -1740,6 +1776,64 @@ def _facet_rows(connection: sqlite3.Connection, table: str, column: str) -> list
|
|||
return [{"value": row[0], "count": row[1]} for row in rows]
|
||||
|
||||
|
||||
def _tag_facet_rows(connection: sqlite3.Connection) -> list[dict[str, object]]:
|
||||
rows = connection.execute(
|
||||
"""
|
||||
SELECT value, COUNT(*) AS count
|
||||
FROM nodes, json_each(nodes.tags_json)
|
||||
GROUP BY value
|
||||
ORDER BY count DESC, value
|
||||
"""
|
||||
).fetchall()
|
||||
return [{"value": row["value"], "count": row["count"]} for row in rows]
|
||||
|
||||
|
||||
def _capability_facet_rows(connection: sqlite3.Connection) -> list[dict[str, object]]:
|
||||
logic_count = connection.execute("SELECT COUNT(*) FROM logic_owners").fetchone()[0]
|
||||
source_count = connection.execute(
|
||||
"SELECT COUNT(*) FROM nodes WHERE source_path <> ''"
|
||||
).fetchone()[0]
|
||||
return [
|
||||
{"value": "logic", "count": logic_count},
|
||||
{"value": "source", "count": source_count},
|
||||
]
|
||||
|
||||
|
||||
def _node_filter_clauses(
|
||||
*,
|
||||
family: str | None,
|
||||
kind: str | None,
|
||||
language: str | None,
|
||||
capability: str | None,
|
||||
) -> tuple[list[str], list[object]]:
|
||||
clauses: list[str] = []
|
||||
values: list[object] = []
|
||||
for label, value in (("family", family), ("kind", kind), ("language", language)):
|
||||
if value is not None and len(value) > 160:
|
||||
raise DocForgeError("invalid_filter", f"Node {label} filter is invalid")
|
||||
if family:
|
||||
clauses.append("nodes.family = ?")
|
||||
values.append(family)
|
||||
if kind == "callable":
|
||||
clauses.append(
|
||||
"EXISTS (SELECT 1 FROM json_each(nodes.tags_json) "
|
||||
"WHERE value IN ('function', 'method', 'nested-function'))"
|
||||
)
|
||||
elif kind:
|
||||
clauses.append("EXISTS (SELECT 1 FROM json_each(nodes.tags_json) WHERE value = ?)")
|
||||
values.append(kind)
|
||||
if language:
|
||||
clauses.append("EXISTS (SELECT 1 FROM json_each(nodes.tags_json) WHERE value = ?)")
|
||||
values.append(language)
|
||||
if capability == "logic":
|
||||
clauses.append("EXISTS (SELECT 1 FROM logic_owners WHERE owner_node_id = nodes.node_id)")
|
||||
elif capability == "source":
|
||||
clauses.append("nodes.source_path <> ''")
|
||||
elif capability is not None:
|
||||
raise DocForgeError("invalid_filter", "Node capability filter is unsupported")
|
||||
return clauses, values
|
||||
|
||||
|
||||
def _read_browser_asset(name: str) -> str:
|
||||
return resources.files("docforge.assets").joinpath(name).read_text(encoding="utf-8")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue