repobench 0.1.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.
Files changed (56) hide show
  1. agentfit/__init__.py +3 -0
  2. agentfit/analysis/__init__.py +0 -0
  3. agentfit/analysis/metrics.py +58 -0
  4. agentfit/analysis/recommendation.py +104 -0
  5. agentfit/analysis/statistics.py +71 -0
  6. agentfit/benchmark/__init__.py +0 -0
  7. agentfit/benchmark/coverage.py +51 -0
  8. agentfit/benchmark/health.py +191 -0
  9. agentfit/benchmark/sampling.py +152 -0
  10. agentfit/cli/__init__.py +0 -0
  11. agentfit/cli/analyze.py +576 -0
  12. agentfit/cli/app.py +210 -0
  13. agentfit/cli/benchmark.py +295 -0
  14. agentfit/cli/candidates.py +119 -0
  15. agentfit/cli/config_cmd.py +106 -0
  16. agentfit/cli/doctor.py +240 -0
  17. agentfit/cli/init.py +284 -0
  18. agentfit/cli/report.py +224 -0
  19. agentfit/cli/run.py +358 -0
  20. agentfit/cli/task.py +169 -0
  21. agentfit/cli/telemetry.py +103 -0
  22. agentfit/cli/utils.py +46 -0
  23. agentfit/config.py +32 -0
  24. agentfit/harbor/__init__.py +0 -0
  25. agentfit/harbor/exporter.py +206 -0
  26. agentfit/harbor/parser.py +109 -0
  27. agentfit/harbor/runner.py +321 -0
  28. agentfit/logging.py +46 -0
  29. agentfit/mining/__init__.py +0 -0
  30. agentfit/mining/candidates.py +219 -0
  31. agentfit/models.py +375 -0
  32. agentfit/reporting/__init__.py +0 -0
  33. agentfit/reporting/json.py +61 -0
  34. agentfit/reporting/terminal.py +171 -0
  35. agentfit/repository/__init__.py +0 -0
  36. agentfit/repository/detection.py +424 -0
  37. agentfit/repository/git.py +309 -0
  38. agentfit/repository/github.py +261 -0
  39. agentfit/repository/workload.py +339 -0
  40. agentfit/storage/__init__.py +0 -0
  41. agentfit/storage/database.py +486 -0
  42. agentfit/storage/migrations/__init__.py +0 -0
  43. agentfit/tasks/__init__.py +0 -0
  44. agentfit/tasks/instruction.py +165 -0
  45. agentfit/tasks/leakage.py +139 -0
  46. agentfit/tasks/verifier.py +220 -0
  47. agentfit/utils.py +80 -0
  48. agentfit/validation/__init__.py +0 -0
  49. agentfit/validation/determinism.py +52 -0
  50. agentfit/validation/environment.py +160 -0
  51. agentfit/validation/noop.py +81 -0
  52. agentfit/validation/oracle.py +102 -0
  53. repobench-0.1.0.dist-info/METADATA +247 -0
  54. repobench-0.1.0.dist-info/RECORD +56 -0
  55. repobench-0.1.0.dist-info/WHEEL +4 -0
  56. repobench-0.1.0.dist-info/entry_points.txt +2 -0
