Reviewer suggestion model
1. Required owner groups
For each changed file the effective rules are resolved (one on GitHub, one per section on GitLab). Every rule with owners, outside optional sections, yields a group: "any one of these owners must review". Identical owner lists merge across files, and the section's approval count becomes the number of distinct reviewers the group needs.
Group members are expanded through the roster (a team becomes its members, an email its user) and then filtered: the PR author, anyone on the vacation list on the reference date, and roster-inactive users are excluded, with the reason recorded and shown.
2. Expertise
For each candidate, over the changed files only:
raw = sum over their changes of (lines added + deleted) * 0.5 ** (age_days / half_life)
Default half-life is 90 days. Example: 1000 lines changed 365 days ago give 1000 * 0.5^(365/90) = 60, less than 100 lines changed yesterday (about 99). Expertise is normalised by the best candidate in the pool (norm = raw / max_raw, so 0..1).
3. Score
score = norm_expertise - load_penalty * open_reviews (load_penalty defaults to 0.1)
One open review costs a tenth of the best expert's expertise.
4. Greedy set cover
Repeat while some group still needs reviewers: pick the candidate who belongs to the most still-unsatisfied groups, breaking ties by score (then login, for determinism). Picking a candidate reduces the remaining need of every group they belong to by one. Finally, redundant picks are pruned if the groups they cover stay satisfied.
Worked example. Changes touch services/ledger (owner @alice), services/payments (@acme/payments = alice, bram, carla) and apps/web (@acme/web = gwen, hiro, ines). Alice belongs to two groups, so she is picked first, even with a low score; the payments group is then satisfied. Within web, the best score among gwen, hiro and ines wins. Result: two to three reviewers cover every group. If alice were the author or on vacation, the @alice group would be reported as uncovered, and carla or bram (by score) would cover payments.
Greedy set cover is not guaranteed minimal in general, but it is within a logarithmic factor and exact for the common case where groups are disjoint or nested.
5. Fallback
If no rule requires an owner (all files unowned or optional), the top-scoring eligible roster contributors to the touched files are suggested (default one), flagged as a fallback.
Generation
generate reuses the decay (half-life 180 days) per directory: share of a contributor = their decayed lines / total decayed lines of eligible contributors (active roster members with at least --min-commits commits there). A roster team whose members' shares sum to --team-share (0.6) replaces its members; otherwise individuals above --min-share (0.25) are listed, up to --max-owners. Directories shallower than --depth get a rule only when ownership is clear (a team, or one person with at least 50%); children are emitted only when their owners differ from the nearest ancestor's.