sabnock-docker-arena / index.html
Redhanuman's picture
Add Sabnock Docker triage arena
37ab2f8 verified
Raw
History Blame Contribute Delete
48.1 kB
<!doctype html>
<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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
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>