codereview-loop 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """codereview panel diversity — Epic E3 enforcement.
3
+
4
+ The check operates on the COMPOSED panel for a run — the seats
5
+ handed to core.run_review, before any of them is called — and
6
+ answers two questions:
7
+
8
+ 1. Does the panel span at least N distinct vendor families?
9
+ (N = `[diversity] min_families` in the project policy.)
10
+ 2. If the builder's own vendor family is on the panel, can it
11
+ be excluded without dropping the panel below that minimum?
12
+ (Only when the policy opts in: `[diversity]
13
+ exclude_builder_family`.)
14
+
15
+ Families come from seat metadata — the registry family tags the
16
+ seats were built with, or the operator's --seat-meta override —
17
+ never from a seat's display name. A seat whose family is
18
+ 'unknown' (a fully custom spec) never counts toward the minimum:
19
+ the same convention policy.evaluate_policy applies to its
20
+ post-hoc `panel.min_families` rule. The two rules are distinct
21
+ on purpose: [diversity] judges the panel a run BRINGS (this
22
+ module, at composition); [panel] min_families judges the seats
23
+ that actually SERVED (policy.evaluate_policy, after the round).
24
+
25
+ Builder identity: run_review's `builder_family` argument is the
26
+ only input. No run path in the CLI knows the builder's family
27
+ today — a --builder-cmd is an opaque command string — so there
28
+ the exclusion simply never engages; this module never invents a
29
+ builder identity from a command line, an author string, or a
30
+ diff. When a future caller knows the family, it passes it and
31
+ the exclusion applies, recorded like everything else here.
32
+
33
+ Exclusion is a composition act, not a lens act: an excluded
34
+ seat is removed from the panel before tasks are derived, and
35
+ its lens is NEVER reassigned by this module. With an explicit
36
+ seat->lens binding, the existing S1 machinery treats the
37
+ excluded seat exactly like any absent seat — the lens is served
38
+ by its configured fallback or the run degrades with a
39
+ 'lens unserved' cause. With legacy seat-order assignment, the
40
+ remaining seats fill the lenses by the ordinary rule.
41
+
42
+ Outcomes (core.run_review wires them): the assessment is
43
+ recorded verbatim in the report's `diversity` section and as an
44
+ applied `diversity.min_families` rule in the policy section. A
45
+ violation labels the run degraded with the cause
46
+ 'panel diversity below minimum'. Under `[diversity] enforce =
47
+ "degrade"` (the default) that is all: the violation is recorded
48
+ as an ADVISORY policy violation and cannot change the gate.
49
+ Under `enforce = "block"` it is additionally a blocking policy
50
+ violation, gate-changing exactly like any other.
51
+
52
+ With no [diversity] configuration (or no policy at all) the
53
+ check does not run: no record section, no applied rule, no
54
+ behaviour change — byte-identical to before Epic E3.
55
+
56
+ Deterministic and pure: no model calls, no I/O. Verification
57
+ rounds are out of scope — run_verification carries no project
58
+ policy today, so there is no diversity surface there either.
59
+ """
60
+
61
+ from __future__ import annotations
62
+
63
+ UNKNOWN_FAMILY = "unknown"
64
+
65
+
66
+ def _norm_family(fam):
67
+ """P2-03: family tags are operator-typed and compared
68
+ case-insensitively — 'OpenAI', 'openai' and ' openai ' are
69
+ the one family. Normalisation lives ONLY at this boundary:
70
+ counting and exclusion compare normalised forms, and the
71
+ tags stored anywhere else are never rewritten."""
72
+ return str(fam).strip().casefold() if fam else ""
73
+
74
+
75
+ def known_families(seat_families):
76
+ """The distinct KNOWN vendor families in a {seat: family}
77
+ mapping, sorted. 'unknown'/empty tags never count — a panel
78
+ of untagged seats has zero provable diversity."""
79
+ return sorted({fam for fam in
80
+ (_norm_family(f) for f in seat_families.values())
81
+ if fam and fam != UNKNOWN_FAMILY})
82
+
83
+
84
+ def policy_config(policy):
85
+ """The effective [diversity] configuration of a loaded
86
+ policy dict, or None when the check is not configured (no
87
+ policy, no table, or the table at its defaults: minimum 0
88
+ and exclusion off). `enforce` defaults to 'degrade'."""
89
+ if not policy:
90
+ return None
91
+ div = policy.get("diversity") or {}
92
+ min_families = div.get("min_families") or 0
93
+ exclude = bool(div.get("exclude_builder_family"))
94
+ if not min_families and not exclude:
95
+ return None
96
+ return {"min_families": int(min_families),
97
+ "exclude_builder_family": exclude,
98
+ "enforce": div.get("enforce") or "degrade"}
99
+
100
+
101
+ def assess_panel(seat_families, *, min_families=0,
102
+ builder_family=None, exclude_builder=False):
103
+ """Assess one composed panel. `seat_families` maps seat
104
+ name -> family tag for every seat on the panel.
105
+
106
+ Returns the record-ready assessment:
107
+
108
+ min_families the configured minimum (0 = none)
109
+ builder_family the builder's family as given (or None)
110
+ families_before distinct known families, pre-exclusion
111
+ families distinct known families, post-exclusion
112
+ excluded_seats builder-family seats removed (sorted)
113
+ builder_excluded True when exclusion actually happened
114
+ builder_kept_reason why exclusion did not happen, when
115
+ it was requested but did not apply (None
116
+ when it applied or was not requested, and
117
+ when no panel seat carries the builder's
118
+ family — nothing was there to keep)
119
+ violation families < min_families (min > 0)
120
+ detail the one-line verdict, used verbatim in
121
+ the policy violation when there is one
122
+ """
123
+ # P2-03: comparisons run on normalised family tags (see
124
+ # _norm_family); `bf` keeps the as-given form for the record.
125
+ seats = {name: _norm_family(fam)
126
+ for name, fam in seat_families.items()}
127
+ families_before = known_families(seats)
128
+ excluded = []
129
+ kept_reason = None
130
+ bf = str(builder_family).strip() if builder_family else None
131
+ bf_norm = _norm_family(builder_family)
132
+ if exclude_builder:
133
+ if not bf_norm or bf_norm == UNKNOWN_FAMILY:
134
+ kept_reason = ("builder family unknown — exclusion "
135
+ "cannot apply")
136
+ else:
137
+ builder_seats = sorted(
138
+ name for name, fam in seats.items()
139
+ if fam == bf_norm)
140
+ if builder_seats:
141
+ rest = {name: fam for name, fam in seats.items()
142
+ if fam != bf_norm}
143
+ if rest and len(known_families(rest)) \
144
+ >= min_families:
145
+ excluded = builder_seats
146
+ seats = rest
147
+ elif not rest:
148
+ kept_reason = ("excluding the builder's "
149
+ "family would empty the panel")
150
+ else:
151
+ kept_reason = (
152
+ "excluding the builder's family would "
153
+ "leave the panel below the configured "
154
+ f"minimum of {min_families} distinct "
155
+ "families")
156
+ families = known_families(seats)
157
+ violation = min_families > 0 and len(families) < min_families
158
+ if violation:
159
+ detail = (
160
+ f"composed panel spans {len(families)} distinct "
161
+ f"vendor families ({', '.join(families) or 'none'}); "
162
+ f"policy requires at least {min_families}")
163
+ elif min_families > 0:
164
+ detail = (
165
+ f"composed panel spans {len(families)} distinct "
166
+ f"vendor families ({', '.join(families) or 'none'}); "
167
+ f"policy minimum of {min_families} met")
168
+ else:
169
+ detail = (
170
+ f"composed panel spans {len(families)} distinct "
171
+ f"vendor families ({', '.join(families) or 'none'}); "
172
+ "no minimum configured")
173
+ return {
174
+ "min_families": int(min_families),
175
+ "builder_family": bf,
176
+ "families_before": families_before,
177
+ "families": families,
178
+ "excluded_seats": excluded,
179
+ "builder_excluded": bool(excluded),
180
+ "builder_kept_reason": kept_reason,
181
+ "violation": violation,
182
+ "detail": detail,
183
+ }