@giamme/forge 0.0.0-stage → 0.9.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 +205 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/SKILL.md +123 -0
- package/VERSION +1 -0
- package/docs/README.md +26 -0
- package/docs/add-ons.md +33 -0
- package/docs/artifacts.md +69 -0
- package/docs/commands.md +315 -0
- package/docs/dataset.md +20 -0
- package/docs/decomposed-runs.md +268 -0
- package/docs/development.md +95 -0
- package/docs/forge-ui.md +92 -0
- package/docs/fractal.md +181 -0
- package/docs/install.md +160 -0
- package/docs/memory.md +156 -0
- package/docs/models.md +118 -0
- package/docs/reviews.md +231 -0
- package/docs/safety.md +55 -0
- package/docs/troubleshooting.md +54 -0
- package/forge +79 -0
- package/package.json +27 -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 +365 -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 +671 -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-runs.py +121 -0
- package/scripts/forge-runtime.py +586 -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 +240 -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 +84 -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 +347 -0
- package/ui/THIRD_PARTY_NOTICES.md +177 -0
- package/ui/docs/api.md +970 -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 +214 -0
- package/ui/forge_ui/findings.py +132 -0
- package/ui/forge_ui/fractal.py +582 -0
- package/ui/forge_ui/models.py +216 -0
- package/ui/forge_ui/reader.py +1424 -0
- package/ui/forge_ui/server.py +312 -0
- package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
- package/ui/forge_ui/static/app.js +1079 -0
- package/ui/forge_ui/static/cloud3d.js +647 -0
- package/ui/forge_ui/static/cloudmodel.js +375 -0
- package/ui/forge_ui/static/cloudui.js +397 -0
- package/ui/forge_ui/static/convo.js +295 -0
- package/ui/forge_ui/static/datatable.js +232 -0
- package/ui/forge_ui/static/diffview.js +235 -0
- package/ui/forge_ui/static/favicon.svg +1 -0
- package/ui/forge_ui/static/files.js +776 -0
- package/ui/forge_ui/static/findings3d.js +134 -0
- package/ui/forge_ui/static/gantt.js +97 -0
- package/ui/forge_ui/static/graph.js +266 -0
- package/ui/forge_ui/static/icons.js +94 -0
- package/ui/forge_ui/static/index.html +67 -0
- package/ui/forge_ui/static/iso.js +816 -0
- package/ui/forge_ui/static/leaderboard.js +311 -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 +562 -0
- package/ui/forge_ui/static/runmodel.js +667 -0
- package/ui/forge_ui/static/scene3d.js +1471 -0
- package/ui/forge_ui/static/search.js +120 -0
- package/ui/forge_ui/static/shell.js +137 -0
- package/ui/forge_ui/static/stagectl.js +150 -0
- package/ui/forge_ui/static/style.css +1073 -0
- package/ui/forge_ui/static/theme.js +25 -0
- package/ui/forge_ui/transcripts.py +608 -0
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
"""Labeled decision cases derived from the run dataset, in the System One wire format.
|
|
2
|
+
|
|
3
|
+
The store keeps raw events. This folds them into (state, questions, expected) cases for
|
|
4
|
+
the decisions Jev makes in forge, each labeled by what actually happened afterwards:
|
|
5
|
+
|
|
6
|
+
routing will the first attempt at this task pass review? plus the tier the
|
|
7
|
+
orchestrator or planner declared (agreement, not outcome)
|
|
8
|
+
retry will this retry pass, given the findings it was handed?
|
|
9
|
+
review will the independent reviewer pass this diff (state names the reviewer)?
|
|
10
|
+
drift which files the task did not declare did it really edit?
|
|
11
|
+
citation does this review describe this diff, or another task's from the same repo?
|
|
12
|
+
memory is this recorded fact about a file the task went on to edit?
|
|
13
|
+
|
|
14
|
+
States are built the way forge_jev builds them for the same site, so a model scored here
|
|
15
|
+
is scored on the inputs forge would send it. Where a site's answer cannot be labeled by
|
|
16
|
+
an outcome (QA effort sizing, the five routing rubrics, failure triage) it is left out
|
|
17
|
+
rather than labeled by opinion. Hosted Jev's own answers, where forge recorded them, go to
|
|
18
|
+
meta.jsonl as baselines; they are never part of a case.
|
|
19
|
+
|
|
20
|
+
The output holds prompts, diffs and review text from the repos forge ran in. It is written
|
|
21
|
+
0600 into a 0700 dir and must stay out of any public repo.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import random
|
|
29
|
+
import re
|
|
30
|
+
import subprocess
|
|
31
|
+
import time
|
|
32
|
+
from collections import Counter, defaultdict
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from forge_jev import outcome
|
|
36
|
+
from forge_jev.features import task_size
|
|
37
|
+
from forge_jev.questions import first_pass, retry_pass # noqa: F401 shared with live forge
|
|
38
|
+
|
|
39
|
+
from . import SKILL, connect, data_home, read_blob
|
|
40
|
+
from .harvest import review_completeness
|
|
41
|
+
|
|
42
|
+
SLICES = ('routing', 'retry', 'review', 'drift', 'citation', 'memory')
|
|
43
|
+
|
|
44
|
+
# The caps forge_jev applies to the same fields (routing.py, review.py, cli.py memory-slice).
|
|
45
|
+
FILE_EXCERPT_CHARS = 600
|
|
46
|
+
MAX_EXCERPT_FILES = 6
|
|
47
|
+
MEMORY_CHARS = 4000
|
|
48
|
+
MAX_SIBLINGS = 40
|
|
49
|
+
SIBLING_TITLE_CHARS = 120
|
|
50
|
+
DIFF_CHARS = 24000
|
|
51
|
+
REVIEW_CHARS = 12000
|
|
52
|
+
SLICE_TASK_CHARS = 4000
|
|
53
|
+
|
|
54
|
+
DRIFT_QUESTIONS = 6
|
|
55
|
+
MEMORY_QUESTIONS = 6
|
|
56
|
+
|
|
57
|
+
_DIFF_BLOCK = re.compile(r'```diff\n(.*?)\n```', re.S)
|
|
58
|
+
_ANCHOR = re.compile(r'\[([^\[\]]+)\]\s*$')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _sha(*parts) -> str:
|
|
62
|
+
return hashlib.sha256('\0'.join(str(p) for p in parts).encode()).hexdigest()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _provenance(src, forge_version, registry_sha, code_sha, forge_rev) -> dict | None:
|
|
66
|
+
"""Which forge produced an event, or None when that is unknown.
|
|
67
|
+
|
|
68
|
+
Rows imported before schema 2 carry the importer's version, not the one that ran the
|
|
69
|
+
run; an import knows its provenance only when a live begin() recorded it (code_sha set).
|
|
70
|
+
"""
|
|
71
|
+
if src == 'import' and not code_sha:
|
|
72
|
+
return None
|
|
73
|
+
return dict(forge_version=forge_version, registry_sha=registry_sha, code_sha=code_sha, forge_rev=forge_rev)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_ESTIMATES: dict = {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def forge_rev_estimate(ts: float) -> str | None:
|
|
80
|
+
"""The newest forge commit before `ts`: a guess for runs captured with no provenance.
|
|
81
|
+
|
|
82
|
+
The dev checkout is live-edited, so a run may have used uncommitted code; this names
|
|
83
|
+
the commit it most likely started from, never more. Read-only, in the forge checkout.
|
|
84
|
+
"""
|
|
85
|
+
if not ts or not (SKILL / '.git').exists():
|
|
86
|
+
return None
|
|
87
|
+
key = int(ts)
|
|
88
|
+
if key not in _ESTIMATES:
|
|
89
|
+
try:
|
|
90
|
+
out = subprocess.run(['git', '-C', str(SKILL), 'log', '-1', '--format=%H', '--before=@%d' % key],
|
|
91
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=10,
|
|
92
|
+
env=dict(os.environ, GIT_OPTIONAL_LOCKS='0'))
|
|
93
|
+
rev = out.stdout.decode().strip() if out.returncode == 0 else ''
|
|
94
|
+
_ESTIMATES[key] = rev or None
|
|
95
|
+
except (OSError, subprocess.SubprocessError):
|
|
96
|
+
_ESTIMATES[key] = None
|
|
97
|
+
return _ESTIMATES[key]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _words(value) -> list[str]:
|
|
101
|
+
if isinstance(value, list):
|
|
102
|
+
return [str(v) for v in value if v]
|
|
103
|
+
return [w for w in str(value or '').replace(',', ' ').split() if w and w != '-']
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Store:
|
|
107
|
+
def __init__(self, db):
|
|
108
|
+
self.events = []
|
|
109
|
+
for kind, src, repo, run, run_id, mode, task, attempt, ts, data, *prov in db.execute(
|
|
110
|
+
'SELECT kind, src, repo, run, run_id, mode, task, attempt, ts, data, '
|
|
111
|
+
'forge_version, registry_sha, code_sha, forge_rev FROM events'):
|
|
112
|
+
self.events.append(dict(kind=kind, src=src, repo=repo, run=run, run_id=run_id, mode=mode,
|
|
113
|
+
task=task, attempt=attempt, ts=ts or 0, d=json.loads(data),
|
|
114
|
+
prov=_provenance(src, *prov)))
|
|
115
|
+
self.repo_paths = dict(db.execute('SELECT repo, path FROM repos'))
|
|
116
|
+
self._blobs = {}
|
|
117
|
+
|
|
118
|
+
def text(self, ref) -> str:
|
|
119
|
+
if not isinstance(ref, dict) or not ref.get('sha'):
|
|
120
|
+
return ''
|
|
121
|
+
sha = ref['sha']
|
|
122
|
+
if sha not in self._blobs:
|
|
123
|
+
raw = read_blob(sha)
|
|
124
|
+
self._blobs[sha] = raw.decode('utf-8', 'replace') if raw is not None else ''
|
|
125
|
+
return self._blobs[sha]
|
|
126
|
+
|
|
127
|
+
def of(self, kind):
|
|
128
|
+
return [e for e in self.events if e['kind'] == kind]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _question_builders():
|
|
132
|
+
from forge_jev import questions as q
|
|
133
|
+
return q
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# --- new rubrics: the outcome versions of questions forge asks without a label --------
|
|
137
|
+
|
|
138
|
+
def declared_tier() -> dict:
|
|
139
|
+
# decompose.md's difficulty table, verbatim.
|
|
140
|
+
return dict(type='choice', instructions=(
|
|
141
|
+
'Given the task and its context (in state), which difficulty tier does it belong to? '
|
|
142
|
+
'Rate the judgment the work requires, not how much code it produces: a five-hundred-line '
|
|
143
|
+
'mechanical rename is low, a ten-line concurrency fix is high.'),
|
|
144
|
+
criteria={
|
|
145
|
+
'low': 'mechanical and local: rename, move, docs, a test for behaviour that already '
|
|
146
|
+
'exists. Little design judgment; a mistake is obvious.',
|
|
147
|
+
'medium': 'self-contained implementation against a clear spec; some design choices, '
|
|
148
|
+
'bounded blast radius.',
|
|
149
|
+
'high': 'needs design judgment, touches cross-cutting behaviour, has subtle edge cases '
|
|
150
|
+
'or state/concurrency, or the right approach is not obvious from the goal.'})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def review_passes() -> dict:
|
|
154
|
+
return dict(type='noul', instructions=(
|
|
155
|
+
'A diff is about to be reviewed for correctness by an independent model that did not '
|
|
156
|
+
'write it (the diff and the task it implements are in state). Will that reviewer PASS '
|
|
157
|
+
'the diff?'),
|
|
158
|
+
criteria={
|
|
159
|
+
'true': 'The diff does what the task asks, completely, with nothing a careful reviewer '
|
|
160
|
+
'would block on: requirements met, edge cases handled, nothing else broken.',
|
|
161
|
+
'false': 'A careful reviewer would FAIL it: a requirement is missing or wrong, an edge '
|
|
162
|
+
'case breaks, a needed test is missing, or the change breaks behaviour '
|
|
163
|
+
'elsewhere.'})
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def review_severity() -> dict:
|
|
167
|
+
# forge-contract.py's severity scale, verbatim: the reviewer graded against these words.
|
|
168
|
+
return dict(type='choice', instructions=(
|
|
169
|
+
'An independent model reviews this diff against its task (both in state) and labels each '
|
|
170
|
+
'problem it is sure of by impact. What is the most severe problem it will find?'),
|
|
171
|
+
criteria={
|
|
172
|
+
'none': 'Nothing it is sure of: the diff does what the task asks.',
|
|
173
|
+
'P3': 'minor: cosmetic, naming, nit-level correctness',
|
|
174
|
+
'P2': 'edge-case bug, race that needs unusual timing, missing hardening',
|
|
175
|
+
'P1': 'wrong behaviour on realistic input, or breaks an existing contract',
|
|
176
|
+
'P0': 'data loss, security hole, crash or wrong result on the main path'})
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
SURVIVES_AT = 0.8
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def review_survives() -> dict:
|
|
183
|
+
return dict(type='noul', instructions=(
|
|
184
|
+
'This diff (in state) is about to pass review and be merged into the user\'s branch. In the '
|
|
185
|
+
'weeks after it lands, will most of the lines it adds stay in the code, neither rewritten '
|
|
186
|
+
'nor reverted?'),
|
|
187
|
+
criteria={
|
|
188
|
+
'true': 'At least %d%% of the added lines are still there, unchanged, and nothing reverted '
|
|
189
|
+
'the change: it was right the first time.' % (SURVIVES_AT * 100),
|
|
190
|
+
'false': 'Much of it was rewritten soon after, or the change was reverted: it landed, but '
|
|
191
|
+
'it was not what the code needed.'})
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# --- indexes ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
class Runs:
|
|
197
|
+
"""Plans, attempts and dispatches of every run, joined by (run, task)."""
|
|
198
|
+
|
|
199
|
+
def __init__(self, store: Store):
|
|
200
|
+
self.store = store
|
|
201
|
+
self.plans = defaultdict(list)
|
|
202
|
+
for e in store.of('plan'):
|
|
203
|
+
self.plans[e['run']].append(e)
|
|
204
|
+
for revisions in self.plans.values():
|
|
205
|
+
revisions.sort(key=lambda e: e['ts'])
|
|
206
|
+
self.attempts = defaultdict(list)
|
|
207
|
+
for e in store.of('attempt'):
|
|
208
|
+
if e['mode'] == 'parallel' and e['task'] and e['d'].get('status'):
|
|
209
|
+
self.attempts[(e['run'], e['task'])].append(e)
|
|
210
|
+
for rows in self.attempts.values():
|
|
211
|
+
rows.sort(key=lambda e: (e['attempt'] or 0, e['d'].get('wall_started') or e['ts']))
|
|
212
|
+
self.dispatches = defaultdict(list)
|
|
213
|
+
for e in store.of('dispatch'):
|
|
214
|
+
if e['task']:
|
|
215
|
+
self.dispatches[(e['run'], e['task'], e['d'].get('role'))].append(e)
|
|
216
|
+
self.accepted = {(e['run'], e['d'].get('task')) for e in store.of('invocation')
|
|
217
|
+
if e['d'].get('command') == 'accept' and e['d'].get('exit') == 0}
|
|
218
|
+
self.interrupted = {(e['run'], e['task']) for e in store.of('event')
|
|
219
|
+
if e['d'].get('name') == 'sched_terminal' and e['d'].get('status') == 'INTERRUPTED'}
|
|
220
|
+
self.censored = Counter()
|
|
221
|
+
# What `forge dataset outcomes` saw after integration, per reviewed commit; the
|
|
222
|
+
# longest horizon observed wins, since it is the stronger observation.
|
|
223
|
+
self.reviewed = {(e['run'], e['task'], e['attempt']): e['d']['reviewed_commit']
|
|
224
|
+
for e in store.of('attempt') if e['d'].get('reviewed_commit')}
|
|
225
|
+
self.survival = {}
|
|
226
|
+
for e in store.of('outcome'):
|
|
227
|
+
d = e['d']
|
|
228
|
+
if d.get('name') == 'survival' and d.get('status') == 'ok' and d.get('added'):
|
|
229
|
+
held = self.survival.get(d['reviewed'])
|
|
230
|
+
if held is None or d['horizon_days'] > held['horizon_days']:
|
|
231
|
+
self.survival[d['reviewed']] = d
|
|
232
|
+
# Live Clef predictions (forge_jev/predict.py), recorded before the attempt they are
|
|
233
|
+
# about: {(run, task, attempt): [(ts, p, model, assignment)]}, oldest first.
|
|
234
|
+
self.clef = defaultdict(list)
|
|
235
|
+
for e in sorted(store.of('jev'), key=lambda e: e['ts']):
|
|
236
|
+
d = e['d']
|
|
237
|
+
site = d.get('site')
|
|
238
|
+
if site not in ('clef-first-pass', 'clef-retry-pass') or not d.get('ok'):
|
|
239
|
+
continue
|
|
240
|
+
qid = 'first_pass' if site == 'clef-first-pass' else 'retry_pass'
|
|
241
|
+
p = ((d.get('answers') or {}).get(qid) or {}).get('noul')
|
|
242
|
+
n = d.get('intended_attempt') if site == 'clef-first-pass' else d.get('attempt')
|
|
243
|
+
if isinstance(p, (int, float)) and n:
|
|
244
|
+
self.clef[(e['run'], e['task'] or d.get('task'), int(n))].append(
|
|
245
|
+
(e['ts'], float(p), d.get('model'), d.get('assignment') or {}))
|
|
246
|
+
self.integrate_verification = {}
|
|
247
|
+
for e in sorted(store.of('verification'), key=lambda e: e['ts']):
|
|
248
|
+
if e['d'].get('scope') == 'integrate' and e['d'].get('status'):
|
|
249
|
+
self.integrate_verification[e['run']] = e['d']['status']
|
|
250
|
+
self.started = {}
|
|
251
|
+
for e in store.events:
|
|
252
|
+
if e['run'] and e['ts'] and (e['run'] not in self.started or e['ts'] < self.started[e['run']]):
|
|
253
|
+
self.started[e['run']] = e['ts']
|
|
254
|
+
|
|
255
|
+
def plan_for(self, run, task, at):
|
|
256
|
+
"""The plan revision that held this task when its attempt started."""
|
|
257
|
+
holding = [p for p in self.plans.get(run, []) if task in (p['d'].get('tasks') or {})]
|
|
258
|
+
if not holding:
|
|
259
|
+
return None
|
|
260
|
+
before = [p for p in holding if p['ts'] <= at + 1]
|
|
261
|
+
return before[-1] if before else holding[0]
|
|
262
|
+
|
|
263
|
+
def prompt(self, run, task, plan=None) -> str:
|
|
264
|
+
for e in self.attempts.get((run, task), []):
|
|
265
|
+
text = self.store.text((e['d'].get('blobs') or {}).get('prompt'))
|
|
266
|
+
if text.strip():
|
|
267
|
+
return text
|
|
268
|
+
if plan:
|
|
269
|
+
return self.store.text(((plan['d'].get('tasks') or {}).get(task) or {}).get('prompt'))
|
|
270
|
+
return ''
|
|
271
|
+
|
|
272
|
+
def resolved(self, run, task, role, attempt) -> dict:
|
|
273
|
+
rows = [e for e in self.dispatches.get((run, task, role), []) if e['attempt'] == attempt]
|
|
274
|
+
rows = rows or self.dispatches.get((run, task, role), [])
|
|
275
|
+
for e in rows:
|
|
276
|
+
d = e['d']
|
|
277
|
+
if d.get('model'):
|
|
278
|
+
return dict(model=d.get('model'), effort=d.get('effort'), harness=d.get('harness'))
|
|
279
|
+
return {}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _spec(spec) -> dict:
|
|
283
|
+
parts = str(spec or '').split(':')
|
|
284
|
+
out = dict(model=parts[0] or None)
|
|
285
|
+
if len(parts) > 1:
|
|
286
|
+
out['effort'] = parts[1]
|
|
287
|
+
if len(parts) > 2:
|
|
288
|
+
out['harness'] = parts[2]
|
|
289
|
+
return out
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _file_excerpts(store: Store, plan, files) -> dict:
|
|
293
|
+
try:
|
|
294
|
+
snapshot = json.loads(store.text((plan['d'].get('blobs') or {}).get('snapshot')) or '{}')
|
|
295
|
+
except ValueError:
|
|
296
|
+
snapshot = {}
|
|
297
|
+
out = {}
|
|
298
|
+
for rel in _words(files)[:MAX_EXCERPT_FILES]:
|
|
299
|
+
if rel in snapshot and snapshot[rel] is not None:
|
|
300
|
+
out[rel] = str(snapshot[rel])[:FILE_EXCERPT_CHARS]
|
|
301
|
+
elif snapshot:
|
|
302
|
+
out[rel] = '(does not exist yet)'
|
|
303
|
+
return out
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _siblings(plan, task_id) -> list:
|
|
307
|
+
return outcome.siblings([dict(row, id=tid) for tid, row in (plan['d'].get('tasks') or {}).items()], task_id)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _attempts(runs: Runs, run, task, rows) -> list:
|
|
311
|
+
"""One entry per real attempt number, oldest first: {n, events, outcome, reason}.
|
|
312
|
+
|
|
313
|
+
Refunded and infra-stopped executions spent no attempt. A QA-only rerun keeps its
|
|
314
|
+
attempt's number, so the number's last execution holds its final status. outcome is
|
|
315
|
+
True or False, or None when the attempt says nothing about the implementer (reason
|
|
316
|
+
names why): a later success must never be labeled as the first attempt's.
|
|
317
|
+
"""
|
|
318
|
+
by_n = {}
|
|
319
|
+
for e in rows:
|
|
320
|
+
d = e['d']
|
|
321
|
+
if d.get('refunded') or d.get('infra') or not d.get('status'):
|
|
322
|
+
continue
|
|
323
|
+
by_n.setdefault(e['attempt'] or 0, []).append(e)
|
|
324
|
+
out = []
|
|
325
|
+
for n in sorted(by_n):
|
|
326
|
+
events = by_n[n]
|
|
327
|
+
outcome, reason = _outcome(runs, run, task, n, events[-1]['d']['status'])
|
|
328
|
+
out.append(dict(n=n, events=events, outcome=outcome, reason=reason))
|
|
329
|
+
return out
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _outcome(runs: Runs, run, task, n, status):
|
|
333
|
+
if status in ('PASS', 'FAIL'):
|
|
334
|
+
return status == 'PASS', None
|
|
335
|
+
if status in ('TIMEOUT', 'ERROR'):
|
|
336
|
+
# Both statuses come from three places: the implementer's dispatch, the reviewer's,
|
|
337
|
+
# and forge's own steps between them (diff, commit, snapshot). Only an implementer
|
|
338
|
+
# that ran out of time failed the task; a crash of a harness is not task difficulty.
|
|
339
|
+
if any(x['attempt'] == n for x in runs.dispatches.get((run, task, 'qa'), [])):
|
|
340
|
+
return None, 'reviewer_' + status.lower()
|
|
341
|
+
exits = [x['d'].get('exit') for x in runs.dispatches.get((run, task, 'dwarf'), []) if x['attempt'] == n]
|
|
342
|
+
if status == 'TIMEOUT' and exits and exits[-1] == 7:
|
|
343
|
+
return False, None
|
|
344
|
+
return None, ('dwarf_' if exits and exits[-1] not in (0, None) else 'forge_') + status.lower()
|
|
345
|
+
return None, str(status).lower()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _verdicts(runs: Runs, run, task) -> dict:
|
|
349
|
+
"""The reviewer's own verdict per attempt number, before any severity gate."""
|
|
350
|
+
out = {}
|
|
351
|
+
for x in runs.dispatches.get((run, task, 'qa'), []):
|
|
352
|
+
if x['d'].get('verdict') in ('PASS', 'FAIL'):
|
|
353
|
+
out[x['attempt']] = x['d']['verdict']
|
|
354
|
+
return out
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _course(runs: Runs, run, task, attempts) -> dict:
|
|
358
|
+
"""How the task went, for meta: what a single first-pass bit leaves out."""
|
|
359
|
+
raw = _verdicts(runs, run, task)
|
|
360
|
+
first = attempts[0]['n'] if attempts else None
|
|
361
|
+
final = attempts[-1]['events'][-1]['d'] if attempts else {}
|
|
362
|
+
accepted = (run, task) in runs.accepted
|
|
363
|
+
if any(a['outcome'] for a in attempts):
|
|
364
|
+
stop = 'passed'
|
|
365
|
+
elif accepted:
|
|
366
|
+
stop = 'accepted'
|
|
367
|
+
elif final.get('noretry'):
|
|
368
|
+
stop = 'noretry'
|
|
369
|
+
elif (run, task) in runs.interrupted:
|
|
370
|
+
stop = 'interrupted'
|
|
371
|
+
else:
|
|
372
|
+
stop = 'no_pass_observed'
|
|
373
|
+
return dict(attempts_observed=len(attempts), raw_verdict=raw.get(first),
|
|
374
|
+
gate_pass=bool(attempts) and attempts[0]['events'][-1]['d'].get('status') == 'PASS'
|
|
375
|
+
and raw.get(first) == 'FAIL',
|
|
376
|
+
first_raw_pass_attempt=min((n for n, v in raw.items() if v == 'PASS'), default=None),
|
|
377
|
+
human_accept=accepted, stop_reason=stop)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# --- slices ----------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
def _task_state(runs: Runs, run, task, attempt):
|
|
383
|
+
"""forge_jev.routing's state for this task as planned when `attempt` started, plus who ran and reviewed it.
|
|
384
|
+
|
|
385
|
+
Returns (state, plan row, plan event), or Nones when the plan or the task's requirements are gone.
|
|
386
|
+
"""
|
|
387
|
+
store = runs.store
|
|
388
|
+
plan = runs.plan_for(run, task, attempt['d'].get('wall_started') or attempt['ts'])
|
|
389
|
+
if plan is None:
|
|
390
|
+
return None, None, None
|
|
391
|
+
row = plan['d']['tasks'][task]
|
|
392
|
+
prompt = store.text(row.get('prompt')) or runs.prompt(run, task)
|
|
393
|
+
if not prompt.strip():
|
|
394
|
+
return None, None, None
|
|
395
|
+
dwarf = runs.resolved(run, task, 'dwarf', attempt['attempt']) or _spec(attempt['d'].get('dwarf'))
|
|
396
|
+
reviewer = runs.resolved(run, task, 'qa', attempt['attempt']) or _spec(attempt['d'].get('qa'))
|
|
397
|
+
state = outcome.assemble(
|
|
398
|
+
goal=store.text((plan['d'].get('blobs') or {}).get('goal')) or plan['d'].get('goal') or '',
|
|
399
|
+
task_id=task, title=row.get('title', ''), files=row.get('files', ''), deps=row.get('deps', ''),
|
|
400
|
+
prompt=prompt, approach=store.text(row.get('approach')), plan_tasks=_siblings(plan, task),
|
|
401
|
+
file_excerpts=_file_excerpts(store, plan, row.get('files')),
|
|
402
|
+
memory=store.text((plan['d'].get('blobs') or {}).get('memory')),
|
|
403
|
+
dwarf=dwarf, reviewer=reviewer,
|
|
404
|
+
size=task_size(prompt=prompt, declared_files=row.get('files'),
|
|
405
|
+
plan_tasks=len(plan['d'].get('tasks') or {}), deps=row.get('deps'),
|
|
406
|
+
decompose_level=plan['d'].get('decompose_level')))
|
|
407
|
+
return state, row, plan
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _meta(runs: Runs, run, plan_event, outcome_event) -> dict:
|
|
411
|
+
"""Which forge decided and which ran, and the options that shape the outcome."""
|
|
412
|
+
out = dict(decision_provenance=plan_event.get('prov'), outcome_provenance=outcome_event.get('prov'),
|
|
413
|
+
code_changed=bool(outcome_event['d'].get('code_changed')),
|
|
414
|
+
options=plan_event['d'].get('options') or {}, started=runs.started.get(run),
|
|
415
|
+
integrate_verification=runs.integrate_verification.get(run))
|
|
416
|
+
if not out['decision_provenance'] and not out['outcome_provenance']:
|
|
417
|
+
out['forge_rev_estimate'] = forge_rev_estimate(out['started'])
|
|
418
|
+
return out
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def routing_cases(runs: Runs):
|
|
422
|
+
for (run, task), rows in runs.attempts.items():
|
|
423
|
+
attempts = _attempts(runs, run, task, rows)
|
|
424
|
+
if not attempts:
|
|
425
|
+
continue
|
|
426
|
+
if attempts[0]['outcome'] is None:
|
|
427
|
+
runs.censored['routing:' + attempts[0]['reason']] += 1
|
|
428
|
+
continue
|
|
429
|
+
first = attempts[0]['events'][0]
|
|
430
|
+
state, row, plan = _task_state(runs, run, task, first)
|
|
431
|
+
if state is None:
|
|
432
|
+
continue
|
|
433
|
+
dwarf = state['dwarf']
|
|
434
|
+
questions = dict(first_pass=first_pass())
|
|
435
|
+
expected = dict(first_pass=attempts[0]['outcome'])
|
|
436
|
+
declared = row.get('declared_difficulty')
|
|
437
|
+
if row.get('difficulty_source') in ('orchestrator', 'planner') and declared in ('low', 'medium', 'high'):
|
|
438
|
+
questions['tier'] = declared_tier()
|
|
439
|
+
expected['tier'] = declared
|
|
440
|
+
routing = row.get('routing') or {}
|
|
441
|
+
baseline = {}
|
|
442
|
+
if routing.get('composite') not in (None, 'None'):
|
|
443
|
+
# Hosted Jev's composite difficulty: higher means harder, so it ranks failures.
|
|
444
|
+
baseline['first_pass'] = dict(source='hosted-jev composite', risk=float(routing['composite']))
|
|
445
|
+
if routing.get('tier') in ('low', 'medium', 'high'):
|
|
446
|
+
baseline['tier'] = dict(source='hosted-jev tier', predicted=routing['tier'],
|
|
447
|
+
confidence=_float(routing.get('confidence')))
|
|
448
|
+
if declared in ('low', 'medium', 'high'):
|
|
449
|
+
baseline.setdefault('first_pass_declared', dict(source='declared difficulty', risk=
|
|
450
|
+
dict(low=0.0, medium=0.5, high=1.0)[declared]))
|
|
451
|
+
live = _clef_before(runs, run, task, 1, first['d'].get('wall_started') or first['ts'])
|
|
452
|
+
if live:
|
|
453
|
+
# Followed: attempt 1 ran the dwarf the prediction was about. A gate edit or a
|
|
454
|
+
# `retry --dwarf` in between makes it a prediction about a model that never ran.
|
|
455
|
+
baseline['first_pass_clef'] = dict(source='live clef first-pass', p=live[1], model=live[2],
|
|
456
|
+
followed=live[3].get('dwarf') == first['d'].get('dwarf'))
|
|
457
|
+
yield dict(slice='routing', key=(run, task), run=run, repo=first['repo'], task=task,
|
|
458
|
+
state=state, questions=questions, expected=expected, baseline=baseline,
|
|
459
|
+
label_source=dict(first_pass='final status of the first real attempt',
|
|
460
|
+
tier='declared_difficulty (%s)' % row.get('difficulty_source')),
|
|
461
|
+
dwarf=dwarf.get('model'),
|
|
462
|
+
meta=dict(_meta(runs, run, plan, attempts[0]['events'][-1]), **_course(runs, run, task, attempts),
|
|
463
|
+
route=_route(row, first)))
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
ROUTE_FIELDS = ('decision', 'tier', 'epsilon', 'candidates', 'default', 'chosen', 'explored', 'propensity')
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _route(row, first_event) -> dict | None:
|
|
470
|
+
"""The routing decision (plan --explore) behind this task's model, and whether attempt 1 followed it.
|
|
471
|
+
|
|
472
|
+
An approval-gate edit or a `retry --dwarf` replaces the model after the draw; the
|
|
473
|
+
propensity then describes a choice that never ran, so `followed` says which is which.
|
|
474
|
+
"""
|
|
475
|
+
decisions = row.get('route') or []
|
|
476
|
+
if not decisions:
|
|
477
|
+
return None
|
|
478
|
+
decision = decisions[-1]
|
|
479
|
+
out = {k: decision.get(k) for k in ROUTE_FIELDS}
|
|
480
|
+
out['followed'] = first_event['d'].get('dwarf') == decision.get('chosen')
|
|
481
|
+
return out
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def assignments(runs: Runs) -> list:
|
|
485
|
+
"""Every routing decision with what its task's first attempt did, exported cases or not.
|
|
486
|
+
|
|
487
|
+
The randomized cohort for estimating how another model would have done: the export's
|
|
488
|
+
own filters (a task with no prompt left, a censored first attempt) must not select it.
|
|
489
|
+
"""
|
|
490
|
+
from .routing import _started, decision_before
|
|
491
|
+
out = []
|
|
492
|
+
for run, revisions in sorted(runs.plans.items(), key=lambda kv: str(kv[0])):
|
|
493
|
+
decisions = defaultdict(list)
|
|
494
|
+
for plan in revisions:
|
|
495
|
+
for tid, row in (plan['d'].get('tasks') or {}).items():
|
|
496
|
+
decisions[tid].extend(row.get('route') or [])
|
|
497
|
+
for tid in sorted(t for t, rows in decisions.items() if rows):
|
|
498
|
+
attempts = _attempts(runs, run, tid, runs.attempts.get((run, tid), []))
|
|
499
|
+
first = attempts[0] if attempts else None
|
|
500
|
+
# The draw attempt 1 ran under: a later re-plan must not relabel it.
|
|
501
|
+
decision = decision_before(decisions[tid], _started(runs, run, tid, first) if first else None)
|
|
502
|
+
if decision is None:
|
|
503
|
+
continue
|
|
504
|
+
ran = first['events'][0]['d'].get('dwarf') if first else None
|
|
505
|
+
out.append(dict({k: decision.get(k) for k in ROUTE_FIELDS}, run=run, task=tid,
|
|
506
|
+
followed=(ran == decision.get('chosen')) if first else None, ran=ran,
|
|
507
|
+
first_pass=first['outcome'] if first else None,
|
|
508
|
+
censored=(first['reason'] if first['outcome'] is None else None) if first else 'not_run'))
|
|
509
|
+
return out
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def retry_cases(runs: Runs):
|
|
513
|
+
"""Every attempt after the first: retry with the findings it was given, or escalate?"""
|
|
514
|
+
store = runs.store
|
|
515
|
+
for (run, task), rows in runs.attempts.items():
|
|
516
|
+
for a in _attempts(runs, run, task, rows)[1:]:
|
|
517
|
+
if a['outcome'] is None:
|
|
518
|
+
runs.censored['retry:' + a['reason']] += 1
|
|
519
|
+
continue
|
|
520
|
+
# The findings were handed over when the attempt began; a QA-only rerun of the
|
|
521
|
+
# same number carries them without having produced them.
|
|
522
|
+
findings = next((store.text((e['d'].get('blobs') or {}).get('retry_findings')) for e in a['events']
|
|
523
|
+
if store.text((e['d'].get('blobs') or {}).get('retry_findings')).strip()), '')
|
|
524
|
+
if not findings.strip():
|
|
525
|
+
continue
|
|
526
|
+
start = a['events'][0]
|
|
527
|
+
state, _row, plan = _task_state(runs, run, task, start)
|
|
528
|
+
if state is None:
|
|
529
|
+
continue
|
|
530
|
+
state = outcome.retry(state, attempt=a['n'], previous_review=findings)
|
|
531
|
+
yield dict(slice='retry', key=(run, task, a['n']), run=run, repo=start['repo'], task=task,
|
|
532
|
+
state=state, questions=dict(retry_pass=retry_pass()),
|
|
533
|
+
expected=dict(retry_pass=a['outcome']), baseline=_retry_baseline(runs, run, task, a['n']),
|
|
534
|
+
label_source=dict(retry_pass='final status of this retry attempt'),
|
|
535
|
+
dwarf=state['dwarf'].get('model'), meta=_meta(runs, run, plan, a['events'][-1]))
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _clef_before(runs: Runs, run, task, attempt, started):
|
|
539
|
+
"""The last plan-time Clef prediction about attempt 1 made before it started, or None."""
|
|
540
|
+
found = None
|
|
541
|
+
for item in runs.clef.get((run, task, attempt), []):
|
|
542
|
+
if item[0] <= (started or 0):
|
|
543
|
+
found = item
|
|
544
|
+
return found
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _retry_baseline(runs: Runs, run, task, n) -> dict:
|
|
548
|
+
"""The Clef prediction about retry attempt n. It is made once, as the attempt begins
|
|
549
|
+
(after its receipt opened, so not timed against it): the attempt number is the join."""
|
|
550
|
+
live = (runs.clef.get((run, task, n)) or [None])[0]
|
|
551
|
+
return dict(retry_pass_clef=dict(source='live clef retry-pass', p=live[1], model=live[2])) if live else {}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _float(value):
|
|
555
|
+
try:
|
|
556
|
+
return float(value)
|
|
557
|
+
except (TypeError, ValueError):
|
|
558
|
+
return None
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _qa_rows(runs: Runs):
|
|
562
|
+
"""Every QA dispatch with a verdict, with the diff it was shown and its task text."""
|
|
563
|
+
store = runs.store
|
|
564
|
+
effort = {}
|
|
565
|
+
for (run, task), rows in runs.attempts.items():
|
|
566
|
+
for e in rows:
|
|
567
|
+
text = store.text((e['d'].get('blobs') or {}).get('qa_effort_jev'))
|
|
568
|
+
if text.strip():
|
|
569
|
+
word, _, conf = text.strip().partition('\t')
|
|
570
|
+
effort[(run, task, e['attempt'])] = (word, _float(conf))
|
|
571
|
+
for (run, task, role), rows in runs.dispatches.items():
|
|
572
|
+
if role != 'qa':
|
|
573
|
+
continue
|
|
574
|
+
for e in rows:
|
|
575
|
+
d = e['d']
|
|
576
|
+
if d.get('verdict') not in ('PASS', 'FAIL'):
|
|
577
|
+
continue
|
|
578
|
+
prompt = store.text((d.get('blobs') or {}).get('prompt'))
|
|
579
|
+
blocks = _DIFF_BLOCK.findall(prompt)
|
|
580
|
+
if not blocks or not blocks[-1].strip():
|
|
581
|
+
continue
|
|
582
|
+
found = d.get('findings') or []
|
|
583
|
+
yield dict(run=run, task=task, attempt=e['attempt'], repo=e['repo'], verdict=d['verdict'],
|
|
584
|
+
diff=blocks[-1], review=store.text((d.get('blobs') or {}).get('message')),
|
|
585
|
+
effort=effort.get((run, task, e['attempt'])), model=d.get('model'),
|
|
586
|
+
reviewer=dict(model=d.get('model'), effort=d.get('effort'), harness=d.get('harness')),
|
|
587
|
+
# Receipts from before findings_parse existed: an empty list then cannot
|
|
588
|
+
# tell a clean review from a parser failure, which was rare; read it as clean.
|
|
589
|
+
completeness=d.get('findings_parse') or review_completeness(d['verdict'], found),
|
|
590
|
+
worst=_worst(found))
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _worst(found) -> str:
|
|
594
|
+
severities = [f['severity'] for f in found if f.get('status') == 'CONFIRMED' and f.get('severity') is not None]
|
|
595
|
+
return 'P%d' % min(severities) if severities else 'none'
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
EFFORT_RANK = dict(low=0, medium=1, high=2, xhigh=3, max=4)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def shadow_pairs(runs: Runs) -> dict:
|
|
602
|
+
"""{(run, task, attempt): (primary qa dispatch, shadow dispatch)} for every usable shadow review.
|
|
603
|
+
|
|
604
|
+
Usable means the shadow ran to its end on an untouched snapshot (`shadow_review` says
|
|
605
|
+
done), both reviewers gave a verdict, and the shadow reviewed the commit that attempt
|
|
606
|
+
committed. Anything else would count noise in the shadow as disagreement.
|
|
607
|
+
"""
|
|
608
|
+
ended = {}
|
|
609
|
+
for e in runs.store.of('event'):
|
|
610
|
+
if e['d'].get('name') == 'shadow_review':
|
|
611
|
+
ended[(e['run'], e['task'], _int(e['d'].get('attempt')))] = e['d'].get('status')
|
|
612
|
+
out = {}
|
|
613
|
+
for (run, task, role), rows in runs.dispatches.items():
|
|
614
|
+
if role != 'qa_shadow':
|
|
615
|
+
continue
|
|
616
|
+
for shadow in rows:
|
|
617
|
+
n = shadow['attempt']
|
|
618
|
+
if ended.get((run, task, n)) != 'done' or shadow['d'].get('verdict') not in ('PASS', 'FAIL'):
|
|
619
|
+
continue
|
|
620
|
+
primary = [d for d in runs.dispatches.get((run, task, 'qa'), [])
|
|
621
|
+
if d['attempt'] == n and d['d'].get('verdict') in ('PASS', 'FAIL')]
|
|
622
|
+
reviewed = (shadow['d'].get('shadow') or {}).get('reviewed')
|
|
623
|
+
if primary and reviewed and reviewed == runs.reviewed.get((run, task, n)):
|
|
624
|
+
out[(run, task, n)] = (primary[-1], shadow)
|
|
625
|
+
return out
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def agreement(runs: Runs) -> dict:
|
|
629
|
+
"""How often two reviewers of one diff agree, overall and per (primary, shadow) model pair."""
|
|
630
|
+
pairs = shadow_pairs(runs)
|
|
631
|
+
by_pair = defaultdict(lambda: [0, 0])
|
|
632
|
+
for primary, shadow in pairs.values():
|
|
633
|
+
key = '%s vs %s' % (primary['d'].get('model'), shadow['d'].get('model'))
|
|
634
|
+
by_pair[key][0] += 1
|
|
635
|
+
by_pair[key][1] += primary['d']['verdict'] == shadow['d']['verdict']
|
|
636
|
+
shadows = sum(len(rows) for (_, _, role), rows in runs.dispatches.items() if role == 'qa_shadow')
|
|
637
|
+
agree = sum(a for _, a in by_pair.values())
|
|
638
|
+
return dict(shadow_reviews=shadows, pairs=len(pairs), agree=agree,
|
|
639
|
+
by_pair={k: dict(pairs=n, agree=a) for k, (n, a) in sorted(by_pair.items())})
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _int(value):
|
|
643
|
+
try:
|
|
644
|
+
return int(value)
|
|
645
|
+
except (TypeError, ValueError):
|
|
646
|
+
return None
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def review_cases(runs: Runs):
|
|
650
|
+
shadows = shadow_pairs(runs)
|
|
651
|
+
seen = {}
|
|
652
|
+
for r in _qa_rows(runs):
|
|
653
|
+
task = runs.prompt(r['run'], r['task'])[:REVIEW_CHARS]
|
|
654
|
+
key = _sha(task, r['diff'])
|
|
655
|
+
seen.setdefault(key, []).append((r, task))
|
|
656
|
+
for key, items in seen.items():
|
|
657
|
+
verdicts = {r['verdict'] for r, _ in items}
|
|
658
|
+
if len(verdicts) != 1:
|
|
659
|
+
# Two reviews of the same diff disagreed: the label is noise, not an outcome.
|
|
660
|
+
continue
|
|
661
|
+
r, task = items[0]
|
|
662
|
+
diff = r['diff']
|
|
663
|
+
# Who reviews decides the outcome as much as the diff: the same diff meets a 0.93 pass rate
|
|
664
|
+
# from one reviewer and 0.39 from another, so the reviewer is part of the state.
|
|
665
|
+
state = dict(diff=diff[:DIFF_CHARS], task=task, truncated=len(diff) > DIFF_CHARS, reviewer=r['reviewer'])
|
|
666
|
+
baseline = {}
|
|
667
|
+
if r['effort'] and r['effort'][0] in EFFORT_RANK:
|
|
668
|
+
baseline['passes'] = dict(source='hosted-jev qa effort', risk=EFFORT_RANK[r['effort'][0]] / 4.0)
|
|
669
|
+
questions, expected = dict(passes=review_passes()), dict(passes=r['verdict'] == 'PASS')
|
|
670
|
+
label_source = dict(passes='QA verdict')
|
|
671
|
+
# A graded label only where every finding the reviewer was sure of carries a severity:
|
|
672
|
+
# skipping the rest keeps "none" honest instead of counting unreadable reviews as clean.
|
|
673
|
+
if all(item['completeness'] in ('clean', 'graded') for item, _ in items) and \
|
|
674
|
+
len({item['worst'] for item, _ in items}) == 1:
|
|
675
|
+
questions['severity'], expected['severity'] = review_severity(), r['worst']
|
|
676
|
+
label_source['severity'] = 'worst CONFIRMED finding (%s)' % r['completeness']
|
|
677
|
+
meta = dict(completeness=r['completeness'])
|
|
678
|
+
# A second reviewer of the same diff, never consulted by the run: agreement is how
|
|
679
|
+
# much of this label is the diff and how much is who happened to review it.
|
|
680
|
+
paired = next((shadows[k] for k in ((i['run'], i['task'], i['attempt']) for i, _ in items) if k in shadows), None)
|
|
681
|
+
if paired:
|
|
682
|
+
primary, shadow = paired
|
|
683
|
+
meta['shadow'] = dict(verdict=shadow['d']['verdict'], reviewer=shadow['d'].get('model'),
|
|
684
|
+
worst=_worst(shadow['d'].get('findings') or []))
|
|
685
|
+
meta['consensus'] = shadow['d']['verdict'] == primary['d']['verdict']
|
|
686
|
+
# The one label here no reviewer decides: whether the merged diff's lines stayed.
|
|
687
|
+
landed = next((runs.survival[c] for c in (runs.reviewed.get((i['run'], i['task'], i['attempt']))
|
|
688
|
+
for i, _ in items) if c in runs.survival), None)
|
|
689
|
+
if landed:
|
|
690
|
+
kept = landed['surviving'] / landed['added']
|
|
691
|
+
questions['survives'] = review_survives()
|
|
692
|
+
expected['survives'] = kept >= SURVIVES_AT and not landed.get('reverted_by')
|
|
693
|
+
label_source['survives'] = 'git blame %d days after integrate' % landed['horizon_days']
|
|
694
|
+
meta['survival'] = dict(added=landed['added'], surviving=landed['surviving'],
|
|
695
|
+
reverted_by=landed.get('reverted_by') or [], horizon_days=landed['horizon_days'])
|
|
696
|
+
yield dict(slice='review', key=key, run=r['run'], repo=r['repo'], task=r['task'], state=state,
|
|
697
|
+
questions=questions, expected=expected, baseline=baseline, label_source=label_source,
|
|
698
|
+
reviewer=r['model'], meta=meta)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
_SKIP_PREFIXES = ('.git/', '.forge/', 'node_modules/', 'vendor/', 'target/', 'dist/',
|
|
702
|
+
'build/', '.venv/', 'venv/', '__pycache__/')
|
|
703
|
+
_SKIP_SUFFIXES = ('.lock', '.min.js', '.map', '.png', '.jpg', '.jpeg', '.gif', '.svg',
|
|
704
|
+
'.ico', '.pdf', '.zip', '.woff', '.woff2', '.ttf')
|
|
705
|
+
_SKIP_NAMES = ('package-lock.json', 'npm-shrinkwrap.json', 'composer.lock',
|
|
706
|
+
'Gemfile.lock', 'go.sum', 'pnpm-lock.yaml', 'yarn.lock')
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _candidate(name: str) -> bool:
|
|
710
|
+
return not (name.startswith(_SKIP_PREFIXES) or name.endswith(_SKIP_SUFFIXES)
|
|
711
|
+
or name.rsplit('/', 1)[-1] in _SKIP_NAMES)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _tracked(store: Store, plan, repo, base) -> list[str]:
|
|
715
|
+
listing = store.text((plan['d'].get('blobs') or {}).get('inventory')) if plan else ''
|
|
716
|
+
if listing.strip():
|
|
717
|
+
return [line for line in listing.splitlines() if line.strip()]
|
|
718
|
+
path = store.repo_paths.get(repo)
|
|
719
|
+
if not path or not base or not Path(path).is_dir():
|
|
720
|
+
return []
|
|
721
|
+
try:
|
|
722
|
+
out = subprocess.run(['git', '-C', path, 'ls-tree', '-r', '--name-only', base],
|
|
723
|
+
capture_output=True, text=True, timeout=20)
|
|
724
|
+
except (OSError, subprocess.SubprocessError):
|
|
725
|
+
return []
|
|
726
|
+
return out.stdout.splitlines() if out.returncode == 0 else []
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _final_pass(rows):
|
|
730
|
+
passed = [e for e in rows if e['d'].get('status') == 'PASS']
|
|
731
|
+
return passed[-1] if passed else None
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
DRIFT_JOIN_S = 24 * 3600
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _drift_baseline(store: Store):
|
|
738
|
+
"""Hosted Jev's jev-drift answers: {(repo, task id): [(ts, {file: p})]}, oldest first.
|
|
739
|
+
|
|
740
|
+
Keyed by when, not only by what: a task id repeats across runs of one repo, and most
|
|
741
|
+
drift requests predate the run uid, so drift_cases takes the request made last before
|
|
742
|
+
the task's attempts began (see _drift_answers).
|
|
743
|
+
"""
|
|
744
|
+
out = defaultdict(list)
|
|
745
|
+
for e in sorted(store.of('jev'), key=lambda e: e['ts']):
|
|
746
|
+
if e['d'].get('site') != 'jev-drift':
|
|
747
|
+
continue
|
|
748
|
+
try:
|
|
749
|
+
request = json.loads(store.text((e['d'].get('blobs') or {}).get('request')) or '{}')
|
|
750
|
+
except ValueError:
|
|
751
|
+
continue
|
|
752
|
+
task = ((request.get('state') or {}).get('task') or {}).get('id')
|
|
753
|
+
answers = e['d'].get('answers') or {}
|
|
754
|
+
files = {}
|
|
755
|
+
for qid, question in (request.get('questions') or {}).items():
|
|
756
|
+
found = re.search(r"the file '([^']+)'", question.get('instructions', ''))
|
|
757
|
+
value = (answers.get(qid) or {}).get('noul') if isinstance(answers.get(qid), dict) else answers.get(qid)
|
|
758
|
+
if found and isinstance(value, (int, float)):
|
|
759
|
+
files[found.group(1)] = float(value)
|
|
760
|
+
if files:
|
|
761
|
+
out[(e['repo'], task)].append((e['ts'], files))
|
|
762
|
+
return out
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _drift_answers(baseline, repo, task, started) -> dict:
|
|
766
|
+
"""The drift answers of the last request for this task before `started`, within a day."""
|
|
767
|
+
found = {}
|
|
768
|
+
for ts, files in baseline.get((repo, task), []):
|
|
769
|
+
if ts <= started and started - ts <= DRIFT_JOIN_S:
|
|
770
|
+
found = files
|
|
771
|
+
return found
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def drift_cases(runs: Runs, rng_seed: int):
|
|
775
|
+
q = _question_builders()
|
|
776
|
+
store = runs.store
|
|
777
|
+
jev = _drift_baseline(store)
|
|
778
|
+
for (run, task), rows in runs.attempts.items():
|
|
779
|
+
final = _final_pass(rows)
|
|
780
|
+
if final is None:
|
|
781
|
+
continue
|
|
782
|
+
d = final['d']
|
|
783
|
+
changed = [f for f in _words(d.get('changed_files')) if _candidate(f)]
|
|
784
|
+
declared = set(_words(d.get('declared_files')))
|
|
785
|
+
if not changed:
|
|
786
|
+
continue
|
|
787
|
+
plan = runs.plan_for(run, task, d.get('wall_started') or final['ts'])
|
|
788
|
+
tracked = [f for f in _tracked(store, plan, final['repo'], d.get('base_ref')) if _candidate(f)]
|
|
789
|
+
if len(tracked) < 20:
|
|
790
|
+
continue
|
|
791
|
+
rng = random.Random(_sha(rng_seed, run, task))
|
|
792
|
+
positives = [f for f in changed if f not in declared]
|
|
793
|
+
rng.shuffle(positives)
|
|
794
|
+
positives = positives[:3]
|
|
795
|
+
if not positives and rng.random() < 0.5:
|
|
796
|
+
continue
|
|
797
|
+
near_dirs = {f.rsplit('/', 1)[0] for f in list(declared) + changed if '/' in f}
|
|
798
|
+
pool = [f for f in tracked if f not in declared and f not in changed]
|
|
799
|
+
near = [f for f in pool if f.rsplit('/', 1)[0] in near_dirs]
|
|
800
|
+
far = [f for f in pool if f not in set(near)]
|
|
801
|
+
rng.shuffle(near)
|
|
802
|
+
rng.shuffle(far)
|
|
803
|
+
want = DRIFT_QUESTIONS - len(positives) if positives else 4
|
|
804
|
+
negatives = near[:(want + 1) // 2]
|
|
805
|
+
negatives += far[:want - len(negatives)]
|
|
806
|
+
files = positives + negatives
|
|
807
|
+
rng.shuffle(files)
|
|
808
|
+
row = ((plan['d'].get('tasks') or {}).get(task) or {}) if plan else {}
|
|
809
|
+
prompt = store.text(row.get('prompt')) or runs.prompt(run, task)
|
|
810
|
+
if not prompt.strip():
|
|
811
|
+
continue
|
|
812
|
+
state = dict(goal=(store.text((plan['d'].get('blobs') or {}).get('goal')) if plan else '') or '',
|
|
813
|
+
task=dict(id=task, title=row.get('title') or d.get('title', ''),
|
|
814
|
+
declared_files=','.join(sorted(declared))),
|
|
815
|
+
prompt=prompt, approach=store.text(row.get('approach')))
|
|
816
|
+
started = min((e['d'].get('wall_started') or e['ts']) for e in rows)
|
|
817
|
+
jev_files = _drift_answers(jev, final['repo'], task, started)
|
|
818
|
+
questions, expected, baseline = {}, {}, {}
|
|
819
|
+
for i, name in enumerate(files):
|
|
820
|
+
questions['f%d' % i] = q.drift_prediction(name)
|
|
821
|
+
expected['f%d' % i] = name in positives
|
|
822
|
+
if name in jev_files:
|
|
823
|
+
baseline['f%d' % i] = dict(source='hosted-jev drift', p=jev_files[name])
|
|
824
|
+
yield dict(slice='drift', key=(run, task), run=run, repo=final['repo'], task=task, state=state,
|
|
825
|
+
questions=questions, expected=expected, baseline=baseline,
|
|
826
|
+
label_source=dict(all='file in the passing attempt\'s changed files'))
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def citation_cases(runs: Runs, rng_seed: int):
|
|
830
|
+
q = _question_builders()
|
|
831
|
+
fails = [r for r in _qa_rows(runs) if r['verdict'] == 'FAIL' and r['review'].strip()]
|
|
832
|
+
by_repo = defaultdict(list)
|
|
833
|
+
for r in fails:
|
|
834
|
+
by_repo[r['repo']].append(r)
|
|
835
|
+
seen = set()
|
|
836
|
+
for r in fails:
|
|
837
|
+
key = _sha(r['review'])
|
|
838
|
+
if key in seen:
|
|
839
|
+
continue
|
|
840
|
+
seen.add(key)
|
|
841
|
+
rng = random.Random(_sha(rng_seed, key))
|
|
842
|
+
own = rng.random() < 0.5
|
|
843
|
+
diff = r['diff']
|
|
844
|
+
if not own:
|
|
845
|
+
mine = set(re.findall(r'^\+\+\+ b/(\S+)', diff, re.M))
|
|
846
|
+
others = [o for o in by_repo[r['repo']] if o['task'] != r['task']
|
|
847
|
+
and not mine & set(re.findall(r'^\+\+\+ b/(\S+)', o['diff'], re.M))]
|
|
848
|
+
if not others:
|
|
849
|
+
continue
|
|
850
|
+
diff = rng.choice(others)['diff']
|
|
851
|
+
state = dict(review=r['review'][:REVIEW_CHARS], diff=diff[:DIFF_CHARS],
|
|
852
|
+
truncated=dict(review=len(r['review']) > REVIEW_CHARS, diff=len(diff) > DIFF_CHARS))
|
|
853
|
+
yield dict(slice='citation', key=key, run=r['run'], repo=r['repo'], task=r['task'], state=state,
|
|
854
|
+
questions=dict(citation=q.findings_cite_the_diff()), expected=dict(citation=own),
|
|
855
|
+
baseline={}, label_source=dict(citation='review paired with its own diff (true) or '
|
|
856
|
+
'another task\'s diff from the same repo (false)'))
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _area(path: str) -> str:
|
|
860
|
+
parts = path.split('/')
|
|
861
|
+
return '/'.join(parts[:2]) if len(parts) > 2 else parts[0]
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def memory_cases(runs: Runs, rng_seed: int):
|
|
865
|
+
q = _question_builders()
|
|
866
|
+
store = runs.store
|
|
867
|
+
for (run, task), rows in runs.attempts.items():
|
|
868
|
+
final = _final_pass(rows)
|
|
869
|
+
if final is None:
|
|
870
|
+
continue
|
|
871
|
+
d = final['d']
|
|
872
|
+
changed = set(_words(d.get('changed_files')))
|
|
873
|
+
touched = changed | set(_words(d.get('declared_files')))
|
|
874
|
+
if not changed:
|
|
875
|
+
continue
|
|
876
|
+
plan = runs.plan_for(run, task, d.get('wall_started') or final['ts'])
|
|
877
|
+
memory = store.text((plan['d'].get('blobs') or {}).get('memory')) if plan else ''
|
|
878
|
+
facts = [line for line in memory.splitlines() if line.startswith('- ')]
|
|
879
|
+
positives, negatives = [], []
|
|
880
|
+
areas = {_area(f) for f in touched}
|
|
881
|
+
for line in facts:
|
|
882
|
+
found = _ANCHOR.search(line)
|
|
883
|
+
if not found:
|
|
884
|
+
continue
|
|
885
|
+
anchors = _words(found.group(1))
|
|
886
|
+
if any(a in changed for a in anchors):
|
|
887
|
+
positives.append(line)
|
|
888
|
+
elif not any(_area(a) in areas for a in anchors):
|
|
889
|
+
negatives.append(line)
|
|
890
|
+
if not positives or not negatives:
|
|
891
|
+
continue
|
|
892
|
+
rng = random.Random(_sha(rng_seed, run, task))
|
|
893
|
+
rng.shuffle(positives)
|
|
894
|
+
rng.shuffle(negatives)
|
|
895
|
+
positives = positives[:2]
|
|
896
|
+
lines = positives + negatives[:MEMORY_QUESTIONS - len(positives)]
|
|
897
|
+
rng.shuffle(lines)
|
|
898
|
+
task_text = runs.prompt(run, task, plan)[:SLICE_TASK_CHARS]
|
|
899
|
+
if not task_text.strip():
|
|
900
|
+
continue
|
|
901
|
+
questions = {'l%d' % i: q.memory_relevant(line, task_text) for i, line in enumerate(lines)}
|
|
902
|
+
expected = {'l%d' % i: line in positives for i, line in enumerate(lines)}
|
|
903
|
+
yield dict(slice='memory', key=(run, task), run=run, repo=final['repo'], task=task,
|
|
904
|
+
state=dict(task=task_text, facts=lines), questions=questions, expected=expected,
|
|
905
|
+
baseline={}, label_source=dict(all='fact anchored in a file the task edited (true) '
|
|
906
|
+
'or in an area it never touched (false)'))
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
# --- sampling and output ---------------------------------------------------------------
|
|
910
|
+
|
|
911
|
+
def _sample(cases: list, limit: int | None, seed: int) -> list:
|
|
912
|
+
"""Spread a capped slice across runs, then across labels within a run."""
|
|
913
|
+
if not limit or len(cases) <= limit:
|
|
914
|
+
return cases
|
|
915
|
+
rng = random.Random(seed)
|
|
916
|
+
by_run = defaultdict(list)
|
|
917
|
+
for case in cases:
|
|
918
|
+
by_run[case['run']].append(case)
|
|
919
|
+
for rows in by_run.values():
|
|
920
|
+
rng.shuffle(rows)
|
|
921
|
+
picked = []
|
|
922
|
+
while len(picked) < limit and any(by_run.values()):
|
|
923
|
+
for run in sorted(by_run, key=lambda r: _sha(seed, r)):
|
|
924
|
+
if by_run[run] and len(picked) < limit:
|
|
925
|
+
picked.append(by_run[run].pop())
|
|
926
|
+
return picked
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def build(db, *, slices=SLICES, limit: int | None = None, seed: int = 7, stats: dict | None = None) -> list:
|
|
930
|
+
"""Every case of the given slices. `stats`, when given, receives the censored-label counts."""
|
|
931
|
+
store = Store(db)
|
|
932
|
+
runs = Runs(store)
|
|
933
|
+
builders = dict(routing=lambda: routing_cases(runs), retry=lambda: retry_cases(runs),
|
|
934
|
+
review=lambda: review_cases(runs),
|
|
935
|
+
drift=lambda: drift_cases(runs, seed), citation=lambda: citation_cases(runs, seed),
|
|
936
|
+
memory=lambda: memory_cases(runs, seed))
|
|
937
|
+
out = []
|
|
938
|
+
for name in slices:
|
|
939
|
+
cases = list(builders[name]())
|
|
940
|
+
for case in cases:
|
|
941
|
+
case['id'] = '%s-%s' % (name, _sha(seed, *([case['key']] if isinstance(case['key'], str)
|
|
942
|
+
else case['key']))[:12])
|
|
943
|
+
out.extend(sorted(_sample(cases, limit, seed), key=lambda c: c['id']))
|
|
944
|
+
if stats is not None:
|
|
945
|
+
stats['censored'] = dict(sorted(runs.censored.items()))
|
|
946
|
+
stats['assignments'] = assignments(runs)
|
|
947
|
+
return out
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _private_write(path: Path, text: str) -> None:
|
|
951
|
+
fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
952
|
+
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
|
953
|
+
f.write(text)
|
|
954
|
+
os.chmod(path, 0o600)
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def write(cases: list, out: Path, *, seed: int, limit, stats: dict | None = None) -> dict:
|
|
958
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
959
|
+
os.chmod(out, 0o700)
|
|
960
|
+
lines, meta = [], []
|
|
961
|
+
for c in cases:
|
|
962
|
+
lines.append(json.dumps(dict(id=c['id'], state=c['state'], questions=c['questions'],
|
|
963
|
+
expected=c['expected']), ensure_ascii=False, sort_keys=True))
|
|
964
|
+
meta.append(json.dumps(dict(c.get('meta') or {}, id=c['id'], slice=c['slice'], run=c['run'], repo=c['repo'],
|
|
965
|
+
task=c['task'], baseline=c['baseline'], label_source=c['label_source'],
|
|
966
|
+
dwarf=c.get('dwarf'), reviewer=c.get('reviewer'),
|
|
967
|
+
state_chars=len(json.dumps(c['state'], ensure_ascii=False)),
|
|
968
|
+
question_chars=len(json.dumps(c['questions'], ensure_ascii=False))),
|
|
969
|
+
ensure_ascii=False, sort_keys=True))
|
|
970
|
+
body = '\n'.join(lines) + '\n'
|
|
971
|
+
_private_write(out / 'dataset.jsonl', body)
|
|
972
|
+
_private_write(out / 'meta.jsonl', '\n'.join(meta) + '\n')
|
|
973
|
+
routed = (stats or {}).get('assignments') or []
|
|
974
|
+
if routed:
|
|
975
|
+
_private_write(out / 'assignments.jsonl', ''.join(json.dumps(a, sort_keys=True) + '\n' for a in routed))
|
|
976
|
+
counts = defaultdict(lambda: defaultdict(int))
|
|
977
|
+
for c in cases:
|
|
978
|
+
for qid, label in c['expected'].items():
|
|
979
|
+
counts[c['slice']][str(label)] += 1
|
|
980
|
+
manifest = dict(created=time.strftime('%Y-%m-%dT%H:%M:%S'), seed=seed, limit_per_slice=limit,
|
|
981
|
+
cases=len(cases), sha256=hashlib.sha256(body.encode()).hexdigest(),
|
|
982
|
+
censored=(stats or {}).get('censored', {}),
|
|
983
|
+
slices={s: dict(cases=sum(1 for c in cases if c['slice'] == s), labels=dict(v))
|
|
984
|
+
for s, v in counts.items()})
|
|
985
|
+
_private_write(out / 'manifest.json', json.dumps(manifest, indent=2, sort_keys=True) + '\n')
|
|
986
|
+
return manifest
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def main(args) -> int:
|
|
990
|
+
db = connect()
|
|
991
|
+
try:
|
|
992
|
+
stats = {}
|
|
993
|
+
cases = build(db, slices=args.slices or SLICES, limit=args.limit, seed=args.seed, stats=stats)
|
|
994
|
+
finally:
|
|
995
|
+
db.close()
|
|
996
|
+
if args.out:
|
|
997
|
+
out = Path(args.out)
|
|
998
|
+
else:
|
|
999
|
+
(data_home() / 'exports').mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
1000
|
+
os.chmod(data_home() / 'exports', 0o700)
|
|
1001
|
+
out = data_home() / 'exports' / 'forge-decisions'
|
|
1002
|
+
manifest = write(cases, out, seed=args.seed, limit=args.limit, stats=stats)
|
|
1003
|
+
print('wrote %d cases to %s' % (manifest['cases'], out / 'dataset.jsonl'))
|
|
1004
|
+
for name, info in sorted(manifest['slices'].items()):
|
|
1005
|
+
print(' %-9s %4d cases labels %s' % (name, info['cases'],
|
|
1006
|
+
', '.join('%s %d' % kv for kv in sorted(info['labels'].items()))))
|
|
1007
|
+
if manifest['censored']:
|
|
1008
|
+
print(' left out, no label: %s' % ', '.join('%s %d' % kv for kv in manifest['censored'].items()))
|
|
1009
|
+
return 0
|