@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.
Files changed (152) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/LICENSE +21 -0
  3. package/README.md +194 -2
  4. package/SKILL.md +123 -0
  5. package/VERSION +1 -0
  6. package/docs/README.md +26 -0
  7. package/docs/add-ons.md +33 -0
  8. package/docs/artifacts.md +69 -0
  9. package/docs/commands.md +315 -0
  10. package/docs/dataset.md +20 -0
  11. package/docs/decomposed-runs.md +268 -0
  12. package/docs/development.md +95 -0
  13. package/docs/forge-ui.md +92 -0
  14. package/docs/fractal.md +181 -0
  15. package/docs/install.md +160 -0
  16. package/docs/memory.md +156 -0
  17. package/docs/models.md +118 -0
  18. package/docs/reviews.md +231 -0
  19. package/docs/safety.md +55 -0
  20. package/docs/troubleshooting.md +54 -0
  21. package/forge +79 -0
  22. package/package.json +27 -4
  23. package/references/dataset.md +329 -0
  24. package/references/decompose.md +667 -0
  25. package/references/fractal-dashboard.md +21 -0
  26. package/references/fractal.md +307 -0
  27. package/references/harnesses.md +365 -0
  28. package/references/jev.md +865 -0
  29. package/references/memory.md +228 -0
  30. package/references/planning.md +42 -0
  31. package/references/ripwire.md +47 -0
  32. package/references/runtime.md +64 -0
  33. package/references/solo.md +113 -0
  34. package/references/ui.md +112 -0
  35. package/registry.tsv +81 -0
  36. package/scripts/forge-artifact.sh +78 -0
  37. package/scripts/forge-contract.py +430 -0
  38. package/scripts/forge-dataset.py +6 -0
  39. package/scripts/forge-dispatch.sh +671 -0
  40. package/scripts/forge-fail-first.py +195 -0
  41. package/scripts/forge-fractal-dispatch.sh +4 -0
  42. package/scripts/forge-fractal-options.sh +54 -0
  43. package/scripts/forge-fractal.py +6 -0
  44. package/scripts/forge-guard.py +266 -0
  45. package/scripts/forge-install-fractal.sh +4 -0
  46. package/scripts/forge-install-ripwire.sh +52 -0
  47. package/scripts/forge-install.sh +135 -0
  48. package/scripts/forge-jev-options.sh +259 -0
  49. package/scripts/forge-jev.py +6 -0
  50. package/scripts/forge-lib-plans.sh +785 -0
  51. package/scripts/forge-lib-review.sh +494 -0
  52. package/scripts/forge-lib-task.sh +302 -0
  53. package/scripts/forge-lib-verify.sh +30 -0
  54. package/scripts/forge-memory.sh +575 -0
  55. package/scripts/forge-metrics.sh +39 -0
  56. package/scripts/forge-parallel-options.sh +76 -0
  57. package/scripts/forge-parallel.sh +1717 -0
  58. package/scripts/forge-plan-check.py +118 -0
  59. package/scripts/forge-prompt.py +45 -0
  60. package/scripts/forge-ripwire.py +136 -0
  61. package/scripts/forge-route.py +344 -0
  62. package/scripts/forge-runs.py +121 -0
  63. package/scripts/forge-runtime.py +586 -0
  64. package/scripts/forge-schedule.py +312 -0
  65. package/scripts/forge-shadow.py +305 -0
  66. package/scripts/forge-solo.sh +347 -0
  67. package/scripts/forge-ui-options.sh +42 -0
  68. package/scripts/forge-ui.py +435 -0
  69. package/scripts/forge-update.sh +71 -0
  70. package/scripts/forge-verify-coverage.py +956 -0
  71. package/scripts/forge-version.py +240 -0
  72. package/scripts/forge_dataset/__init__.py +439 -0
  73. package/scripts/forge_dataset/capture.py +330 -0
  74. package/scripts/forge_dataset/checks.py +109 -0
  75. package/scripts/forge_dataset/cli.py +233 -0
  76. package/scripts/forge_dataset/export.py +1009 -0
  77. package/scripts/forge_dataset/harvest.py +716 -0
  78. package/scripts/forge_dataset/importer.py +412 -0
  79. package/scripts/forge_dataset/outcomes.py +275 -0
  80. package/scripts/forge_dataset/routing.py +278 -0
  81. package/scripts/forge_fractal/__init__.py +84 -0
  82. package/scripts/forge_fractal/bridge.py +46 -0
  83. package/scripts/forge_fractal/cli.py +202 -0
  84. package/scripts/forge_fractal/dashboard.css +138 -0
  85. package/scripts/forge_fractal/dashboard.html +41 -0
  86. package/scripts/forge_fractal/dashboard.js +687 -0
  87. package/scripts/forge_fractal/decomposition.py +262 -0
  88. package/scripts/forge_fractal/execution.py +659 -0
  89. package/scripts/forge_fractal/inspection.py +261 -0
  90. package/scripts/forge_fractal/install.py +158 -0
  91. package/scripts/forge_fractal/invocation.py +94 -0
  92. package/scripts/forge_fractal/pipeline.py +124 -0
  93. package/scripts/forge_fractal/selection.py +179 -0
  94. package/scripts/forge_jev/__init__.py +376 -0
  95. package/scripts/forge_jev/backtest.py +322 -0
  96. package/scripts/forge_jev/cli.py +980 -0
  97. package/scripts/forge_jev/client.py +418 -0
  98. package/scripts/forge_jev/corpus.py +156 -0
  99. package/scripts/forge_jev/coupling.py +244 -0
  100. package/scripts/forge_jev/features.py +24 -0
  101. package/scripts/forge_jev/gates.py +100 -0
  102. package/scripts/forge_jev/memory.py +163 -0
  103. package/scripts/forge_jev/outcome.py +49 -0
  104. package/scripts/forge_jev/predict.py +204 -0
  105. package/scripts/forge_jev/questions.py +661 -0
  106. package/scripts/forge_jev/review.py +73 -0
  107. package/scripts/forge_jev/routing.py +271 -0
  108. package/scripts/forge_jev/subset.py +123 -0
  109. package/scripts/forge_jev/verify.py +453 -0
  110. package/scripts/forge_registry.py +140 -0
  111. package/ui/README.md +347 -0
  112. package/ui/THIRD_PARTY_NOTICES.md +177 -0
  113. package/ui/docs/api.md +970 -0
  114. package/ui/forge_ui/__init__.py +20 -0
  115. package/ui/forge_ui/__main__.py +135 -0
  116. package/ui/forge_ui/diffs.py +454 -0
  117. package/ui/forge_ui/executions.py +436 -0
  118. package/ui/forge_ui/export.py +214 -0
  119. package/ui/forge_ui/findings.py +132 -0
  120. package/ui/forge_ui/fractal.py +582 -0
  121. package/ui/forge_ui/models.py +216 -0
  122. package/ui/forge_ui/reader.py +1424 -0
  123. package/ui/forge_ui/server.py +312 -0
  124. package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
  125. package/ui/forge_ui/static/app.js +1079 -0
  126. package/ui/forge_ui/static/cloud3d.js +647 -0
  127. package/ui/forge_ui/static/cloudmodel.js +375 -0
  128. package/ui/forge_ui/static/cloudui.js +397 -0
  129. package/ui/forge_ui/static/convo.js +295 -0
  130. package/ui/forge_ui/static/datatable.js +232 -0
  131. package/ui/forge_ui/static/diffview.js +235 -0
  132. package/ui/forge_ui/static/favicon.svg +1 -0
  133. package/ui/forge_ui/static/files.js +776 -0
  134. package/ui/forge_ui/static/findings3d.js +134 -0
  135. package/ui/forge_ui/static/gantt.js +97 -0
  136. package/ui/forge_ui/static/graph.js +266 -0
  137. package/ui/forge_ui/static/icons.js +94 -0
  138. package/ui/forge_ui/static/index.html +67 -0
  139. package/ui/forge_ui/static/iso.js +816 -0
  140. package/ui/forge_ui/static/leaderboard.js +311 -0
  141. package/ui/forge_ui/static/map3d.js +45 -0
  142. package/ui/forge_ui/static/modelstats.js +249 -0
  143. package/ui/forge_ui/static/notify.js +179 -0
  144. package/ui/forge_ui/static/panel.js +562 -0
  145. package/ui/forge_ui/static/runmodel.js +667 -0
  146. package/ui/forge_ui/static/scene3d.js +1471 -0
  147. package/ui/forge_ui/static/search.js +120 -0
  148. package/ui/forge_ui/static/shell.js +137 -0
  149. package/ui/forge_ui/static/stagectl.js +150 -0
  150. package/ui/forge_ui/static/style.css +1073 -0
  151. package/ui/forge_ui/static/theme.js +25 -0
  152. 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