|
| 1 | +"""Step 3a: ownership closure. |
| 2 | +
|
| 3 | +The module tree tracks a subset of the code graph (on svelte 764 of 2239 |
| 4 | +components). A changed component outside that subset has no owner, so it |
| 5 | +can never enter a leaf's Own and, when its neighbours' leaves are otherwise |
| 6 | +quiet, is dropped. Here the owner map is extended to a total function: an |
| 7 | +untracked changed component gets an *effective owner* by the very rules that |
| 8 | +place a new component in Step 2 (same file, same directory with one leaf, |
| 9 | +neighbour majority, routing agent). The component then enters Own of that |
| 10 | +leaf. Activation only: the tree on disk is not changed, and the closure is |
| 11 | +recomputed at every step. |
| 12 | +""" |
| 13 | + |
| 14 | +from __future__ import annotations |
| 15 | + |
| 16 | +import logging |
| 17 | +from dataclasses import dataclass |
| 18 | +from typing import Any |
| 19 | + |
| 20 | +from codewiki.src.be.dependency_analyzer.models.core import Node |
| 21 | +from codewiki.src.be.updater import tree as T |
| 22 | +from codewiki.src.be.updater.graph_diff import GraphDiff |
| 23 | +from codewiki.src.be.updater.options import UpdateOptions |
| 24 | +from codewiki.src.be.updater.tree_repair import ( |
| 25 | + RULE_AGENT, |
| 26 | + OrphanRouter, |
| 27 | + RepairResult, |
| 28 | + RoutingDecision, |
| 29 | + route_by_rules, |
| 30 | +) |
| 31 | + |
| 32 | +logger = logging.getLogger(__name__) |
| 33 | + |
| 34 | +PURPOSE_OWNERSHIP = "ownership" |
| 35 | + |
| 36 | + |
| 37 | +@dataclass |
| 38 | +class Adoption: |
| 39 | + """Effective owner of one changed component that no leaf tracks.""" |
| 40 | + |
| 41 | + decision: RoutingDecision |
| 42 | + deleted: bool = False # the component exists only in the old graph |
| 43 | + |
| 44 | + @property |
| 45 | + def component_id(self) -> str: |
| 46 | + return self.decision.component_id |
| 47 | + |
| 48 | + @property |
| 49 | + def leaf_path(self) -> tuple[str, ...]: |
| 50 | + assert self.decision.leaf_path is not None |
| 51 | + return self.decision.leaf_path |
| 52 | + |
| 53 | + @property |
| 54 | + def rule(self) -> str: |
| 55 | + return self.decision.rule |
| 56 | + |
| 57 | + def to_dict(self) -> dict[str, Any]: |
| 58 | + return { |
| 59 | + "component_id": self.component_id, |
| 60 | + "rule": self.rule, |
| 61 | + "leaf_path": list(self.leaf_path), |
| 62 | + "detail": self.decision.detail, |
| 63 | + "deleted": self.deleted, |
| 64 | + } |
| 65 | + |
| 66 | + |
| 67 | +def untracked_changed( |
| 68 | + diff: GraphDiff, |
| 69 | + old_owner: dict[str, tuple[str, ...]], |
| 70 | + new_owner: dict[str, tuple[str, ...]], |
| 71 | + repair: RepairResult | None, |
| 72 | +) -> list[str]: |
| 73 | + """Changed or deleted ids with no (class-resolved) owner in either tree, |
| 74 | + minus added components the repair step deliberately left untracked.""" |
| 75 | + left_out = set() |
| 76 | + if repair is not None: |
| 77 | + left_out = {d.component_id for d in repair.routing if d.leaf_path is None} |
| 78 | + out = [] |
| 79 | + for cid in sorted(diff.changed_ids | set(diff.deleted)): |
| 80 | + if cid in left_out: |
| 81 | + continue |
| 82 | + if T.resolve_owner(new_owner, cid) or T.resolve_owner(old_owner, cid): |
| 83 | + continue |
| 84 | + out.append(cid) |
| 85 | + return out |
| 86 | + |
| 87 | + |
| 88 | +def close_ownership( |
| 89 | + diff: GraphDiff, |
| 90 | + old_tree: dict[str, Any], |
| 91 | + new_tree: dict[str, Any], |
| 92 | + old_graph: dict[str, Node], |
| 93 | + new_graph: dict[str, Node], |
| 94 | + opts: UpdateOptions, |
| 95 | + route_orphans: OrphanRouter | None = None, |
| 96 | + repair: RepairResult | None = None, |
| 97 | +) -> dict[str, Adoption]: |
| 98 | + """Return ``{component_id: Adoption}`` for every changed untracked component |
| 99 | + that rules 1-3, or the routing agent as rule 4, can give an owner.""" |
| 100 | + if not opts.use_ownership_closure: |
| 101 | + return {} |
| 102 | + old_owner = T.owner_map(old_tree) |
| 103 | + new_owner = T.owner_map(new_tree) |
| 104 | + candidates = untracked_changed(diff, old_owner, new_owner, repair) |
| 105 | + if not candidates: |
| 106 | + return {} |
| 107 | + |
| 108 | + adopted: dict[str, Adoption] = {} |
| 109 | + unplaced: list[str] = [] |
| 110 | + for cid in candidates: |
| 111 | + deleted = cid not in new_graph and cid in old_graph |
| 112 | + graph, owner = (old_graph, old_owner) if deleted else (new_graph, new_owner) |
| 113 | + node = graph.get(cid) |
| 114 | + if node is None: |
| 115 | + continue |
| 116 | + decision = route_by_rules(cid, node, owner, graph, opts) |
| 117 | + if decision is None: |
| 118 | + if deleted: |
| 119 | + continue # no page can answer for a vanished untracked component |
| 120 | + unplaced.append(cid) |
| 121 | + continue |
| 122 | + adopted[cid] = Adoption(decision, deleted) |
| 123 | + |
| 124 | + if unplaced and route_orphans is not None and opts.use_routing_agent: |
| 125 | + context = { |
| 126 | + "tree": new_tree, |
| 127 | + "owner": new_owner, |
| 128 | + "graph": new_graph, |
| 129 | + "purpose": PURPOSE_OWNERSHIP, |
| 130 | + } |
| 131 | + for d in route_orphans(unplaced, context): |
| 132 | + if d.component_id not in unplaced or d.component_id in adopted: |
| 133 | + continue |
| 134 | + if d.leaf_path is None or d.new_leaf or T.node_at(new_tree, d.leaf_path) is None: |
| 135 | + continue # left untracked, or a new leaf: the tree is not changed here |
| 136 | + d.rule = RULE_AGENT |
| 137 | + adopted[d.component_id] = Adoption(d) |
| 138 | + |
| 139 | + by_rule: dict[str, int] = {} |
| 140 | + for a in adopted.values(): |
| 141 | + by_rule[a.rule] = by_rule.get(a.rule, 0) + 1 |
| 142 | + logger.info( |
| 143 | + "Ownership closure: %d untracked changed, %d adopted %s, %d left as context", |
| 144 | + len(candidates), |
| 145 | + len(adopted), |
| 146 | + by_rule, |
| 147 | + len(candidates) - len(adopted), |
| 148 | + ) |
| 149 | + return adopted |
0 commit comments