agentfit/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """AgentFit — Living repository-native evals for coding agents."""
2
+
3
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,58 @@
1
+ """Configuration metrics aggregation from trials."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import statistics
6
+ from typing import Iterable
7
+
8
+ from agentfit.logging import get_logger
9
+ from agentfit.models import ConfigMetrics, Trial
10
+ from agentfit.analysis.statistics import wilson_ci
11
+
12
+ log = get_logger("analysis.metrics")
13
+
14
+
15
+ def compute_config_metrics(trials: Iterable[Trial]) -> ConfigMetrics:
16
+ """Compute aggregated metrics for one agent configuration."""
17
+ trials = list(trials)
18
+ metrics = ConfigMetrics()
19
+
20
+ if not trials:
21
+ return metrics
22
+
23
+ solved = [t for t in trials if t.solved]
24
+ metrics.solved = len(solved)
25
+ metrics.total = len(trials)
26
+ metrics.pass_rate = len(solved) / len(trials)
27
+
28
+ # Wilson 95% CI
29
+ ci_lower, ci_upper = wilson_ci(len(solved), len(trials))
30
+ metrics.ci_lower = ci_lower
31
+ metrics.ci_upper = ci_upper
32
+
33
+ # Economics
34
+ costs = [t.cost_usd for t in trials if t.cost_usd is not None]
35
+ if costs:
36
+ metrics.total_cost = sum(costs)
37
+ metrics.mean_cost_task = metrics.total_cost / len(costs)
38
+ if metrics.solved > 0:
39
+ metrics.cost_per_solve = metrics.total_cost / metrics.solved
40
+
41
+ # Efficiency
42
+ prompt_tokens = [t.prompt_tokens for t in trials if t.prompt_tokens is not None]
43
+ completion_tokens = [t.completion_tokens for t in trials if t.completion_tokens is not None]
44
+ metrics.total_prompt_tokens = sum(prompt_tokens)
45
+ metrics.total_completion_tokens = sum(completion_tokens)
46
+ if metrics.solved > 0:
47
+ total_tokens = metrics.total_prompt_tokens + metrics.total_completion_tokens
48
+ metrics.tokens_per_solve = round(total_tokens / metrics.solved)
49
+
50
+ # Performance
51
+ durations = [t.duration_ms for t in trials if t.duration_ms is not None]
52
+ if durations:
53
+ metrics.p50_duration_ms = round(statistics.median(durations))
54
+ sorted_d = sorted(durations)
55
+ p90_idx = min(len(sorted_d) - 1, round(0.9 * (len(sorted_d) - 1)))
56
+ metrics.p90_duration_ms = sorted_d[p90_idx]
57
+
58
+ return metrics
@@ -0,0 +1,104 @@
1
+ """Cost-aware recommendation logic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterable
6
+
7
+ from agentfit.logging import get_logger
8
+ from agentfit.models import ConfigMetrics
9
+
10
+ log = get_logger("analysis.recommendation")
11
+
12
+
13
+ def recommend(
14
+ metrics_dict: dict[str, ConfigMetrics],
15
+ ) -> tuple[str | None, str]:
16
+ """Recommend a default agent configuration.
17
+
18
+ Policy (``cost_effective``):
19
+ 1. Find the configuration with the highest observed pass rate.
20
+ 2. Identify configurations whose quality difference is not
21
+ statistically conclusive (overlapping Wilson CIs).
22
+ 3. Within that set, choose the lowest cost per verified solve.
23
+
24
+ Returns (config_name, reason).
25
+ """
26
+ if not metrics_dict:
27
+ return None, "No configurations with results available."
28
+
29
+ # Consider only configs with trials
30
+ with_results = {name: m for name, m in metrics_dict.items() if m.total > 0}
31
+ if not with_results:
32
+ return None, "No configurations have completed trials."
33
+
34
+ # 1. Best observed pass rate
35
+ best_name = max(with_results, key=lambda n: with_results[n].pass_rate)
36
+ best = with_results[best_name]
37
+
38
+ # 2. Statistically indistinguishable set (overlapping Wilson CIs)
39
+ indistinguishable = []
40
+ for name, m in with_results.items():
41
+ if _cis_overlap(best, m):
42
+ indistinguishable.append(name)
43
+
44
+ # 3. Lowest cost per verified solve among indistinguishable
45
+ with_cost = [
46
+ (name, with_results[name])
47
+ for name in indistinguishable
48
+ if with_results[name].cost_per_solve is not None
49
+ ]
50
+
51
+ if with_cost:
52
+ recommended = min(with_cost, key=lambda nm: nm[1].cost_per_solve)[0]
53
+ reason = (
54
+ f"lowest cost per verified solve among configurations "
55
+ f"statistically indistinguishable from observed best quality."
56
+ )
57
+ else:
58
+ # No cost data: pick best observed pass rate
59
+ recommended = best_name
60
+ reason = "best observed pass rate (no cost data available)."
61
+
62
+ log.info("Recommendation: %s (%s)", recommended, reason)
63
+ return recommended, reason
64
+
65
+
66
+ def pareto_frontier(
67
+ metrics_dict: dict[str, ConfigMetrics],
68
+ ) -> list[str]:
69
+ """Identify configurations on the Pareto frontier.
70
+
71
+ A configuration is dominated if another config has >= pass rate AND
72
+ <= cost per solve (with at least one strict inequality).
73
+
74
+ Returns config names on the frontier (highest quality first).
75
+ """
76
+ with_cost = {
77
+ name: m for name, m in metrics_dict.items()
78
+ if m.total > 0 and m.cost_per_solve is not None
79
+ }
80
+ if not with_cost:
81
+ return list(metrics_dict.keys())
82
+
83
+ frontier: list[str] = []
84
+ for name, m in with_cost.items():
85
+ dominated = False
86
+ for other_name, other in with_cost.items():
87
+ if other_name == name:
88
+ continue
89
+ if (other.pass_rate >= m.pass_rate
90
+ and other.cost_per_solve <= m.cost_per_solve
91
+ and (other.pass_rate > m.pass_rate
92
+ or other.cost_per_solve < m.cost_per_solve)):
93
+ dominated = True
94
+ break
95
+ if not dominated:
96
+ frontier.append(name)
97
+
98
+ frontier.sort(key=lambda n: with_cost[n].pass_rate, reverse=True)
99
+ return frontier
100
+
101
+
102
+ def _cis_overlap(a: ConfigMetrics, b: ConfigMetrics) -> bool:
103
+ """Check if two Wilson CIs overlap (statistically indistinguishable)."""
104
+ return not (a.ci_upper < b.ci_lower or b.ci_upper < a.ci_lower)
@@ -0,0 +1,71 @@
1
+ """Statistical utilities: Wilson CI and paired bootstrap."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import random
7
+ from typing import Iterable, Sequence
8
+
9
+ from agentfit.logging import get_logger
10
+
11
+ log = get_logger("analysis.statistics")
12
+
13
+ _Z_95 = 1.959963984540054 # z-value for 95% confidence
14
+
15
+
16
+ def wilson_ci(successes: int, total: int, z: float = _Z_95) -> tuple[float, float]:
17
+ """Wilson score interval for a binomial proportion.
18
+
19
+ Returns (lower, upper) bounds. If total is 0, returns (0, 0).
20
+ """
21
+ if total <= 0:
22
+ return 0.0, 0.0
23
+
24
+ p = successes / total
25
+ z2 = z * z
26
+ denom = 1 + z2 / total
27
+ center = (p + z2 / (2 * total)) / denom
28
+ margin = z * math.sqrt(p * (1 - p) / total + z2 / (4 * total * total)) / denom
29
+
30
+ return max(0.0, center - margin), min(1.0, center + margin)
31
+
32
+
33
+ def paired_bootstrap_difference(
34
+ outcomes_a: Sequence[bool],
35
+ outcomes_b: Sequence[bool],
36
+ n_bootstrap: int = 10_000,
37
+ seed: int = 42,
38
+ ) -> tuple[float, float, float]:
39
+ """Paired bootstrap difference of pass rates.
40
+
41
+ Performs a paired bootstrap (resampling task indices with replacement)
42
+ with a deterministic seed.
43
+
44
+ Returns (observed_difference_pp, ci_lower_pp, ci_upper_pp) where
45
+ difference = pass_rate_a - pass_rate_b (in percentage points).
46
+ """
47
+ if len(outcomes_a) != len(outcomes_b):
48
+ raise ValueError("Paired bootstrap requires equal-length outcome arrays")
49
+
50
+ n = len(outcomes_a)
51
+ if n == 0:
52
+ return 0.0, 0.0, 0.0
53
+
54
+ a = [1 if x else 0 for x in outcomes_a]
55
+ b = [1 if x else 0 for x in outcomes_b]
56
+
57
+ observed_diff = (sum(a) - sum(b)) / n * 100
58
+
59
+ rng = random.Random(seed)
60
+ diffs: list[float] = []
61
+ for _ in range(n_bootstrap):
62
+ idx = [rng.randrange(n) for _ in range(n)]
63
+ sum_a = sum(a[i] for i in idx)
64
+ sum_b = sum(b[i] for i in idx)
65
+ diffs.append((sum_a - sum_b) / n * 100)
66
+
67
+ diffs.sort()
68
+ lo_idx = max(0, int(0.025 * n_bootstrap) - 1)
69
+ hi_idx = min(n_bootstrap - 1, int(0.975 * n_bootstrap) - 1)
70
+
71
+ return observed_diff, diffs[lo_idx], diffs[hi_idx]
File without changes
@@ -0,0 +1,51 @@
1
+ """Coverage calculation: TVD converted to coverage percentages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from typing import Any
7
+
8
+
9
+ def calculate_coverage(
10
+ benchmark_dist: dict[str, dict[str, float]],
11
+ workload_dist: dict[str, dict[str, float]],
12
+ ) -> dict[str, float]:
13
+ """Calculate coverage per dimension.
14
+
15
+ Args:
16
+ benchmark_dist: {dimension: {category: probability}}
17
+ workload_dist: {dimension: {category: probability}}
18
+
19
+ Returns:
20
+ {dimension: coverage_percentage} where coverage = 100 * (1 - TVD).
21
+ """
22
+ coverage: dict[str, float] = {}
23
+
24
+ all_dims = set(benchmark_dist.keys()) | set(workload_dist.keys())
25
+ for dim in all_dims:
26
+ b = benchmark_dist.get(dim, {})
27
+ w = workload_dist.get(dim, {})
28
+ tvd = _tvd_from_dicts(b, w)
29
+ coverage[dim] = round(100 * (1 - tvd), 1)
30
+
31
+ return coverage
32
+
33
+
34
+ def distributions_from_counts(
35
+ benchmark_counts: dict[str, Counter],
36
+ workload_counts: dict[str, Counter],
37
+ ) -> dict[str, float]:
38
+ """Convenience: compute coverage from Counter objects."""
39
+ b = {dim: {k: c / sum(c.values()) if sum(c.values()) else 0.0
40
+ for k, c in counts.items()} for dim, counts in benchmark_counts.items()}
41
+ w = {dim: {k: c / sum(c.values()) if sum(c.values()) else 0.0
42
+ for k, c in counts.items()} for dim, counts in workload_counts.items()}
43
+ return calculate_coverage(b, w)
44
+
45
+
46
+ def _tvd_from_dicts(bench: dict[str, float], workload: dict[str, float]) -> float:
47
+ """Total Variation Distance between two probability dicts."""
48
+ keys = set(bench.keys()) | set(workload.keys())
49
+ if not keys:
50
+ return 0.0
51
+ return 0.5 * sum(abs(bench.get(k, 0.0) - workload.get(k, 0.0)) for k in keys)
@@ -0,0 +1,191 @@
1
+ """Benchmark Health calculation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+ from agentfit.logging import get_logger
9
+ from agentfit.models import BenchmarkHealth, CandidateTask, NetworkIsolation
10
+
11
+ log = get_logger("benchmark.health")
12
+
13
+ # Weights from the PRD
14
+ _W_REPRESENTATIVENESS = 0.40
15
+ _W_VALIDATION = 0.20
16
+ _W_LEAKAGE = 0.15
17
+ _W_RECENCY = 0.15
18
+ _W_DIVERSITY = 0.10
19
+
20
+
21
+ def calculate_health(
22
+ benchmark_tasks: list[CandidateTask],
23
+ workload_universe: list[CandidateTask] | None = None,
24
+ ) -> BenchmarkHealth:
25
+ """Compute the Benchmark Health score components.
26
+
27
+ Components (0-100 each):
28
+ - Representativeness: TVD-based coverage of workload distributions
29
+ - Validation: no-op/oracle/determinism validation strength
30
+ - Leakage: history sanitization, network isolation, credential removal
31
+ - Recency: temporal distribution of tasks
32
+ - Diversity: repetition of subsystem/complexity
33
+
34
+ Overall = weighted sum.
35
+ """
36
+ health = BenchmarkHealth()
37
+
38
+ if not benchmark_tasks:
39
+ return health
40
+
41
+ health.representativeness = _representativeness(benchmark_tasks, workload_universe)
42
+ health.validation = _validation(benchmark_tasks)
43
+ health.leakage = _leakage(benchmark_tasks)
44
+ health.recency = _recency(benchmark_tasks)
45
+ health.diversity = _diversity(benchmark_tasks)
46
+
47
+ health.overall = round(
48
+ _W_REPRESENTATIVENESS * health.representativeness
49
+ + _W_VALIDATION * health.validation
50
+ + _W_LEAKAGE * health.leakage
51
+ + _W_RECENCY * health.recency
52
+ + _W_DIVERSITY * health.diversity
53
+ )
54
+
55
+ return health
56
+
57
+
58
+ def _representativeness(
59
+ tasks: list[CandidateTask],
60
+ workload: list[CandidateTask] | None,
61
+ ) -> int:
62
+ """Representativeness from distribution coverage (TVD-based)."""
63
+ if not workload:
64
+ return 100
65
+
66
+ from agentfit.benchmark.coverage import calculate_coverage
67
+
68
+ # Build distributions
69
+ def _dist(tasks_list: list[CandidateTask], key: str) -> dict[str, float]:
70
+ from collections import Counter
71
+ counts = Counter(getattr(t, key) for t in tasks_list)
72
+ total = sum(counts.values())
73
+ if total == 0:
74
+ return {}
75
+ return {k: v / total for k, v in counts.items()}
76
+
77
+ bench_dist = {
78
+ "task_type": _dist(tasks, "task_type"),
79
+ "subsystem": _dist(tasks, "subsystem"),
80
+ "complexity": _dist(tasks, "complexity"),
81
+ }
82
+ work_dist = {
83
+ "task_type": _dist(workload, "task_type"),
84
+ "subsystem": _dist(workload, "subsystem"),
85
+ "complexity": _dist(workload, "complexity"),
86
+ }
87
+
88
+ coverage = calculate_coverage(bench_dist, work_dist)
89
+ return round(sum(coverage.values()) / max(len(coverage), 1))
90
+
91
+
92
+ def _validation(tasks: list[CandidateTask]) -> int:
93
+ """Validation confidence from eligibility flags."""
94
+ if not tasks:
95
+ return 0
96
+
97
+ scores = []
98
+ for t in tasks:
99
+ score = 100
100
+ e = t.eligibility
101
+
102
+ # No-op validated (must fail)
103
+ if getattr(e, "noop", None) is not True:
104
+ score -= 30
105
+ # Oracle validated (must pass)
106
+ if getattr(e, "oracle", None) is not True:
107
+ score -= 30
108
+ # Regression validated
109
+ if getattr(e, "regression", None) is not True:
110
+ score -= 10
111
+ # Determinism validated
112
+ if getattr(e, "determinism", None) is not True:
113
+ score -= 20
114
+ # Instruction provenance: Tier C reduces confidence
115
+ prov = getattr(t, "instruction_provenance", None)
116
+ if prov is not None and prov.value == "C":
117
+ score -= 15
118
+
119
+ scores.append(max(0, min(score, 100)))
120
+
121
+ return round(sum(scores) / len(scores))
122
+
123
+
124
+ def _leakage(tasks: list[CandidateTask]) -> int:
125
+ """Leakage resistance score."""
126
+ if not tasks:
127
+ return 0
128
+
129
+ scores = []
130
+ for t in tasks:
131
+ score = 100
132
+ # Network isolation
133
+ net = getattr(t, "network_isolation", NetworkIsolation.NONE)
134
+ if net == NetworkIsolation.NONE:
135
+ score -= 25
136
+ elif net == NetworkIsolation.PARTIAL:
137
+ score -= 10
138
+
139
+ # Leakage risk from scanner
140
+ risk = getattr(t, "leakage_risk", 0.0)
141
+ score -= round(risk * 40)
142
+
143
+ # History eligibility (sanitized snapshot implies history available)
144
+ e = getattr(t, "eligibility", None)
145
+ if e is not None and getattr(e, "leakage", None) is True:
146
+ pass # history sanitized
147
+ elif e is not None and getattr(e, "leakage", None) is False:
148
+ score -= 30
149
+
150
+ scores.append(max(0, min(score, 100)))
151
+
152
+ return round(sum(scores) / len(scores))
153
+
154
+
155
+ def _recency(tasks: list[CandidateTask]) -> int:
156
+ """Recency score: how recent are the tasks (0-100)."""
157
+ now = datetime.now(timezone.utc)
158
+ scores = []
159
+ for t in tasks:
160
+ created = getattr(t, "created_at", None)
161
+ if created is None:
162
+ scores.append(50)
163
+ continue
164
+ if created.tzinfo is None:
165
+ created = created.replace(tzinfo=timezone.utc)
166
+ age_days = max(0, (now - created).days)
167
+ # 180-day window: recency decays linearly
168
+ score = max(0, 100 - age_days * 100 / 180)
169
+ scores.append(score)
170
+
171
+ if not scores:
172
+ return 0
173
+ return round(sum(scores) / len(scores))
174
+
175
+
176
+ def _diversity(tasks: list[CandidateTask]) -> int:
177
+ """Diversity: penalize repeated subsystem/complexity combinations."""
178
+ if not tasks:
179
+ return 0
180
+
181
+ from collections import Counter
182
+ combos = Counter(
183
+ (t.subsystem or "unknown", t.complexity.value if t.complexity else "unknown")
184
+ for t in tasks
185
+ )
186
+ total = len(tasks)
187
+ max_repeat = max(combos.values())
188
+
189
+ # Ideal: all distinct (max_repeat=1). Score drops as max_repeat grows.
190
+ score = max(0, 100 - (max_repeat - 1) * 100 / max(total, 1))
191
+ return round(score)
@@ -0,0 +1,152 @@
1
+ """Representative benchmark sampling via greedy stratified optimization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from dataclasses import dataclass, field
7
+
8
+ from agentfit.config import AgentFitConfig
9
+ from agentfit.logging import get_logger
10
+ from agentfit.models import CandidateTask, TaskStatus, TaskType, Complexity
11
+
12
+ log = get_logger("benchmark.sampling")
13
+
14
+
15
+ @dataclass
16
+ class _Dist:
17
+ """A categorical distribution for one dimension."""
18
+
19
+ counts: Counter[str] = field(default_factory=Counter)
20
+
21
+ @property
22
+ def total(self) -> int:
23
+ return sum(self.counts.values())
24
+
25
+ def prob(self, key: str) -> float:
26
+ if self.total == 0:
27
+ return 0.0
28
+ return self.counts.get(key, 0) / self.total
29
+
30
+ def add(self, key: str) -> None:
31
+ self.counts[key] += 1
32
+
33
+
34
+ def workload_distribution(
35
+ workload: list[CandidateTask],
36
+ ) -> tuple[_Dist, _Dist, _Dist]:
37
+ """Build task_type, subsystem, and complexity distributions."""
38
+ type_dist = _Dist()
39
+ sub_dist = _Dist()
40
+ comp_dist = _Dist()
41
+
42
+ for task in workload:
43
+ type_dist.add(task.task_type.value)
44
+ sub_dist.add(task.subsystem or "unknown")
45
+ comp_dist.add(task.complexity.value)
46
+
47
+ return type_dist, sub_dist, comp_dist
48
+
49
+
50
+ def select_benchmark(
51
+ candidates: list[CandidateTask],
52
+ workload: list[CandidateTask],
53
+ config: AgentFitConfig,
54
+ ) -> list[CandidateTask]:
55
+ """Select a representative sample from VALID candidates.
56
+
57
+ Implements greedy stratified optimization: for each slot, pick the
58
+ candidate that most reduces the TVD between the benchmark distribution
59
+ and the workload distribution, with a diversity penalty.
60
+
61
+ Returns a list of selected candidates (up to config.benchmark.size).
62
+ """
63
+ valid = [c for c in candidates if c.status == TaskStatus.VALID]
64
+ if not valid:
65
+ log.warning("No VALID candidates available for sampling")
66
+ return []
67
+
68
+ size = config.benchmark.size
69
+ if size <= 0:
70
+ size = 24
71
+
72
+ size = min(size, len(valid))
73
+ weights = config.benchmark.dimensions
74
+
75
+ # Workload distributions (target)
76
+ w_type, w_sub, w_comp = workload_distribution(workload or valid)
77
+
78
+ # Benchmark distributions (current selection)
79
+ b_type = _Dist()
80
+ b_sub = _Dist()
81
+ b_comp = _Dist()
82
+
83
+ selected: list[CandidateTask] = []
84
+ available = list(valid)
85
+
86
+ for _ in range(size):
87
+ best_candidate = None
88
+ best_score = float("inf")
89
+
90
+ for cand in available:
91
+ # Simulate inclusion
92
+ sim_type = _Dist(b_type.counts.copy())
93
+ sim_sub = _Dist(b_sub.counts.copy())
94
+ sim_comp = _Dist(b_comp.counts.copy())
95
+ sim_type.add(cand.task_type.value)
96
+ sim_sub.add(cand.subsystem or "unknown")
97
+ sim_comp.add(cand.complexity.value)
98
+
99
+ # Total variation distance per dimension
100
+ tvd_type = _tvd(sim_type, w_type)
101
+ tvd_sub = _tvd(sim_sub, w_sub)
102
+ tvd_comp = _tvd(sim_comp, w_comp)
103
+
104
+ score = (
105
+ weights.task_type * tvd_type
106
+ + weights.subsystem * tvd_sub
107
+ + weights.complexity * tvd_comp
108
+ )
109
+
110
+ # Diversity penalty: penalize candidates too similar to selected
111
+ penalty = _diversity_penalty(cand, selected)
112
+ score += penalty
113
+
114
+ if score < best_score:
115
+ best_score = score
116
+ best_candidate = cand
117
+
118
+ if best_candidate is None:
119
+ break
120
+
121
+ selected.append(best_candidate)
122
+ available.remove(best_candidate)
123
+ b_type.add(best_candidate.task_type.value)
124
+ b_sub.add(best_candidate.subsystem or "unknown")
125
+ b_comp.add(best_candidate.complexity.value)
126
+
127
+ log.info("Selected %d tasks for benchmark (requested %d)", len(selected), size)
128
+ return selected
129
+
130
+
131
+ def _tvd(bench: _Dist, workload: _Dist) -> float:
132
+ """Total Variation Distance between two categorical distributions."""
133
+ keys = set(bench.counts.keys()) | set(workload.counts.keys())
134
+ return 0.5 * sum(abs(bench.prob(k) - workload.prob(k)) for k in keys)
135
+
136
+
137
+ def _diversity_penalty(candidate: CandidateTask, selected: list[CandidateTask]) -> float:
138
+ """Penalize candidates that are too similar to already-selected ones."""
139
+ if not selected:
140
+ return 0.0
141
+
142
+ penalty = 0.0
143
+ for sel in selected:
144
+ # Same PR family or same subsystem + complexity => high similarity
145
+ if sel.subsystem == candidate.subsystem and sel.complexity == candidate.complexity:
146
+ penalty += 0.10
147
+ if sel.task_type == candidate.task_type and sel.subsystem == candidate.subsystem:
148
+ penalty += 0.08
149
+ if sel.subsystem == candidate.subsystem:
150
+ penalty += 0.04
151
+
152
+ return penalty
File without changes