self-bench 0.3.4 → 0.3.6

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 (207) hide show
  1. package/README.md +16 -14
  2. package/biome.json +7 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +7 -7
  5. package/dist/api.js.map +1 -1
  6. package/dist/build-metadata.js +1 -1
  7. package/dist/cli.js +75 -15
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config.js +1 -1
  10. package/dist/contracts.d.ts +1 -0
  11. package/dist/contracts.d.ts.map +1 -1
  12. package/dist/contracts.js +6 -5
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/extensions/discovery.d.ts.map +1 -1
  15. package/dist/extensions/discovery.js +12 -5
  16. package/dist/extensions/discovery.js.map +1 -1
  17. package/dist/harbor-task.d.ts.map +1 -1
  18. package/dist/harbor-task.js +3 -1
  19. package/dist/harbor-task.js.map +1 -1
  20. package/dist/project-paths.d.ts +2 -0
  21. package/dist/project-paths.d.ts.map +1 -0
  22. package/dist/project-paths.js +15 -0
  23. package/dist/project-paths.js.map +1 -0
  24. package/dist/provenance-associations.d.ts +49 -0
  25. package/dist/provenance-associations.d.ts.map +1 -0
  26. package/dist/provenance-associations.js +220 -0
  27. package/dist/provenance-associations.js.map +1 -0
  28. package/dist/provenance.d.ts +34 -13
  29. package/dist/provenance.d.ts.map +1 -1
  30. package/dist/provenance.js +81 -9
  31. package/dist/provenance.js.map +1 -1
  32. package/dist/reaudit-main.js +3 -3
  33. package/dist/reaudit-main.js.map +1 -1
  34. package/dist/repair-main.js +4 -4
  35. package/dist/repair-main.js.map +1 -1
  36. package/dist/{sandbox.d.ts → sandbox/contracts.d.ts} +5 -3
  37. package/dist/sandbox/contracts.d.ts.map +1 -0
  38. package/dist/sandbox/contracts.js +9 -0
  39. package/dist/sandbox/contracts.js.map +1 -0
  40. package/dist/sandbox/index.d.ts +5 -0
  41. package/dist/sandbox/index.d.ts.map +1 -0
  42. package/dist/{sandbox.js → sandbox/index.js} +6 -5
  43. package/dist/sandbox/index.js.map +1 -0
  44. package/dist/sandbox/programs/author.d.ts +3 -0
  45. package/dist/sandbox/programs/author.d.ts.map +1 -0
  46. package/dist/{sandbox-author.js → sandbox/programs/author.js} +2 -2
  47. package/dist/sandbox/programs/author.js.map +1 -0
  48. package/dist/sandbox/programs/prepare-repair.d.ts +7 -0
  49. package/dist/sandbox/programs/prepare-repair.d.ts.map +1 -0
  50. package/dist/sandbox/programs/prepare-repair.js +30 -0
  51. package/dist/sandbox/programs/prepare-repair.js.map +1 -0
  52. package/dist/sandbox/programs/repair.d.ts +3 -0
  53. package/dist/sandbox/programs/repair.d.ts.map +1 -0
  54. package/dist/{sandbox-repair.js → sandbox/programs/repair.js} +7 -28
  55. package/dist/sandbox/programs/repair.js.map +1 -0
  56. package/dist/sandbox/programs/review.d.ts +2 -0
  57. package/dist/sandbox/programs/review.d.ts.map +1 -0
  58. package/dist/{sandbox-review.js → sandbox/programs/review.js} +2 -2
  59. package/dist/sandbox/programs/review.js.map +1 -0
  60. package/dist/sandbox/programs/validation-repair.d.ts +3 -0
  61. package/dist/sandbox/programs/validation-repair.d.ts.map +1 -0
  62. package/dist/{sandbox-validation-repair.js → sandbox/programs/validation-repair.js} +7 -28
  63. package/dist/sandbox/programs/validation-repair.js.map +1 -0
  64. package/dist/{docker-executor.d.ts → sandbox/providers/docker/executor.d.ts} +3 -3
  65. package/dist/sandbox/providers/docker/executor.d.ts.map +1 -0
  66. package/dist/{docker-executor.js → sandbox/providers/docker/executor.js} +2 -2
  67. package/dist/sandbox/providers/docker/executor.js.map +1 -0
  68. package/dist/{modal-executor.d.ts → sandbox/providers/modal/executor.d.ts} +3 -3
  69. package/dist/sandbox/providers/modal/executor.d.ts.map +1 -0
  70. package/dist/{modal-executor.js → sandbox/providers/modal/executor.js} +50 -19
  71. package/dist/sandbox/providers/modal/executor.js.map +1 -0
  72. package/dist/{vercel-command.d.ts → sandbox/providers/vercel/command.d.ts} +3 -3
  73. package/dist/sandbox/providers/vercel/command.d.ts.map +1 -0
  74. package/dist/{vercel-command.js → sandbox/providers/vercel/command.js} +3 -3
  75. package/dist/sandbox/providers/vercel/command.js.map +1 -0
  76. package/dist/{vercel-executor.d.ts → sandbox/providers/vercel/executor.d.ts} +3 -3
  77. package/dist/sandbox/providers/vercel/executor.d.ts.map +1 -0
  78. package/dist/{vercel-executor.js → sandbox/providers/vercel/executor.js} +5 -5
  79. package/dist/sandbox/providers/vercel/executor.js.map +1 -0
  80. package/dist/{vercel-fetch.d.ts → sandbox/providers/vercel/fetch.d.ts} +1 -1
  81. package/dist/sandbox/providers/vercel/fetch.d.ts.map +1 -0
  82. package/dist/{vercel-fetch.js → sandbox/providers/vercel/fetch.js} +1 -1
  83. package/dist/sandbox/providers/vercel/fetch.js.map +1 -0
  84. package/dist/{sandbox-timeout.d.ts → sandbox/timeout.d.ts} +2 -2
  85. package/dist/sandbox/timeout.d.ts.map +1 -0
  86. package/dist/{sandbox-timeout.js → sandbox/timeout.js} +1 -1
  87. package/dist/sandbox/timeout.js.map +1 -0
  88. package/dist/sandbox-author.bundle.js +11 -8
  89. package/dist/sandbox-repair.bundle.js +43 -35
  90. package/dist/sandbox-review.bundle.js +2 -2
  91. package/dist/sandbox-validation-repair.bundle.js +50 -41
  92. package/dist/{vercel-cli.d.ts → setup/vercel/cli.d.ts} +2 -2
  93. package/dist/setup/vercel/cli.d.ts.map +1 -0
  94. package/dist/{vercel-cli.js → setup/vercel/cli.js} +2 -2
  95. package/dist/setup/vercel/cli.js.map +1 -0
  96. package/dist/setup/vercel/index.d.ts +4 -0
  97. package/dist/setup/vercel/index.d.ts.map +1 -0
  98. package/dist/setup/vercel/index.js +3 -0
  99. package/dist/setup/vercel/index.js.map +1 -0
  100. package/dist/{vercel-setup-probe.d.ts → setup/vercel/probe.d.ts} +2 -2
  101. package/dist/setup/vercel/probe.d.ts.map +1 -0
  102. package/dist/{vercel-setup-probe.js → setup/vercel/probe.js} +2 -2
  103. package/dist/setup/vercel/probe.js.map +1 -0
  104. package/dist/{vercel-profile.d.ts → setup/vercel/profile.d.ts} +1 -1
  105. package/dist/setup/vercel/profile.d.ts.map +1 -0
  106. package/dist/{vercel-profile.js → setup/vercel/profile.js} +2 -2
  107. package/dist/setup/vercel/profile.js.map +1 -0
  108. package/dist/{vercel-runtime-image.d.ts → setup/vercel/runtime-image.d.ts} +5 -5
  109. package/dist/setup/vercel/runtime-image.d.ts.map +1 -0
  110. package/dist/{vercel-runtime-image.js → setup/vercel/runtime-image.js} +2 -2
  111. package/dist/setup/vercel/runtime-image.js.map +1 -0
  112. package/dist/{vercel-setup.d.ts → setup/vercel/setup.d.ts} +8 -8
  113. package/dist/setup/vercel/setup.d.ts.map +1 -0
  114. package/dist/{vercel-setup.js → setup/vercel/setup.js} +8 -8
  115. package/dist/setup/vercel/setup.js.map +1 -0
  116. package/dist/temporal/activities.d.ts +12 -0
  117. package/dist/temporal/activities.d.ts.map +1 -1
  118. package/dist/temporal/activities.js +80 -33
  119. package/dist/temporal/activities.js.map +1 -1
  120. package/dist/temporal/workflow.d.ts +1 -1
  121. package/dist/temporal/workflow.d.ts.map +1 -1
  122. package/dist/temporal/workflow.js +90 -65
  123. package/dist/temporal/workflow.js.map +1 -1
  124. package/docs/operations.md +39 -5
  125. package/docs/task-construction.md +3 -1
  126. package/package.json +7 -5
  127. package/review/index.html +14 -0
  128. package/review/src/App.tsx +338 -0
  129. package/review/src/export-loader.test.ts +57 -0
  130. package/review/src/export-loader.ts +151 -0
  131. package/review/src/main.tsx +15 -0
  132. package/review/src/styles.css +162 -0
  133. package/review/src/types.ts +47 -0
  134. package/review/tsconfig.json +16 -0
  135. package/review/vite.config.ts +20 -0
  136. package/scripts/build-sandbox-programs.ts +22 -0
  137. package/scripts/verify-package.ts +52 -7
  138. package/scripts/write-build-metadata.ts +1 -1
  139. package/src/api.ts +7 -6
  140. package/src/cli.ts +99 -15
  141. package/src/config.ts +1 -1
  142. package/src/contracts.ts +7 -5
  143. package/src/extensions/discovery.ts +14 -5
  144. package/src/harbor-task.ts +6 -1
  145. package/src/project-paths.ts +15 -0
  146. package/src/provenance-associations.ts +311 -0
  147. package/src/provenance.ts +132 -28
  148. package/src/reaudit-main.ts +3 -3
  149. package/src/repair-main.ts +4 -4
  150. package/src/{sandbox.ts → sandbox/contracts.ts} +11 -20
  151. package/src/sandbox/index.ts +23 -0
  152. package/src/{sandbox-author.ts → sandbox/programs/author.ts} +1 -1
  153. package/src/sandbox/programs/prepare-repair.ts +44 -0
  154. package/src/{sandbox-repair.ts → sandbox/programs/repair.ts} +7 -31
  155. package/src/{sandbox-review.ts → sandbox/programs/review.ts} +1 -1
  156. package/src/{sandbox-validation-repair.ts → sandbox/programs/validation-repair.ts} +6 -30
  157. package/src/{docker-executor.ts → sandbox/providers/docker/executor.ts} +3 -3
  158. package/src/{modal-executor.ts → sandbox/providers/modal/executor.ts} +66 -25
  159. package/src/{vercel-command.ts → sandbox/providers/vercel/command.ts} +3 -3
  160. package/src/{vercel-executor.ts → sandbox/providers/vercel/executor.ts} +5 -5
  161. package/src/{sandbox-timeout.ts → sandbox/timeout.ts} +1 -1
  162. package/src/{vercel-cli.ts → setup/vercel/cli.ts} +1 -1
  163. package/src/setup/vercel/index.ts +3 -0
  164. package/src/{vercel-setup-probe.ts → setup/vercel/probe.ts} +5 -2
  165. package/src/{vercel-profile.ts → setup/vercel/profile.ts} +4 -1
  166. package/src/{vercel-runtime-image.ts → setup/vercel/runtime-image.ts} +5 -5
  167. package/src/{vercel-setup.ts → setup/vercel/setup.ts} +8 -8
  168. package/src/temporal/activities.ts +164 -63
  169. package/src/temporal/workflow.ts +120 -71
  170. package/tsconfig.json +1 -1
  171. package/dist/docker-executor.d.ts.map +0 -1
  172. package/dist/docker-executor.js.map +0 -1
  173. package/dist/modal-executor.d.ts.map +0 -1
  174. package/dist/modal-executor.js.map +0 -1
  175. package/dist/sandbox-author.d.ts +0 -3
  176. package/dist/sandbox-author.d.ts.map +0 -1
  177. package/dist/sandbox-author.js.map +0 -1
  178. package/dist/sandbox-repair.d.ts +0 -3
  179. package/dist/sandbox-repair.d.ts.map +0 -1
  180. package/dist/sandbox-repair.js.map +0 -1
  181. package/dist/sandbox-review.d.ts +0 -2
  182. package/dist/sandbox-review.d.ts.map +0 -1
  183. package/dist/sandbox-review.js.map +0 -1
  184. package/dist/sandbox-timeout.d.ts.map +0 -1
  185. package/dist/sandbox-timeout.js.map +0 -1
  186. package/dist/sandbox-validation-repair.d.ts +0 -3
  187. package/dist/sandbox-validation-repair.d.ts.map +0 -1
  188. package/dist/sandbox-validation-repair.js.map +0 -1
  189. package/dist/sandbox.d.ts.map +0 -1
  190. package/dist/sandbox.js.map +0 -1
  191. package/dist/vercel-cli.d.ts.map +0 -1
  192. package/dist/vercel-cli.js.map +0 -1
  193. package/dist/vercel-command.d.ts.map +0 -1
  194. package/dist/vercel-command.js.map +0 -1
  195. package/dist/vercel-executor.d.ts.map +0 -1
  196. package/dist/vercel-executor.js.map +0 -1
  197. package/dist/vercel-fetch.d.ts.map +0 -1
  198. package/dist/vercel-fetch.js.map +0 -1
  199. package/dist/vercel-profile.d.ts.map +0 -1
  200. package/dist/vercel-profile.js.map +0 -1
  201. package/dist/vercel-runtime-image.d.ts.map +0 -1
  202. package/dist/vercel-runtime-image.js.map +0 -1
  203. package/dist/vercel-setup-probe.d.ts.map +0 -1
  204. package/dist/vercel-setup-probe.js.map +0 -1
  205. package/dist/vercel-setup.d.ts.map +0 -1
  206. package/dist/vercel-setup.js.map +0 -1
  207. /package/src/{vercel-fetch.ts → sandbox/providers/vercel/fetch.ts} +0 -0
