Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Sabnock Docker Arena</title> | |
| <style> | |
| :root { | |
| color-scheme: dark; | |
| --bg: #080b0f; | |
| --panel: #0f141b; | |
| --panel-2: #151b24; | |
| --panel-3: #0c1117; | |
| --line: #283443; | |
| --text: #e7ecf3; | |
| --muted: #95a0af; | |
| --accent: #4fb3ff; | |
| --accent-soft: rgba(79, 179, 255, 0.12); | |
| --ok: #48d597; | |
| --bad: #ff6b6b; | |
| --warn: #f0c36a; | |
| --code: #070a0e; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| background: | |
| radial-gradient(circle at 12% -10%, rgba(79, 179, 255, 0.10), transparent 28rem), | |
| linear-gradient(180deg, #0a0e13 0%, var(--bg) 18rem); | |
| color: var(--text); | |
| font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| letter-spacing: 0; | |
| } | |
| button, | |
| select, | |
| textarea { | |
| font: inherit; | |
| } | |
| .shell { | |
| width: min(1760px, 100%); | |
| margin: 0 auto; | |
| padding: 18px 28px 30px; | |
| } | |
| .hero { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| padding: 20px 22px; | |
| background: linear-gradient(135deg, rgba(17, 24, 32, 0.96), rgba(12, 16, 21, 0.96) 58%, rgba(17, 17, 13, 0.96)); | |
| display: flex; | |
| align-items: flex-end; | |
| justify-content: space-between; | |
| gap: 20px; | |
| } | |
| h1 { | |
| margin: 0 0 8px; | |
| font-size: 34px; | |
| line-height: 1.1; | |
| } | |
| .hero p { | |
| margin: 0; | |
| max-width: 900px; | |
| color: var(--muted); | |
| line-height: 1.55; | |
| font-size: 15px; | |
| } | |
| .hero-kicker { | |
| color: var(--accent); | |
| font-size: 12px; | |
| font-weight: 700; | |
| letter-spacing: 0.08em; | |
| margin-bottom: 8px; | |
| text-transform: uppercase; | |
| } | |
| .hero-actions { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| justify-content: flex-end; | |
| min-width: 260px; | |
| } | |
| .hero-actions a { | |
| border: 1px solid var(--line); | |
| border-radius: 6px; | |
| color: var(--text); | |
| background: #101823; | |
| padding: 9px 11px; | |
| text-decoration: none; | |
| font-size: 13px; | |
| } | |
| .hero-actions a.primary-link { | |
| border-color: #1b5f92; | |
| background: #0d3b5e; | |
| } | |
| .metrics { | |
| display: grid; | |
| grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| gap: 12px; | |
| margin: 14px 0; | |
| } | |
| .metric { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| padding: 14px 16px; | |
| background: linear-gradient(180deg, #111720, #0d1219); | |
| } | |
| .metric span { | |
| display: block; | |
| color: var(--muted); | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| } | |
| .metric strong { | |
| display: block; | |
| font-size: 26px; | |
| margin-top: 4px; | |
| } | |
| .toolbar, | |
| .tabs { | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| align-items: center; | |
| margin: 12px 0; | |
| } | |
| .toolbar.spread { | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| } | |
| .toolbar label { | |
| color: var(--muted); | |
| font-size: 13px; | |
| } | |
| select, | |
| textarea { | |
| border: 1px solid var(--line); | |
| border-radius: 6px; | |
| color: var(--text); | |
| background: var(--panel); | |
| } | |
| select { | |
| min-width: 170px; | |
| padding: 10px 11px; | |
| } | |
| button { | |
| border: 1px solid var(--line); | |
| border-radius: 6px; | |
| color: var(--text); | |
| background: var(--panel-2); | |
| padding: 10px 13px; | |
| cursor: pointer; | |
| } | |
| button.active { | |
| border-color: var(--accent); | |
| background: var(--accent-soft); | |
| } | |
| button.primary { | |
| background: #0d3b5e; | |
| border-color: #1b5f92; | |
| } | |
| .triage-grid { | |
| display: grid; | |
| grid-template-columns: minmax(360px, 0.82fr) minmax(0, 1.18fr); | |
| gap: 14px; | |
| align-items: start; | |
| } | |
| .field-label { | |
| display: block; | |
| color: var(--muted); | |
| font-size: 12px; | |
| font-weight: 700; | |
| margin-bottom: 8px; | |
| text-transform: uppercase; | |
| } | |
| .triage-textarea { | |
| min-height: 430px; | |
| } | |
| .triage-status { | |
| color: var(--muted); | |
| font-size: 13px; | |
| line-height: 1.5; | |
| margin: 0; | |
| } | |
| .diagnosis { | |
| border: 1px solid #1f5b89; | |
| border-radius: 8px; | |
| background: linear-gradient(180deg, rgba(13, 59, 94, 0.28), rgba(9, 16, 23, 0.98)); | |
| padding: 16px; | |
| margin-bottom: 12px; | |
| } | |
| .diagnosis .eyebrow, | |
| .rank-card .eyebrow { | |
| color: var(--accent); | |
| display: block; | |
| font-size: 11px; | |
| font-weight: 800; | |
| letter-spacing: 0.08em; | |
| margin-bottom: 7px; | |
| text-transform: uppercase; | |
| } | |
| .diagnosis h3 { | |
| font-size: 25px; | |
| line-height: 1.16; | |
| margin: 0 0 8px; | |
| } | |
| .diagnosis p { | |
| color: #b9c5d4; | |
| line-height: 1.55; | |
| margin: 0; | |
| } | |
| .confidence { | |
| align-items: center; | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 14px; | |
| } | |
| .confidence-bar { | |
| background: #091019; | |
| border: 1px solid var(--line); | |
| border-radius: 999px; | |
| flex: 1; | |
| height: 10px; | |
| overflow: hidden; | |
| } | |
| .confidence-fill { | |
| background: linear-gradient(90deg, var(--accent), var(--ok)); | |
| height: 100%; | |
| width: 0; | |
| } | |
| .signal-grid { | |
| display: grid; | |
| grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin-bottom: 12px; | |
| } | |
| .signal { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: var(--panel-3); | |
| padding: 12px; | |
| min-height: 92px; | |
| } | |
| .signal span { | |
| color: var(--muted); | |
| display: block; | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| } | |
| .signal strong { | |
| display: block; | |
| font-size: 15px; | |
| line-height: 1.35; | |
| margin-top: 7px; | |
| word-break: break-word; | |
| } | |
| .repair-list, | |
| .nearest-list { | |
| display: grid; | |
| gap: 10px; | |
| } | |
| .repair-item, | |
| .rank-card { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: #0b1118; | |
| padding: 12px; | |
| } | |
| .repair-item { | |
| color: #d8e1ec; | |
| line-height: 1.5; | |
| } | |
| .rank-card { | |
| display: grid; | |
| grid-template-columns: minmax(0, 1fr) auto; | |
| gap: 10px; | |
| align-items: start; | |
| } | |
| .rank-card h4 { | |
| font-size: 15px; | |
| line-height: 1.35; | |
| margin: 0 0 5px; | |
| } | |
| .rank-card p { | |
| color: var(--muted); | |
| font-size: 13px; | |
| line-height: 1.45; | |
| margin: 0; | |
| } | |
| .rank-score { | |
| border: 1px solid #245173; | |
| border-radius: 999px; | |
| color: #d8ecff; | |
| background: #0b2133; | |
| font-size: 12px; | |
| font-weight: 700; | |
| padding: 5px 8px; | |
| white-space: nowrap; | |
| } | |
| .mini-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 12px; | |
| margin-top: 12px; | |
| } | |
| .grid { | |
| display: grid; | |
| grid-template-columns: 430px minmax(0, 1fr); | |
| gap: 14px; | |
| align-items: start; | |
| } | |
| .panel { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: rgba(15, 20, 27, 0.96); | |
| min-width: 0; | |
| box-shadow: 0 16px 50px rgba(0, 0, 0, 0.18); | |
| } | |
| .panel-head { | |
| padding: 13px 15px; | |
| border-bottom: 1px solid var(--line); | |
| display: flex; | |
| justify-content: space-between; | |
| gap: 10px; | |
| align-items: center; | |
| } | |
| .panel-head h2 { | |
| margin: 0; | |
| font-size: 15px; | |
| } | |
| .panel-body { | |
| padding: 14px; | |
| } | |
| .task-list { | |
| height: min(720px, calc(100vh - 330px)); | |
| min-height: 420px; | |
| overflow: auto; | |
| } | |
| .task-row { | |
| width: 100%; | |
| display: block; | |
| text-align: left; | |
| padding: 12px 14px; | |
| border: 0; | |
| border-bottom: 1px solid var(--line); | |
| border-radius: 0; | |
| background: transparent; | |
| } | |
| .task-row.active { | |
| border-left: 3px solid var(--accent); | |
| padding-left: 11px; | |
| } | |
| .task-row:hover, | |
| .task-row.active { | |
| background: #151d28; | |
| } | |
| .task-row strong { | |
| display: block; | |
| font-size: 13.5px; | |
| margin-bottom: 4px; | |
| line-height: 1.35; | |
| } | |
| .task-row span { | |
| display: block; | |
| color: var(--muted); | |
| font-size: 12.5px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .task-title { | |
| margin: 0 0 8px; | |
| font-size: 26px; | |
| line-height: 1.2; | |
| } | |
| .summary { | |
| color: var(--muted); | |
| line-height: 1.55; | |
| margin-bottom: 12px; | |
| font-size: 14.5px; | |
| } | |
| .chips { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| margin: 8px 0 12px; | |
| } | |
| .chip { | |
| border: 1px solid var(--line); | |
| border-radius: 999px; | |
| color: #cbd5e1; | |
| background: #0b1118; | |
| padding: 4px 8px; | |
| font-size: 12px; | |
| } | |
| .code-section { | |
| margin-top: 16px; | |
| } | |
| .section-title { | |
| margin: 0 0 8px; | |
| color: #dbe5f0; | |
| font-size: 13px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| } | |
| pre { | |
| margin: 0; | |
| border: 1px solid var(--line); | |
| border-radius: 6px; | |
| padding: 13px; | |
| max-height: 430px; | |
| overflow: auto; | |
| background: var(--code); | |
| color: #d7dee8; | |
| line-height: 1.52; | |
| font-size: 12.5px; | |
| white-space: pre; | |
| } | |
| textarea { | |
| width: 100%; | |
| min-height: 340px; | |
| padding: 12px; | |
| resize: vertical; | |
| background: var(--code); | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; | |
| font-size: 12.5px; | |
| line-height: 1.52; | |
| } | |
| .cols { | |
| display: grid; | |
| grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); | |
| gap: 12px; | |
| margin-top: 12px; | |
| } | |
| .patch-grid { | |
| display: grid; | |
| grid-template-columns: minmax(0, 1fr); | |
| gap: 12px; | |
| } | |
| .score { | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| padding: 12px; | |
| background: var(--panel); | |
| line-height: 1.5; | |
| } | |
| .score.pass { | |
| border-color: #246b4d; | |
| } | |
| .score.fail { | |
| border-color: #774042; | |
| } | |
| .score ul { | |
| margin: 8px 0 0; | |
| padding-left: 18px; | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 13px; | |
| } | |
| th, | |
| td { | |
| border-bottom: 1px solid var(--line); | |
| padding: 9px 10px; | |
| text-align: left; | |
| vertical-align: top; | |
| } | |
| th { | |
| color: var(--muted); | |
| font-weight: 600; | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| @media (max-width: 960px) { | |
| .metrics, | |
| .triage-grid, | |
| .grid, | |
| .cols, | |
| .signal-grid, | |
| .mini-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| .shell { | |
| padding: 12px; | |
| } | |
| .hero { | |
| display: block; | |
| } | |
| .hero-actions { | |
| justify-content: flex-start; | |
| margin-top: 14px; | |
| } | |
| .task-list { | |
| height: 420px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <main class="shell"> | |
| <section class="hero"> | |
| <div> | |
| <div class="hero-kicker">AI Agent Failure Benchmark</div> | |
| <h1>Sabnock Docker</h1> | |
| <p>Test whether coding agents can repair Dockerfile, Compose, dependency, security, and container runtime failures. Browse tasks, inspect failure logs, and score patches directly in the browser.</p> | |
| </div> | |
| <div class="hero-actions"> | |
| <a class="primary-link" href="https://huggingface.co/datasets/Redhanuman/sabnock-docker" target="_blank" rel="noreferrer">Dataset</a> | |
| <a href="https://huggingface.co/datasets/Redhanuman/sabnock-docker/blob/main/BENCHMARK.md" target="_blank" rel="noreferrer">Benchmark</a> | |
| <a href="https://huggingface.co/datasets/Redhanuman/sabnock-docker/blob/main/SUBMISSIONS.md" target="_blank" rel="noreferrer">Submit</a> | |
| </div> | |
| </section> | |
| <section class="metrics"> | |
| <div class="metric"><span>Tasks</span><strong id="metricTasks">0</strong></div> | |
| <div class="metric"><span>Failure Types</span><strong id="metricFailures">0</strong></div> | |
| <div class="metric"><span>Ecosystems</span><strong id="metricEcosystems">0</strong></div> | |
| <div class="metric"><span>Challenge</span><strong id="metricChallenge">0</strong></div> | |
| </section> | |
| <nav class="tabs"> | |
| <button class="active" data-tab="triage">Triage</button> | |
| <button data-tab="browse">Benchmark</button> | |
| <button data-tab="score">Patch Scorer</button> | |
| <button data-tab="leaderboard">Leaderboard</button> | |
| </nav> | |
| <section id="triage" class="tab"> | |
| <div class="triage-grid"> | |
| <section class="panel"> | |
| <div class="panel-head"> | |
| <h2>Docker Failure Triage</h2> | |
| <span id="triageMode" class="chip">rules ranker</span> | |
| </div> | |
| <div class="panel-body"> | |
| <label class="field-label" for="triageInput">Build / Runtime Log</label> | |
| <textarea id="triageInput" class="triage-textarea" spellcheck="false" placeholder="Paste a Docker, Compose, CI, or container runtime failure log"></textarea> | |
| <div class="toolbar spread"> | |
| <div class="toolbar"> | |
| <button id="triageButton" class="primary">Analyze Log</button> | |
| <button id="sampleLogButton">Load Benchmark Log</button> | |
| <button id="semanticButton">Load AI Ranker</button> | |
| </div> | |
| <p id="triageStatus" class="triage-status">No backend. No token. Runs in this browser.</p> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="panel"> | |
| <div class="panel-head"> | |
| <h2>Diagnosis</h2> | |
| <span id="triageTaskCount" class="chip">0 tasks indexed</span> | |
| </div> | |
| <div class="panel-body"> | |
| <div id="triageDiagnosis" class="diagnosis"> | |
| <span class="eyebrow">Waiting</span> | |
| <h3>Load a log to start.</h3> | |
| <p>The ranker compares your failure against the benchmark corpus and surfaces the most likely repair path.</p> | |
| <div class="confidence"> | |
| <div class="confidence-bar"><div class="confidence-fill" style="width: 0%"></div></div> | |
| <strong>0%</strong> | |
| </div> | |
| </div> | |
| <div class="signal-grid"> | |
| <div class="signal"><span>Failure Type</span><strong id="detectedFailure">-</strong></div> | |
| <div class="signal"><span>Ecosystem</span><strong id="detectedEcosystem">-</strong></div> | |
| <div class="signal"><span>Next File</span><strong id="detectedFile">-</strong></div> | |
| </div> | |
| <div class="mini-grid"> | |
| <div class="code-section"> | |
| <h3 class="section-title">Repair Pattern</h3> | |
| <div id="repairPlan" class="repair-list"></div> | |
| </div> | |
| <div class="code-section"> | |
| <h3 class="section-title">Nearest Benchmark Tasks</h3> | |
| <div id="nearestTasks" class="nearest-list"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| </section> | |
| <section id="browse" class="tab hidden"> | |
| <div class="toolbar"> | |
| <label>Split <select id="splitFilter"></select></label> | |
| <label>Ecosystem <select id="ecosystemFilter"></select></label> | |
| <label>Difficulty <select id="difficultyFilter"></select></label> | |
| </div> | |
| <div class="grid"> | |
| <aside class="panel"> | |
| <div class="panel-head"> | |
| <h2>Tasks</h2> | |
| <span id="taskCount" class="chip">0</span> | |
| </div> | |
| <div id="taskList" class="task-list"></div> | |
| </aside> | |
| <section class="panel"> | |
| <div class="panel-head"> | |
| <h2 id="taskHeading">Loading</h2> | |
| <select id="fileSelect"></select> | |
| </div> | |
| <div class="panel-body"> | |
| <h3 id="taskTitle" class="task-title"></h3> | |
| <div id="taskSummary" class="summary"></div> | |
| <div id="taskChips" class="chips"></div> | |
| <div class="cols"> | |
| <div class="code-section"> | |
| <h3 class="section-title">Failure Log</h3> | |
| <pre id="failureLog"></pre> | |
| </div> | |
| <div class="code-section"> | |
| <h3 class="section-title">Selected File</h3> | |
| <pre id="fileContent"></pre> | |
| </div> | |
| </div> | |
| <div class="patch-grid"> | |
| <div class="code-section"> | |
| <h3 class="section-title">Oracle Patch</h3> | |
| <pre id="oraclePatch"></pre> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| </section> | |
| <section id="score" class="tab hidden"> | |
| <div class="panel"> | |
| <div class="panel-head"> | |
| <h2>Patch Scorer</h2> | |
| <select id="scoreTaskSelect"></select> | |
| </div> | |
| <div class="panel-body"> | |
| <textarea id="patchInput" spellcheck="false" placeholder="Paste unified diff here"></textarea> | |
| <div class="toolbar"> | |
| <button id="scoreButton" class="primary">Score Patch</button> | |
| <button id="loadOracleButton">Load Oracle Patch</button> | |
| </div> | |
| <div id="scoreOutput" class="score">Paste a patch and score it.</div> | |
| </div> | |
| </div> | |
| </section> | |
| <section id="leaderboard" class="tab hidden"> | |
| <div class="panel"> | |
| <div class="panel-head"> | |
| <h2>Leaderboard Scaffold</h2> | |
| </div> | |
| <div class="panel-body"> | |
| <table> | |
| <thead> | |
| <tr><th>Agent</th><th>Type</th><th>Passed</th><th>Total</th><th>Rate</th><th>Notes</th></tr> | |
| </thead> | |
| <tbody> | |
| <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> | |
| <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> | |
| <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> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| const state = { | |
| tasks: [], | |
| filtered: [], | |
| selectedId: null, | |
| sampleIndex: 0, | |
| semantic: { | |
| ready: false, | |
| loading: false, | |
| extractor: null, | |
| embeddings: [] | |
| } | |
| }; | |
| const byId = (id) => document.getElementById(id); | |
| const unique = (items) => Array.from(new Set(items)).sort(); | |
| const FAILURE_PROFILES = { | |
| missing_build_context_file: { | |
| label: "Missing build-context file", | |
| evidence: ["no such file or directory", "could not open requirements file", "not found", "COPY failed", "failed to compute cache key"], | |
| repair: [ | |
| "Copy dependency manifests before installing dependencies.", | |
| "Make sure the file exists inside the Docker build context.", | |
| "Keep layer order strict: manifest first, install second, source last." | |
| ], | |
| fileHints: ["Dockerfile", "requirements.txt", "package.json"] | |
| }, | |
| lockfile_missing: { | |
| label: "Missing lockfile", | |
| evidence: ["npm ci", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "lockfile"], | |
| repair: [ | |
| "Use the package manager command that matches committed files.", | |
| "If no lockfile is present, use npm install instead of npm ci.", | |
| "Copy package manifests before dependency installation." | |
| ], | |
| fileHints: ["Dockerfile", "package.json", "package-lock.json"] | |
| }, | |
| multistage_artifact_mismatch: { | |
| label: "Multi-stage artifact mismatch", | |
| evidence: ["copy --from", "no such file", "stat", "dist", "target", "build stage", "builder"], | |
| repair: [ | |
| "Align the builder output path with the runtime COPY path.", | |
| "Name stages explicitly and copy from the correct stage.", | |
| "Verify the build command writes the artifact the runtime stage expects." | |
| ], | |
| fileHints: ["Dockerfile"] | |
| }, | |
| compose_readiness: { | |
| label: "Compose service readiness", | |
| evidence: ["connection refused", "database is starting up", "depends_on", "healthcheck", "postgres", "mysql"], | |
| repair: [ | |
| "Add a service healthcheck for the dependency.", | |
| "Use depends_on with service_healthy when Compose supports it.", | |
| "Keep app startup separate from dependency readiness." | |
| ], | |
| fileHints: ["docker-compose.yml", "compose.yml"] | |
| }, | |
| dockerignore_artifact_excluded: { | |
| label: ".dockerignore excludes required artifact", | |
| evidence: [".dockerignore", "excluded", "dist", "build", "copy failed", "failed to calculate checksum"], | |
| repair: [ | |
| "Stop excluding the production artifact needed by the image.", | |
| "Use negation rules for required outputs.", | |
| "Keep dev-only ignores separate from runtime artifacts." | |
| ], | |
| fileHints: [".dockerignore", "Dockerfile"] | |
| }, | |
| native_dependency_missing: { | |
| label: "Native dependency missing", | |
| evidence: ["openssl", "libssl", "pg_config", "gcc", "make", "musl", "cargo", "failed building wheel", "native"], | |
| repair: [ | |
| "Install OS packages required by native extensions.", | |
| "Use the correct Alpine/Debian package names for the base image.", | |
| "Build dependencies can stay in builder stages when possible." | |
| ], | |
| fileHints: ["Dockerfile"] | |
| }, | |
| python_src_layout_import: { | |
| label: "Python src-layout import failure", | |
| evidence: ["module not found", "modulenotfounderror", "src", "pythonpath", "no module named"], | |
| repair: [ | |
| "Copy the src directory into the image.", | |
| "Install the package or set PYTHONPATH for src-layout projects.", | |
| "Run the app using the package/module path that exists in-container." | |
| ], | |
| fileHints: ["Dockerfile", "pyproject.toml"] | |
| }, | |
| poetry_root_install_order: { | |
| label: "Poetry root install order", | |
| evidence: ["poetry install", "pyproject.toml", "readme", "no file/folder found", "package mode", "root package"], | |
| repair: [ | |
| "Install dependencies without the root package before copying source.", | |
| "Copy package metadata and README files required by Poetry.", | |
| "Copy source before installing the project itself." | |
| ], | |
| fileHints: ["Dockerfile", "pyproject.toml"] | |
| }, | |
| compose_bind_mount_masks_dependencies: { | |
| label: "Bind mount masks installed dependencies", | |
| evidence: ["bind mount", "node_modules", "site-packages", "volume", "cannot find module", "module not found"], | |
| repair: [ | |
| "Avoid mounting over installed dependency directories.", | |
| "Use a named volume for node_modules or dependency caches.", | |
| "Mount only source paths needed for development." | |
| ], | |
| fileHints: ["docker-compose.yml", "compose.yml"] | |
| }, | |
| build_secret_leak: { | |
| label: "Build secret leak", | |
| evidence: ["secret", "token", "apikey", "api_key", "arg", "env", "history", "docker buildx build --check"], | |
| repair: [ | |
| "Do not pass secrets through ARG or ENV.", | |
| "Use BuildKit secret mounts for build-time credentials.", | |
| "Remove tokens from image layers and final environment." | |
| ], | |
| fileHints: ["Dockerfile"] | |
| }, | |
| apt_update_missing: { | |
| label: "APT index missing", | |
| evidence: ["unable to locate package", "apt-get install", "apt install", "apt update", "package has no installation candidate"], | |
| repair: [ | |
| "Run apt-get update in the same layer as apt-get install.", | |
| "Use --no-install-recommends for smaller images.", | |
| "Remove apt lists after install to keep the image lean." | |
| ], | |
| fileHints: ["Dockerfile"] | |
| }, | |
| container_network_binding: { | |
| label: "Container binds to localhost", | |
| evidence: ["127.0.0.1", "localhost", "0.0.0.0", "connection refused", "port", "host"], | |
| repair: [ | |
| "Bind the server to 0.0.0.0 inside the container.", | |
| "Keep the exposed/listening port consistent with the run command.", | |
| "Do not bind app servers only to loopback." | |
| ], | |
| fileHints: ["Dockerfile", "app.py", "server.js"] | |
| } | |
| }; | |
| function linesToText(lines) { | |
| return (lines || []).join("\n"); | |
| } | |
| function taskById(id) { | |
| return state.tasks.find((task) => task.id === id); | |
| } | |
| function fillSelect(select, values) { | |
| select.innerHTML = ""; | |
| for (const value of values) { | |
| const option = document.createElement("option"); | |
| option.value = value; | |
| option.textContent = value; | |
| select.appendChild(option); | |
| } | |
| } | |
| function tokenize(text) { | |
| return String(text || "") | |
| .toLowerCase() | |
| .replace(/[^a-z0-9_./:-]+/g, " ") | |
| .split(/\s+/) | |
| .filter((token) => token.length > 2 && !["the", "and", "for", "with", "from", "this", "that"].includes(token)); | |
| } | |
| function taskSearchText(task) { | |
| return [ | |
| task.id, | |
| task.title, | |
| task.summary, | |
| task.failure_type, | |
| task.ecosystem, | |
| task.difficulty, | |
| ...(task.tags || []), | |
| linesToText(task.failure_log), | |
| ...(task.files || []).map((file) => `${file.path}\n${linesToText(file.content)}`), | |
| linesToText(task.oracle_patch) | |
| ].join("\n"); | |
| } | |
| function scoreProfile(logText, failureType) { | |
| const profile = FAILURE_PROFILES[failureType]; | |
| if (!profile) return 0; | |
| const lower = logText.toLowerCase(); | |
| let score = 0; | |
| for (const phrase of profile.evidence) { | |
| if (lower.includes(phrase)) score += phrase.length > 8 ? 2 : 1; | |
| } | |
| const spaced = failureType.replaceAll("_", " "); | |
| if (lower.includes(spaced)) score += 3; | |
| return score; | |
| } | |
| function lexicalRankTasks(logText) { | |
| const queryTokens = Array.from(new Set(tokenize(logText))); | |
| const lower = logText.toLowerCase(); | |
| return state.tasks.map((task) => { | |
| const title = task.title.toLowerCase(); | |
| const summary = task.summary.toLowerCase(); | |
| const failureLog = linesToText(task.failure_log).toLowerCase(); | |
| const tags = (task.tags || []).join(" ").toLowerCase(); | |
| const files = (task.files || []).map((file) => `${file.path}\n${linesToText(file.content)}`).join("\n").toLowerCase(); | |
| let score = scoreProfile(logText, task.failure_type) * 6; | |
| for (const token of queryTokens) { | |
| if (title.includes(token)) score += 4; | |
| if (summary.includes(token)) score += 2.5; | |
| if (failureLog.includes(token)) score += 2; | |
| if (tags.includes(token)) score += 2; | |
| if (files.includes(token)) score += 0.8; | |
| } | |
| if (lower.includes(task.ecosystem)) score += 3; | |
| return { task, score, lexical: score, semantic: 0 }; | |
| }).sort((a, b) => b.score - a.score); | |
| } | |
| function topProfile(logText, ranked) { | |
| const scores = Object.keys(FAILURE_PROFILES).map((failureType) => ({ | |
| failureType, | |
| score: scoreProfile(logText, failureType) | |
| })).sort((a, b) => b.score - a.score); | |
| if (scores[0] && scores[0].score > 0) return scores[0].failureType; | |
| return ranked[0] ? ranked[0].task.failure_type : "missing_build_context_file"; | |
| } | |
| function detectEcosystem(logText, ranked) { | |
| const lower = logText.toLowerCase(); | |
| const signals = [ | |
| ["python", ["python", "pip", "poetry", "flask", "django", "pyproject", "requirements.txt"]], | |
| ["node", ["node", "npm", "pnpm", "yarn", "package.json", "node_modules"]], | |
| ["go", ["golang", " go ", "go.mod", "go build"]], | |
| ["rust", ["rust", "cargo", "openssl", "libssl"]], | |
| ["compose", ["compose", "docker-compose", "depends_on", "postgres", "mysql"]], | |
| ["frontend", ["vite", "next", "react", "dist"]], | |
| ["nginx", ["nginx", "default.conf"]] | |
| ]; | |
| for (const [name, terms] of signals) { | |
| if (terms.some((term) => lower.includes(term))) return name; | |
| } | |
| return ranked[0] ? ranked[0].task.ecosystem : "-"; | |
| } | |
| function detectFile(logText, failureType, ranked) { | |
| const lower = logText.toLowerCase(); | |
| const fileMatches = Array.from(lower.matchAll(/(?:^|\s)([./\w-]+(?:dockerfile|compose\.ya?ml|docker-compose\.ya?ml|package-lock\.json|package\.json|requirements\.txt|pyproject\.toml|\.dockerignore|app\.py|server\.js))/gmi)) | |
| .map((match) => match[1].replace(/^\.?\//, "")); | |
| if (fileMatches[0]) return fileMatches[0]; | |
| const profile = FAILURE_PROFILES[failureType]; | |
| if (profile && profile.fileHints[0]) return profile.fileHints[0]; | |
| return ranked[0] && ranked[0].task.files[0] ? ranked[0].task.files[0].path : "-"; | |
| } | |
| function confidenceFromRanks(ranked, failureType, logText) { | |
| const top = ranked[0] ? ranked[0].score : 0; | |
| const second = ranked[1] ? ranked[1].score : 0; | |
| const profileBoost = scoreProfile(logText, failureType) * 5; | |
| const gap = Math.max(0, top - second); | |
| const raw = 34 + Math.min(36, top * 1.4) + Math.min(18, gap * 2.2) + Math.min(12, profileBoost); | |
| return Math.max(0, Math.min(96, Math.round(raw))); | |
| } | |
| async function rankTasks(logText) { | |
| const lexical = lexicalRankTasks(logText); | |
| if (!state.semantic.ready || !state.semantic.embeddings.length) return lexical; | |
| const [queryEmbedding] = await embedTexts([logText]); | |
| return lexical.map((item, index) => { | |
| const semantic = cosine(queryEmbedding, state.semantic.embeddings[state.tasks.indexOf(item.task)] || []); | |
| const score = item.lexical + semantic * 34; | |
| return { ...item, score, semantic, rankBeforeSemantic: index + 1 }; | |
| }).sort((a, b) => b.score - a.score); | |
| } | |
| function cosine(a, b) { | |
| if (!a.length || !b.length) return 0; | |
| let dot = 0; | |
| let normA = 0; | |
| let normB = 0; | |
| for (let i = 0; i < Math.min(a.length, b.length); i += 1) { | |
| dot += a[i] * b[i]; | |
| normA += a[i] * a[i]; | |
| normB += b[i] * b[i]; | |
| } | |
| return normA && normB ? dot / (Math.sqrt(normA) * Math.sqrt(normB)) : 0; | |
| } | |
| function tensorToRows(tensor, expectedRows) { | |
| if (tensor && typeof tensor.tolist === "function") { | |
| const listed = tensor.tolist(); | |
| if (Array.isArray(listed[0])) return listed; | |
| return [listed]; | |
| } | |
| const data = Array.from(tensor && tensor.data ? tensor.data : []); | |
| if (expectedRows <= 1) return [data]; | |
| const dims = tensor && (tensor.dims || tensor.shape) ? (tensor.dims || tensor.shape) : []; | |
| const width = dims.length >= 2 ? dims[dims.length - 1] : Math.floor(data.length / expectedRows); | |
| const rows = []; | |
| for (let row = 0; row < expectedRows; row += 1) { | |
| rows.push(data.slice(row * width, (row + 1) * width)); | |
| } | |
| return rows; | |
| } | |
| async function embedTexts(texts) { | |
| const rows = []; | |
| const chunkSize = 12; | |
| for (let i = 0; i < texts.length; i += chunkSize) { | |
| const chunk = texts.slice(i, i + chunkSize); | |
| const output = await state.semantic.extractor(chunk, { pooling: "mean", normalize: true }); | |
| rows.push(...tensorToRows(output, chunk.length)); | |
| } | |
| return rows; | |
| } | |
| async function loadSemanticRanker() { | |
| if (state.semantic.ready || state.semantic.loading) return; | |
| state.semantic.loading = true; | |
| byId("semanticButton").disabled = true; | |
| byId("triageStatus").textContent = "Loading browser AI ranker. First load downloads and caches the model."; | |
| try { | |
| const { pipeline, env } = await import("https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1"); | |
| env.allowLocalModels = false; | |
| state.semantic.extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2", { dtype: "q8" }); | |
| state.semantic.embeddings = await embedTexts(state.tasks.map(taskSearchText)); | |
| state.semantic.ready = true; | |
| byId("triageMode").textContent = "AI semantic ranker"; | |
| byId("semanticButton").textContent = "AI Ranker Ready"; | |
| byId("triageStatus").textContent = "AI ranker loaded in-browser. No server token used."; | |
| await analyzeLog(); | |
| } catch (error) { | |
| byId("triageStatus").textContent = `AI ranker unavailable here, rules ranker is still active: ${error.message}`; | |
| byId("semanticButton").disabled = false; | |
| } finally { | |
| state.semantic.loading = false; | |
| } | |
| } | |
| function loadSampleLog() { | |
| const task = state.tasks[state.sampleIndex % state.tasks.length]; | |
| state.sampleIndex += 1; | |
| if (!task) return; | |
| selectTask(task.id); | |
| byId("triageInput").value = linesToText(task.failure_log); | |
| analyzeLog(); | |
| } | |
| async function analyzeLog() { | |
| const input = byId("triageInput").value.trim(); | |
| const logText = input || linesToText((state.tasks[0] || {}).failure_log || []); | |
| if (!input) byId("triageInput").value = logText; | |
| const ranked = await rankTasks(logText); | |
| const failureType = topProfile(logText, ranked); | |
| const profile = FAILURE_PROFILES[failureType] || FAILURE_PROFILES.missing_build_context_file; | |
| const ecosystem = detectEcosystem(logText, ranked); | |
| const file = detectFile(logText, failureType, ranked); | |
| const confidence = confidenceFromRanks(ranked, failureType, logText); | |
| renderDiagnosis(profile, failureType, ecosystem, file, confidence, ranked); | |
| } | |
| function renderDiagnosis(profile, failureType, ecosystem, file, confidence, ranked) { | |
| byId("detectedFailure").textContent = profile.label; | |
| byId("detectedEcosystem").textContent = ecosystem; | |
| byId("detectedFile").textContent = file; | |
| byId("triageDiagnosis").innerHTML = ` | |
| <span class="eyebrow">${escapeHtml(state.semantic.ready ? "AI + rules diagnosis" : "Rules diagnosis")}</span> | |
| <h3>${escapeHtml(profile.label)}</h3> | |
| <p>${escapeHtml(profile.repair[0])}</p> | |
| <div class="confidence"> | |
| <div class="confidence-bar"><div class="confidence-fill" style="width: ${confidence}%"></div></div> | |
| <strong>${confidence}%</strong> | |
| </div>`; | |
| byId("repairPlan").innerHTML = profile.repair.map((item, index) => ` | |
| <div class="repair-item"><strong>${index + 1}.</strong> ${escapeHtml(item)}</div> | |
| `).join(""); | |
| byId("nearestTasks").innerHTML = ranked.slice(0, 5).map((item) => { | |
| const task = item.task; | |
| const score = Math.max(0, Math.round(item.score)); | |
| return ` | |
| <div class="rank-card" data-task-id="${escapeHtml(task.id)}"> | |
| <div> | |
| <span class="eyebrow">${escapeHtml(task.id)} | ${escapeHtml(task.ecosystem)} | ${escapeHtml(task.failure_type)}</span> | |
| <h4>${escapeHtml(task.title)}</h4> | |
| <p>${escapeHtml(task.summary)}</p> | |
| </div> | |
| <div class="rank-score">${score}</div> | |
| </div> | |
| `; | |
| }).join(""); | |
| for (const card of document.querySelectorAll("[data-task-id]")) { | |
| card.addEventListener("click", () => { | |
| selectTask(card.dataset.taskId); | |
| showTab("browse"); | |
| }); | |
| } | |
| byId("triageTaskCount").textContent = `${state.tasks.length} tasks indexed`; | |
| byId("triageStatus").textContent = state.semantic.ready | |
| ? "Ranked with in-browser embeddings plus failure rules." | |
| : "Ranked with local failure rules. Load AI Ranker for semantic matching."; | |
| } | |
| function initFilters() { | |
| fillSelect(byId("splitFilter"), ["all", ...unique(state.tasks.map((task) => task.split))]); | |
| fillSelect(byId("ecosystemFilter"), ["all", ...unique(state.tasks.map((task) => task.ecosystem))]); | |
| fillSelect(byId("difficultyFilter"), ["all", "easy", "medium", "hard"]); | |
| fillSelect(byId("scoreTaskSelect"), state.tasks.map((task) => `${task.id} | ${task.title}`)); | |
| } | |
| function updateMetrics() { | |
| byId("metricTasks").textContent = state.tasks.length; | |
| byId("metricFailures").textContent = unique(state.tasks.map((task) => task.failure_type)).length; | |
| byId("metricEcosystems").textContent = unique(state.tasks.map((task) => task.ecosystem)).length; | |
| byId("metricChallenge").textContent = state.tasks.filter((task) => task.split === "challenge").length; | |
| byId("oraclePassed").textContent = state.tasks.length; | |
| byId("oracleTotal").textContent = state.tasks.length; | |
| byId("noopTotal").textContent = state.tasks.length; | |
| byId("publicTotal").textContent = state.tasks.length; | |
| } | |
| function applyFilters() { | |
| const split = byId("splitFilter").value; | |
| const ecosystem = byId("ecosystemFilter").value; | |
| const difficulty = byId("difficultyFilter").value; | |
| state.filtered = state.tasks.filter((task) => { | |
| return (split === "all" || task.split === split) && | |
| (ecosystem === "all" || task.ecosystem === ecosystem) && | |
| (difficulty === "all" || task.difficulty === difficulty); | |
| }); | |
| byId("taskCount").textContent = `${state.filtered.length} shown`; | |
| renderTaskList(); | |
| if (!state.filtered.some((task) => task.id === state.selectedId)) { | |
| selectTask(state.filtered[0] ? state.filtered[0].id : null); | |
| } | |
| } | |
| function renderTaskList() { | |
| const list = byId("taskList"); | |
| list.innerHTML = ""; | |
| for (const task of state.filtered) { | |
| const row = document.createElement("button"); | |
| row.className = `task-row ${task.id === state.selectedId ? "active" : ""}`; | |
| row.innerHTML = `<strong>${task.id} | ${escapeHtml(task.title)}</strong><span>${task.split} | ${task.ecosystem} | ${task.difficulty} | ${task.failure_type}</span>`; | |
| row.addEventListener("click", () => selectTask(task.id)); | |
| list.appendChild(row); | |
| } | |
| } | |
| function selectTask(taskId) { | |
| state.selectedId = taskId; | |
| renderTaskList(); | |
| const task = taskById(taskId); | |
| if (!task) { | |
| byId("taskHeading").textContent = "No task"; | |
| return; | |
| } | |
| byId("taskHeading").textContent = `${task.id}`; | |
| byId("taskTitle").textContent = task.title; | |
| byId("taskSummary").textContent = task.summary; | |
| byId("failureLog").textContent = linesToText(task.failure_log); | |
| byId("oraclePatch").textContent = linesToText(task.oracle_patch); | |
| byId("taskChips").innerHTML = [ | |
| task.split, | |
| task.ecosystem, | |
| task.difficulty, | |
| task.failure_type, | |
| ...task.tags | |
| ].map((tag) => `<span class="chip">${escapeHtml(tag)}</span>`).join(""); | |
| const fileSelect = byId("fileSelect"); | |
| fillSelect(fileSelect, task.files.map((file) => file.path)); | |
| fileSelect.onchange = () => showFile(task.id, fileSelect.value); | |
| showFile(task.id, fileSelect.value); | |
| } | |
| function showFile(taskId, path) { | |
| const task = taskById(taskId); | |
| const file = task.files.find((item) => item.path === path); | |
| byId("fileContent").textContent = file ? linesToText(file.content) : ""; | |
| } | |
| function scoreSelectedPatch() { | |
| const selectValue = byId("scoreTaskSelect").value; | |
| const taskId = selectValue.split(" | ")[0]; | |
| const task = taskById(taskId); | |
| const patch = byId("patchInput").value; | |
| const result = scorePatch(task, patch); | |
| const output = byId("scoreOutput"); | |
| output.className = `score ${result.passed ? "pass" : "fail"}`; | |
| 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>`; | |
| } | |
| function scorePatch(task, patchText) { | |
| const messages = []; | |
| let passedChecks = 0; | |
| let totalChecks = 0; | |
| const applyResult = applyPatch(task, patchText); | |
| totalChecks += 1; | |
| if (applyResult.ok) { | |
| passedChecks += 1; | |
| messages.push("PASS patch applies cleanly"); | |
| } else { | |
| messages.push(`FAIL patch does not apply cleanly: ${applyResult.error}`); | |
| } | |
| const touched = touchedFiles(patchText); | |
| const scoring = task.scoring || {}; | |
| for (const path of scoring.must_touch || []) { | |
| totalChecks += 1; | |
| if (touched.has(path)) { | |
| passedChecks += 1; | |
| messages.push(`PASS must touch ${path}`); | |
| } else { | |
| messages.push(`FAIL missing touched file ${path}`); | |
| } | |
| } | |
| for (const rule of scoring.must_contain || []) { | |
| totalChecks += 1; | |
| const finalText = applyResult.files[rule.path] || ""; | |
| if (finalText.includes(rule.text)) { | |
| passedChecks += 1; | |
| messages.push(`PASS final ${rule.path} contains ${JSON.stringify(rule.text)}`); | |
| } else { | |
| messages.push(`FAIL final ${rule.path} missing ${JSON.stringify(rule.text)}`); | |
| } | |
| } | |
| for (const rule of scoring.must_not_contain || []) { | |
| totalChecks += 1; | |
| const finalText = applyResult.files[rule.path] || ""; | |
| if (!finalText.includes(rule.text)) { | |
| passedChecks += 1; | |
| messages.push(`PASS final ${rule.path} does not contain ${JSON.stringify(rule.text)}`); | |
| } else { | |
| messages.push(`FAIL final ${rule.path} still contains ${JSON.stringify(rule.text)}`); | |
| } | |
| } | |
| return { | |
| applied: applyResult.ok, | |
| passed: totalChecks > 0 && passedChecks === totalChecks, | |
| passedChecks, | |
| totalChecks, | |
| score: totalChecks > 0 ? passedChecks / totalChecks : 0, | |
| messages | |
| }; | |
| } | |
| function touchedFiles(patchText) { | |
| const touched = new Set(); | |
| for (const line of patchText.split(/\r?\n/)) { | |
| if (line.startsWith("+++ b/")) touched.add(line.slice(6)); | |
| if (line.startsWith("--- a/")) touched.add(line.slice(6)); | |
| } | |
| touched.delete("/dev/null"); | |
| return touched; | |
| } | |
| function applyPatch(task, patchText) { | |
| const files = {}; | |
| for (const file of task.files) files[file.path] = [...file.content]; | |
| const lines = patchText.split(/\r?\n/); | |
| let i = 0; | |
| try { | |
| while (i < lines.length) { | |
| if (!lines[i].startsWith("--- a/")) { | |
| i += 1; | |
| continue; | |
| } | |
| const oldPath = lines[i].slice(6); | |
| i += 1; | |
| if (i >= lines.length || !lines[i].startsWith("+++ b/")) { | |
| throw new Error(`missing +++ line after ${oldPath}`); | |
| } | |
| const newPath = lines[i].slice(6); | |
| const path = newPath === "/dev/null" ? oldPath : newPath; | |
| const original = files[path] ? [...files[path]] : []; | |
| const output = []; | |
| let oldIndex = 0; | |
| i += 1; | |
| while (i < lines.length && !lines[i].startsWith("--- a/")) { | |
| if (!lines[i].startsWith("@@")) { | |
| i += 1; | |
| continue; | |
| } | |
| const match = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(lines[i]); | |
| if (!match) throw new Error(`bad hunk header: ${lines[i]}`); | |
| const start = Number(match[1]) - 1; | |
| while (oldIndex < start) { | |
| output.push(original[oldIndex] || ""); | |
| oldIndex += 1; | |
| } | |
| i += 1; | |
| while (i < lines.length && !lines[i].startsWith("@@") && !lines[i].startsWith("--- a/")) { | |
| const line = lines[i]; | |
| if (line === "\\ No newline at end of file") { | |
| i += 1; | |
| continue; | |
| } | |
| const marker = line[0]; | |
| const text = line.slice(1); | |
| if (marker === " ") { | |
| if ((original[oldIndex] || "") !== text) throw new Error(`context mismatch in ${path}`); | |
| output.push(text); | |
| oldIndex += 1; | |
| } else if (marker === "-") { | |
| if ((original[oldIndex] || "") !== text) throw new Error(`remove mismatch in ${path}`); | |
| oldIndex += 1; | |
| } else if (marker === "+") { | |
| output.push(text); | |
| } else if (line.length === 0) { | |
| i += 1; | |
| continue; | |
| } else { | |
| throw new Error(`bad patch line: ${line}`); | |
| } | |
| i += 1; | |
| } | |
| } | |
| while (oldIndex < original.length) { | |
| output.push(original[oldIndex] || ""); | |
| oldIndex += 1; | |
| } | |
| files[path] = output; | |
| } | |
| const finalFiles = {}; | |
| for (const [path, content] of Object.entries(files)) { | |
| finalFiles[path] = content.join("\n") + "\n"; | |
| } | |
| return { ok: true, files: finalFiles, error: null }; | |
| } catch (error) { | |
| const finalFiles = {}; | |
| for (const [path, content] of Object.entries(files)) { | |
| finalFiles[path] = content.join("\n") + "\n"; | |
| } | |
| return { ok: false, files: finalFiles, error: error.message }; | |
| } | |
| } | |
| function loadOraclePatch() { | |
| const taskId = byId("scoreTaskSelect").value.split(" | ")[0]; | |
| const task = taskById(taskId); | |
| byId("patchInput").value = linesToText(task.oracle_patch); | |
| scoreSelectedPatch(); | |
| } | |
| function escapeHtml(value) { | |
| return String(value) | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """) | |
| .replaceAll("'", "'"); | |
| } | |
| function showTab(tabId) { | |
| for (const other of document.querySelectorAll("[data-tab]")) { | |
| other.classList.toggle("active", other.dataset.tab === tabId); | |
| } | |
| for (const tab of document.querySelectorAll(".tab")) { | |
| tab.classList.toggle("hidden", tab.id !== tabId); | |
| } | |
| } | |
| function initTabs() { | |
| for (const button of document.querySelectorAll("[data-tab]")) { | |
| button.addEventListener("click", () => showTab(button.dataset.tab)); | |
| } | |
| } | |
| async function main() { | |
| initTabs(); | |
| const response = await fetch("data/sabnock_docker_v1.json"); | |
| state.tasks = await response.json(); | |
| initFilters(); | |
| updateMetrics(); | |
| byId("splitFilter").addEventListener("change", applyFilters); | |
| byId("ecosystemFilter").addEventListener("change", applyFilters); | |
| byId("difficultyFilter").addEventListener("change", applyFilters); | |
| byId("scoreButton").addEventListener("click", scoreSelectedPatch); | |
| byId("loadOracleButton").addEventListener("click", loadOraclePatch); | |
| byId("triageButton").addEventListener("click", analyzeLog); | |
| byId("sampleLogButton").addEventListener("click", loadSampleLog); | |
| byId("semanticButton").addEventListener("click", loadSemanticRanker); | |
| let triageTimer = null; | |
| byId("triageInput").addEventListener("input", () => { | |
| window.clearTimeout(triageTimer); | |
| triageTimer = window.setTimeout(analyzeLog, 350); | |
| }); | |
| applyFilters(); | |
| loadSampleLog(); | |
| } | |
| main().catch((error) => { | |
| document.body.innerHTML = `<main class="shell"><section class="hero"><h1>Failed to load</h1><p>${escapeHtml(error.message)}</p></section></main>`; | |
| }); | |
| </script> | |
| </body> | |
| </html> | |