File size: 24,030 Bytes
2d3d73f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"use strict";
(() => {
  const contract = window.BenchContract;
  const T = {
    en: {
      search:"Search benchmark", language:"Language", show:"Show", all:"All", withResults:"With a published number", clear:"Clear filters",
      benchmark:"Benchmark", results:"Results", notPublished:"Not published yet",
      noPacket:"No results are published yet. The list below is the plan: 50 benchmarks, their sources and metrics. For models whose training data we have, a number appears only after a contamination scan.",
      loadError:"A pinned file could not be verified, so nothing is shown.", rejected:"Results are hidden because the result file did not pass the checks",
      empty:"No benchmark matches the filters.", shown:"Showing", of:"of", benchmarks:"benchmarks",
      pills:{total:"benchmarks", langs:"38 EN · 12 PL", models:"models", published:"with a published number"},
      higher:"higher is better", lower:"lower is better", ci:"95% CI", noCI:"no interval", random:"random", majority:"majority", delta:"difference",
      majorityShare:"predicts its most common class in", none:"none", refMissing:"reference: not available",
      scan:{clean:"clean", flagged:"flagged", partially_scanned:"scan incomplete", unscanned:"scan pending", unknown:"training data unknown"},
      noNumber:{partially_scanned:"No number: the scan did not cover enough items.", unscanned:"No number until the contamination scan is done."},
      hitFree:"Without items found in training data", hitFreeAnswer:"Without hits in question or answer fields", hits:"items with hits", n:"n",
      noHitFree:"no items without hits", tooFew:"too few items for a number",
      mostly:"fewer than 50 items without hits in training data", inSlayer:"in the Slayer training corpus", slayerSubset:"subsets defined by the Slayer training corpus", mostlyVerdict:"No better/worse from the full sample here: only a paired test on the items without answer-field hits can give one.",
      pending:n => `${n} benchmarks are not measured yet; adding them cannot change the verdicts shown; it can settle rows now inconclusive and adds verdicts for the new rows.`,
      constant:"constant answer", constantNote:"One class for all items: not compared between models.",
      states:{ok:"Measured",not_run:"Not run",blocked_access:"Data access blocked",blocked_runtime:"Runtime blocked",blocked_security:"Blocked by security review",blocked_judge:"No accepted judge",unsupported:"Not supported",failed:"Run failed",skipped:"Skipped"},
      verdicts:{better:"better than",worse:"worse than",inconclusive:"inconclusive vs"}, verdictNote:"paired test, Holm-adjusted", verdictNoteSubset:"paired test on items without answer-field hits, Holm-adjusted",
      amendments:{answer_leading_space_strip:"Scoring corrected after first run (leading space), see METHOD.", other:"Scoring amended after first run, see METHOD."},
      scoringNotes:{source_extractor_floor:"Floor: the source 0-shot extractor reads the text between the first and last $, so answers in \\boxed{} score 0. Not a comparison of ability, see METHOD.", other:"Scoring note, see METHOD."},
      details:"Source and sample", population:"population / sample / scored", revision:"revision", licence:"licence (dataset card)", files:"Pinned files",
      packet:"result file", run:"run", protocol:"protocol"
    },
    pl: {
      search:"Szukaj benchmarku", language:"Język", show:"Pokaż", all:"Wszystkie", withResults:"Z opublikowaną liczbą", clear:"Wyczyść filtry",
      benchmark:"Benchmark", results:"Wyniki", notPublished:"Jeszcze nie opublikowano",
      noPacket:"Nie ma jeszcze opublikowanych wyników. Poniżej jest plan: 50 benchmarków, ich źródła i miary. Dla modeli, których dane treningowe mamy, liczba pojawia się dopiero po skanie kontaminacji.",
      loadError:"Nie udało się zweryfikować przypiętego pliku, więc niczego nie pokazuję.", rejected:"Wyniki ukryte, bo plik wyników nie przeszedł kontroli",
      empty:"Żaden benchmark nie pasuje do filtrów.", shown:"Pokazano", of:"z", benchmarks:"benchmarków",
      pills:{total:"benchmarków", langs:"38 EN · 12 PL", models:"modele", published:"z opublikowaną liczbą"},
      higher:"więcej = lepiej", lower:"mniej = lepiej", ci:"95% CI", noCI:"bez przedziału", random:"losowo", majority:"większość", delta:"różnica",
      majorityShare:"najczęstsza własna klasa w", none:"brak", refMissing:"odniesienie: brak",
      scan:{clean:"czysty", flagged:"trafienia", partially_scanned:"skan niepełny", unscanned:"skan w toku", unknown:"dane treningowe nieznane"},
      noNumber:{partially_scanned:"Bez liczby: skan nie objął dość pozycji.", unscanned:"Bez liczby do czasu skanu kontaminacji."},
      hitFree:"Bez pozycji znalezionych w danych treningowych", hitFreeAnswer:"Bez trafień w polach pytania i odpowiedzi", hits:"pozycji z trafieniem", n:"n",
      noHitFree:"brak pozycji bez trafień", tooFew:"za mało pozycji na liczbę",
      mostly:"mniej niż 50 pozycji bez trafień w danych treningowych", inSlayer:"w korpusie treningowym Slayera", slayerSubset:"podzbiory wyznaczone korpusem treningowym Slayera", mostlyVerdict:"Tu nie ma werdyktu lepszy/gorszy z całej próbki: może go dać tylko test sparowany na pozycjach bez trafień w polach odpowiedzi.",
      pending:n => `${n} benchmarków nie jest jeszcze zmierzonych; ich dodanie nie zmieni pokazanych werdyktów; może rozstrzygnąć wiersze dziś nierozstrzygnięte i doda werdykty dla nowych wierszy.`,
      constant:"stała odpowiedź", constantNote:"Jedna klasa dla wszystkich pozycji: nie porównujemy między modelami.",
      states:{ok:"Pomiar",not_run:"Nie uruchomiono",blocked_access:"Brak dostępu do danych",blocked_runtime:"Blokada środowiska",blocked_security:"Zablokowane przez przegląd bezpieczeństwa",blocked_judge:"Brak przyjętego sędziego",unsupported:"Nieobsługiwane",failed:"Błąd biegu",skipped:"Pominięte"},
      verdicts:{better:"lepszy niż",worse:"gorszy niż",inconclusive:"nierozstrzygnięte wobec"}, verdictNote:"test sparowany, poprawka Holma", verdictNoteSubset:"test sparowany na pozycjach bez trafień w polach odpowiedzi, poprawka Holma",
      amendments:{answer_leading_space_strip:"Punktacja poprawiona po pierwszym biegu (spacja na początku), zob. METHOD.", other:"Punktacja zmieniona po pierwszym biegu, zob. METHOD."},
      scoringNotes:{source_extractor_floor:"Podłoga: ekstraktor źródła (0-shot) czyta tekst między pierwszym a ostatnim $, więc odpowiedzi w \\boxed{} dostają 0. To nie jest porównanie umiejętności, zob. METHOD.", other:"Uwaga o punktacji, zob. METHOD."},
      details:"Źródło i próbka", population:"populacja / próbka / ocenione", revision:"rewizja", licence:"licencja (karta zbioru)", files:"Przypięte pliki",
      packet:"plik wyników", run:"bieg", protocol:"protokół"
    }
  };
  const state = {lang:"en", tasks:null, packet:null, index:null, packetSHA:null, error:null, files:[], query:"", language:"all", show:"all"};

  function el(tag, text, cls) {
    const n = document.createElement(tag);
    if (text !== undefined && text !== null) n.textContent = String(text);
    if (cls) n.className = cls;
    return n;
  }
  function fmt(v, unit) {
    const digits = unit === "byte_ppl" ? 3 : unit === "percent" ? 1 : 3;
    return new Intl.NumberFormat(state.lang === "pl" ? "pl-PL" : "en-GB", {minimumFractionDigits:digits, maximumFractionDigits:digits}).format(v);
  }
  const unitLabel = u => u === "percent" ? "%" : u === "byte_ppl" ? "byte-PPL" : "";
  const pct = v => new Intl.NumberFormat(state.lang === "pl" ? "pl-PL" : "en-GB", {style:"percent", maximumFractionDigits:1}).format(v);
  function modelName(m) { return m.repo.split("/").pop(); }

  // Thresholds and the "most items found in training data" class come from the contract, which also guarantees that every
  // non-withheld verdict in an accepted packet follows the display rules (binding rules 2026-10-29 16:55 and 17:07).
  const MIN_HIT_FREE_N = contract.MIN_HIT_FREE_N;
  const mostlyTrained = taskId => state.index ? contract.mostlyTrained(state.index.observations, taskId, state.packet.protocol_id) : null;
  // Whether the cell carries a number at all, and why not otherwise.
  function numberRule(o) {
    if (o.state !== "ok") return {show:false, reason:null};
    if (["clean","flagged","unknown"].includes(o.contamination)) return {show:true};
    return {show:false, reason:o.contamination};
  }
  function hitFreeLine(label, value, n, rate, ci, unit, t) {
    const tail = `${t.n} ${n}; ${pct(rate)} ${t.hits}`;
    if (n === 0) return `${label}: ${t.noHitFree}`;
    if (n < MIN_HIT_FREE_N) return `${label}: ${t.tooFew} (${tail})`;
    const interval = ci ? `${t.ci} ${fmt(ci.lower, unit)} – ${fmt(ci.upper, unit)}` : t.noCI;
    return `${label}: ${fmt(value, unit)}, ${interval} (${tail})`;
  }

  const isConstant = x => !!(x && x.reference && x.reference.constant_prediction === true);

  function cell(task, model, t) {
    const td = el("td");
    const o = state.index && state.index.observations.get(contract.key(task.id, model.model_id, state.packet.protocol_id));
    if (!o) { td.append(el("span", t.notPublished, "state")); return td; }
    const mostly = mostlyTrained(task.id);
    const rule = numberRule(o);
    const tags = el("div", undefined, "tags");
    if (o.state !== "ok") {
      td.append(el("span", t.states[o.state], "state"));
      if (o.reason) td.append(el("span", o.reason, "meta"));
    } else if (!rule.show) {
      td.append(el("span", t.noNumber[rule.reason], "state"));
    } else {
      const v = el("span", fmt(o.value, o.unit), mostly === null ? "value" : "value dim");
      if (unitLabel(o.unit)) v.append(el("small", unitLabel(o.unit)));
      td.append(v);
      td.append(el("span", o.ci95 ? `${t.ci} ${fmt(o.ci95.lower, o.unit)} – ${fmt(o.ci95.upper, o.unit)}` : t.noCI, "meta"));
      if (mostly !== null) td.append(el("span", `${t.mostly} (${pct(mostly)} ${t.hits}${o.contamination === "unknown" ? `, ${t.inSlayer}` : ""})`, "mostly"));
      const hasA = Number.isInteger(o.n_hit_free), hasB = Number.isInteger(o.n_hit_free_answer_fields);
      if (hasA) td.append(el("span", hitFreeLine(t.hitFree, o.value_hit_free, o.n_hit_free, o.hit_rate, o.ci95_hit_free, o.unit, t), "ref"));
      if (hasB) td.append(el("span", hitFreeLine(t.hitFreeAnswer, o.value_hit_free_answer_fields, o.n_hit_free_answer_fields, o.hit_rate_answer_fields, o.ci95_hit_free_answer_fields, o.unit, t), "ref"));
      if ((hasA || hasB) && o.contamination === "unknown") td.append(el("span", t.slayerSubset, "meta"));
      const r = o.reference;
      const refs = [];
      if (r && r.chance_value !== null) refs.push(`${t.random} ${fmt(r.chance_value, o.unit)}`);
      if (r && r.majority_value !== null) refs.push(`${t.majority} ${fmt(r.majority_value, o.unit)}`);
      td.append(el("span", refs.length ? refs.join(" · ") : t.refMissing, "ref"));
      if (r && r.majority_prediction_share !== null) td.append(el("span", `${t.majorityShare} ${pct(r.majority_prediction_share)}`, "meta"));
      if (isConstant(o)) tags.append(el("span", t.constant, "tag constant"));
    }
    const scanClass = {clean:"clean", flagged:"flagged", unknown:"unknown"}[o.contamination] || "pending";
    tags.append(el("span", t.scan[o.contamination], `tag ${scanClass}`));
    td.append(tags);
    if (rule.show && isConstant(o)) td.append(el("span", t.constantNote, "meta"));
    if (rule.show && mostly !== null) td.append(el("span", t.mostlyVerdict, "meta"));
    for (const c of state.index.contrasts) {
      if (c.task_id !== task.id || c.model_id !== model.model_id || c.verdict === "withheld") continue;
      const ref = state.index.models.get(c.model_ref);
      const p = new Intl.NumberFormat(state.lang === "pl" ? "pl-PL" : "en-GB", {maximumSignificantDigits:2}).format(c.p_holm);
      const note = c.subset === "full" ? t.verdictNote : `${t.verdictNoteSubset}, ${t.n} ${c.n}`;
      const signed = v => `${v > 0 ? "+" : ""}${fmt(v, c.unit)}`;
      const isNum = v => typeof v === "number" && Number.isFinite(v);
      const diff = isNum(c.delta) ? `${t.delta} ${signed(c.delta)}${c.ci95 ? ` (${t.ci} ${signed(c.ci95.lower)} – ${signed(c.ci95.upper)})` : ""} · ` : "";
      td.append(el("span", `${t.verdicts[c.verdict]} ${modelName(ref)}`, "verdict"), el("span", `${diff}${note}: p = ${p}`, "meta"));
    }
    const d = el("details");
    d.append(el("summary", t.details));
    d.append(el("p", `${t.population}: ${o.n_population ?? "—"} / ${o.n_selected ?? "—"} / ${o.n_scored}`));
    d.append(el("p", `results_sha256 ${o.results_sha256 ?? "—"}`));
    if (o.contamination_receipt_sha256) d.append(el("p", `scan receipt ${o.contamination_receipt_sha256}`));
    td.append(d);
    return td;
  }

  // Polish labels for the tasks_50 domain values; the English view shows the value itself.
  const DOMAIN_PL = {
    syntax:"składnia", science_reasoning:"rozumowanie naukowe", language_modeling:"modelowanie języka", morphosyntax:"morfoskładnia",
    commonsense:"zdrowy rozsądek", physical_reasoning:"rozumowanie fizyczne", coreference:"koreferencja", science_knowledge:"wiedza naukowa",
    reading:"czytanie", causal_reasoning:"rozumowanie przyczynowe", entailment:"wynikanie", word_sense:"znaczenie słowa",
    adversarial_entailment:"wynikanie (adwersaryjne)", contextual_prediction:"przewidywanie z kontekstu", logical_reasoning:"rozumowanie logiczne",
    mathematics_mc:"matematyka (wybór)", sentiment:"wydźwięk", topic_classification:"klasyfikacja tematu", question_relevance:"trafność pytania",
    paraphrase:"parafraza", summary_matching:"dopasowanie streszczenia", toxicity_detection:"wykrywanie toksyczności",
    sentence_entity_classification:"klasyfikacja zdań i encji", question_answering:"odpowiadanie na pytania", academic_knowledge:"wiedza akademicka",
    mathematics_generation:"matematyka (generowanie)", competition_mathematics:"matematyka konkursowa", truthfulness:"prawdomówność",
    instruction_following:"wykonywanie poleceń", native_exam_knowledge:"wiedza z egzaminów krajowych", unanswerable_reading:"czytanie z pytaniami bez odpowiedzi",
    discrete_reading:"czytanie z liczeniem", conversational_reading:"czytanie w dialogu", closed_book_knowledge:"wiedza bez kontekstu",
    biomedical_reading:"czytanie biomedyczne", medical_exam_knowledge:"wiedza z egzaminów medycznych", social_reasoning:"rozumowanie społeczne",
    formal_reasoning:"rozumowanie formalne", adversarial_paraphrase:"parafraza (adwersaryjna)", acceptability:"poprawność zdania", entity_reading:"czytanie o encjach"
  };
  const domainLabel = d => (state.lang === "pl" && DOMAIN_PL[d]) || d.replace(/_/g, " ");

  function taskHeader(task, t) {
    const th = el("th", task.name, "task");
    th.scope = "row";
    th.append(el("span", `${task.language} · ${domainLabel(task.domain)}`, "meta"));
    th.append(el("span", `${task.metric.split(",")[0]} · ${task.higher_is_better ? t.higher : t.lower}`, "meta"));
    // Public scoring amendments are a property of the pinned benchmark list, never of a result file.
    for (const code of task.scoring_amendments || []) th.append(el("span", t.amendments[code] || t.amendments.other, "amend"));
    for (const code of task.scoring_notes || []) th.append(el("span", t.scoringNotes[code] || t.scoringNotes.other, "amend"));
    const d = el("details");
    d.append(el("summary", t.details));
    for (const s of task.license.source_revisions) {
      const p = el("p");
      const a = el("a", s.repo_id);
      a.href = `https://huggingface.co/datasets/${s.repo_id}`;
      a.rel = "noopener";
      p.append(a, document.createTextNode(` · ${t.revision} ${s.revision.slice(0, 8)}`));
      d.append(p);
    }
    d.append(el("p", `${t.licence}: ${task.license.declared_tags.join(", ") || t.none}`));
    d.append(el("p", `${task.split} · ${task.shots}-shot · ${task.instrument}`));
    th.append(d);
    return th;
  }

  function render() {
    const t = T[state.lang];
    const app = document.getElementById("app");
    const pills = document.getElementById("pills");
    pills.replaceChildren();
    const pill = (b, txt) => { const p = el("span", undefined, "pill"); p.append(el("b", b), document.createTextNode(` ${txt}`)); pills.append(p); };
    if (!state.tasks) {
      app.replaceChildren(el("p", `${t.loadError} (${state.error})`, "notice error"));
      return;
    }
    const rows = state.tasks.rows;
    // Models whose training corpus was scanned come first; models with unknown training data follow, in packet order.
    const scanned = m => state.index && [...state.index.observations.values()].some(o => o.model_id === m.model_id && o.contamination !== "unknown");
    const models = state.packet ? [...state.packet.models].sort((a, b) => scanned(b) - scanned(a)) : [];
    const published = new Set();
    if (state.index) for (const o of state.index.observations.values()) if (numberRule(o).show) published.add(o.task_id);
    pill(rows.length, t.pills.total);
    pills.append(el("span", t.pills.langs, "pill"));
    if (models.length) pill(models.length, t.pills.models);
    pill(published.size, t.pills.published);
    // Benchmarks where some model has no measurement yet (binding sentence 2026-10-29 20:18).
    const pendingEl = document.getElementById("pending");
    const unmeasured = state.index ? rows.filter(r => models.some(m => { const o = state.index.observations.get(contract.key(r.id, m.model_id, state.packet.protocol_id)); return !o || o.state !== "ok"; })).length : 0;
    pendingEl.hidden = unmeasured === 0;
    pendingEl.textContent = unmeasured ? t.pending(unmeasured) : "";

    const nodes = [];
    if (state.error && state.tasks) nodes.push(el("p", `${t.rejected}: ${state.error}`, "notice error"));
    else if (!state.packet) nodes.push(el("p", t.noPacket, "notice"));

    const controls = el("div", undefined, "controls");
    const search = el("input"); search.type = "search"; search.value = state.query;
    const lang = el("select");
    for (const [v, txt] of [["all", t.all], ["EN", "EN"], ["PL", "PL"]]) { const op = el("option", txt); op.value = v; lang.append(op); }
    lang.value = state.language;
    const show = el("select");
    for (const [v, txt] of [["all", t.all], ["published", t.withResults]]) { const op = el("option", txt); op.value = v; show.append(op); }
    show.value = state.show;
    show.disabled = !state.packet;
    const clear = el("button", t.clear, "clear"); clear.type = "button";
    const lab = (txt, input) => { const l = el("label", txt); l.append(input); return l; };
    controls.append(lab(t.search, search), lab(t.language, lang), lab(t.show, show), clear);
    const summary = el("p", undefined, "summary");
    summary.setAttribute("role", "status");
    const wrap = el("div", undefined, "wrap");
    wrap.tabIndex = 0;
    wrap.setAttribute("role", "region");
    wrap.setAttribute("aria-label", t.results);
    const table = el("table");
    table.setAttribute("aria-labelledby", "results-title");
    const head = el("tr");
    const th0 = el("th", t.benchmark); th0.scope = "col"; head.append(th0);
    if (models.length) for (const m of models) {
      const th = el("th", modelName(m)); th.scope = "col";
      th.append(el("span", m.revision ? `${t.revision} ${m.revision.slice(0, 8)}` : "", "meta"));
      head.append(th);
    } else { const th = el("th", t.results); th.scope = "col"; head.append(th); }
    const thead = el("thead"); thead.append(head);
    const tbody = el("tbody");
    table.append(thead, tbody);
    wrap.append(table);
    const empty = el("p", t.empty, "summary");
    nodes.push(controls, summary, wrap, empty);
    app.replaceChildren(...nodes);

    function fill() {
      const q = state.query.trim().toLocaleLowerCase(state.lang);
      const visible = rows.filter(r => (state.language === "all" || r.language === state.language) &&
        (state.show === "all" || published.has(r.id)) &&
        `${r.name} ${r.benchmark_id} ${r.domain}`.toLocaleLowerCase(state.lang).includes(q));
      const frag = document.createDocumentFragment();
      for (const r of visible) {
        const tr = el("tr");
        tr.append(taskHeader(r, t));
        if (models.length) for (const m of models) tr.append(cell(r, m, t));
        else tr.append(el("td", t.notPublished, "state"));
        frag.append(tr);
      }
      tbody.replaceChildren(frag);
      summary.textContent = `${t.shown} ${visible.length} ${t.of} ${rows.length} ${t.benchmarks}`;
      empty.hidden = visible.length !== 0;
      wrap.hidden = visible.length === 0;
    }
    search.addEventListener("input", () => { state.query = search.value; fill(); });
    lang.addEventListener("change", () => { state.language = lang.value; fill(); });
    show.addEventListener("change", () => { state.show = show.value; fill(); });
    clear.addEventListener("click", () => { state.query = ""; state.language = "all"; state.show = "all"; search.value = ""; lang.value = "all"; show.value = "all"; fill(); search.focus(); });
    fill();

    const files = document.getElementById("files");
    files.replaceChildren(el("p", t.files));
    for (const f of state.files) { const p = el("p"); p.append(el("code", `${f.name} · sha256 ${f.sha}`)); files.append(p); }
    if (state.packet) files.append(el("p", `${t.packet}: ${t.run} ${state.packet.run_id} · ${t.protocol} ${state.packet.protocol_id}`));
  }

  async function read(path, pin) {
    const res = await fetch(path, {cache:"no-store"});
    if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`);
    const bytes = await res.arrayBuffer();
    const digest = await crypto.subtle.digest("SHA-256", bytes);
    const sha = [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, "0")).join("");
    if (pin && sha !== pin) throw new Error(`${path}: pinned bytes changed`);
    state.files.push({name:path, sha});
    return {bytes, sha};
  }
  const json = r => JSON.parse(new TextDecoder().decode(r.bytes));

  function setLang(l) {
    state.lang = l === "pl" ? "pl" : "en";
    document.body.dataset.lang = state.lang;
    document.documentElement.lang = state.lang;
    document.querySelectorAll(".lang button").forEach(b => b.setAttribute("aria-pressed", String(b.dataset.set === state.lang)));
    try { localStorage.setItem("slayer-lang", state.lang); } catch (e) {}
    render();
  }

  async function start() {
    try {
      const tasks = await read("tasks_50.json", contract.PINS.tasks);
      await read("results.schema.json", contract.PINS.schema);
      await read("METHOD.md", contract.PINS.method);
      await read("contamination_field_roles.json", contract.PINS.fieldRoles);
      state.tasks = json(tasks);
      if (state.tasks.rows.length !== 50) throw new Error("tasks_50.json: expected 50 rows");
    } catch (e) { state.error = e.message; state.tasks = null; return; }
    try {
      const feed = json(await read("results-feed.json"));
      if (feed.results !== null) {
        if (feed.results !== "results.json") throw new Error("only results.json next to this page is allowed");
        // The feed names the exact reviewed packet; any other bytes are refused.
        if (!/^[0-9a-f]{64}$/.test(feed.sha256 || "")) throw new Error("results-feed.json: missing sha256 of results.json");
        const res = await read("results.json", feed.sha256);
        const packet = json(res);
        state.index = contract.validate(packet, state.tasks);
        state.packet = packet;
        state.packetSHA = res.sha;
      }
    } catch (e) { state.error = e.message; state.packet = null; state.index = null; }
  }

  document.querySelectorAll(".lang button").forEach(b => b.addEventListener("click", () => setLang(b.dataset.set)));
  let saved = null; try { saved = localStorage.getItem("slayer-lang"); } catch (e) {}
  const q = new URLSearchParams(location.search).get("lang");
  start().finally(() => setLang(q || saved || "en"));
})();