33
44Depth pass. The PR window is the pin's own first-parent history (bounded by
55``init.pr_window_count`` / ``pr_window_max_age_days``, measured from the
6- pin's commit time). Modules are taken by churn in that window, highest first;
7- a module's owner is the one that owns most of its files MOST SPECIFICALLY
8- (``init_coverage.most_specific``: a catch-all prefix never wins over a
9- component's), and a module whose owner already bears rules is skipped (it adds
10- nothing to the metric). For each other module one generator call reads its code at the
11- pin (byte-bounded, with line numbers) and proposes rules with line-ranged
12- evidence; the rules go through the shared screening — the docs redundancy
13- filter (D5), evidence inside the module, pinned claims, placement, the
14- advisory judge — and are appended to the owner's rule page (never editing an
15- existing rule; a full page spills to a sibling). The pass stops when the
16- rule-bearing PR-weighted coverage reaches ``init.coverage_target``, the budget
17- runs out, or no hot module is left.
6+ pin's commit time). The unit of work is a GROUP: the files of one module that
7+ share one most specific owner (``init_coverage.most_specific``: a catch-all
8+ prefix never wins over a component's), so a module whose files belong to
9+ several owners is deepened once per owner — a hot file is never left behind
10+ because its module's majority owner already has rules. Groups are taken by
11+ churn in the window, highest first; a group whose owner already bears rules is
12+ skipped (it adds nothing to the metric). For each other group one generator
13+ call reads the group's code at the pin (byte-bounded, with line numbers) and
14+ proposes rules with line-ranged evidence; the rules go through the shared
15+ screening — the docs redundancy filter (D5), evidence inside the group's
16+ files, pinned claims, placement, the advisory judge — and are appended to the
17+ owner's rule page (never editing an existing rule; a full page spills to a
18+ sibling). The pass stops when the rule-bearing PR-weighted coverage reaches
19+ ``init.coverage_target``, the budget runs out, or no hot group is left, and it
20+ always says which in the record's notes (and so the PR body): a pass never
21+ ends below the target without the reason and the hottest files still
22+ without rules.
1823
1924The same PR turns the repository's knowledge service on in shadow mode: a
2025textual edit of exactly the ``enabled`` / ``mode`` lines of the adapter's
4247MAX_CODE_BYTES = 100_000
4348MAX_CODE_FILES = 60
4449MAX_HOT_LIST = 30
50+ MAX_STOP_FILES = 5
4551_HEADER = re .compile (r"^knowledge_lifecycle:\s*(?:#.*)?$" )
4652_KEY_LINE = re .compile (r"^(?P<indent>[ \t]+)(?P<key>enabled|mode):(?P<space>[ \t]*)(?P<value>[^#\n]*?)"
4753 r"(?P<comment>[ \t]+#[^\n]*)?(?P<eol>\r?\n?)$" )
@@ -127,31 +133,46 @@ def _build(self, tree: Path) -> InitRecord:
127133 max_age_days = init .pr_window_max_age_days )
128134 include = make_include (init .source_roots , init .exclude , suffixes (language ))
129135 before = pr_weighted_coverage (prs , owners , include = include , rule_pages = self ._rule_pages (owners ))
130- churn = self ._churn (prs , modules , include )
136+ groups , unrouted = self ._groups (prs , modules , owners , include )
137+ if unrouted :
138+ self .record .notes .append (f"{ len (unrouted )} hot source file(s) no route reaches (modules stage), "
139+ f"no rules: " + ", " .join (f"{ p } ({ n } )" for p , n in unrouted [:MAX_STOP_FILES ]))
131140 self ._ids = self ._id_source ()
132141 written : dict [str , str ] = {}
133142 evidence = []
134143 current = before
135- for index , (key , count ) in enumerate (churn ):
136- if current .rule_bearing_ratio >= init .coverage_target :
137- self .record .notes .append (f"coverage target { init .coverage_target :.0%} reached; "
138- f"{ len (churn ) - index } hot module(s) left as they are" )
144+ target = init .coverage_target
145+ skipped = 0
146+ stop = ""
147+ for index , (key , owner , files , count ) in enumerate (groups ):
148+ if current .rule_bearing_ratio >= target :
149+ stop = (f"coverage target { target :.0%} reached; "
150+ f"{ len (groups ) - index } hot group(s) left as they are" )
139151 break
140- files = modules [key ]["files" ]
141- owner = self ._owner (files , owners )
142- if owner is None :
143- self .record .notes .append (f"module { key } : no route reaches it (modules stage), no rules" )
144- continue
145152 if owner .path in self ._rule_pages (owners ):
146- continue # its owner already bears rules: nothing to add to the metric
153+ skipped += 1 # its owner already bears rules: nothing to add to the metric
154+ continue
147155 try :
148156 kept = self ._deepen (tree , key , files , owner )
149157 except BudgetExhausted as exc :
150- self .record .unfinished += [f"module { k } : { exc } " for k , _ in churn [index :]]
158+ self .record .unfinished += [f"module { k } (owner { o .owner } ): { exc } " for k , o , _ , _ in groups [index :]]
159+ stop = (f"budget exhausted with { len (groups ) - index } hot group(s) left; "
160+ f"{ self ._short_of (current .rule_bearing_ratio , target )} " )
151161 break
152162 written .update ({c .rule_id : c .section for c in kept })
153163 evidence += [e for c in kept for e in c .evidence ]
154164 current = pr_weighted_coverage (prs , owners , include = include , rule_pages = self ._rule_pages (owners ))
165+ if not stop :
166+ stop = (f"coverage target { target :.0%} reached; no hot group left"
167+ if current .rule_bearing_ratio >= target
168+ else f"every hot group visited; { self ._short_of (current .rule_bearing_ratio , target )} " )
169+ if current .rule_bearing_ratio < target :
170+ hottest = self ._without_rules (prs , owners , include )[:MAX_STOP_FILES ]
171+ if hottest :
172+ stop += "; hottest files without rules: " + ", " .join (f"{ p } ({ n } )" for p , n in hottest )
173+ self .record .notes .append (f"deepen stopped: { stop } " )
174+ if skipped :
175+ self .record .notes .append (f"{ skipped } hot group(s) skipped: their owner already bears rules" )
155176 self .record .coverage = {
156177 "pr_routed" : {"before" : round (before .routed_ratio , 4 ), "after" : round (current .routed_ratio , 4 )},
157178 "pr_rule_bearing" : {"before" : round (before .rule_bearing_ratio , 4 ),
@@ -168,12 +189,36 @@ def _build(self, tree: Path) -> InitRecord:
168189
169190 # -- the window ----------------------------------------------------------------
170191 @staticmethod
171- def _churn (prs , modules , include ) -> list [tuple [str , int ]]:
172- """Modules by source-file changes in the window, highest first."""
173- from .init_coverage import churn_by_module
192+ def _short_of (ratio : float , target : float ) -> str :
193+ return f"target not reached ({ ratio :.0%} < { target :.0%} )"
174194
175- filtered = [[f for f in files if include (f )] for files in prs ]
176- return [(k , n ) for k , n in churn_by_module (filtered , modules ) if n > 0 ]
195+ @staticmethod
196+ def _groups (prs , modules , owners : list [Owner ], include
197+ ) -> tuple [list [tuple [str , Owner , list [str ], int ]], list [tuple [str , int ]]]:
198+ """The hot groups — ``(module, owner, files, churn)`` for every module
199+ and most specific owner of some of its files, churn = changes of those
200+ files in the window — highest churn first (ties by module, then owner),
201+ and the changed source files of modules no route reaches, by churn. A
202+ file several owners own equally goes to the first in routing order."""
203+ changes : Counter [str ] = Counter ()
204+ for files in prs :
205+ for path in {PurePosixPath (f .strip ().strip ("/" )).as_posix () for f in files }:
206+ if include (path ):
207+ changes [path ] += 1
208+ grouped : dict [tuple [str , str ], tuple [Owner , list [str ]]] = {}
209+ unrouted : Counter [str ] = Counter ()
210+ for key in sorted (modules ):
211+ for path in modules [key ]["files" ]:
212+ owner = next (iter (most_specific (path , owners )), None )
213+ if owner is None :
214+ if changes [path ]:
215+ unrouted [path ] = changes [path ]
216+ continue
217+ grouped .setdefault ((key , owner .owner ), (owner , []))[1 ].append (path )
218+ groups = [(key , owner , files , sum (changes [f ] for f in files ))
219+ for (key , _ ), (owner , files ) in grouped .items ()]
220+ groups = sorted ((g for g in groups if g [3 ] > 0 ), key = lambda g : (- g [3 ], g [0 ], g [1 ].owner ))
221+ return groups , sorted (unrouted .items (), key = lambda item : (- item [1 ], item [0 ]))
177222
178223 def _without_rules (self , prs , owners : list [Owner ], include ) -> list [tuple [str , int ]]:
179224 """Changed source files of the window whose most specific owners bear
@@ -186,17 +231,6 @@ def _without_rules(self, prs, owners: list[Owner], include) -> list[tuple[str, i
186231 missed [path ] += 1
187232 return sorted (missed .items (), key = lambda item : (- item [1 ], item [0 ]))
188233
189- @staticmethod
190- def _owner (files : list [str ], owners : list [Owner ]) -> Owner | None :
191- """The owner that owns most of a module's files most specifically
192- (routing order breaks ties): a broader owner that merely also reaches
193- them never takes a component's rules."""
194- counts = {o .owner : 0 for o in owners }
195- for path in files :
196- for owner in most_specific (path , owners ):
197- counts [owner .owner ] += 1
198- return max ((o for o in owners if counts [o .owner ]), key = lambda o : counts [o .owner ], default = None )
199-
200234 # -- rule pages ------------------------------------------------------------------
201235 def _active_rules (self , path : str ) -> int :
202236 text = self .head .get (path )
@@ -285,7 +319,7 @@ def validate(data: dict) -> None:
285319 for candidate in self ._to_candidates (data , page , f"code:{ key } " ):
286320 outside = [e .get ("path" ) for e in candidate .evidence if str (e .get ("path" ) or "" ) not in inside ]
287321 if outside :
288- self ._drop (candidate , f"evidence outside module { key } : { outside } " )
322+ self ._drop (candidate , f"evidence outside module { key } 's files owned by { owner . owner } : { outside } " )
289323 continue
290324 candidates .append (candidate )
291325 kept = self ._screen (candidates )
0 commit comments