File size: 23,032 Bytes
c0e0aa1
 
 
 
 
 
 
 
 
 
 
 
f3c3f17
1dd51b8
c0e0aa1
 
7eebc8f
f3c3f17
34edf9e
 
 
 
 
f3c3f17
 
 
 
 
 
 
34edf9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef93ba8
34edf9e
ef93ba8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7eebc8f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34edf9e
 
 
 
7eebc8f
34edf9e
c0e0aa1
 
 
 
34edf9e
c0e0aa1
 
 
 
 
 
 
 
 
 
 
34edf9e
c0e0aa1
 
f3c3f17
 
 
 
34edf9e
c0e0aa1
34edf9e
c0e0aa1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34edf9e
 
c0e0aa1
 
 
f3c3f17
c0e0aa1
 
 
 
 
 
 
34edf9e
 
 
 
 
c0e0aa1
34edf9e
c0e0aa1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34edf9e
c0e0aa1
 
 
 
 
 
 
 
 
34edf9e
c0e0aa1
 
 
 
 
34edf9e
 
f3c3f17
34edf9e
 
 
 
 
 
f3c3f17
 
 
34edf9e
 
 
 
f3c3f17
 
 
 
 
 
34edf9e
 
 
 
 
 
 
 
f3c3f17
 
 
 
 
 
 
34edf9e
 
 
 
 
 
 
 
 
f3c3f17
 
 
34edf9e
 
 
 
 
 
 
 
 
 
f3c3f17
34edf9e
 
 
 
 
 
 
 
 
 
1dd51b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34edf9e
1dd51b8
 
 
 
34edf9e
1dd51b8
 
 
 
 
 
 
 
 
 
34edf9e
 
 
 
 
 
c0e0aa1
34edf9e
 
