luplo.core.impact ================= .. py:module:: luplo.core.impact .. autoapi-nested-parse:: Impact analysis — traverse typed edges to find an item's blast radius. Given an item, walk outgoing ``links`` edges whose ``link_type`` is one of ``depends`` / ``blocks`` / ``supersedes`` / ``conflicts`` up to a fixed depth ceiling, and return the set of items reachable through those edges. The ceiling (:data:`MAX_IMPACT_DEPTH`) is a product-level design principle, enforced server-side: there is no config knob, no ``--deep`` override, no per-tenant exception. If a caller needs more than five hops, the model needs decomposing — not this limit raising. Traversal is **outgoing only** (``links.from_item_id = parent``). Edge direction has an intended meaning for each type; the traversal layer does not second-guess it. Cycles are handled inside the recursive CTE via a path array: an item that is already on the current walk is not traversed a second time. Attributes ---------- .. autoapisummary:: luplo.core.impact.TRAVERSABLE_LINK_TYPES luplo.core.impact.MAX_IMPACT_DEPTH luplo.core.impact.MIN_IMPACT_DEPTH Classes ------- .. autoapisummary:: luplo.core.impact.ImpactEdge luplo.core.impact.ImpactNode luplo.core.impact.ImpactResult Functions --------- .. autoapisummary:: luplo.core.impact.impact Module Contents --------------- .. py:data:: TRAVERSABLE_LINK_TYPES :type: frozenset[str] Edge types that ``impact`` will walk. Other link types are ignored. .. py:data:: MAX_IMPACT_DEPTH :type: int :value: 5 Hard ceiling on traversal depth. Not user-configurable. .. py:data:: MIN_IMPACT_DEPTH :type: int :value: 1 Depth ``0`` would return only the root, so the smallest useful value is ``1``. .. py:class:: ImpactEdge One hop in an impact traversal, from ``parent_id`` to ``child_id``. .. py:attribute:: parent_id :type: str .. py:attribute:: child_id :type: str .. py:attribute:: link_type :type: str .. py:attribute:: depth :type: int .. py:class:: ImpactNode An item reached by traversal, together with the edge that first reached it. ``depth`` is the shortest-path depth from the root (``1`` means the item is a direct neighbour of the root). .. py:attribute:: item :type: luplo.core.models.Item .. py:attribute:: depth :type: int .. py:attribute:: via :type: ImpactEdge .. py:class:: ImpactResult Structured output of :func:`impact`. ``nodes`` is deduplicated: every item appears once, at its shortest-path depth. Ordering is ``(depth ASC, title ASC, link_type ASC)`` — stable across runs so diffs are readable. .. py:attribute:: root :type: luplo.core.models.Item .. py:attribute:: nodes :type: list[ImpactNode] .. py:attribute:: depth_requested :type: int .. py:function:: impact(conn: psycopg.AsyncConnection[Any], item_id: str, project_id: str, *, depth: int = MAX_IMPACT_DEPTH) -> ImpactResult :async: Run an impact analysis from *item_id*. :param conn: Async psycopg connection. :param item_id: Root item (full ID or hex prefix — resolved via :func:`luplo.core.id_resolve.resolve_uuid_prefix`). :param project_id: Project scope. Traversal never crosses projects; any edge pointing at an item outside this project is dropped. :param depth: Maximum hops to traverse. Clamped to ``[MIN_IMPACT_DEPTH, MAX_IMPACT_DEPTH]`` — out-of-range values raise :class:`ValidationError`. :returns: An :class:`ImpactResult` carrying the root item and the list of reachable items ordered by ``(depth, title, link_type)``. :raises ValidationError: If ``depth`` is outside the allowed range. :raises NotFoundError: If the root item does not exist in this project or is soft-deleted.