"""Pure, bounded portable-graph planning over one immutable graph generation.""" from __future__ import annotations import re from dataclasses import dataclass from typing import Literal, cast from .errors import DocForgeError from .models import Edge, Node, ProjectSnapshot from .project import project_root_fingerprint from .projection_contract import GraphViewPlanV1 GraphViewMode = Literal["nodes", "flow", "web", "logic"] MAX_GRAPH_VIEW_DEPTH = 32 MAX_GRAPH_VIEW_NODES = 1_000 MAX_GRAPH_VIEW_EDGES = 4_000 MAX_GRAPH_VIEW_WORK = 1_000_000 MAX_GRAPH_VIEW_FILTERS = 64 MAX_GRAPH_VIEW_STRING_CHARS = 1_024 MAX_GRAPH_VIEW_QUERY_CHARS = 10_000 _QUERY_TOKEN = re.compile(r"\w+", re.UNICODE) _DETAIL_FIELDS = ( "node_id", "title", "family", "authority", "status", "tags", "summary", "content_hash", ) @dataclass(frozen=True) class GraphViewRequestV1: """One closed, inert graph selection request.""" view_id: str title: str root_node_id: str | None = None query: str | None = None initial_mode: GraphViewMode = "nodes" depth: int = 1 max_nodes: int = 100 max_edges: int = 400 max_work: int = 100_000 families: tuple[str, ...] = () relations: tuple[str, ...] = () authorities: tuple[str, ...] = () statuses: tuple[str, ...] = () tags: tuple[str, ...] = () include_logic: bool = False @dataclass(frozen=True) class _ValidatedRequest: view_id: str title: str root_node_id: str | None query: str | None initial_mode: GraphViewMode depth: int max_nodes: int max_edges: int max_work: int families: tuple[str, ...] relations: tuple[str, ...] authorities: tuple[str, ...] statuses: tuple[str, ...] tags: tuple[str, ...] include_logic: bool def _invalid(message: str, **details: object) -> DocForgeError: return DocForgeError("invalid_graph_view_request", message, **details) def _string(value: object, *, field: str, maximum: int = MAX_GRAPH_VIEW_STRING_CHARS) -> str: if not isinstance(value, str) or not value.strip() or len(value) > maximum or "\0" in value: raise _invalid("Graph view request string is invalid", field=field) return value.strip() def _filter_values(values: object, *, field: str) -> tuple[str, ...]: if not isinstance(values, tuple): raise _invalid("Graph view filter is invalid", field=field) tuple_values = cast(tuple[object, ...], values) if not all(isinstance(value, str) for value in tuple_values): raise _invalid("Graph view filter is invalid", field=field) if len(tuple_values) > MAX_GRAPH_VIEW_FILTERS: raise _invalid("Graph view filter is invalid", field=field) normalized = tuple(_string(value, field=field) for value in cast(tuple[str, ...], tuple_values)) if len(normalized) != len(set(normalized)): raise _invalid("Graph view filter contains duplicates", field=field) return tuple(sorted(normalized)) def _validated_request(request: GraphViewRequestV1) -> _ValidatedRequest: view_id = _string(request.view_id, field="view_id") title = _string(request.title, field="title") if (request.root_node_id is None) == (request.query is None): raise _invalid("Choose exactly one exact root or lexical query") root_node_id = ( None if request.root_node_id is None else _string(request.root_node_id, field="root_node_id") ) query = ( None if request.query is None else _string( request.query, field="query", maximum=MAX_GRAPH_VIEW_QUERY_CHARS, ) ) if request.initial_mode not in {"nodes", "flow", "web", "logic"}: raise _invalid("Graph view initial mode is unsupported") if type(request.depth) is not int or not 1 <= request.depth <= MAX_GRAPH_VIEW_DEPTH: raise _invalid( "Graph view depth is outside the fixed boundary", maximum=MAX_GRAPH_VIEW_DEPTH, ) for field, value, minimum, maximum in ( ("max_nodes", request.max_nodes, 1, MAX_GRAPH_VIEW_NODES), ("max_edges", request.max_edges, 0, MAX_GRAPH_VIEW_EDGES), ("max_work", request.max_work, 1, MAX_GRAPH_VIEW_WORK), ): if type(value) is not int or not minimum <= value <= maximum: raise _invalid( "Graph view bound is outside the fixed boundary", field=field, minimum=minimum, maximum=maximum, ) if type(request.include_logic) is not bool: raise _invalid("Graph view Logic selection must be Boolean") return _ValidatedRequest( view_id=view_id, title=title, root_node_id=root_node_id, query=query, initial_mode=request.initial_mode, depth=request.depth, max_nodes=request.max_nodes, max_edges=request.max_edges, max_work=request.max_work, families=_filter_values(request.families, field="families"), relations=_filter_values(request.relations, field="relations"), authorities=_filter_values(request.authorities, field="authorities"), statuses=_filter_values(request.statuses, field="statuses"), tags=_filter_values(request.tags, field="tags"), include_logic=request.include_logic, ) def _validated_graph( snapshot: ProjectSnapshot, ) -> tuple[tuple[Node, ...], tuple[Edge, ...], dict[str, Node]]: nodes = tuple(sorted(snapshot.nodes, key=lambda node: node.node_id)) node_by_id = {node.node_id: node for node in nodes} if len(node_by_id) != len(nodes): raise DocForgeError( "invalid_projection", "Graph view snapshot contains duplicate node identities", ) edges = tuple( sorted( snapshot.edges, key=lambda edge: (edge.source_id, edge.relation, edge.target_id), ) ) edge_keys = {(edge.source_id, edge.relation, edge.target_id) for edge in edges} if len(edge_keys) != len(edges) or any( edge.source_id not in node_by_id or edge.target_id not in node_by_id for edge in edges ): raise DocForgeError( "invalid_projection", "Graph view snapshot contains invalid relationships", ) return nodes, edges, node_by_id def _eligible(node: Node, request: _ValidatedRequest) -> bool: return ( (not request.families or node.family in request.families) and (not request.authorities or node.authority in request.authorities) and (not request.statuses or node.status in request.statuses) and (not request.tags or set(request.tags).issubset(node.tags)) ) def _relation_allowed(edge: Edge, request: _ValidatedRequest) -> bool: return not request.relations or edge.relation in request.relations def _lexical_text(node: Node) -> str: return " ".join( ( node.node_id, node.title, node.summary, node.family, node.authority, node.status, *node.tags, ) ).casefold() def _lexical_nodes( nodes: tuple[Node, ...], request: _ValidatedRequest, omissions: list[dict[str, object]], ) -> tuple[set[str], int, bool]: query = request.query maximum_nodes = request.max_nodes maximum_work = request.max_work assert query is not None terms = tuple(dict.fromkeys(_QUERY_TOKEN.findall(query.casefold()))) if not terms: raise _invalid("Lexical graph scope contains no searchable text") selected: set[str] = set() work = 0 work_limited = False for node in nodes: if work >= maximum_work: work_limited = True break work += 1 if not _eligible(node, request) or not all(term in _lexical_text(node) for term in terms): continue if len(selected) >= maximum_nodes: omissions.append( { "code": "node_result_limit", "subject": "nodes", "limit": maximum_nodes, "minimum_omitted": 1, } ) break selected.add(node.node_id) return selected, work, work_limited def _root_nodes( edges: tuple[Edge, ...], node_by_id: dict[str, Node], request: _ValidatedRequest, omissions: list[dict[str, object]], ) -> tuple[set[str], int, bool]: root_node_id = request.root_node_id maximum_nodes = request.max_nodes maximum_work = request.max_work depth = request.depth assert root_node_id is not None root = node_by_id.get(root_node_id) if root is None: raise DocForgeError( "missing_node", "No node has the requested stable ID", node_id=root_node_id, ) if not _eligible(root, request): raise _invalid("Exact graph root is excluded by the closed node filters") selected = {root_node_id} frontier = {root_node_id} work = 0 work_limited = False node_limited = False for _ in range(depth): if not frontier: break next_frontier: set[str] = set() for edge in edges: if work >= maximum_work: work_limited = True break work += 1 if not _relation_allowed(edge, request): continue candidate: str | None = None if edge.source_id in frontier: candidate = edge.target_id elif edge.target_id in frontier: candidate = edge.source_id if candidate is None or candidate in selected: continue node = node_by_id[candidate] if not _eligible(node, request): continue if len(selected) >= maximum_nodes: node_limited = True continue selected.add(candidate) next_frontier.add(candidate) if work_limited: break frontier = next_frontier if node_limited: omissions.append( { "code": "node_result_limit", "subject": "nodes", "limit": maximum_nodes, "minimum_omitted": 1, } ) return selected, work, work_limited def _selected_edges( edges: tuple[Edge, ...], selected_ids: set[str], request: _ValidatedRequest, *, initial_work: int, omissions: list[dict[str, object]], ) -> tuple[list[Edge], int, bool]: maximum_edges = request.max_edges maximum_work = request.max_work selected: list[Edge] = [] work = initial_work work_limited = False edge_limited = False for edge in edges: if work >= maximum_work: work_limited = True break work += 1 if ( edge.source_id not in selected_ids or edge.target_id not in selected_ids or not _relation_allowed(edge, request) ): continue if len(selected) >= maximum_edges: edge_limited = True break selected.append(edge) if edge_limited: omissions.append( { "code": "edge_result_limit", "subject": "edges", "limit": maximum_edges, "minimum_omitted": 1, } ) return selected, work, work_limited def _node_payload(node: Node) -> dict[str, object]: return { "node_id": node.node_id, "title": node.title, "family": node.family, "authority": node.authority, "status": node.status, "tags": sorted(node.tags), "summary": node.summary, "content_hash": node.content_hash, } def _edge_payload(edge: Edge) -> dict[str, str]: return { "source_id": edge.source_id, "relation": edge.relation, "target_id": edge.target_id, } def build_graph_view_plan( snapshot: ProjectSnapshot, request: GraphViewRequestV1, allow_logic: bool, ) -> GraphViewPlanV1: """Build one deterministic, path-free graph plan without rendering or storage access.""" if type(allow_logic) is not bool: raise _invalid("Graph view Logic policy must be Boolean") normalized = _validated_request(request) nodes, edges, node_by_id = _validated_graph(snapshot) omissions: list[dict[str, object]] = [] if normalized.root_node_id is not None: selected_ids, work, work_limited = _root_nodes( edges, node_by_id, normalized, omissions, ) scope: dict[str, object] = { "kind": "exact_root", "root_node_id": normalized.root_node_id, "depth": normalized.depth, } else: selected_ids, work, work_limited = _lexical_nodes( nodes, normalized, omissions, ) scope = { "kind": "lexical", "query": normalized.query, } selected_edges, work, edge_work_limited = _selected_edges( edges, selected_ids, normalized, initial_work=work, omissions=omissions, ) work_limited = work_limited or edge_work_limited if work_limited: omissions.append( { "code": "work_limit", "subject": "selection", "limit": normalized.max_work, "examined": work, "minimum_omitted": 1, } ) logic_requested = normalized.include_logic if logic_requested and not allow_logic: omissions.append( { "code": "logic_forbidden", "subject": "logic", "minimum_omitted": 1, } ) omissions.sort(key=lambda item: (str(item["code"]), str(item["subject"]))) selected_nodes = [node_by_id[node_id] for node_id in sorted(selected_ids)] filters: dict[str, list[str]] = { "families": list(normalized.families), "relations": list(normalized.relations), "authorities": list(normalized.authorities), "statuses": list(normalized.statuses), "tags": list(normalized.tags), } return GraphViewPlanV1.create( { "project": { "project_id": snapshot.descriptor.project_id, "project_root_fingerprint": project_root_fingerprint(snapshot.descriptor.root), "adapter": snapshot.descriptor.adapter, "revision": snapshot.revision, "source_hash": snapshot.source_hash, }, "view": { "view_id": normalized.view_id, "title": normalized.title, "initial_mode": normalized.initial_mode, "scope": scope, "filters": filters, "detail_fields": list(_DETAIL_FIELDS), }, "bounds": { "depth": normalized.depth, "max_nodes": normalized.max_nodes, "max_edges": normalized.max_edges, "max_work": normalized.max_work, }, "policy": { "visibility": "selected_graph_only", "source_paths": "excluded", "source_bodies": "excluded", "database_queries": "forbidden", "executable_content": "forbidden", "logic": "allowed" if allow_logic else "forbidden", "logic_requested": logic_requested, }, "graph": { "root_node_id": normalized.root_node_id, "nodes": [_node_payload(node) for node in selected_nodes], "edges": [_edge_payload(edge) for edge in selected_edges], "logic_projections": [], }, "omissions": omissions, "diagnostics": { "selection": scope["kind"], "returned_nodes": len(selected_nodes), "returned_edges": len(selected_edges), "examined_work_units": work, "truncated": bool(omissions), "ordering": "node_id;source_id,relation,target_id", }, } )