@giamme/forge 0.0.0-stage → 0.8.0

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