88 ``scope_prefixes`` in ``_routes.yaml``;
99* **PR-weighted coverage** — over a window of merged PRs, the share of changed
1010 source files (counted once per PR that changed them) that route to an owner
11- (*routed*), and to an owner whose page carries rules (*rule-bearing*).
11+ (*routed*), and whose MOST SPECIFIC owner's page carries rules
12+ (*rule-bearing*; see ``most_specific``).
13+
14+ Specificity matters because prefixes nest: a catch-all owner (``pkg/``) reaches
15+ every file a component owner (``pkg/sub/``) reaches. If any matching owner's
16+ rules counted, one rule on the catch-all page would make the whole package
17+ rule-bearing and the metric would stop measuring anything (kb init pilot,
18+ 2026-09-30: 10% -> 100% from one module's rules).
1219
1320An empty population counts as fully covered (ratio 1.0): there is nothing a
1421route could miss.
@@ -80,6 +87,49 @@ def routes_file(path: str, owners: Sequence[Owner]) -> list[Owner]:
8087 return [o for o in owners if any (path .startswith (p ) for p in o .prefixes )]
8188
8289
90+ def _page_depth (owner : Owner ) -> int :
91+ return len (PurePosixPath (owner .path ).parts )
92+
93+
94+ def most_specific (path : str , owners : Sequence [Owner ]) -> list [Owner ]:
95+ """The owners that own ``path`` most specifically, in routing order: those
96+ whose longest matching prefix is the longest of all matches; among those,
97+ the ones whose page lies deepest in the knowledge tree (a component's own
98+ page, not an aggregate entry page that lists the same area). Ties on both
99+ return every tied owner; no match returns []."""
100+ best : tuple [int , int ] | None = None
101+ out : list [Owner ] = []
102+ for owner in owners :
103+ length = max ((len (p ) for p in owner .prefixes if path .startswith (p )), default = - 1 )
104+ if length < 0 :
105+ continue
106+ key = (length , _page_depth (owner ))
107+ if best is None or key > best :
108+ best , out = key , [owner ]
109+ elif key == best :
110+ out .append (owner )
111+ return out
112+
113+
114+ def shadowing (owners : Sequence [Owner ]) -> list [tuple [str , str , str , str ]]:
115+ """``(owner, prefix, other owner, other prefix)`` for every prefix that is a
116+ strict ancestor of another owner's prefix: the broader owner still routes
117+ those files, but only the more specific one counts for them (and gets
118+ their rules)."""
119+ out = []
120+ for owner in owners :
121+ for prefix in owner .prefixes :
122+ if not prefix .endswith ("/" ):
123+ continue # a file prefix names one file, never an ancestor
124+ for other in owners :
125+ if other is owner :
126+ continue
127+ for inner in other .prefixes :
128+ if inner != prefix and inner .startswith (prefix ):
129+ out .append ((owner .owner , prefix , other .owner , inner ))
130+ return out
131+
132+
83133def module_coverage (modules : Mapping [str , Mapping ], owners : Sequence [Owner ]) -> Coverage :
84134 covered , uncovered = [], []
85135 for key in sorted (modules ):
@@ -121,9 +171,12 @@ def include(path: str) -> bool:
121171def pr_weighted_coverage (prs : Sequence [Sequence [str ]], owners : Sequence [Owner ], * ,
122172 include : Callable [[str ], bool ],
123173 rule_pages : set [str ] | None = None ) -> PrCoverage :
124- """Each file a PR changes counts once for that PR. ``rule_pages`` are the
125- knowledge paths (as ``_routes.yaml`` names them) holding at least one
126- active rule; without them nothing is rule-bearing."""
174+ """Each file a PR changes counts once for that PR. A file is *routed* when
175+ any owner reaches it, and *rule-bearing* when one of its ``most_specific``
176+ owners has its path in ``rule_pages`` (the knowledge paths, as
177+ ``_routes.yaml`` names them, holding at least one active rule; without
178+ them nothing is rule-bearing). A broader owner's rules never count for a
179+ file a more specific owner reaches."""
127180 pages = rule_pages or set ()
128181 total = routed = rule_bearing = 0
129182 missed : Counter [str ] = Counter ()
@@ -135,7 +188,7 @@ def pr_weighted_coverage(prs: Sequence[Sequence[str]], owners: Sequence[Owner],
135188 hits = routes_file (path , owners )
136189 if hits :
137190 routed += 1
138- if any (o .path in pages for o in hits ):
191+ if any (o .path in pages for o in most_specific ( path , hits ) ):
139192 rule_bearing += 1
140193 else :
141194 missed [path ] += 1
0 commit comments