Redhanuman commited on
Commit
69b963a
·
verified ·
1 Parent(s): 3553d3d

Initial Sabnock Docker static arena

Browse files
Files changed (4) hide show
  1. LICENSE +16 -0
  2. README.md +8 -5
  3. data/sabnock_docker_v1.json +0 -0
  4. index.html +737 -17
LICENSE ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+
4
+ Copyright 2026 Redhanuman
5
+
6
+ Licensed under the Apache License, Version 2.0 (the "License");
7
+ you may not use this file except in compliance with the License.
8
+ You may obtain a copy of the License at
9
+
10
+ http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+ Unless required by applicable law or agreed to in writing, software
13
+ distributed under the License is distributed on an "AS IS" BASIS,
14
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ See the License for the specific language governing permissions and
16
+ limitations under the License.
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
  title: Sabnock Docker Arena
3
- emoji: 🌍
4
- colorFrom: purple
5
- colorTo: blue
6
  sdk: static
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
1
  ---
2
  title: Sabnock Docker Arena
 
 
 
3
  sdk: static
4
+ license: apache-2.0
5
  ---
6
 
7
+ # Sabnock Docker Arena
8
+
9
+ Static browser dashboard for Sabnock Docker.
10
+
11
+ This Space lets users browse the benchmark, filter tasks, inspect files/logs, view oracle patches, and score candidate unified-diff patches in the browser.
12
+
13
+ It uses `sdk: static` so it can run on Hugging Face without paid Gradio hardware.
data/sabnock_docker_v1.json ADDED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -1,19 +1,739 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Sabnock Docker Arena</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: dark;
10
+ --bg: #0b0d10;
11
+ --panel: #10141a;
12
+ --panel-2: #151a22;
13
+ --line: #27303b;
14
+ --text: #e7ecf3;
15
+ --muted: #95a0af;
16
+ --accent: #4fb3ff;
17
+ --ok: #48d597;
18
+ --bad: #ff6b6b;
19
+ --warn: #f0c36a;
20
+ --code: #080a0d;
21
+ }
22
+
23
+ * {
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ body {
28
+ margin: 0;
29
+ background: var(--bg);
30
+ color: var(--text);
31
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
32
+ letter-spacing: 0;
33
+ }
34
+
35
+ button,
36
+ select,
37
+ textarea {
38
+ font: inherit;
39
+ }
40
+
41
+ .shell {
42
+ width: min(1500px, 100%);
43
+ margin: 0 auto;
44
+ padding: 20px;
45
+ }
46
+
47
+ .hero {
48
+ border: 1px solid var(--line);
49
+ border-radius: 8px;
50
+ padding: 22px;
51
+ background: linear-gradient(135deg, #111820, #0c1015 58%, #11110d);
52
+ }
53
+
54
+ h1 {
55
+ margin: 0 0 8px;
56
+ font-size: 32px;
57
+ line-height: 1.1;
58
+ }
59
+
60
+ .hero p {
61
+ margin: 0;
62
+ max-width: 980px;
63
+ color: var(--muted);
64
+ line-height: 1.55;
65
+ }
66
+
67
+ .metrics {
68
+ display: grid;
69
+ grid-template-columns: repeat(4, minmax(0, 1fr));
70
+ gap: 10px;
71
+ margin: 12px 0;
72
+ }
73
+
74
+ .metric {
75
+ border: 1px solid var(--line);
76
+ border-radius: 8px;
77
+ padding: 12px 14px;
78
+ background: var(--panel);
79
+ }
80
+
81
+ .metric span {
82
+ display: block;
83
+ color: var(--muted);
84
+ font-size: 12px;
85
+ text-transform: uppercase;
86
+ }
87
+
88
+ .metric strong {
89
+ display: block;
90
+ font-size: 26px;
91
+ margin-top: 4px;
92
+ }
93
+
94
+ .toolbar,
95
+ .tabs {
96
+ display: flex;
97
+ gap: 8px;
98
+ flex-wrap: wrap;
99
+ align-items: center;
100
+ margin: 12px 0;
101
+ }
102
+
103
+ .toolbar label {
104
+ color: var(--muted);
105
+ font-size: 13px;
106
+ }
107
+
108
+ select,
109
+ textarea {
110
+ border: 1px solid var(--line);
111
+ border-radius: 6px;
112
+ color: var(--text);
113
+ background: var(--panel);
114
+ }
115
+
116
+ select {
117
+ min-width: 150px;
118
+ padding: 9px 10px;
119
+ }
120
+
121
+ button {
122
+ border: 1px solid var(--line);
123
+ border-radius: 6px;
124
+ color: var(--text);
125
+ background: var(--panel-2);
126
+ padding: 9px 12px;
127
+ cursor: pointer;
128
+ }
129
+
130
+ button.active {
131
+ border-color: var(--accent);
132
+ background: #112338;
133
+ }
134
+
135
+ button.primary {
136
+ background: #0d3b5e;
137
+ border-color: #1b5f92;
138
+ }
139
+
140
+ .grid {
141
+ display: grid;
142
+ grid-template-columns: minmax(320px, 0.9fr) minmax(500px, 1.4fr);
143
+ gap: 12px;
144
+ }
145
+
146
+ .panel {
147
+ border: 1px solid var(--line);
148
+ border-radius: 8px;
149
+ background: var(--panel);
150
+ min-width: 0;
151
+ }
152
+
153
+ .panel-head {
154
+ padding: 12px 14px;
155
+ border-bottom: 1px solid var(--line);
156
+ display: flex;
157
+ justify-content: space-between;
158
+ gap: 10px;
159
+ align-items: center;
160
+ }
161
+
162
+ .panel-head h2 {
163
+ margin: 0;
164
+ font-size: 15px;
165
+ }
166
+
167
+ .panel-body {
168
+ padding: 14px;
169
+ }
170
+
171
+ .task-list {
172
+ max-height: 560px;
173
+ overflow: auto;
174
+ }
175
+
176
+ .task-row {
177
+ width: 100%;
178
+ display: block;
179
+ text-align: left;
180
+ padding: 10px 12px;
181
+ border: 0;
182
+ border-bottom: 1px solid var(--line);
183
+ border-radius: 0;
184
+ background: transparent;
185
+ }
186
+
187
+ .task-row:hover,
188
+ .task-row.active {
189
+ background: #141b24;
190
+ }
191
+
192
+ .task-row strong {
193
+ display: block;
194
+ font-size: 13px;
195
+ margin-bottom: 4px;
196
+ }
197
+
198
+ .task-row span {
199
+ display: block;
200
+ color: var(--muted);
201
+ font-size: 12px;
202
+ white-space: nowrap;
203
+ overflow: hidden;
204
+ text-overflow: ellipsis;
205
+ }
206
+
207
+ .task-title {
208
+ margin: 0 0 8px;
209
+ font-size: 22px;
210
+ }
211
+
212
+ .summary {
213
+ color: var(--muted);
214
+ line-height: 1.55;
215
+ margin-bottom: 12px;
216
+ }
217
+
218
+ .chips {
219
+ display: flex;
220
+ flex-wrap: wrap;
221
+ gap: 6px;
222
+ margin: 8px 0 12px;
223
+ }
224
+
225
+ .chip {
226
+ border: 1px solid var(--line);
227
+ border-radius: 999px;
228
+ color: #cbd5e1;
229
+ background: #0b1118;
230
+ padding: 4px 8px;
231
+ font-size: 12px;
232
+ }
233
+
234
+ pre {
235
+ margin: 0;
236
+ border: 1px solid var(--line);
237
+ border-radius: 6px;
238
+ padding: 12px;
239
+ max-height: 360px;
240
+ overflow: auto;
241
+ background: var(--code);
242
+ color: #d7dee8;
243
+ line-height: 1.45;
244
+ font-size: 12px;
245
+ white-space: pre;
246
+ }
247
+
248
+ textarea {
249
+ width: 100%;
250
+ min-height: 280px;
251
+ padding: 12px;
252
+ resize: vertical;
253
+ background: var(--code);
254
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
255
+ font-size: 12px;
256
+ line-height: 1.45;
257
+ }
258
+
259
+ .cols {
260
+ display: grid;
261
+ grid-template-columns: 1fr 1fr;
262
+ gap: 12px;
263
+ margin-top: 12px;
264
+ }
265
+
266
+ .score {
267
+ border: 1px solid var(--line);
268
+ border-radius: 8px;
269
+ padding: 12px;
270
+ background: var(--panel);
271
+ line-height: 1.5;
272
+ }
273
+
274
+ .score.pass {
275
+ border-color: #246b4d;
276
+ }
277
+
278
+ .score.fail {
279
+ border-color: #774042;
280
+ }
281
+
282
+ .score ul {
283
+ margin: 8px 0 0;
284
+ padding-left: 18px;
285
+ }
286
+
287
+ table {
288
+ width: 100%;
289
+ border-collapse: collapse;
290
+ font-size: 13px;
291
+ }
292
+
293
+ th,
294
+ td {
295
+ border-bottom: 1px solid var(--line);
296
+ padding: 9px 10px;
297
+ text-align: left;
298
+ vertical-align: top;
299
+ }
300
+
301
+ th {
302
+ color: var(--muted);
303
+ font-weight: 600;
304
+ }
305
+
306
+ .hidden {
307
+ display: none;
308
+ }
309
+
310
+ @media (max-width: 960px) {
311
+ .metrics,
312
+ .grid,
313
+ .cols {
314
+ grid-template-columns: 1fr;
315
+ }
316
+
317
+ .shell {
318
+ padding: 12px;
319
+ }
320
+ }
321
+ </style>
322
+ </head>
323
+ <body>
324
+ <main class="shell">
325
+ <section class="hero">
326
+ <h1>Sabnock Docker</h1>
327
+ <p>A benchmark for testing whether AI coding agents can repair Dockerfile, Compose, dependency, security, and container runtime failures. This static arena runs fully in the browser.</p>
328
+ </section>
329
+
330
+ <section class="metrics">
331
+ <div class="metric"><span>Tasks</span><strong id="metricTasks">0</strong></div>
332
+ <div class="metric"><span>Failure Types</span><strong id="metricFailures">0</strong></div>
333
+ <div class="metric"><span>Ecosystems</span><strong id="metricEcosystems">0</strong></div>
334
+ <div class="metric"><span>Challenge</span><strong id="metricChallenge">0</strong></div>
335
+ </section>
336
+
337
+ <nav class="tabs">
338
+ <button class="active" data-tab="browse">Benchmark</button>
339
+ <button data-tab="score">Patch Scorer</button>
340
+ <button data-tab="leaderboard">Leaderboard</button>
341
+ </nav>
342
+
343
+ <section id="browse" class="tab">
344
+ <div class="toolbar">
345
+ <label>Split <select id="splitFilter"></select></label>
346
+ <label>Ecosystem <select id="ecosystemFilter"></select></label>
347
+ <label>Difficulty <select id="difficultyFilter"></select></label>
348
+ </div>
349
+
350
+ <div class="grid">
351
+ <aside class="panel">
352
+ <div class="panel-head">
353
+ <h2>Tasks</h2>
354
+ <span id="taskCount" class="chip">0</span>
355
+ </div>
356
+ <div id="taskList" class="task-list"></div>
357
+ </aside>
358
+
359
+ <section class="panel">
360
+ <div class="panel-head">
361
+ <h2 id="taskHeading">Loading</h2>
362
+ <select id="fileSelect"></select>
363
+ </div>
364
+ <div class="panel-body">
365
+ <h3 id="taskTitle" class="task-title"></h3>
366
+ <div id="taskSummary" class="summary"></div>
367
+ <div id="taskChips" class="chips"></div>
368
+ <div class="cols">
369
+ <div>
370
+ <h3>Failure Log</h3>
371
+ <pre id="failureLog"></pre>
372
+ </div>
373
+ <div>
374
+ <h3>File</h3>
375
+ <pre id="fileContent"></pre>
376
+ </div>
377
+ </div>
378
+ <h3>Oracle Patch</h3>
379
+ <pre id="oraclePatch"></pre>
380
+ </div>
381
+ </section>
382
+ </div>
383
+ </section>
384
+
385
+ <section id="score" class="tab hidden">
386
+ <div class="panel">
387
+ <div class="panel-head">
388
+ <h2>Patch Scorer</h2>
389
+ <select id="scoreTaskSelect"></select>
390
+ </div>
391
+ <div class="panel-body">
392
+ <textarea id="patchInput" spellcheck="false" placeholder="Paste unified diff here"></textarea>
393
+ <div class="toolbar">
394
+ <button id="scoreButton" class="primary">Score Patch</button>
395
+ <button id="loadOracleButton">Load Oracle Patch</button>
396
+ </div>
397
+ <div id="scoreOutput" class="score">Paste a patch and score it.</div>
398
+ </div>
399
+ </div>
400
+ </section>
401
+
402
+ <section id="leaderboard" class="tab hidden">
403
+ <div class="panel">
404
+ <div class="panel-head">
405
+ <h2>Leaderboard Scaffold</h2>
406
+ </div>
407
+ <div class="panel-body">
408
+ <table>
409
+ <thead>
410
+ <tr><th>Agent</th><th>Type</th><th>Passed</th><th>Total</th><th>Rate</th><th>Notes</th></tr>
411
+ </thead>
412
+ <tbody>
413
+ <tr><td>Oracle patch</td><td>reference</td><td id="oraclePassed">0</td><td id="oracleTotal">0</td><td>100%</td><td>Upper bound. Every official patch applies and satisfies final-file checks.</td></tr>
414
+ <tr><td>No-op patch</td><td>baseline</td><td>0</td><td id="noopTotal">0</td><td>0%</td><td>Lower bound. Empty patch fails every task.</td></tr>
415
+ <tr><td>Public agents</td><td>coming soon</td><td>-</td><td id="publicTotal">0</td><td>-</td><td>Run agents locally, submit JSONL, rank pass@1, apply rate, runtime, and cost.</td></tr>
416
+ </tbody>
417
+ </table>
418
+ </div>
419
+ </div>
420
+ </section>
421
+ </main>
422
+
423
+ <script>
424
+ const state = {
425
+ tasks: [],
426
+ filtered: [],
427
+ selectedId: null
428
+ };
429
+
430
+ const byId = (id) => document.getElementById(id);
431
+ const unique = (items) => Array.from(new Set(items)).sort();
432
+
433
+ function linesToText(lines) {
434
+ return (lines || []).join("\n");
435
+ }
436
+
437
+ function taskById(id) {
438
+ return state.tasks.find((task) => task.id === id);
439
+ }
440
+
441
+ function fillSelect(select, values) {
442
+ select.innerHTML = "";
443
+ for (const value of values) {
444
+ const option = document.createElement("option");
445
+ option.value = value;
446
+ option.textContent = value;
447
+ select.appendChild(option);
448
+ }
449
+ }
450
+
451
+ function initFilters() {
452
+ fillSelect(byId("splitFilter"), ["all", ...unique(state.tasks.map((task) => task.split))]);
453
+ fillSelect(byId("ecosystemFilter"), ["all", ...unique(state.tasks.map((task) => task.ecosystem))]);
454
+ fillSelect(byId("difficultyFilter"), ["all", "easy", "medium", "hard"]);
455
+ fillSelect(byId("scoreTaskSelect"), state.tasks.map((task) => `${task.id} | ${task.title}`));
456
+ }
457
+
458
+ function updateMetrics() {
459
+ byId("metricTasks").textContent = state.tasks.length;
460
+ byId("metricFailures").textContent = unique(state.tasks.map((task) => task.failure_type)).length;
461
+ byId("metricEcosystems").textContent = unique(state.tasks.map((task) => task.ecosystem)).length;
462
+ byId("metricChallenge").textContent = state.tasks.filter((task) => task.split === "challenge").length;
463
+ byId("oraclePassed").textContent = state.tasks.length;
464
+ byId("oracleTotal").textContent = state.tasks.length;
465
+ byId("noopTotal").textContent = state.tasks.length;
466
+ byId("publicTotal").textContent = state.tasks.length;
467
+ }
468
+
469
+ function applyFilters() {
470
+ const split = byId("splitFilter").value;
471
+ const ecosystem = byId("ecosystemFilter").value;
472
+ const difficulty = byId("difficultyFilter").value;
473
+ state.filtered = state.tasks.filter((task) => {
474
+ return (split === "all" || task.split === split) &&
475
+ (ecosystem === "all" || task.ecosystem === ecosystem) &&
476
+ (difficulty === "all" || task.difficulty === difficulty);
477
+ });
478
+ byId("taskCount").textContent = `${state.filtered.length} shown`;
479
+ renderTaskList();
480
+ if (!state.filtered.some((task) => task.id === state.selectedId)) {
481
+ selectTask(state.filtered[0] ? state.filtered[0].id : null);
482
+ }
483
+ }
484
+
485
+ function renderTaskList() {
486
+ const list = byId("taskList");
487
+ list.innerHTML = "";
488
+ for (const task of state.filtered) {
489
+ const row = document.createElement("button");
490
+ row.className = `task-row ${task.id === state.selectedId ? "active" : ""}`;
491
+ row.innerHTML = `<strong>${task.id} | ${escapeHtml(task.title)}</strong><span>${task.split} | ${task.ecosystem} | ${task.difficulty} | ${task.failure_type}</span>`;
492
+ row.addEventListener("click", () => selectTask(task.id));
493
+ list.appendChild(row);
494
+ }
495
+ }
496
+
497
+ function selectTask(taskId) {
498
+ state.selectedId = taskId;
499
+ renderTaskList();
500
+ const task = taskById(taskId);
501
+ if (!task) {
502
+ byId("taskHeading").textContent = "No task";
503
+ return;
504
+ }
505
+
506
+ byId("taskHeading").textContent = `${task.id}`;
507
+ byId("taskTitle").textContent = task.title;
508
+ byId("taskSummary").textContent = task.summary;
509
+ byId("failureLog").textContent = linesToText(task.failure_log);
510
+ byId("oraclePatch").textContent = linesToText(task.oracle_patch);
511
+ byId("taskChips").innerHTML = [
512
+ task.split,
513
+ task.ecosystem,
514
+ task.difficulty,
515
+ task.failure_type,
516
+ ...task.tags
517
+ ].map((tag) => `<span class="chip">${escapeHtml(tag)}</span>`).join("");
518
+
519
+ const fileSelect = byId("fileSelect");
520
+ fillSelect(fileSelect, task.files.map((file) => file.path));
521
+ fileSelect.onchange = () => showFile(task.id, fileSelect.value);
522
+ showFile(task.id, fileSelect.value);
523
+ }
524
+
525
+ function showFile(taskId, path) {
526
+ const task = taskById(taskId);
527
+ const file = task.files.find((item) => item.path === path);
528
+ byId("fileContent").textContent = file ? linesToText(file.content) : "";
529
+ }
530
+
531
+ function scoreSelectedPatch() {
532
+ const selectValue = byId("scoreTaskSelect").value;
533
+ const taskId = selectValue.split(" | ")[0];
534
+ const task = taskById(taskId);
535
+ const patch = byId("patchInput").value;
536
+ const result = scorePatch(task, patch);
537
+ const output = byId("scoreOutput");
538
+ output.className = `score ${result.passed ? "pass" : "fail"}`;
539
+ output.innerHTML = `<strong>${result.passed ? "PASS" : "FAIL"}</strong><br>Score: ${result.passedChecks}/${result.totalChecks} (${Math.round(result.score * 100)}%)<br>Patch applied: ${result.applied}<ul>${result.messages.map((msg) => `<li>${escapeHtml(msg)}</li>`).join("")}</ul>`;
540
+ }
541
+
542
+ function scorePatch(task, patchText) {
543
+ const messages = [];
544
+ let passedChecks = 0;
545
+ let totalChecks = 0;
546
+ const applyResult = applyPatch(task, patchText);
547
+
548
+ totalChecks += 1;
549
+ if (applyResult.ok) {
550
+ passedChecks += 1;
551
+ messages.push("PASS patch applies cleanly");
552
+ } else {
553
+ messages.push(`FAIL patch does not apply cleanly: ${applyResult.error}`);
554
+ }
555
+
556
+ const touched = touchedFiles(patchText);
557
+ const scoring = task.scoring || {};
558
+
559
+ for (const path of scoring.must_touch || []) {
560
+ totalChecks += 1;
561
+ if (touched.has(path)) {
562
+ passedChecks += 1;
563
+ messages.push(`PASS must touch ${path}`);
564
+ } else {
565
+ messages.push(`FAIL missing touched file ${path}`);
566
+ }
567
+ }
568
+
569
+ for (const rule of scoring.must_contain || []) {
570
+ totalChecks += 1;
571
+ const finalText = applyResult.files[rule.path] || "";
572
+ if (finalText.includes(rule.text)) {
573
+ passedChecks += 1;
574
+ messages.push(`PASS final ${rule.path} contains ${JSON.stringify(rule.text)}`);
575
+ } else {
576
+ messages.push(`FAIL final ${rule.path} missing ${JSON.stringify(rule.text)}`);
577
+ }
578
+ }
579
+
580
+ for (const rule of scoring.must_not_contain || []) {
581
+ totalChecks += 1;
582
+ const finalText = applyResult.files[rule.path] || "";
583
+ if (!finalText.includes(rule.text)) {
584
+ passedChecks += 1;
585
+ messages.push(`PASS final ${rule.path} does not contain ${JSON.stringify(rule.text)}`);
586
+ } else {
587
+ messages.push(`FAIL final ${rule.path} still contains ${JSON.stringify(rule.text)}`);
588
+ }
589
+ }
590
+
591
+ return {
592
+ applied: applyResult.ok,
593
+ passed: totalChecks > 0 && passedChecks === totalChecks,
594
+ passedChecks,
595
+ totalChecks,
596
+ score: totalChecks > 0 ? passedChecks / totalChecks : 0,
597
+ messages
598
+ };
599
+ }
600
+
601
+ function touchedFiles(patchText) {
602
+ const touched = new Set();
603
+ for (const line of patchText.split(/\r?\n/)) {
604
+ if (line.startsWith("+++ b/")) touched.add(line.slice(6));
605
+ if (line.startsWith("--- a/")) touched.add(line.slice(6));
606
+ }
607
+ touched.delete("/dev/null");
608
+ return touched;
609
+ }
610
+
611
+ function applyPatch(task, patchText) {
612
+ const files = {};
613
+ for (const file of task.files) files[file.path] = [...file.content];
614
+ const lines = patchText.split(/\r?\n/);
615
+ let i = 0;
616
+ try {
617
+ while (i < lines.length) {
618
+ if (!lines[i].startsWith("--- a/")) {
619
+ i += 1;
620
+ continue;
621
+ }
622
+ const oldPath = lines[i].slice(6);
623
+ i += 1;
624
+ if (i >= lines.length || !lines[i].startsWith("+++ b/")) {
625
+ throw new Error(`missing +++ line after ${oldPath}`);
626
+ }
627
+ const newPath = lines[i].slice(6);
628
+ const path = newPath === "/dev/null" ? oldPath : newPath;
629
+ const original = files[path] ? [...files[path]] : [];
630
+ const output = [];
631
+ let oldIndex = 0;
632
+ i += 1;
633
+
634
+ while (i < lines.length && !lines[i].startsWith("--- a/")) {
635
+ if (!lines[i].startsWith("@@")) {
636
+ i += 1;
637
+ continue;
638
+ }
639
+ const match = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(lines[i]);
640
+ if (!match) throw new Error(`bad hunk header: ${lines[i]}`);
641
+ const start = Number(match[1]) - 1;
642
+ while (oldIndex < start) {
643
+ output.push(original[oldIndex] || "");
644
+ oldIndex += 1;
645
+ }
646
+ i += 1;
647
+ while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("--- a/")) {
648
+ const line = lines[i];
649
+ if (line === "\") {
650
+ i += 1;
651
+ continue;
652
+ }
653
+ const marker = line[0];
654
+ const text = line.slice(1);
655
+ if (marker === " ") {
656
+ if ((original[oldIndex] || "") !== text) throw new Error(`context mismatch in ${path}`);
657
+ output.push(text);
658
+ oldIndex += 1;
659
+ } else if (marker === "-") {
660
+ if ((original[oldIndex] || "") !== text) throw new Error(`remove mismatch in ${path}`);
661
+ oldIndex += 1;
662
+ } else if (marker === "+") {
663
+ output.push(text);
664
+ } else if (line.length === 0) {
665
+ i += 1;
666
+ continue;
667
+ } else {
668
+ throw new Error(`bad patch line: ${line}`);
669
+ }
670
+ i += 1;
671
+ }
672
+ }
673
+ while (oldIndex < original.length) {
674
+ output.push(original[oldIndex] || "");
675
+ oldIndex += 1;
676
+ }
677
+ files[path] = output;
678
+ }
679
+ const finalFiles = {};
680
+ for (const [path, content] of Object.entries(files)) {
681
+ finalFiles[path] = content.join("\n") + "\n";
682
+ }
683
+ return { ok: true, files: finalFiles, error: null };
684
+ } catch (error) {
685
+ const finalFiles = {};
686
+ for (const [path, content] of Object.entries(files)) {
687
+ finalFiles[path] = content.join("\n") + "\n";
688
+ }
689
+ return { ok: false, files: finalFiles, error: error.message };
690
+ }
691
+ }
692
+
693
+ function loadOraclePatch() {
694
+ const taskId = byId("scoreTaskSelect").value.split(" | ")[0];
695
+ const task = taskById(taskId);
696
+ byId("patchInput").value = linesToText(task.oracle_patch);
697
+ scoreSelectedPatch();
698
+ }
699
+
700
+ function escapeHtml(value) {
701
+ return String(value)
702
+ .replaceAll("&", "&amp;")
703
+ .replaceAll("<", "&lt;")
704
+ .replaceAll(">", "&gt;")
705
+ .replaceAll('"', "&quot;")
706
+ .replaceAll("'", "&#039;");
707
+ }
708
+
709
+ function initTabs() {
710
+ for (const button of document.querySelectorAll("[data-tab]")) {
711
+ button.addEventListener("click", () => {
712
+ for (const other of document.querySelectorAll("[data-tab]")) other.classList.remove("active");
713
+ for (const tab of document.querySelectorAll(".tab")) tab.classList.add("hidden");
714
+ button.classList.add("active");
715
+ byId(button.dataset.tab).classList.remove("hidden");
716
+ });
717
+ }
718
+ }
719
+
720
+ async function main() {
721
+ initTabs();
722
+ const response = await fetch("data/sabnock_docker_v1.json");
723
+ state.tasks = await response.json();
724
+ initFilters();
725
+ updateMetrics();
726
+ byId("splitFilter").addEventListener("change", applyFilters);
727
+ byId("ecosystemFilter").addEventListener("change", applyFilters);
728
+ byId("difficultyFilter").addEventListener("change", applyFilters);
729
+ byId("scoreButton").addEventListener("click", scoreSelectedPatch);
730
+ byId("loadOracleButton").addEventListener("click", loadOraclePatch);
731
+ applyFilters();
732
+ }
733
+
734
+ main().catch((error) => {
735
+ document.body.innerHTML = `<main class="shell"><section class="hero"><h1>Failed to load</h1><p>${escapeHtml(error.message)}</p></section></main>`;
736
+ });
737
+ </script>
738
+ </body>
739
  </html>