99from typing import Any
1010
1111from codewiki .src .be .backend import LLMBackend
12- from codewiki .src .be .cluster_modules import cluster_modules
12+ from codewiki .src .be .cluster_modules import cluster_modules , get_clustering_input_token_count
1313from codewiki .src .be .dependency_analyzer .models .core import Node
1414from codewiki .src .be .updater import pages as P
1515from codewiki .src .be .updater import tree as T
4141)
4242from codewiki .src .be .updater .routing import RoutingAgent
4343from codewiki .src .be .updater .stale_scan import StaleScanner
44- from codewiki .src .be .updater .tree_repair import RepairResult , repair_tree
44+ from codewiki .src .be .updater .tree_repair import (
45+ RULE_AGENT ,
46+ RepairResult ,
47+ RoutingDecision ,
48+ repair_tree ,
49+ )
4550from codewiki .src .config import MODULE_TREE_FILENAME , Config
4651from codewiki .src .utils import file_manager
4752
@@ -85,6 +90,17 @@ def _load_state(self, old_graph_path: str) -> tuple[dict[str, Node], dict[str, A
8590 self .record .detector_notes .append ("whole-repository mode: one virtual leaf (overview)" )
8691 return old_graph , tree
8792
93+ def _route_to_overview (
94+ self , orphans : list [str ], context : dict [str , Any ]
95+ ) -> list [RoutingDecision ]:
96+ """Whole-repository mode: every orphan belongs to the single page."""
97+ return [
98+ RoutingDecision (
99+ cid , RULE_AGENT , (P .OVERVIEW_STEM ,), detail = "whole-repository mode: single page"
100+ )
101+ for cid in orphans
102+ ]
103+
88104 def _module_path (self , path : tuple [str , ...]) -> list [str ]:
89105 if self .whole_repo and path == (P .OVERVIEW_STEM ,):
90106 return []
@@ -253,6 +269,12 @@ async def _run(
253269 if self .opts .use_routing_agent
254270 else None
255271 )
272+ if self .whole_repo :
273+ # One page documents everything: orphans go to the virtual leaf
274+ # without an LLM call and without creating leaves. Each created
275+ # leaf would otherwise map back onto the overview page and rewrite
276+ # it once more in Step 5 (issue #113).
277+ router = self ._route_to_overview
256278 repair : RepairResult = repair_tree (
257279 old_tree , diff , new_graph , tracked_new , self .opts , route_orphans = router
258280 )
@@ -288,11 +310,43 @@ async def _run(
288310 ratios ["fired" ] = (
289311 ratios ["r_leaf" ] >= self .opts .tau_full or ratios ["r_tree" ] >= self .opts .tau_tree
290312 )
291- if self .whole_repo and ratios ["fired" ]:
292- # One virtual leaf: any change is 100% active by construction, and
293- # patching that single page is exactly the incremental step.
294- ratios ["fired" ] = False
295- ratios ["note" ] = "whole-repository mode: fallback rule not applied"
313+ if self .whole_repo :
314+ # One virtual leaf: any change is 100% active by construction, so
315+ # r_leaf says nothing. What matters is whether the *current* scope
316+ # still fits one page under the same threshold a fresh build uses
317+ # to skip clustering. A whole-repo baseline built from a narrow
318+ # --include and then updated against the full repo does not, and
319+ # feeding that to the single-page agent runs unbounded (issue #113).
320+ scope = [c for c in leaf_nodes if c in new_graph ]
321+ tokens = get_clustering_input_token_count (scope , new_graph )
322+ threshold = self .config .max_token_per_module
323+ ratios ["clustering_tokens" ] = tokens
324+ ratios ["tau_cluster" ] = threshold
325+ if tokens <= threshold :
326+ ratios ["fired" ] = False
327+ note = "whole-repository mode: scope still fits one page; fallback rule not applied"
328+ ratios ["note" ] = note
329+ logger .info (
330+ "Whole-repository mode: current scope is %d clustering tokens "
331+ "(threshold %d, %d leaf nodes); updating the single page in place" ,
332+ tokens ,
333+ threshold ,
334+ len (scope ),
335+ )
336+ else :
337+ ratios ["fired" ] = True
338+ ratios ["note" ] = (
339+ "whole-repository baseline but current scope exceeds the clustering "
340+ "threshold; a fresh build would cluster, so fall back to a full build"
341+ )
342+ rec .detector_notes .append (ratios ["note" ])
343+ logger .warning (
344+ "Whole-repository baseline cannot be updated in place: current scope is "
345+ "%d clustering tokens (threshold %d, %d leaf nodes); falling back to a full build" ,
346+ tokens ,
347+ threshold ,
348+ len (scope ),
349+ )
296350 rec .fallback = ratios
297351 if ratios ["fired" ]:
298352 rec .outcome = OUTCOME_FULL_FALLBACK
@@ -311,6 +365,13 @@ async def _run(
311365
312366 # ---- Step 5: sequential leaf agents
313367 order = order_active (active , new_tree , new_graph )
368+ if self .whole_repo and len (order ) > 1 :
369+ # Every active unit is the same overview page; regenerate it once.
370+ overview_path = (P .OVERVIEW_STEM ,)
371+ order = [overview_path if overview_path in reports else order [0 ]]
372+ rec .detector_notes .append (
373+ f"whole-repository mode: { len (active )} active units collapsed into one overview run"
374+ )
314375 dep = T .leaf_dependents (new_tree , new_graph )
315376 inv = inverse (ref_index )
316377 rec .active = [
0 commit comments