f3c3f17
34edf9e
 
 
f3c3f17
34edf9e
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
"use strict";
const $ = (id) => document.getElementById(id);
const packs = {
  support: {path: "support", defaultControl: "new-key-on-retry", defaultCase: "retry-after-commit"},
  coding: {path: "coding", defaultControl: "boolean-equals-one", defaultCase: "cas-type-sensitivity"},
};
const stories = {
  "new-key-on-retry": "The service committed the first note, then returned a retryable error. A new idempotency key on retry created a second note. Move through the trace to see both state changes.",
  "boolean-equals-one": "The implementation treats JSON true and 1 as equal. Compare-and-swap accepts a mismatch: a 92.5% aggregate score still leaves a broken contract.",
  reference: "The known-good control satisfies every recorded check. Compare its behavior with a deliberately faulty implementation to audit the grader.",
};
let audits = {}, pack = "support", evaluation, selectedCase;
const auditDigests = {};
let comparison, baseline, current;
const pretty = (data) => JSON.stringify(data, null, 2);
const percent = (value) => value === null ? "Unassessed" : `${Number((value * 100).toFixed(4))}%`;
const repetitions = {};
async function fetchRecord(path, withIdentity = false) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 15000);
  try {
    const response = await fetch(path, {signal: controller.signal});
    if (!response.ok) throw new Error(`Evidence request failed (${response.status})`);
    if (!withIdentity) return await response.json();
    const bytes = await response.arrayBuffer();
    const record = JSON.parse(new TextDecoder("utf-8", {fatal: true}).decode(bytes));
    const digest = globalThis.crypto?.subtle
      ? await crypto.subtle.digest("SHA-256", bytes) : null;
    return {record, sha256: digest
      ? Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, "0")).join("") : null};
  } finally {
    clearTimeout(timer);
  }
}
async function loadSection(name, load, retry = false) {
  const status = $(name === "audit" ? "load-status" : name + "-status");
  const workspace = $(name === "audit" ? "workspace" : name + "-workspace");
  const button = $(name + "-retry"), recovery = $(name + "-recovery");
  button.disabled = true;
  status.hidden = false;
  status.textContent = "Loading recorded evidence…";
  workspace.setAttribute("aria-busy", "true");
  try {
    const partial = await load();
    workspace.hidden = false;
    status.hidden = !partial;
    status.textContent = partial || "";
    recovery.hidden = !partial;
    const restored = name === "audit" && restoreView();
    if (retry && !restored) workspace.focus();
  } catch {
    status.textContent = "This evidence could not be loaded. Retry this section or open its standalone report; other sections remain available.";
    recovery.hidden = false;
  } finally {
    workspace.setAttribute("aria-busy", "false");
    button.disabled = false;
  }
}
async function loadSuite() {
  const suite = await fetchRecord("suite/suite.json");
  const jobs = {};
  for (const name of ["partial", "protected"]) {
    const job = suite.jobs.find(row => row.id === "support-" + name);
    jobs[name] = job;
    badge($("suite-" + name + "-verdict"), job.decision.accepted, job.decision.accepted ? "GATE ACCEPTED" : "GATE REJECTED");
    $("suite-" + name + "-score").textContent = percent(job.observed.mean_score);
    $("suite-" + name + "-resolution").textContent = `${job.observed.resolved_attempts} / ${job.observed.completed_attempts} attempts fully resolved`;
    $("suite-" + name + "-gate").textContent =
      `Minimum mean score: ${percent(job.gate.min_mean_score)}\nMinimum resolution rate: ${percent(job.gate.min_resolution_rate)}\nRequired dimensions: ${job.gate.required_dimensions.join(", ") || "none"}`;
  }
  $("suite-summary").textContent = `${suite.accepted_jobs} / ${suite.total_jobs} gates accepted. ${suite.fully_resolved_jobs} / ${suite.total_jobs} jobs fully resolved. ${suite.invalid_jobs} invalid jobs. The coding reference passes its default strict gate.`;
  $("suite-provenance").textContent = pretty({
    manifest_sha256: suite.manifest_sha256,
    same_candidate: jobs.partial.candidate_sha256 === jobs.protected.candidate_sha256,
    candidate_sha256: jobs.partial.candidate_sha256,
    grader_sha256: jobs.partial.grader_sha256,
    cases_sha256: jobs.partial.cases_sha256,
    runtime: jobs.partial.runtime,
    permissive_gate: jobs.partial.decision,
    protected_gate: jobs.protected.decision,
    interpretation: suite.interpretation,
  });
}
function chooseRepetition(name) {
  const record = repetitions[name];
  if (!record) return;
  for (const control of ["reference", "faulty"]) $("repeat-" + control).setAttribute("aria-pressed", String(control === name));
  $("repeat-resolved").textContent = `${record.resolved_attempts} / ${record.requested_attempts}`;
  $("repeat-score").textContent = percent(record.mean_score);
  $("repeat-invalid").textContent = String(record.invalid_attempts);
  $("repeat-variable").textContent = String(record.variable_checks);
  $("repeat-attempts").replaceChildren();
  for (const attempt of record.attempts) {
    const card = document.createElement("a");
    card.className = `repeat-attempt ${attempt.resolved ? "pass" : "fail"}`;
    card.href = `repeat/${name}/attempts/${String(attempt.attempt).padStart(4, "0")}/index.html`;
    const label = document.createElement("span"); label.textContent = `ATTEMPT ${String(attempt.attempt).padStart(2, "0")}`;
    const score = document.createElement("strong"); score.textContent = percent(attempt.score);
    const verdict = document.createElement("span"); verdict.className = "badge";
    verdict.textContent = !attempt.valid ? "INVALID RUN" : attempt.resolved ? "RESOLVED" : "NOT RESOLVED";
    const link = document.createElement("span"); link.className = "caption"; link.textContent = "Open full evidence \u2197";
    card.append(label, score, verdict, link); $("repeat-attempts").append(card);
  }
  $("repeat-cases").replaceChildren();
  for (const row of record.cases) {
    const details = document.createElement("details");
    const summary = document.createElement("summary");
    const title = document.createElement("span"); title.textContent = `${row.case_id} / seed ${row.seed}`;
    const outcome = document.createElement("span"); outcome.className = row.failed ? "fail" : "pass";
    outcome.textContent = `${row.passed} / ${row.assessed} passed`;
    summary.append(title, outcome); details.append(summary);
    const checks = document.createElement("div"); checks.className = "check-list";
    for (const [check, count] of Object.entries(row.checks)) {
      const item = document.createElement("span"); item.className = count.passed < count.assessed ? "fail" : "pass";
      item.textContent = `${check}: ${count.passed} / ${count.assessed}`; checks.append(item);
    }
    details.append(checks);
    const invalid = document.createElement("p"); invalid.className = "caption";
    invalid.textContent = `${row.unassessed} unassessed observations. Check counts use assessed observations only.`;
    details.append(invalid); $("repeat-cases").append(details);
  }
  $("repeat-context").textContent = name === "faulty"
    ? "A stable score can describe a stable defect. All three attempts fail the retry-after-commit note check; none is selected or discarded."
    : "All three reference attempts resolve the same four public cases. This is a functional control; repeated passes do not expand task coverage.";
  for (const [id, file] of [["report", "index.html"], ["json", "repetition.json"], ["events", "events.jsonl"]]) $("repeat-" + id).href = `repeat/${name}/${file}`;
  $("repeat-provenance").textContent = pretty({
    candidate_sha256: record.candidate_sha256, grader_sha256: record.grader_sha256,
    cases_sha256: record.cases_sha256, task: record.task, seeds: record.seeds,
    runtime: record.runtime, completed_attempts: record.completed_attempts,
    assessed_attempts: record.assessed_attempts, invalid_attempts: record.invalid_attempts,
    interpretation: record.interpretation,
  });
}
for (const name of ["reference", "faulty"]) $("repeat-" + name).addEventListener("click", () => chooseRepetition(name));
async function loadRepetitions() {
  await Promise.all(["reference", "faulty"].map(async (name) => {
    repetitions[name] = await fetchRecord(`repeat/${name}/repetition.json`);
  }));
  chooseRepetition("faulty");
}
function badge(element, passed, text) {
  element.className = `badge ${passed ? "pass" : "fail"}`;
  element.textContent = text;
}
function choosePack(nextPack, view) {
  pack = nextPack;
  for (const name of Object.keys(packs)) $(name + "-task").setAttribute("aria-pressed", String(pack === name));
  const select = $("control");
  select.replaceChildren();
  for (const row of [{name: "reference"}, ...audits[pack].mutants]) {
    const option = document.createElement("option");
    option.value = row.name;
    option.textContent = row.name === "reference" ? "Reference / known-good control" : row.name;
    select.append(option);
  }
  select.value = packs[pack].defaultControl;
  if (view?.control && [...select.options].some(option => option.value === view.control)) select.value = view.control;
  $("report-link").href = `${packs[pack].path}/index.html`;
  $("download-link").href = `${packs[pack].path}/audit.json`;
  $("share-case").disabled = !auditDigests[pack];
  $("audit-identity").textContent = auditDigests[pack]
    ? `Loaded audit SHA-256: ${auditDigests[pack].slice(0, 16)}… Full fingerprint in the reproduction record.`
    : "Audit fingerprint unavailable. Open this page over HTTPS to share an evidence-bound link; the report and downloads still work.";
  chooseControl(view);
}
function chooseControl(view) {
  const name = $("control").value;
  const audit = audits[pack];
  const mutant = audit.mutants.find((row) => row.name === name);
  evaluation = name === "reference" ? audit.reference : mutant.evaluation;
  $("score").textContent = percent(evaluation.score);
  badge($("verdict"), evaluation.resolved, evaluation.valid === false ? "INVALID RUN" : evaluation.resolved ? "FULLY RESOLVED" : "NOT RESOLVED");
  $("result-context").textContent = evaluation.valid === false ? "An environment failure is not a scored candidate failure." : "Full resolution requires every case to pass. A partial score is not acceptance.";
  $("checks-count").textContent = `${evaluation.cases.filter((row) => row.passed).length} / ${evaluation.cases.length}`;
  $("story").textContent = stories[name] || `Declared negative control: ${name}. Its intended detection dimension is ${mutant.target_dimension}. Select a failing case to inspect the recorded evidence.`;
  $("dimensions").replaceChildren();
  for (const [key, value] of Object.entries(evaluation.dimensions)) {
    const div = document.createElement("div"); div.className = "dimension";
    const label = document.createElement("div");
    const title = document.createElement("span"); title.textContent = key;
    const score = document.createElement("span"); score.textContent = percent(value.score);
    label.append(title, score);
    const meter = document.createElement("meter"); meter.min = 0; meter.max = 1; meter.value = value.score ?? 0; meter.setAttribute("aria-label", `${key}: ${score.textContent}`);
    div.append(label, meter); $("dimensions").append(div);
  }
  $("cases").replaceChildren();
  evaluation.cases.forEach((row, index) => {
    const button = document.createElement("button"); button.type = "button";
    const status = document.createElement("span"); status.className = row.passed ? "pass" : "fail"; status.textContent = row.passed ? "PASS" : row.status.toUpperCase();
    button.append(status, document.createTextNode(row.case_id));
    button.setAttribute("aria-controls", "case-detail");
    button.addEventListener("click", () => { chooseCase(index); rememberView(); $("case-title").focus(); });
    $("cases").append(button);
  });
  $("provenance").textContent = pretty({
    audit_sha256: auditDigests[pack],
    task: evaluation.task, created_at: evaluation.created_at, seeds: evaluation.seeds,
    evalarc_version: evaluation.evalarc_version, candidate_sha256: evaluation.candidate_sha256,
    grader_sha256: evaluation.grader_sha256, cases_sha256: evaluation.cases_sha256,
    runtime: evaluation.runtime, agent_cost_usd: evaluation.agent_cost_usd, agent_tokens: evaluation.agent_tokens,
  });
  let index = evaluation.cases.findIndex((row) => !row.passed);
  if (index < 0) index = evaluation.cases.findIndex((row) => row.case_id === packs[pack].defaultCase);
  if (view) {
    const requested = evaluation.cases.findIndex(row => row.case_id === view.case && String(row.seed) === view.seed);
    if (requested >= 0) index = requested;
  }
  chooseCase(Math.max(index, 0), view?.step);
}
function chooseCase(index, step = 0) {
  selectedCase = evaluation.cases[index];
  [...$("cases").children].forEach((button, i) => button.setAttribute("aria-pressed", String(i === index)));
  $("case-title").textContent = `${selectedCase.case_id} / seed ${selectedCase.seed}`;
  badge($("case-verdict"), selectedCase.passed, selectedCase.status.toUpperCase());
  $("case-checks").replaceChildren();
  for (const [name, passed] of Object.entries(selectedCase.checks)) {
    const item = document.createElement("span"); item.className = passed ? "pass" : "fail"; item.textContent = `${name}: ${passed ? "pass" : "fail"}`; $("case-checks").append(item);
  }
  $("case-error").textContent = selectedCase.error || "";
  const hasTrace = Array.isArray(selectedCase.trace) && selectedCase.trace.length > 0;
  $("trace-panel").hidden = !hasTrace;
  $("coding-panel").hidden = hasTrace;
  if (hasTrace) {
    $("states").textContent = pretty({initial_state: selectedCase.initial_state, final_state: selectedCase.final_state});
    $("trace-step").max = selectedCase.trace.length - 1;
    $("trace-step").value = Math.max(0, Math.min(selectedCase.trace.length - 1, step));
    showStep();
  } else {
    $("coding-evidence").textContent = pretty(selectedCase);
  }
}
function showStep() {
  const index = Number($("trace-step").value);
  const step = selectedCase.trace[index];
  $("step-label").textContent = `${index + 1} / ${selectedCase.trace.length}`;
  $("trace-step").setAttribute("aria-valuetext", `Step ${index + 1} of ${selectedCase.trace.length}`);
  $("trace-action").textContent = pretty(Object.fromEntries(Object.entries(step).filter(([key]) => key !== "changes")));
  $("trace-changes").textContent = step.changes && Object.keys(step.changes).length ? pretty(step.changes) : "No state change in this step.";
  $("previous-step").disabled = index === 0;
  $("next-step").disabled = index === selectedCase.trace.length - 1;
}
function viewFragment() {
  return "#" + new URLSearchParams({
    v: "2", audit: auditDigests[pack], pack, control: $("control").value, case: selectedCase.case_id,
    seed: String(selectedCase.seed), step: $("trace-panel").hidden ? "0" : $("trace-step").value,
  });
}
function rememberView() {
  $("share-url").hidden = true;
  $("share-status").textContent = "";
  if (auditDigests[pack]) {
    try { history.replaceState(null, "", viewFragment()); } catch { /* Restricted embeds can still copy links. */ }
  }
}
function restoreView(focus = true) {
  if (!location.hash.includes("=")) return false;
  const params = new URLSearchParams(location.hash.slice(1));
  const version = params.get("v");
  const keys = new Set(["v", "audit", "pack", "control", "case", "seed", "step"]);
  if (location.hash.length > 2048 || !["1", "2"].includes(version) || !Object.hasOwn(packs, params.get("pack")) ||
      [...params.keys()].some(key => !keys.has(key) || params.getAll(key).length !== 1) ||
      (version === "2" && !/^[a-f0-9]{64}$/.test(params.get("audit") || "")) ||
      (version === "1" && params.has("audit"))) {
    $("share-status").textContent = "This evidence link is not supported. Choose a task, control and case below.";
    return false;
  }
  const requestedPack = params.get("pack");
  if (!audits[requestedPack]) {
    $("share-status").textContent = `The linked ${requestedPack} evidence is unavailable. Retry the missing task pack to open this view.`;
    return false;
  }
  if (version === "2" && params.get("audit") !== auditDigests[requestedPack]) {
    $("share-url").hidden = true;
    $("share-status").textContent = auditDigests[requestedPack]
      ? `Evidence changed: this link expects audit ${params.get("audit").slice(0, 16)}…, but the loaded audit is ${auditDigests[requestedPack].slice(0, 16)}…. The linked view was not restored. Choose a case to review the current recording or open the original saved audit.`
      : "This link's audit identity could not be checked. The linked view was not restored. Open this page over HTTPS or use the original saved audit.";
    return false;
  }
  const step = Number(params.get("step"));
  const view = {control: params.get("control"), case: params.get("case"), seed: params.get("seed"),
    step: Number.isSafeInteger(step) && step >= 0 ? step : 0};
  choosePack(requestedPack, view);
  const exact = $("control").value === view.control && selectedCase.case_id === view.case &&
    String(selectedCase.seed) === view.seed && params.has("step") && String(view.step) === params.get("step") &&
    (selectedCase.trace?.length ? Number($("trace-step").value) === view.step : view.step === 0);
  $("share-url").hidden = true;
  $("share-status").textContent = exact
    ? version === "2"
      ? "Shared evidence restored. The loaded audit bytes match the link's SHA-256; this checks content identity, not authorship."
      : "Legacy link: showing the selected case in the current recording. The original audit identity was not recorded in this link."
    : "Some link values were outside this recording. Showing the nearest available view; check the selected case and step.";
  if (focus) $("case-title").focus();
  return true;
}
$("share-case").addEventListener("click", async () => {
  const url = new URL(location.href);
  url.hash = viewFragment();
  try {
    await navigator.clipboard.writeText(url.href);
    $("share-url").hidden = true;
    $("share-status").textContent = "Evidence link copied with the loaded audit's SHA-256, task, control, seed, case and trace step.";
  } catch {
    $("share-url").hidden = false;
    $("share-url").value = url.href;
    $("share-url").focus();
    $("share-url").select();
    $("share-status").textContent = "Clipboard unavailable here. Select and copy the evidence link.";
  }
});
$("back-to-cases").addEventListener("click", () => $("cases").querySelector('[aria-pressed="true"]')?.focus());
window.addEventListener("hashchange", () => { if (evaluation) restoreView(); });
function chooseTransition(index) {
  const transition = comparison.case_transitions[index];
  [...$("changed-cases").children].forEach((button, i) => button.setAttribute("aria-pressed", String(i === index)));
  $("comparison-result").textContent = transition.regressed_checks.length
    ? `${transition.case_id}: ${transition.regressed_checks.join(", ")} changed from passing to failing. The current policy leaves a duplicate note.`
    : `${transition.case_id}: ${transition.improved_checks.join(", ")} changed from failing to passing. The current policy preserves the open status of this unresolved ticket.`;
  for (const [side, report] of [["before", baseline], ["after", current]]) {
    const row = report.cases.find((item) => item.seed === transition.seed && item.case_id === transition.case_id);
    badge($(side + "-verdict"), row.passed, row.status.toUpperCase());
    $(side + "-checks").replaceChildren();
    for (const [name, passed] of Object.entries(row.checks)) {
      const check = document.createElement("span");
      check.className = passed ? "pass" : "fail";
      check.textContent = `${name}: ${passed ? "pass" : "fail"}`;
      $(side + "-checks").append(check);
    }
    const ticketId = row.trace.find((step) => step.action?.tool === "get_ticket").action.arguments.ticket_id;
    $(side + "-state").textContent = pretty({ticket_id: ticketId, ...row.final_state[ticketId]});
  }
}
async function loadComparison() {
  [comparison, baseline, current] = await Promise.all(["comparison", "baseline", "current"].map(name => fetchRecord(`comparison/${name}.json`)));
  $("before-score").textContent = percent(comparison.baseline.score);
  $("after-score").textContent = percent(comparison.current.score);
  $("regression-count").textContent = `${comparison.regressions.length} REGRESSED CHECK`;
  $("compare-delta").textContent = `+${Number((comparison.score_delta * 100).toFixed(4))} percentage points`;
  $("changed-cases").replaceChildren();
  comparison.case_transitions.forEach((transition, index) => {
    const button = document.createElement("button"); button.type = "button";
    const tag = document.createElement("span"); tag.className = transition.regressed_checks.length ? "fail" : "pass";
    tag.textContent = transition.regressed_checks.length ? "REGRESSED" : "IMPROVED";
    button.append(tag, document.createTextNode(transition.case_id));
    button.addEventListener("click", () => chooseTransition(index));
    $("changed-cases").append(button);
  });
  chooseTransition(0);
}
$("control").addEventListener("change", () => { chooseControl(); rememberView(); });
$("trace-step").addEventListener("input", () => { showStep(); rememberView(); });
$("previous-step").addEventListener("click", () => { $("trace-step").stepDown(); showStep(); rememberView(); });
$("next-step").addEventListener("click", () => { $("trace-step").stepUp(); showStep(); rememberView(); });
for (const name of Object.keys(packs)) $(name + "-task").addEventListener("click", () => {
  if (audits[name]) { choosePack(name); rememberView(); }
});
async function loadAudits() {
  await Promise.allSettled(Object.entries(packs).filter(([name]) => !audits[name]).map(async ([name, config]) => {
    const {record, sha256} = await fetchRecord(`${config.path}/audit.json`, true);
    if (!Array.isArray(record.reference?.cases) || !Array.isArray(record.mutants) ||
        record.mutants.some(row => !Array.isArray(row.evaluation?.cases))) throw new Error("Incomplete audit evidence");
    audits[name] = record;
    auditDigests[name] = sha256;
    $(name + "-task").disabled = false;
  }));
  const available = Object.keys(audits), missing = Object.keys(packs).filter(name => !audits[name]);
  if (!available.length) throw new Error("No task pack available");
  if (!evaluation) choosePack(audits.support ? "support" : available[0]);
  return missing.length ? `Could not load ${missing.join(" and ")} evidence. The other task pack is ready; retry to load the missing pack.` : "";
}
for (const [name, load] of [["suite", loadSuite], ["repeat", loadRepetitions], ["comparison", loadComparison], ["audit", loadAudits]]) {
  $(name + "-retry").addEventListener("click", () => void loadSection(name, load, true));
  void loadSection(name, load);
}