@@ -0,0 +1,338 @@
1
+ import { parsePatchFiles } from "@pierre/diffs";
2
+ import { FileDiff } from "@pierre/diffs/react";
3
+ import React from "react";
4
+ import { buildCleanExport, loadExport } from "./export-loader";
5
+ import type { ExportTask, LoadedExport, RunStatus, RunSummary } from "./types";
6
+
7
+ export function App() {
8
+ const [runs, setRuns] = React.useState<RunSummary[]>([]);
9
+ const [runId, setRunId] = React.useState<string | null>(null);
10
+ const [status, setStatus] = React.useState<RunStatus | null>(null);
11
+ const [loaded, setLoaded] = React.useState<LoadedExport | null>(null);
12
+ const [removed, setRemoved] = React.useState<Set<string>>(new Set());
13
+ const [selectedTaskId, setSelectedTaskId] = React.useState<string | null>(null);
14
+ const [error, setError] = React.useState<string | null>(null);
15
+ const [busy, setBusy] = React.useState(false);
16
+ const [token, setToken] = React.useState("");
17
+ const fileInput = React.useRef<HTMLInputElement>(null);
18
+ const apiFetch = React.useCallback(
19
+ (path: string) =>
20
+ fetch(path, token ? { headers: { Authorization: `Bearer ${token}` } } : undefined),
21
+ [token],
22
+ );
23
+
24
+ const refreshRuns = React.useCallback(async () => {
25
+ try {
26
+ setError(null);
27
+ const response = await apiFetch("/v1/runs");
28
+ if (!response.ok) throw new Error(`Unable to list runs (${response.status})`);
29
+ setRuns((await response.json()) as RunSummary[]);
30
+ } catch (cause) {
31
+ setError(cause instanceof Error ? cause.message : String(cause));
32
+ }
33
+ }, [apiFetch]);
34
+
35
+ React.useEffect(() => {
36
+ void refreshRuns();
37
+ }, [refreshRuns]);
38
+
39
+ const openRun = async (nextRunId: string) => {
40
+ try {
41
+ setBusy(true);
42
+ setError(null);
43
+ setRunId(nextRunId);
44
+ setLoaded(null);
45
+ setRemoved(new Set());
46
+ const [statusResponse, exportResponse] = await Promise.all([
47
+ apiFetch(`/v1/runs/${encodeURIComponent(nextRunId)}`),
48
+ apiFetch(`/v1/runs/${encodeURIComponent(nextRunId)}/export`),
49
+ ]);
50
+ if (!statusResponse.ok) throw new Error(`Unable to load run (${statusResponse.status})`);
51
+ if (!exportResponse.ok) throw new Error(`Export is not ready (${exportResponse.status})`);
52
+ setStatus((await statusResponse.json()) as RunStatus);
53
+ setLoaded(await loadExport(await exportResponse.blob()));
54
+ } catch (cause) {
55
+ setError(cause instanceof Error ? cause.message : String(cause));
56
+ } finally {
57
+ setBusy(false);
58
+ }
59
+ };
60
+
61
+ const openFile = async (file: File) => {
62
+ try {
63
+ setBusy(true);
64
+ setError(null);
65
+ const next = await loadExport(file);
66
+ setLoaded(next);
67
+ setRunId(next.manifest.runId);
68
+ setStatus({
69
+ runId: next.manifest.runId,
70
+ phase: "local export",
71
+ requested: next.manifest.acceptedCount,
72
+ accepted: next.manifest.acceptedCount,
73
+ rejected: 0,
74
+ tasks: [],
75
+ });
76
+ setRemoved(new Set());
77
+ setSelectedTaskId(null);
78
+ } catch (cause) {
79
+ setError(cause instanceof Error ? cause.message : String(cause));
80
+ } finally {
81
+ setBusy(false);
82
+ }
83
+ };
84
+
85
+ const selectedTask =
86
+ loaded?.tasks.find((task) => task.taskId === selectedTaskId) ?? loaded?.tasks[0];
87
+ const toggleRemoved = (taskId: string) =>
88
+ setRemoved((current) => {
89
+ const next = new Set(current);
90
+ if (next.has(taskId)) next.delete(taskId);
91
+ else next.add(taskId);
92
+ return next;
93
+ });
94
+
95
+ const downloadClean = async () => {
96
+ if (!loaded || !runId) return;
97
+ const blob = await buildCleanExport(loaded, removed);
98
+ const url = URL.createObjectURL(blob);
99
+ const link = document.createElement("a");
100
+ link.href = url;
101
+ link.download = `selfbench-${runId}-cleaned.tar.gz`;
102
+ link.click();
103
+ URL.revokeObjectURL(url);
104
+ };
105
+
106
+ return (
107
+ <main className="app-shell">
108
+ <header className="topbar">
109
+ <div>
110
+ <p className="eyebrow">SELFBENCH / EXPORT REVIEW</p>
111
+ <h1>Review accepted tasks before publishing.</h1>
112
+ </div>
113
+ <div className="topbar-actions">
114
+ <input
115
+ className="token-input"
116
+ type="password"
117
+ value={token}
118
+ onChange={(event) => setToken(event.target.value)}
119
+ placeholder="API token (session only)"
120
+ aria-label="API token"
121
+ />
122
+ <input
123
+ ref={fileInput}
124
+ hidden
125
+ type="file"
126
+ accept=".gz,.tgz,application/gzip"
127
+ onChange={(event) => {
128
+ const file = event.target.files?.[0];
129
+ if (file) void openFile(file);
130
+ }}
131
+ />
132
+ <button type="button" className="button" onClick={() => fileInput.current?.click()}>
133
+ Open gzip
134
+ </button>
135
+ <button type="button" className="button" onClick={() => void refreshRuns()}>
136
+ Refresh runs
137
+ </button>
138
+ </div>
139
+ </header>
140
+ {error && <div className="error-banner">{error}</div>}
141
+ <section className="workspace">
142
+ <aside className="run-list surface">
143
+ <div className="section-heading">
144
+ <h2>Runs</h2>
145
+ <span>{runs.length}</span>
146
+ </div>
147
+ {runs.length === 0 && <p className="muted">No runs found.</p>}
148
+ {runs.map((run) => (
149
+ <button
150
+ type="button"
151
+ className={`run-row ${run.runId === runId ? "selected" : ""}`}
152
+ key={run.runId}
153
+ onClick={() => void openRun(run.runId)}
154
+ >
155
+ <strong>{run.runId}</strong>
156
+ <span>{run.status}</span>
157
+ <small>{new Date(run.startedAt).toLocaleString()}</small>
158
+ </button>
159
+ ))}
160
+ </aside>
161
+ <section className="content">
162
+ {!loaded && (
163
+ <div className="empty surface">
164
+ <h2>{busy ? "Loading export…" : "Select a completed run"}</h2>
165
+ <p>Choose a run or open a local export gzip to inspect its accepted tasks.</p>
166
+ </div>
167
+ )}
168
+ {loaded && status && (
169
+ <>
170
+ <div className="summary surface">
171
+ <div>
172
+ <p className="eyebrow">{loaded.manifest.repository.url}</p>
173
+ <h2>{runId}</h2>
174
+ <p className="muted">
175
+ {status.phase} · {loaded.manifest.acceptedCount} accepted tasks · commit{" "}
176
+ {loaded.manifest.repository.commit.slice(0, 12)}
177
+ </p>
178
+ </div>
179
+ <button
180
+ type="button"
181
+ className="button primary"
182
+ onClick={() => void downloadClean()}
183
+ >
184
+ {removed.size
185
+ ? `Download cleaned export (${loaded.tasks.length - removed.size})`
186
+ : "Download export copy"}
187
+ </button>
188
+ </div>
189
+ <div className="review-grid">
190
+ <div className="task-list surface">
191
+ <div className="section-heading">
192
+ <h2>Tasks</h2>
193
+ <span>{removed.size} marked for removal</span>
194
+ </div>
195
+ {loaded.tasks.map((task) => (
196
+ <button
197
+ type="button"
198
+ className={`task-row ${task.taskId === selectedTask?.taskId ? "selected" : ""} ${removed.has(task.taskId) ? "removed" : ""}`}
199
+ key={task.taskId}
200
+ onClick={() => setSelectedTaskId(task.taskId)}
201
+ >
202
+ <strong>{task.taskId}</strong>
203
+ <span>{removed.has(task.taskId) ? "marked for removal" : "keep"}</span>
204
+ </button>
205
+ ))}
206
+ </div>
207
+ {selectedTask && (
208
+ <TaskPanel
209
+ task={selectedTask}
210
+ removed={removed.has(selectedTask.taskId)}
211
+ onToggle={() => toggleRemoved(selectedTask.taskId)}
212
+ />
213
+ )}
214
+ </div>
215
+ </>
216
+ )}
217
+ </section>
218
+ </section>
219
+ </main>
220
+ );
221
+ }
222
+
223
+ function TaskPanel({
224
+ task,
225
+ removed,
226
+ onToggle,
227
+ }: {
228
+ task: ExportTask;
229
+ removed: boolean;
230
+ onToggle: () => void;
231
+ }) {
232
+ const definition = parseJson(task.textFiles.get("definition.json"));
233
+ return (
234
+ <article className="task-detail surface">
235
+ <div className="detail-header">
236
+ <div>
237
+ <p className="eyebrow">TASK</p>
238
+ <h2>{task.taskId}</h2>
239
+ <p className="muted">
240
+ {String(definition?.difficulty ?? "unknown")} · {String(definition?.repo ?? "")}
241
+ </p>
242
+ </div>
243
+ <button
244
+ type="button"
245
+ className={`button ${removed ? "undo" : "danger"}`}
246
+ onClick={onToggle}
247
+ >
248
+ {removed ? "Keep task" : "Mark for removal"}
249
+ </button>
250
+ </div>
251
+ <DetailBlock title="Prompt">
252
+ <pre>
253
+ {String(definition?.prompt ?? task.textFiles.get("instruction.md") ?? "No prompt")}
254
+ </pre>
255
+ </DetailBlock>
256
+ <DetailBlock title="Task definition">
257
+ <pre>{task.textFiles.get("definition.json") ?? "Missing definition.json"}</pre>
258
+ </DetailBlock>
259
+ <DetailBlock title="Gold patch">
260
+ <PatchBlock patch={task.textFiles.get("solution/gold.patch")} />
261
+ </DetailBlock>
262
+ <DetailBlock title="Held-out test patch">
263
+ <PatchBlock patch={task.textFiles.get("tests/test.patch")} />
264
+ </DetailBlock>
265
+ </article>
266
+ );
267
+ }
268
+
269
+ function PatchBlock({ patch }: { patch: string | undefined }) {
270
+ const files = React.useMemo(
271
+ () => (patch ? parsePatchFiles(patch).flatMap((parsed) => parsed.files) : []),
272
+ [patch],
273
+ );
274
+ if (!patch) return <pre>Missing patch</pre>;
275
+ if (!files.length) return <pre>{patch}</pre>;
276
+ const options = {
277
+ themeType: window.matchMedia("(prefers-color-scheme: dark)").matches
278
+ ? ("dark" as const)
279
+ : ("light" as const),
280
+ diffStyle: window.matchMedia("(max-width: 900px)").matches
281
+ ? ("unified" as const)
282
+ : ("split" as const),
283
+ diffIndicators: "bars" as const,
284
+ overflow: "scroll" as const,
285
+ stickyHeader: true,
286
+ };
287
+ return (
288
+ <DiffErrorBoundary fallback={patch}>
289
+ <div className="patch-files">
290
+ {files.map((file) => (
291
+ <FileDiff
292
+ key={`${file.prevName ?? ""}:${file.name}`}
293
+ fileDiff={file}
294
+ disableWorkerPool
295
+ options={options}
296
+ />
297
+ ))}
298
+ </div>
299
+ </DiffErrorBoundary>
300
+ );
301
+ }
302
+
303
+ class DiffErrorBoundary extends React.Component<
304
+ React.PropsWithChildren<{ fallback: string }>,
305
+ { failed: boolean }
306
+ > {
307
+ state = { failed: false };
308
+
309
+ static getDerivedStateFromError() {
310
+ return { failed: true };
311
+ }
312
+
313
+ componentDidCatch(error: unknown) {
314
+ console.error("Pierre diff renderer failed", error);
315
+ }
316
+
317
+ render() {
318
+ return this.state.failed ? <pre>{this.props.fallback}</pre> : this.props.children;
319
+ }
320
+ }
321
+
322
+ function DetailBlock({ title, children }: { title: string; children: React.ReactNode }) {
323
+ return (
324
+ <section className="detail-block">
325
+ <h3>{title}</h3>
326
+ {children}
327
+ </section>
328
+ );
329
+ }
330
+
331
+ function parseJson(value: string | undefined): Record<string, unknown> | null {
332
+ if (!value) return null;
333
+ try {
334
+ return JSON.parse(value) as Record<string, unknown>;
335
+ } catch {
336
+ return null;
337
+ }
338
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildCleanExport, loadExport } from "./export-loader";
3
+ import type { LoadedExport } from "./types";
4
+
5
+ describe("export review bundles", () => {
6
+ test("round-trips nested task archives and removes marked tasks", async () => {
7
+ const loaded: LoadedExport = {
8
+ manifest: {
9
+ schemaVersion: 1,
10
+ runId: "run-123",
11
+ candidateCounts: { easy: 2, medium: 0, hard: 0 },
12
+ repository: { url: "https://github.com/example/repo", commit: "a".repeat(40) },
13
+ version: { schema: 1 },
14
+ acceptedCount: 2,
15
+ tasks: [
16
+ { taskId: "task-a", sha256: "a".repeat(64) },
17
+ { taskId: "task-b", sha256: "b".repeat(64) },
18
+ ],
19
+ },
20
+ tasks: [],
21
+ taskArchives: new Map([
22
+ ["task-a", await taskArchive("task-a")],
23
+ ["task-b", await taskArchive("task-b")],
24
+ ]),
25
+ };
26
+ loaded.tasks = [
27
+ { taskId: "task-a", files: new Map(), textFiles: new Map() },
28
+ { taskId: "task-b", files: new Map(), textFiles: new Map() },
29
+ ];
30
+
31
+ const cleaned = await buildCleanExport(loaded, new Set(["task-b"]));
32
+ const result = await loadExport(cleaned);
33
+
34
+ expect(result.manifest.acceptedCount).toBe(1);
35
+ expect(result.manifest.tasks.map((task) => task.taskId)).toEqual(["task-a"]);
36
+ expect(result.tasks[0]?.textFiles.get("definition.json")).toBe('{"taskId":"task-a"}');
37
+ });
38
+ });
39
+
40
+ async function taskArchive(taskId: string): Promise<Uint8Array> {
41
+ const data = new TextEncoder().encode(JSON.stringify({ taskId }));
42
+ const header = new Uint8Array(512);
43
+ write(header, 0, "harbor-task/definition.json");
44
+ write(header, 124, data.length.toString(8).padStart(11, "0"));
45
+ header[156] = 48;
46
+ const tar = new Uint8Array(512 + Math.ceil(data.length / 512) * 512 + 1024);
47
+ tar.set(header);
48
+ tar.set(data, 512);
49
+ const compressed = await new Response(
50
+ new Blob([tar.buffer]).stream().pipeThrough(new CompressionStream("gzip")),
51
+ ).arrayBuffer();
52
+ return new Uint8Array(compressed);
53
+ }
54
+
55
+ function write(target: Uint8Array, offset: number, value: string): void {
56
+ target.set(new TextEncoder().encode(value), offset);
57
+ }
@@ -0,0 +1,151 @@
1
+ import type { ExportManifest, ExportTask, LoadedExport } from "./types";
2
+
3
+ const textNames = new Set([
4
+ "definition.json",
5
+ "instruction.md",
6
+ "task.toml",
7
+ ".selfbench-manifest.json",
8
+ "solution/gold.patch",
9
+ "tests/test.patch",
10
+ "tests/dependency-setup.patch",
11
+ ]);
12
+
13
+ interface TarEntry {
14
+ name: string;
15
+ data: Uint8Array;
16
+ }
17
+
18
+ export async function loadExport(input: Blob | Uint8Array): Promise<LoadedExport> {
19
+ const archive = input instanceof Blob ? new Uint8Array(await input.arrayBuffer()) : input;
20
+ const outerEntries = untar(await gunzip(archive));
21
+ const manifestBytes = outerEntries.find((entry) => entry.name === "manifest.json")?.data;
22
+ if (!manifestBytes) throw new Error("export is missing manifest.json");
23
+ const manifest = JSON.parse(new TextDecoder().decode(manifestBytes)) as ExportManifest;
24
+ const tasks: ExportTask[] = [];
25
+ const taskArchives = new Map<string, Uint8Array>();
26
+ for (const manifestTask of manifest.tasks) {
27
+ const name = `tasks/${manifestTask.taskId}.tar.gz`;
28
+ const archiveEntry = outerEntries.find((entry) => entry.name === name);
29
+ if (!archiveEntry) throw new Error(`export is missing ${name}`);
30
+ taskArchives.set(manifestTask.taskId, archiveEntry.data);
31
+ const entries = untar(await gunzip(archiveEntry.data));
32
+ const files = new Map<string, Uint8Array>();
33
+ const textFiles = new Map<string, string>();
34
+ for (const entry of entries) {
35
+ const relative = entry.name.replace(/^harbor-task\//, "");
36
+ if (!relative || entry.name.endsWith("/")) continue;
37
+ files.set(relative, entry.data);
38
+ if (textNames.has(relative) || relative.endsWith(".json")) {
39
+ textFiles.set(relative, new TextDecoder().decode(entry.data));
40
+ }
41
+ }
42
+ tasks.push({ taskId: manifestTask.taskId, files, textFiles });
43
+ }
44
+ return { manifest, tasks, taskArchives };
45
+ }
46
+
47
+ export async function buildCleanExport(
48
+ loaded: LoadedExport,
49
+ removedTaskIds: Set<string>,
50
+ ): Promise<Blob> {
51
+ const keptIds = new Set(
52
+ loaded.tasks.filter((task) => !removedTaskIds.has(task.taskId)).map((task) => task.taskId),
53
+ );
54
+ const manifest: ExportManifest = {
55
+ ...loaded.manifest,
56
+ acceptedCount: keptIds.size,
57
+ tasks: loaded.manifest.tasks.filter((task) => keptIds.has(task.taskId)),
58
+ };
59
+ const entries: TarEntry[] = [
60
+ {
61
+ name: "manifest.json",
62
+ data: new TextEncoder().encode(`${JSON.stringify(manifest, null, 2)}\n`),
63
+ },
64
+ ];
65
+ for (const task of loaded.tasks) {
66
+ if (keptIds.has(task.taskId)) {
67
+ const archive = loaded.taskArchives.get(task.taskId);
68
+ if (!archive) throw new Error(`loaded export is missing ${task.taskId}`);
69
+ entries.push({ name: `tasks/${task.taskId}.tar.gz`, data: archive });
70
+ }
71
+ }
72
+ return gzip(tar(entries));
73
+ }
74
+
75
+ async function gunzip(value: Uint8Array): Promise<Uint8Array> {
76
+ return transform(value, new DecompressionStream("gzip"));
77
+ }
78
+
79
+ async function gzip(value: Uint8Array): Promise<Blob> {
80
+ const compressed = await transform(value, new CompressionStream("gzip"));
81
+ return new Blob([compressed.buffer as ArrayBuffer], { type: "application/gzip" });
82
+ }
83
+
84
+ async function transform(value: Uint8Array, stream: TransformStream): Promise<Uint8Array> {
85
+ const output = await new Response(
86
+ new Blob([value.buffer as ArrayBuffer]).stream().pipeThrough(stream),
87
+ ).arrayBuffer();
88
+ return new Uint8Array(output);
89
+ }
90
+
91
+ function untar(buffer: Uint8Array): TarEntry[] {
92
+ const entries: TarEntry[] = [];
93
+ for (let offset = 0; offset + 512 <= buffer.length; ) {
94
+ const header = buffer.subarray(offset, offset + 512);
95
+ if (header.every((byte) => byte === 0)) break;
96
+ const name = readString(header, 0, 100);
97
+ const size = parseOctal(header, 124, 12);
98
+ const type = header[156];
99
+ offset += 512;
100
+ const data = buffer.slice(offset, offset + size);
101
+ if (type !== 5 && name) entries.push({ name, data });
102
+ offset += Math.ceil(size / 512) * 512;
103
+ }
104
+ return entries;
105
+ }
106
+
107
+ function tar(entries: TarEntry[]): Uint8Array {
108
+ const chunks: Uint8Array[] = [];
109
+ for (const entry of entries) {
110
+ const header = new Uint8Array(512);
111
+ writeString(header, 0, 100, entry.name);
112
+ writeOctal(header, 100, 8, 0o644);
113
+ writeOctal(header, 108, 8, 0);
114
+ writeOctal(header, 116, 8, 0);
115
+ writeOctal(header, 124, 12, entry.data.length);
116
+ writeOctal(header, 136, 12, Math.floor(Date.now() / 1000));
117
+ header.fill(32, 148, 156);
118
+ header[156] = 48;
119
+ writeString(header, 257, 6, "ustar\0");
120
+ writeString(header, 263, 2, "00");
121
+ const checksum = header.reduce((sum, byte) => sum + byte, 0);
122
+ writeString(header, 148, 8, `${checksum.toString(8).padStart(6, "0")}\0 `);
123
+ chunks.push(header, entry.data, new Uint8Array((512 - (entry.data.length % 512)) % 512));
124
+ }
125
+ chunks.push(new Uint8Array(1024));
126
+ const output = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.length, 0));
127
+ let offset = 0;
128
+ for (const chunk of chunks) {
129
+ output.set(chunk, offset);
130
+ offset += chunk.length;
131
+ }
132
+ return output;
133
+ }
134
+
135
+ function readString(bytes: Uint8Array, start: number, length: number): string {
136
+ const end = bytes.indexOf(0, start);
137
+ return new TextDecoder().decode(bytes.subarray(start, end < 0 ? start + length : end));
138
+ }
139
+
140
+ function parseOctal(bytes: Uint8Array, start: number, length: number): number {
141
+ const value = readString(bytes, start, length).trim();
142
+ return value ? Number.parseInt(value, 8) : 0;
143
+ }
144
+
145
+ function writeString(bytes: Uint8Array, start: number, length: number, value: string): void {
146
+ bytes.set(new TextEncoder().encode(value).subarray(0, length), start);
147
+ }
148
+
149
+ function writeOctal(bytes: Uint8Array, start: number, length: number, value: number): void {
150
+ writeString(bytes, start, length, value.toString(8).padStart(length - 1, "0"));
151
+ }
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./styles.css";
5
+
6
+ const root = document.getElementById("root");
7
+ if (!root) {
8
+ throw new Error("review root is missing");
9
+ }
10
+
11
+ createRoot(root).render(
12
+ <React.StrictMode>
13
+ <App />
14
+ </React.StrictMode>,
15
+ );