@giamme/forge 0.0.0-stage → 0.8.0
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.
- package/CHANGELOG.md +181 -0
- package/LICENSE +21 -0
- package/README.md +1854 -2
- package/SKILL.md +122 -0
- package/VERSION +1 -0
- package/forge +74 -0
- package/package.json +26 -4
- package/references/dataset.md +329 -0
- package/references/decompose.md +667 -0
- package/references/fractal-dashboard.md +21 -0
- package/references/fractal.md +307 -0
- package/references/harnesses.md +353 -0
- package/references/jev.md +865 -0
- package/references/memory.md +228 -0
- package/references/planning.md +42 -0
- package/references/ripwire.md +47 -0
- package/references/runtime.md +64 -0
- package/references/solo.md +113 -0
- package/references/ui.md +112 -0
- package/registry.tsv +81 -0
- package/scripts/forge-artifact.sh +78 -0
- package/scripts/forge-contract.py +430 -0
- package/scripts/forge-dataset.py +6 -0
- package/scripts/forge-dispatch.sh +665 -0
- package/scripts/forge-fail-first.py +195 -0
- package/scripts/forge-fractal-dispatch.sh +4 -0
- package/scripts/forge-fractal-options.sh +54 -0
- package/scripts/forge-fractal.py +6 -0
- package/scripts/forge-guard.py +266 -0
- package/scripts/forge-install-fractal.sh +4 -0
- package/scripts/forge-install-ripwire.sh +52 -0
- package/scripts/forge-install.sh +135 -0
- package/scripts/forge-jev-options.sh +259 -0
- package/scripts/forge-jev.py +6 -0
- package/scripts/forge-lib-plans.sh +785 -0
- package/scripts/forge-lib-review.sh +494 -0
- package/scripts/forge-lib-task.sh +302 -0
- package/scripts/forge-lib-verify.sh +30 -0
- package/scripts/forge-memory.sh +575 -0
- package/scripts/forge-metrics.sh +39 -0
- package/scripts/forge-parallel-options.sh +76 -0
- package/scripts/forge-parallel.sh +1717 -0
- package/scripts/forge-plan-check.py +118 -0
- package/scripts/forge-prompt.py +45 -0
- package/scripts/forge-ripwire.py +136 -0
- package/scripts/forge-route.py +344 -0
- package/scripts/forge-runtime.py +533 -0
- package/scripts/forge-schedule.py +312 -0
- package/scripts/forge-shadow.py +305 -0
- package/scripts/forge-solo.sh +347 -0
- package/scripts/forge-ui-options.sh +42 -0
- package/scripts/forge-ui.py +435 -0
- package/scripts/forge-update.sh +71 -0
- package/scripts/forge-verify-coverage.py +956 -0
- package/scripts/forge-version.py +198 -0
- package/scripts/forge_dataset/__init__.py +439 -0
- package/scripts/forge_dataset/capture.py +330 -0
- package/scripts/forge_dataset/checks.py +109 -0
- package/scripts/forge_dataset/cli.py +233 -0
- package/scripts/forge_dataset/export.py +1009 -0
- package/scripts/forge_dataset/harvest.py +716 -0
- package/scripts/forge_dataset/importer.py +412 -0
- package/scripts/forge_dataset/outcomes.py +275 -0
- package/scripts/forge_dataset/routing.py +278 -0
- package/scripts/forge_fractal/__init__.py +83 -0
- package/scripts/forge_fractal/bridge.py +46 -0
- package/scripts/forge_fractal/cli.py +202 -0
- package/scripts/forge_fractal/dashboard.css +138 -0
- package/scripts/forge_fractal/dashboard.html +41 -0
- package/scripts/forge_fractal/dashboard.js +687 -0
- package/scripts/forge_fractal/decomposition.py +262 -0
- package/scripts/forge_fractal/execution.py +659 -0
- package/scripts/forge_fractal/inspection.py +261 -0
- package/scripts/forge_fractal/install.py +158 -0
- package/scripts/forge_fractal/invocation.py +94 -0
- package/scripts/forge_fractal/pipeline.py +124 -0
- package/scripts/forge_fractal/selection.py +179 -0
- package/scripts/forge_jev/__init__.py +376 -0
- package/scripts/forge_jev/backtest.py +322 -0
- package/scripts/forge_jev/cli.py +980 -0
- package/scripts/forge_jev/client.py +418 -0
- package/scripts/forge_jev/corpus.py +156 -0
- package/scripts/forge_jev/coupling.py +244 -0
- package/scripts/forge_jev/features.py +24 -0
- package/scripts/forge_jev/gates.py +100 -0
- package/scripts/forge_jev/memory.py +163 -0
- package/scripts/forge_jev/outcome.py +49 -0
- package/scripts/forge_jev/predict.py +204 -0
- package/scripts/forge_jev/questions.py +661 -0
- package/scripts/forge_jev/review.py +73 -0
- package/scripts/forge_jev/routing.py +271 -0
- package/scripts/forge_jev/subset.py +123 -0
- package/scripts/forge_jev/verify.py +453 -0
- package/scripts/forge_registry.py +140 -0
- package/ui/README.md +287 -0
- package/ui/THIRD_PARTY_NOTICES.md +69 -0
- package/ui/docs/api.md +888 -0
- package/ui/forge_ui/__init__.py +20 -0
- package/ui/forge_ui/__main__.py +135 -0
- package/ui/forge_ui/diffs.py +454 -0
- package/ui/forge_ui/executions.py +436 -0
- package/ui/forge_ui/export.py +204 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +583 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1336 -0
- package/ui/forge_ui/server.py +303 -0
- package/ui/forge_ui/static/app.js +924 -0
- package/ui/forge_ui/static/cloud3d.js +646 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +394 -0
- package/ui/forge_ui/static/diffview.js +216 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/index.html +55 -0
- package/ui/forge_ui/static/iso.js +765 -0
- package/ui/forge_ui/static/leaderboard.js +315 -0
- package/ui/forge_ui/static/map3d.js +45 -0
- package/ui/forge_ui/static/modelstats.js +249 -0
- package/ui/forge_ui/static/notify.js +179 -0
- package/ui/forge_ui/static/panel.js +526 -0
- package/ui/forge_ui/static/runmodel.js +651 -0
- package/ui/forge_ui/static/scene3d.js +996 -0
- package/ui/forge_ui/static/search.js +119 -0
- package/ui/forge_ui/static/style.css +618 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Replays git history against Jev to score test-selection and drift-prediction thresholds.
|
|
2
|
+
|
|
3
|
+
A commit's test changes are a LOWER BOUND on relevant tests: a human may touch fewer
|
|
4
|
+
tests than a change could break, so high recall against this label is necessary, not
|
|
5
|
+
sufficient -- it never proves full coverage. Co-change labels also encode one team's
|
|
6
|
+
testing habits, so numbers from one repo do not transfer to another without
|
|
7
|
+
re-measuring there. Both caveats are repeated in report() so they travel with the numbers.
|
|
8
|
+
|
|
9
|
+
Budget discipline: execute=False (the default) makes zero API calls -- it only estimates
|
|
10
|
+
how many requests a real run would cost. When execute=True, every request is appended to
|
|
11
|
+
a lock-protected ledger under out_dir *before* it is issued, and max_requests is a hard
|
|
12
|
+
cap; once hit, remaining records are marked incomplete rather than half-scored. A record
|
|
13
|
+
whose Jev calls fail is excluded from metrics entirely -- treating a failed chunk as "not
|
|
14
|
+
relevant" would manufacture artificial precision.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import fcntl
|
|
19
|
+
import json
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
import subprocess
|
|
22
|
+
import tempfile
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
from . import load_config, threshold, write_json
|
|
26
|
+
from .client import ask, noul
|
|
27
|
+
from .corpus import VENDOR_PATTERN, commits, is_test
|
|
28
|
+
from .questions import drift_prediction, test_relevance
|
|
29
|
+
|
|
30
|
+
MAX_QUESTIONS_PER_REQUEST = 100
|
|
31
|
+
DRIFT_CANDIDATE_CAP = 200 # capped universe of tracked non-test files considered for drift
|
|
32
|
+
DEFAULT_THRESHOLDS = tuple(round(0.05 * i, 2) for i in range(1, 20)) # 0.05 .. 0.95 step 0.05
|
|
33
|
+
|
|
34
|
+
LOWER_BOUND_NOTE = ('Test-touch labels are a LOWER BOUND on relevant tests: a human may touch '
|
|
35
|
+
'fewer tests than a change could break. High recall here is necessary, not '
|
|
36
|
+
'sufficient proof of coverage.')
|
|
37
|
+
NO_TRANSFER_NOTE = ('Co-change labels encode one team\'s testing habits and repo layout. Numbers '
|
|
38
|
+
'from this repo do not transfer to another repo without re-measuring there.')
|
|
39
|
+
# The single most optimistic thing about this whole method, so it travels with every number.
|
|
40
|
+
POST_HOC_NOTE = ('Commit messages are written AFTER the work and often name the very files or '
|
|
41
|
+
'behaviour that changed. A Forge task prompt is written before. These scores are '
|
|
42
|
+
'therefore an UPPER BOUND on live performance.')
|
|
43
|
+
DRIFT_CAP_NOTE = ('Drift candidates are the parent tree\'s tracked non-test files, sorted and '
|
|
44
|
+
'truncated to the cap -- a commit whose real files fall outside that slice is '
|
|
45
|
+
'scored against a universe that cannot contain them.')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def notes(capability: str) -> list[str]:
|
|
49
|
+
"""Caveats that apply to this capability. Drift's label (the commit's actual file set)
|
|
50
|
+
is complete, so the test-selection lower-bound note would be false there."""
|
|
51
|
+
if capability == 'drift':
|
|
52
|
+
return [POST_HOC_NOTE, NO_TRANSFER_NOTE, DRIFT_CAP_NOTE]
|
|
53
|
+
return [LOWER_BOUND_NOTE, POST_HOC_NOTE, NO_TRANSFER_NOTE]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _index_candidates(candidates: list[str]) -> list[tuple[str, str]]:
|
|
57
|
+
"""Assign stable q<N> ids to candidate paths -- a path itself is not a safe question id."""
|
|
58
|
+
return [(f'q{i}', path) for i, path in enumerate(candidates)]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _chunk(pairs: list[tuple[str, str]], size: int = MAX_QUESTIONS_PER_REQUEST):
|
|
62
|
+
for i in range(0, len(pairs), size):
|
|
63
|
+
yield pairs[i:i + size]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _ledger_append(out_dir: Path, entry: dict) -> None:
|
|
67
|
+
path = out_dir / 'requests.jsonl'
|
|
68
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
with path.open('a') as handle:
|
|
70
|
+
fcntl.flock(handle, fcntl.LOCK_EX)
|
|
71
|
+
try:
|
|
72
|
+
handle.write(json.dumps(entry) + '\n')
|
|
73
|
+
handle.flush()
|
|
74
|
+
finally:
|
|
75
|
+
fcntl.flock(handle, fcntl.LOCK_UN)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _drift_candidates(repo, parent: str, cache: dict[str, list[str]], cap: int = DRIFT_CANDIDATE_CAP) -> list[str]:
|
|
79
|
+
"""Repo's tracked non-test files at `parent`, capped and sorted for determinism."""
|
|
80
|
+
if parent in cache:
|
|
81
|
+
return cache[parent]
|
|
82
|
+
try:
|
|
83
|
+
result = subprocess.run(['git', '-C', str(repo), 'ls-tree', '-r', '--name-only', parent],
|
|
84
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
|
|
85
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
86
|
+
cache[parent] = []
|
|
87
|
+
return []
|
|
88
|
+
paths = []
|
|
89
|
+
if result.returncode == 0:
|
|
90
|
+
for line in result.stdout.decode(errors='replace').split('\n'):
|
|
91
|
+
if line and not is_test(line) and not VENDOR_PATTERN.search(line):
|
|
92
|
+
paths.append(line)
|
|
93
|
+
paths = sorted(paths)[:cap]
|
|
94
|
+
cache[parent] = paths
|
|
95
|
+
return paths
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _estimate_requests(n_candidates: int) -> int:
|
|
99
|
+
if n_candidates <= 0:
|
|
100
|
+
return 0
|
|
101
|
+
return -(-n_candidates // MAX_QUESTIONS_PER_REQUEST) # ceil division
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _score(sha: str, candidates: list[str], labels: list[str], answers: dict[str, float]) -> dict:
|
|
105
|
+
"""Package one record's raw probabilities. Threshold selection happens later in sweep()
|
|
106
|
+
so re-tuning a threshold never costs another API call. `labels` is not part of the
|
|
107
|
+
literal contract example but sweep() cannot compute recall without knowing which
|
|
108
|
+
candidate paths are ground truth, so it travels alongside probabilities here."""
|
|
109
|
+
return dict(sha=sha, n_candidates=len(candidates), n_labeled=len(labels),
|
|
110
|
+
labels=sorted(set(labels)), probabilities=dict(answers), incomplete=False)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def score_tests(record: dict, answers: dict[str, float]) -> dict:
|
|
114
|
+
"""Score one test-selection record's answers against its candidates and test_files label."""
|
|
115
|
+
return _score(record['sha'], record['candidates'], record.get('test_files', []), answers)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _evaluate_record(record, *, state, candidates, question_fn, config, out_dir, run_dir,
|
|
119
|
+
site, max_requests, request_count) -> tuple[dict, bool]:
|
|
120
|
+
"""Ask Jev about every candidate for one record, chunked and budget-gated. Returns
|
|
121
|
+
(path->probability, incomplete). A failed or budget-skipped chunk marks the whole
|
|
122
|
+
record incomplete -- its answered paths are still returned for inspection but the
|
|
123
|
+
record is excluded from metrics by the caller."""
|
|
124
|
+
pairs = _index_candidates(candidates)
|
|
125
|
+
probabilities: dict[str, float] = {}
|
|
126
|
+
incomplete = False
|
|
127
|
+
for chunk in _chunk(pairs):
|
|
128
|
+
if request_count[0] >= max_requests:
|
|
129
|
+
incomplete = True
|
|
130
|
+
break
|
|
131
|
+
questions = {qid: question_fn(path) for qid, path in chunk}
|
|
132
|
+
request_count[0] += 1
|
|
133
|
+
_ledger_append(out_dir, dict(at=time.time(), sha=record['sha'], site=site,
|
|
134
|
+
n_questions=len(questions), request_no=request_count[0]))
|
|
135
|
+
result = ask(state, questions, site=site, run_dir=run_dir, config=config)
|
|
136
|
+
if result is None:
|
|
137
|
+
incomplete = True
|
|
138
|
+
break
|
|
139
|
+
for qid, path in chunk:
|
|
140
|
+
value = noul(result, qid)
|
|
141
|
+
if value is not None:
|
|
142
|
+
probabilities[path] = value
|
|
143
|
+
return probabilities, incomplete
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def run(repo, *, limit=None, execute=False, max_requests=200, out_dir=None,
|
|
147
|
+
capability='tests', config=None) -> dict:
|
|
148
|
+
if capability not in ('tests', 'drift'):
|
|
149
|
+
raise ValueError("capability must be 'tests' or 'drift'")
|
|
150
|
+
config = config or load_config()
|
|
151
|
+
out_path = Path(out_dir) if out_dir is not None else Path(tempfile.mkdtemp(prefix='forge-jev-backtest-'))
|
|
152
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
|
|
154
|
+
drift_cache: dict[str, list[str]] = {}
|
|
155
|
+
question_fn = test_relevance if capability == 'tests' else drift_prediction
|
|
156
|
+
site = 'jev-backtest:' + capability
|
|
157
|
+
|
|
158
|
+
n_records = 0
|
|
159
|
+
total_candidates = 0
|
|
160
|
+
estimated_requests = 0
|
|
161
|
+
results: list[dict] = []
|
|
162
|
+
request_count = [0]
|
|
163
|
+
budget_exhausted = False
|
|
164
|
+
|
|
165
|
+
for record in commits(repo, limit=limit):
|
|
166
|
+
if capability == 'tests':
|
|
167
|
+
candidates = record['candidates']
|
|
168
|
+
labels = record['test_files']
|
|
169
|
+
else:
|
|
170
|
+
candidates = _drift_candidates(repo, record['parent'], drift_cache)
|
|
171
|
+
labels = sorted(set(record['all_changed']) & set(candidates))
|
|
172
|
+
|
|
173
|
+
n_records += 1
|
|
174
|
+
total_candidates += len(candidates)
|
|
175
|
+
estimated_requests += _estimate_requests(len(candidates))
|
|
176
|
+
|
|
177
|
+
if not execute:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
if budget_exhausted or request_count[0] >= max_requests:
|
|
181
|
+
budget_exhausted = True
|
|
182
|
+
results.append(_score(record['sha'], candidates, labels, {}) | dict(incomplete=True))
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Label never enters the request state: only the task text and changed source
|
|
186
|
+
# files are sent, never test_files/all_changed.
|
|
187
|
+
state = dict(task=(record['subject'] + '\n' + record['body']).strip(),
|
|
188
|
+
changed_files=record['source_files'])
|
|
189
|
+
probabilities, incomplete = _evaluate_record(
|
|
190
|
+
record, state=state, candidates=candidates, question_fn=question_fn, config=config,
|
|
191
|
+
out_dir=out_path, run_dir=out_path, site=site, max_requests=max_requests,
|
|
192
|
+
request_count=request_count)
|
|
193
|
+
if request_count[0] >= max_requests:
|
|
194
|
+
budget_exhausted = True
|
|
195
|
+
scored = _score(record['sha'], candidates, labels, probabilities)
|
|
196
|
+
scored['incomplete'] = incomplete
|
|
197
|
+
results.append(scored)
|
|
198
|
+
|
|
199
|
+
summary = dict(
|
|
200
|
+
capability=capability,
|
|
201
|
+
repo=str(repo),
|
|
202
|
+
execute=execute,
|
|
203
|
+
n_records=n_records,
|
|
204
|
+
estimated_requests=estimated_requests,
|
|
205
|
+
avg_candidates=(total_candidates / n_records) if n_records else 0.0,
|
|
206
|
+
max_requests=max_requests,
|
|
207
|
+
max_questions_per_request=MAX_QUESTIONS_PER_REQUEST,
|
|
208
|
+
out_dir=str(out_path),
|
|
209
|
+
notes=notes(capability),
|
|
210
|
+
)
|
|
211
|
+
if capability == 'drift':
|
|
212
|
+
summary['drift_candidate_cap'] = DRIFT_CANDIDATE_CAP
|
|
213
|
+
if execute:
|
|
214
|
+
summary['requests_used'] = request_count[0]
|
|
215
|
+
summary['n_incomplete'] = sum(1 for r in results if r['incomplete'])
|
|
216
|
+
summary['results'] = results
|
|
217
|
+
summary['sweep'] = sweep(results)
|
|
218
|
+
|
|
219
|
+
write_json(out_path / 'backtest.json', summary)
|
|
220
|
+
return summary
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def sweep(results: list[dict], thresholds=None) -> list[dict]:
|
|
224
|
+
"""Aggregate scored records across a sweep of thresholds. Incomplete records are
|
|
225
|
+
dropped entirely -- an all-failed run must report zero scored records, never
|
|
226
|
+
manufactured perfect (or perfectly bad) precision."""
|
|
227
|
+
thresholds = list(thresholds) if thresholds is not None else list(DEFAULT_THRESHOLDS)
|
|
228
|
+
usable = [r for r in results if not r.get('incomplete')]
|
|
229
|
+
n_incomplete = len(results) - len(usable)
|
|
230
|
+
|
|
231
|
+
rows = []
|
|
232
|
+
for t in thresholds:
|
|
233
|
+
total_labeled = 0
|
|
234
|
+
total_labeled_selected = 0
|
|
235
|
+
total_candidates = 0
|
|
236
|
+
total_selected = 0
|
|
237
|
+
per_record_recalls = []
|
|
238
|
+
full_recall_records = 0
|
|
239
|
+
selected_counts = []
|
|
240
|
+
|
|
241
|
+
for r in usable:
|
|
242
|
+
probs = r['probabilities']
|
|
243
|
+
labels = set(r.get('labels', []))
|
|
244
|
+
selected = {p for p, v in probs.items() if v is not None and v >= t}
|
|
245
|
+
n_labeled = len(labels)
|
|
246
|
+
n_labeled_selected = len(labels & selected)
|
|
247
|
+
|
|
248
|
+
total_labeled += n_labeled
|
|
249
|
+
total_labeled_selected += n_labeled_selected
|
|
250
|
+
total_candidates += r['n_candidates']
|
|
251
|
+
total_selected += len(selected)
|
|
252
|
+
selected_counts.append(len(selected))
|
|
253
|
+
if n_labeled:
|
|
254
|
+
per_record_recalls.append(n_labeled_selected / n_labeled)
|
|
255
|
+
if n_labeled_selected == n_labeled:
|
|
256
|
+
full_recall_records += 1
|
|
257
|
+
|
|
258
|
+
n_recall_records = len(per_record_recalls)
|
|
259
|
+
rows.append(dict(
|
|
260
|
+
threshold=t,
|
|
261
|
+
micro_recall=(total_labeled_selected / total_labeled) if total_labeled else None,
|
|
262
|
+
# Precision is the headline for drift: a warning that fires on files nobody
|
|
263
|
+
# edits trains people to ignore it. For test selection it is expected to be
|
|
264
|
+
# low by design, since over-selecting only costs runtime.
|
|
265
|
+
precision=(total_labeled_selected / total_selected) if total_selected else None,
|
|
266
|
+
macro_recall=(sum(per_record_recalls) / n_recall_records) if n_recall_records else None,
|
|
267
|
+
reduction=(1 - total_selected / total_candidates) if total_candidates else None,
|
|
268
|
+
records_with_full_recall=(full_recall_records / n_recall_records) if n_recall_records else None,
|
|
269
|
+
mean_selected=(sum(selected_counts) / len(selected_counts)) if selected_counts else None,
|
|
270
|
+
n_records=len(usable),
|
|
271
|
+
n_incomplete=n_incomplete,
|
|
272
|
+
))
|
|
273
|
+
return rows
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _fmt(value, digits=3) -> str:
|
|
277
|
+
return 'n/a' if value is None else f'{value:.{digits}f}'
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def report(summary: dict) -> str:
|
|
281
|
+
lines = [f"Jev backtest -- capability={summary.get('capability')} repo={summary.get('repo')}"]
|
|
282
|
+
lines += summary.get('notes') or notes(summary.get('capability', 'tests'))
|
|
283
|
+
lines.append('')
|
|
284
|
+
|
|
285
|
+
if not summary.get('execute'):
|
|
286
|
+
lines.append(
|
|
287
|
+
f"DRY RUN -- no API calls made. {summary['n_records']} record(s), "
|
|
288
|
+
f"avg {summary['avg_candidates']:.1f} candidates/record, "
|
|
289
|
+
f"~{summary['estimated_requests']} request(s) estimated at "
|
|
290
|
+
f"{summary['max_questions_per_request']} questions/request.")
|
|
291
|
+
return '\n'.join(lines)
|
|
292
|
+
|
|
293
|
+
sweep_rows = summary.get('sweep', [])
|
|
294
|
+
lines.append(f"requests used: {summary.get('requests_used', 0)} "
|
|
295
|
+
f"incomplete records: {summary.get('n_incomplete', 0)}")
|
|
296
|
+
lines.append(f"{'threshold':>9} | {'micro_recall':>12} | {'macro_recall':>12} | "
|
|
297
|
+
f"{'precision':>9} | {'reduction':>9} | {'full_recall':>11} | {'mean_sel':>8}")
|
|
298
|
+
best = None
|
|
299
|
+
for row in sweep_rows:
|
|
300
|
+
micro = row['micro_recall']
|
|
301
|
+
lines.append(f"{row['threshold']:>9.2f} | {_fmt(micro):>12} | {_fmt(row['macro_recall']):>12} | "
|
|
302
|
+
f"{_fmt(row['precision']):>9} | "
|
|
303
|
+
f"{_fmt(row['reduction']):>9} | {_fmt(row['records_with_full_recall']):>11} | "
|
|
304
|
+
f"{_fmt(row['mean_selected'], 1):>8}")
|
|
305
|
+
if micro is not None and micro >= 0.95:
|
|
306
|
+
best = row['threshold'] # ascending order: last hit is the highest qualifying threshold
|
|
307
|
+
|
|
308
|
+
# The two capabilities are tuned against opposite errors, so they get different
|
|
309
|
+
# headlines: a missed test is a missed regression, while a drift warning on files
|
|
310
|
+
# nobody edits is noise that trains people to ignore the warning.
|
|
311
|
+
if summary.get('capability') == 'drift':
|
|
312
|
+
warn = threshold('gate_warn')
|
|
313
|
+
row = min(sweep_rows, key=lambda r: abs(r['threshold'] - warn), default=None)
|
|
314
|
+
if row is not None:
|
|
315
|
+
lines.append(f"\nDrift headline -- precision at the warn threshold "
|
|
316
|
+
f"{row['threshold']:.2f}: {_fmt(row['precision'])} "
|
|
317
|
+
f"(recall {_fmt(row['micro_recall'])}).")
|
|
318
|
+
elif best is not None:
|
|
319
|
+
lines.append(f"\nHighest threshold holding micro recall >= 0.95: {best:.2f}")
|
|
320
|
+
else:
|
|
321
|
+
lines.append('\nNo threshold in this sweep holds micro recall >= 0.95.')
|
|
322
|
+
return '\n'.join(lines)
|