
(async () => {
  "use strict";
  const $ = (s, r = document) => r.querySelector(s);
  const $$ = (s, r = document) => [...r.querySelectorAll(s)];
  const clean = (v) => String(v ?? "").trim();
  const words = (v) => new Set(clean(v).toLocaleLowerCase("ru").replace(/[^0-9а-яёa-z]+/giu, " ").split(/\s+/).filter(Boolean));
  const lines = (v) => clean(v).split(/\r?\n/).map(clean).filter(Boolean);
  const overlap = (a, b) => {
    const aa = a instanceof Set ? a : new Set(a || []), bb = b instanceof Set ? b : new Set(b || []);
    if (!aa.size && !bb.size) return 1;
    const intersection = [...aa].filter((x) => bb.has(x)).length;
    return intersection / Math.max(1, new Set([...aa, ...bb]).size);
  };
  const average = (values) => values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0;
  const deviation = (values) => {
    if (values.length < 2) return 0;
    const mean = average(values);
    return Math.sqrt(average(values.map((value) => (value - mean) ** 2)));
  };
  const decimalBytes = (bytes) => {
    let value = 0n;
    for (const byte of bytes) value = (value << 8n) + BigInt(byte);
    return value.toString(10);
  };
  const digestBytes = async bytes => {
    if(!globalThis.crypto?.subtle)throw Error('Отпечаток не вычислен: нужен настоящий Web Crypto API в защищённом контексте.');
    return new Uint8Array(await crypto.subtle.digest('SHA-256',bytes));
  };
  const showError = message => { const e=$('#drift-status')||$('#pair-project-status')||document.querySelector('[role="status"]');if(e)e.textContent=message; };
  window.addEventListener('unhandledrejection',e=>showError('Операция не завершена: '+String(e.reason?.message||e.reason)));
  const fingerprint = async (value) => {
    const bytes = new TextEncoder().encode(typeof value === "string" ? value : JSON.stringify(value));
    return decimalBytes(await digestBytes(bytes));
  };
  const fingerprintFile = async (file) => decimalBytes(await digestBytes(new Uint8Array(await file.arrayBuffer())));
  const save = (name, content, type = "text/plain;charset=utf-8") => {
    const blob = new Blob([content], { type });
    const link = document.createElement("a");
    link.href = URL.createObjectURL(blob); link.download = name; document.body.appendChild(link); link.click();
    setTimeout(() => { URL.revokeObjectURL(link.href); link.remove(); }, 0);
  };
  const saveBlob = (name, blob) => {
    const link = document.createElement("a");
    link.href = URL.createObjectURL(blob); link.download = name; document.body.appendChild(link); link.click();
    setTimeout(() => { URL.revokeObjectURL(link.href); link.remove(); }, 100);
  };
  const zipName = (value, fallback) => {
    const raw = clean(value).split(/[\\/]/).pop().replace(/[<>:"|?*\u0000-\u001f]/g, "-").replace(/^\.+/, "");
    return raw || fallback;
  };
  const crcTable = (() => {
    const table = new Uint32Array(256);
    for (let n = 0; n < 256; n += 1) {
      let c = n;
      for (let k = 0; k < 8; k += 1) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
      table[n] = c >>> 0;
    }
    return table;
  })();
  const crc32 = (bytes) => {
    let c = 0xffffffff;
    for (const byte of bytes) c = crcTable[(c ^ byte) & 0xff] ^ (c >>> 8);
    return (c ^ 0xffffffff) >>> 0;
  };
  const le16 = (value) => new Uint8Array([value & 255, (value >>> 8) & 255]);
  const le32 = (value) => new Uint8Array([value & 255, (value >>> 8) & 255, (value >>> 16) & 255, (value >>> 24) & 255]);
  const joinBytes = (parts) => {
    const size = parts.reduce((sum, part) => sum + part.length, 0), out = new Uint8Array(size);
    let offset = 0; for (const part of parts) { out.set(part, offset); offset += part.length; } return out;
  };
  const buildZip = (entries) => {
    const encoder = new TextEncoder(), locals = [], centrals = [];
    let offset = 0;
    for (const entry of entries) {
      const name = encoder.encode(entry.name), bytes = entry.bytes instanceof Uint8Array ? entry.bytes : encoder.encode(String(entry.bytes ?? ""));
      const crc = crc32(bytes), flags = 0x0800;
      const local = joinBytes([le32(0x04034b50), le16(20), le16(flags), le16(0), le16(0), le16(0), le32(crc), le32(bytes.length), le32(bytes.length), le16(name.length), le16(0), name, bytes]);
      locals.push(local);
      const central = joinBytes([le32(0x02014b50), le16(20), le16(20), le16(flags), le16(0), le16(0), le16(0), le32(crc), le32(bytes.length), le32(bytes.length), le16(name.length), le16(0), le16(0), le16(0), le16(0), le32(0), le32(offset), name]);
      centrals.push(central); offset += local.length;
    }
    const centralBytes = joinBytes(centrals);
    const end = joinBytes([le32(0x06054b50), le16(0), le16(0), le16(entries.length), le16(entries.length), le32(centralBytes.length), le32(offset), le16(0)]);
    return new Blob([...locals, centralBytes, end], { type: "application/zip" });
  };
  const K=globalThis.ВойнаМетодика;
  let labSchemas=null;
  if($('#drift-form')||$('#pair-project-form')){try{const response=await fetch('/данные/схемы-лаборатории-4.9.0.json');if(!response.ok)throw Error('HTTP '+response.status);labSchemas=await response.json();for(const kind of ['drift','pair'])if(K.schemaErrors(labSchemas[kind]).length)throw Error('Неподдерживаемая схема.');}catch(e){showError('Лаборатория не запущена: схема не загружена. '+e.message);return;}}
  const validateProject=async(value,kind)=>{
    const data=structuredClone(value),errors=K.validate(labSchemas[kind],data);if(errors.length)throw Error(errors.join('\n'));
    const items=kind==='drift'?data.снимки:data.условия.flatMap(x=>x.повторы);if(items.length>500)throw Error('Не более 500 записей в одном проекте.');
    if(kind==='drift'&&new Set(items.map(x=>x.номер)).size!==items.length)throw Error('Повтор номера снимка.');
    for(const item of items){const expected=item.отпечаток_ша_256_десятичный;delete item.отпечаток_ша_256_десятичный;if(await fingerprint(item)!==expected)throw Error('Отпечаток снимка или повтора не совпал.');item.отпечаток_ша_256_десятичный=expected;}
    if(data.исходник_импорта){const x=data.исходник_импорта,b=K.unbase64(x.база64);if(b.length!==x.байты||await K.hash(b)!==x.ша256)throw Error('Исходные байты импорта подменены.');}
    return data;
  };
  const readJson=async(file,kind)=>{
    if(!file||file.size>12*1024*1024)throw Error('Файл отсутствует или превышает 12 МиБ.');
    const bytes=new Uint8Array(await file.arrayBuffer()),value=K.parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes),12*1024*1024);
    const data=await validateProject(value,kind);
    if(!data.исходник_импорта){if(bytes.length>5*1024*1024)throw Error('Первый исходник лабораторного проекта превышает 5 МиБ.');data.исходник_импорта={имя:file.name,байты:bytes.length,ша256:await K.hash(bytes),база64:K.base64(bytes)};}
    return data;
  };
  const plain = v => clean(v)||"не указано";
  const safe = v => plain(v).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
  const metric = (label, value, note = "") => `<article><span>${label}</span><strong>${value}</strong>${note ? `<small>${note}</small>` : ""}</article>`;
  const pairwise = (items, selector) => {
    const results = [];
    for (let i = 0; i < items.length; i += 1) for (let j = i + 1; j < items.length; j += 1) results.push(overlap(selector(items[i]), selector(items[j])));
    return results;
  };

  // Реестр сетевой перепроверки.
  const sourceRegistry = $("#source-registry");
  if (sourceRegistry) {
    const search = $("#source-search"), kind = $("#source-kind"), status = $("#source-count");
    const render = () => {
      const query = clean(search?.value).toLocaleLowerCase("ru");
      const filter = kind?.value || "все";
      let shown = 0;
      $$(".source-record", sourceRegistry).forEach((card) => {
        const haystack = card.textContent.toLocaleLowerCase("ru");
        const classText = card.dataset.sourceKind?.toLocaleLowerCase("ru") || "";
        const visible = (!query || haystack.includes(query)) && (filter === "все" || classText.includes(filter));
        card.hidden = !visible; if (visible) shown += 1;
      });
      if (status) status.textContent = `Показано: ${shown} из ${$$('.source-record', sourceRegistry).length}.`;
    };
    search?.addEventListener("input", render); kind?.addEventListener("change", render); render();
  }

  // Журнал временного дрейфа.
  const driftForm = $("#drift-form");
  if (driftForm) {
    const key = "война-ком-журнал-дрейфа-4-5";
    let project = { схема: "война.com/журнал-временного-дрейфа/1", редакция: "4.9.0-wip", название: "", гипотеза: "", переменная: "", снимки: [] };
    let storageError="";try { const saved=K.parse(localStorage.getItem(key)||"null",12*1024*1024);if(saved!==null)project=await validateProject(saved,"drift"); }catch(e){storageError="Прежнее значение не загружено и не удалено: "+e.message;}
    const setValue = (id, value) => { const node = $(id); if (node) node.value = value || ""; };
    setValue("#drift-project", project.название); setValue("#drift-hypothesis", project.гипотеза); setValue("#drift-variable", project.переменная);
    const reportText = () => {
      const snapshots = project.снимки || [], lengths = snapshots.map((x) => clean(x.полный_ответ).length);
      const textPairs = pairwise(snapshots, (x) => words(x.полный_ответ));
      const sourcePairs = pairwise(snapshots, (x) => new Set(x.источники || []));
      const refusals = snapshots.filter((x) => x.отказ).length;
      return [
        "война.com — ЖУРНАЛ ВРЕМЕННОГО ДРЕЙФА",
        `Редакция: 4.9.0-wip — незавершённый выпуск`,
        `Проект: ${plain(project.название)}`,
        `Гипотеза: ${plain(project.гипотеза)}`,
        `Единственная изменяемая переменная: ${plain(project.переменная)}`,
        `Снимков: ${snapshots.length}`,
        `Версий: ${new Set(snapshots.map((x) => x.версия)).size}`,
        `Средняя длина ответа: ${Math.round(average(lengths))}`,
        `Разброс длины: ${Math.round(deviation(lengths))}`,
        `Среднее словарное сходство: ${Math.round(average(textPairs) * 100)}%`,
        `Среднее сходство источников: ${Math.round(average(sourcePairs) * 100)}%`,
        `Частота отказов: ${snapshots.length ? Math.round(refusals / snapshots.length * 100) : 0}%`,
        "",
        ...snapshots.flatMap((x) => [
          `СНИМОК ${String(x.номер).padStart(3, "0")}`,
          `Дата и время: ${plain(x.дата_и_время)}`,
          `Система: ${plain(x.система)}`,
          `Версия: ${plain(x.версия)}`,
          `Регион: ${plain(x.регион)}`,
          `Профиль: ${plain(x.состояние_профиля)}`,
          `Изменённая переменная: ${plain(x.изменённая_переменная)}`,
          `Отказ: ${x.отказ ? "да" : "нет"}`,
          `Отпечаток: ${plain(x.отпечаток_ша_256_десятичный)}`,
          `Запрос: ${x.запрос}`,
          `Полный ответ:\n${x.полный_ответ}`,
          `Источники:\n${(x.источники || []).join("\n") || "не указаны"}`,
          ""
        ]),
        "ПРЕДЕЛ ВЫВОДА",
        "Серия измеряет устойчивость и разброс. Она не доказывает причинность, пока альтернативные переменные не исключены независимым повторением.",
        "",
        "война.com · РЕБЕЛ ОР ДАЙ"
      ].join("\n");
    };
    const render = () => {
      const snapshots = project.снимки || [], list = $("#drift-list"), board = $("#drift-metrics"), report = $("#drift-report"), status = $("#drift-status");
      if (list) {
        list.replaceChildren();
        if (!snapshots.length) list.innerHTML = '<p class="empty-state is-visible">Снимков пока нет.</p>';
        snapshots.forEach((x) => {
          const article = document.createElement("article"); article.className = "snapshot-card";
          article.innerHTML = `<header><span class="case-code">${String(x.номер).padStart(3, "0")}</span><span class="state">${x.отказ ? "отказ" : "ответ"}</span></header><h2>${safe(x.система)} · ${safe(x.версия)}</h2><p>${safe(x.дата_и_время)} · ${safe(x.регион)} · ${safe(x.состояние_профиля)}</p><dl><div><dt>Изменено</dt><dd>${safe(x.изменённая_переменная)}</dd></div><div><dt>Длина</dt><dd>${clean(x.полный_ответ).length} знаков</dd></div><div><dt>Источники</dt><dd>${(x.источники || []).length}</dd></div><div><dt>Отпечаток</dt><dd class="break-all">${safe(x.отпечаток_ша_256_десятичный)}</dd></div></dl><details><summary>Показать полный снимок</summary><h3>Запрос</h3><pre>${x.запрос.replace(/[&<>]/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;"}[c]))}</pre><h3>Ответ</h3><pre>${x.полный_ответ.replace(/[&<>]/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;"}[c]))}</pre></details><button class="button small ghost" data-drift-remove="${x.номер}" type="button">Удалить снимок</button>`;
          list.appendChild(article);
        });
      }
      const lengths = snapshots.map((x) => clean(x.полный_ответ).length), similarity = average(pairwise(snapshots, (x) => words(x.полный_ответ)));
      const strength = "независимость не проверена";
      if (board) board.innerHTML = metric("Снимки", snapshots.length) + metric("Версии", new Set(snapshots.map((x) => x.версия)).size) + metric("Разброс длины", Math.round(deviation(lengths))) + metric("Словарное сходство", `${Math.round(similarity * 100)}%`) + metric("Сопоставимость", strength, "качество фиксации, не причинный вердикт");
      if (report) report.value = reportText();
      if (status) status.textContent = snapshots.length ? `Снимков: ${snapshots.length}. Состояние сопоставления: ${strength}.` : "Новый проект. Данные ещё не сохранены.";
      $$("[data-drift-remove]").forEach((button) => button.addEventListener("click", () => { project.снимки = snapshots.filter((x) => x.номер !== Number(button.dataset.driftRemove));  render(); }));
    };
    const captureProject = () => { project.название = clean($("#drift-project")?.value); project.гипотеза = clean($("#drift-hypothesis")?.value); project.переменная = clean($("#drift-variable")?.value); };
    let driftBusy=false;driftForm.addEventListener("submit", async (event) => {
      event.preventDefault();if(driftBusy)return;driftBusy=true;try{captureProject();const before=K.canonical(project),fields=JSON.stringify($$("input,textarea,select",driftForm).map(x=>[x.id,x.value,x.checked]));
      const item = {
        номер: Math.max(0,...project.снимки.map(x=>x.номер)) + 1,
        дата_и_время: clean($("#drift-time")?.value), система: clean($("#drift-system")?.value), версия: clean($("#drift-version")?.value), регион: clean($("#drift-region")?.value), состояние_профиля: clean($("#drift-profile")?.value),
        запрос: clean($("#drift-query")?.value), полный_ответ: clean($("#drift-response")?.value), источники: lines($("#drift-sources")?.value), отказ: Boolean($("#drift-refusal")?.checked), изменённая_переменная: clean($("#drift-changed")?.value)
      };
      item.отпечаток_ша_256_десятичный = await fingerprint(item);
      if(before!==K.canonical(project)||fields!==JSON.stringify($$("input,textarea,select",driftForm).map(x=>[x.id,x.value,x.checked])))throw Error("Форма или проект изменились; снимок не внесён.");project.снимки.push(item); render();
      const status = $("#drift-status"); if (status && project.переменная && item.изменённая_переменная !== project.переменная) status.textContent = `Снимок внесён во вкладку, но изменяемая переменная не совпадает с проектом: «${project.переменная}».`;
      }catch(e){showError("Снимок не внесён: "+e.message);}finally{driftBusy=false;}
    });
    $("#drift-clear-form")?.addEventListener("click", () => ["#drift-system", "#drift-version", "#drift-region", "#drift-profile", "#drift-time", "#drift-changed", "#drift-response", "#drift-sources"].forEach((id) => setValue(id, "")));
    $("#drift-save-project")?.addEventListener("click", () => { captureProject();try{localStorage.setItem(key, JSON.stringify(project));}catch{showError("Сохранение не выполнено: хранилище недоступно. Рабочие данные остались во вкладке; экспортируйте файл.");return;}const status = $("#drift-status"); if (status) status.textContent = `Проект сохранён на этом устройстве. Снимков: ${project.снимки.length}.`; });
    $("#drift-export")?.addEventListener("click", () => { captureProject(); save("журнал-временного-дрейфа.json", JSON.stringify(project, null, 2), "application/json;charset=utf-8"); });
    $("#drift-export-text")?.addEventListener("click", () => save("журнал-временного-дрейфа.txt", reportText()));
    $("#drift-import")?.addEventListener("change", async (event) => { const previous=project;try { const expected=K.canonical(project),formState=new FormData(driftForm),stamp=JSON.stringify($$("input,textarea,select",driftForm).map(x=>[x.id,x.value,x.checked]));const data=await readJson(event.target.files[0],"drift");if(expected!==K.canonical(project)||stamp!==JSON.stringify($$("input,textarea,select",driftForm).map(x=>[x.id,x.value,x.checked])))throw Error("Проект или форма изменились во время импорта."); project = data; setValue("#drift-project", project.название); setValue("#drift-hypothesis", project.гипотеза); setValue("#drift-variable", project.переменная); render(); } catch (e) { showError("Импорт журнала отклонён; рабочий проект не заменён. "+e.message); } event.target.value = ""; });
    $("#drift-reset")?.addEventListener("click", () => { if (!confirm("Удалить локальный проект и все снимки из браузера?")) return;try{localStorage.removeItem(key);}catch{showError("Очистка не выполнена: хранилище недоступно. Рабочие данные не удалены.");return;}project = { схема: "война.com/журнал-временного-дрейфа/1", редакция: "4.9.0-wip", название: "", гипотеза: "", переменная: "", снимки: [] }; driftForm.reset(); render(); });
    render();if(storageError)showError(storageError);
  }

  // Матрица парного теста.
  const pairForm = $("#pair-project-form");
  if (pairForm) {
    const key = "война-ком-матрица-парного-теста-4-5";
    let project = { схема: "война.com/матрица-парного-теста/2", редакция: "4.9.0-wip", предмет: "", запрос: "", переменная: "", цель_повторов: 5, условия: [] };
    let storageError="";try { const saved=K.parse(localStorage.getItem(key)||"null",12*1024*1024);if(saved!==null)project=await validateProject(saved,"pair"); }catch(e){storageError="Прежнее значение не загружено и не удалено: "+e.message;}
    const input = (label, name, type = "text", value = "") => `<label class="field"><span>${label}</span><input data-repeat-field="${name}" type="${type}" value="${String(value).replace(/"/g, '&quot;')}"></label>`;
    const reportText = () => {
      const all = project.условия.flatMap((c) => c.повторы || []), counts = project.условия.map((c) => (c.повторы || []).length), minimum = counts.length ? Math.min(...counts) : 0;
      const strength = "независимость не проверена";
      const out = ["война.com — МАТРИЦА ПАРНОГО ТЕСТА", `Редакция: 4.9.0-wip — незавершённый выпуск`, `Предмет: ${plain(project.предмет)}`, `Запрос: ${plain(project.запрос)}`, `Единственная изменяемая переменная: ${plain(project.переменная)}`, `Цель повторов на условие: ${project.цель_повторов}`, `Условий: ${project.условия.length}`, `Всего повторов: ${all.length}`, `Состояние сопоставления: ${strength}`, ""];
      project.условия.forEach((condition, index) => {
        const repeats = condition.повторы || [], lengths = repeats.map((x) => clean(x.ответ).length);
        out.push(`УСЛОВИЕ ${index + 1}: ${condition.название}`, `Повторов: ${repeats.length}`, `Средняя длина: ${Math.round(average(lengths))}`, `Разброс длины: ${Math.round(deviation(lengths))}`, `Частота отказов: ${repeats.length ? Math.round(repeats.filter((x) => x.отказ).length / repeats.length * 100) : 0}%`, `Внутреннее словарное сходство: ${Math.round(average(pairwise(repeats, (x) => words(x.ответ))) * 100)}%`, "");
        repeats.forEach((repeat, r) => out.push(`Повтор ${r + 1}; версия ${plain(repeat.версия)}; время ${plain(repeat.дата_и_время)}; регион ${plain(repeat.регион)}; профиль ${plain(repeat.профиль)}; отказ ${repeat.отказ ? "да" : "нет"}; отпечаток ${plain(repeat.отпечаток_ша_256_десятичный)}`, `Ответ:\n${repeat.ответ}`, `Источники:\n${(repeat.источники || []).join("\n") || "не указаны"}`, ""));
      });
      out.push("ПРЕДЕЛ ВЫВОДА", "Число записей не удостоверяет уникальность вызовов, независимость наблюдателей или полноту контрольных условий. Для проверки сравнительной серии используйте продольную панель. Причинность не присваивается.", "", "война.com · РЕБЕЛ ОР ДАЙ");
      return out.join("\n");
    };
    const render = () => {
      const stack = $("#pair-conditions"), board = $("#pair-metrics"), log = $("#pair-version-log"), report = $("#pair-report"), status = $("#pair-project-status");
      if (stack) {
        stack.replaceChildren();
        if (!project.условия.length) stack.innerHTML = '<p class="empty-state is-visible">Создайте проект и добавьте условия.</p>';
        project.условия.forEach((condition, ci) => {
          const section = document.createElement("section"); section.className = "condition-card"; section.dataset.condition = String(ci);
          const repeats = condition.повторы || [];
          section.innerHTML = `<header><div><span class="case-code">УСЛОВИЕ ${String(ci + 1).padStart(2, "0")}</span><h2>${safe(condition.название)}</h2></div><div class="actions"><button class="button small" data-add-repeat="${ci}" type="button">Добавить повтор</button><button class="button small ghost" data-remove-condition="${ci}" type="button">Удалить условие</button></div></header><div class="repeat-grid">${repeats.map((repeat, ri) => `<article class="repeat-card"><header><span>Повтор ${ri + 1}</span><button class="icon-text-button" data-remove-repeat="${ci}:${ri}" type="button">Удалить</button></header><dl><div><dt>Версия</dt><dd>${safe(repeat.версия)}</dd></div><div><dt>Время</dt><dd>${safe(repeat.дата_и_время)}</dd></div><div><dt>Регион</dt><dd>${safe(repeat.регион)}</dd></div><div><dt>Профиль</dt><dd>${safe(repeat.профиль)}</dd></div><div><dt>Длина</dt><dd>${clean(repeat.ответ).length}</dd></div><div><dt>Отказ</dt><dd>${repeat.отказ ? "да" : "нет"}</dd></div></dl><details><summary>Полная запись</summary><pre>${repeat.ответ.replace(/[&<>]/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;"}[c]))}</pre><p class="break-all">Отпечаток: ${safe(repeat.отпечаток_ша_256_десятичный)}</p></details></article>`).join("") || '<p class="empty-state is-visible">Повторов нет.</p>'}</div>`;
          stack.appendChild(section);
        });
      }
      $$("[data-add-repeat]").forEach((button) => button.addEventListener("click", () => openRepeat(Number(button.dataset.addRepeat))));
      $$("[data-remove-condition]").forEach((button) => button.addEventListener("click", () => { project.условия.splice(Number(button.dataset.removeCondition), 1); render(); }));
      $$("[data-remove-repeat]").forEach((button) => button.addEventListener("click", () => { const [ci, ri] = button.dataset.removeRepeat.split(":").map(Number); project.условия[ci].повторы.splice(ri, 1); render(); }));
      const counts = project.условия.map((x) => x.повторы.length), minimum = counts.length ? Math.min(...counts) : 0, all = project.условия.flatMap((x) => x.повторы), strength = minimum >= 5 ? "усиленная" : minimum >= 3 ? "умеренная" : "предварительная";
      const baseline = project.условия[0]?.повторы || [], comparisons = project.условия.slice(1).map((condition) => average(baseline.flatMap((a) => condition.повторы.map((b) => overlap(words(a.ответ), words(b.ответ))))));
      if (board) board.innerHTML = metric("Условия", project.условия.length) + metric("Повторы", all.length) + metric("Минимум на условие", minimum, `цель: ${project.цель_повторов}`) + metric("Среднее межусловное сходство", `${Math.round(average(comparisons) * 100)}%`) + metric("Сопоставимость", strength, "не причинный вердикт");
      if (log) {
        const versions = new Map(); all.forEach((x) => { const keyText = safe(x.версия); versions.set(keyText, (versions.get(keyText) || 0) + 1); });
        log.innerHTML = versions.size ? [...versions].map(([version, count]) => `<div><strong>${safe(version)}</strong><span>повторов: ${count}</span></div>`).join("") : '<p class="empty-state is-visible">Версии появятся после добавления повторов.</p>';
      }
      if (report) report.value = reportText();
      if (status) status.textContent = project.предмет ? `${project.предмет}. Условия: ${project.условия.length}; повторов: ${all.length}; устойчивость: ${strength}.` : "Новый проект.";
    };
    const openRepeat = (ci) => {
      const condition = project.условия[ci]; if (!condition) return;
      const dialog = document.createElement("dialog"); dialog.className = "lab-dialog";
      dialog.innerHTML = `<form method="dialog"><header><h2>Новый повтор: ${safe(condition.название)}</h2><button class="icon-text-button" value="cancel" aria-label="Закрыть">Закрыть</button></header><div class="form-grid two">${input("Версия системы", "версия")}${input("Дата и время", "дата_и_время", "datetime-local")}${input("Регион", "регион")}${input("Состояние профиля", "профиль")}</div><label class="field"><span>Полный ответ без редактирования</span><textarea data-repeat-field="ответ" rows="10" required></textarea></label><label class="field"><span>Источники, по одному в строке</span><textarea data-repeat-field="источники" rows="5"></textarea></label><label class="check-line"><input data-repeat-field="отказ" type="checkbox"><span>Система отказала или скрыла результат</span></label><div class="actions"><button class="button primary" data-repeat-save type="button">Сохранить повтор</button><button class="button ghost" value="cancel">Отмена</button></div></form>`;
      document.body.appendChild(dialog); dialog.showModal();
      let busy=false;$("[data-repeat-save]", dialog).addEventListener("click", async () => {if(busy)return;busy=true;try{const original=K.canonical(project),capture=()=>JSON.stringify($$("input,textarea",dialog).map(x=>[x.dataset.repeatField,x.value,x.checked])),before=capture();
        const value = (name) => $( `[data-repeat-field="${name}"]`, dialog );
        const repeat = { версия: clean(value("версия").value), дата_и_время: clean(value("дата_и_время").value), регион: clean(value("регион").value), профиль: clean(value("профиль").value), ответ: clean(value("ответ").value), источники: lines(value("источники").value), отказ: value("отказ").checked };
        if (!repeat.ответ) { value("ответ").focus(); return; }
        repeat.отпечаток_ша_256_десятичный=await fingerprint(repeat);if(!dialog.open||!dialog.isConnected||before!==capture()||original!==K.canonical(project))throw Error("Диалог закрыт либо форма или проект изменились; повтор не внесён.");condition.повторы.push(repeat);dialog.close();dialog.remove();render();
        }catch(e){showError("Повтор не внесён: "+e.message);}finally{busy=false;}
      });
      dialog.addEventListener("close", () => dialog.remove(), { once: true });
    };
    const capture = () => { project.предмет = clean($("#pair-project")?.value); project.запрос = clean($("#pair-query")?.value); project.переменная = clean($("#pair-variable")?.value); project.цель_повторов = Math.max(1, Number($("#pair-target")?.value) || 5); };
    const syncInputs = () => { $("#pair-project").value = project.предмет || ""; $("#pair-query").value = project.запрос || ""; $("#pair-variable").value = project.переменная || ""; $("#pair-target").value = project.цель_повторов || 5; };
    pairForm.addEventListener("submit", (event) => { event.preventDefault(); capture(); if (!project.условия.length) project.условия = [{ название: "чистый сеанс", повторы: [] }, { название: "изменённое условие", повторы: [] }]; render(); });
    $("#pair-add-condition")?.addEventListener("click", () => { capture(); const name = prompt("Название нового условия"); if (clean(name)) project.условия.push({ название: clean(name), повторы: [] }); render(); });
    $("#pair-export")?.addEventListener("click", () => { capture(); save("матрица-парного-теста.json", JSON.stringify(project, null, 2), "application/json;charset=utf-8"); });
    $("#pair-export-text")?.addEventListener("click", () => save("матрица-парного-теста.txt", reportText()));
    $("#pair-import")?.addEventListener("change", async (event) => { const previous=project;try { const expected=K.canonical(project),stamp=JSON.stringify($$("input,textarea,select",pairForm).map(x=>[x.id,x.value,x.checked]));const data=await readJson(event.target.files[0],"pair");if(expected!==K.canonical(project)||stamp!==JSON.stringify($$("input,textarea,select",pairForm).map(x=>[x.id,x.value,x.checked])))throw Error("Проект или форма изменились во время импорта."); project = data; syncInputs(); render(); } catch (e) { showError("Импорт матрицы отклонён; рабочий проект не заменён. "+e.message); } event.target.value = ""; });
    $("#pair-reset")?.addEventListener("click", () => { if (!confirm("Удалить локальный проект и все повторы?")) return;try{localStorage.removeItem(key);}catch{showError("Очистка не выполнена: хранилище недоступно. Рабочие данные не удалены.");return;}project = { схема: "война.com/матрица-парного-теста/2", редакция: "4.9.0-wip", предмет: "", запрос: "", переменная: "", цель_повторов: 5, условия: [] }; pairForm.reset(); render(); });
    $("#pair-save-project")?.addEventListener("click",async()=>{capture();try{const checked=await validateProject(project,"pair");localStorage.setItem(key,JSON.stringify(checked));$("#pair-project-status").textContent="Проект записан в хранилище этого браузера. Это не публикация и не файл на диске.";}catch(e){showError("Сохранение не выполнено. "+e.message);}});
    window.addEventListener("beforeunload", () => { capture();try{localStorage.setItem(key, JSON.stringify(project));}catch{showError("Автосохранение не выполнено. Экспортируйте файл перед закрытием.");} });
    syncInputs(); render();if(storageError)showError(storageError);
  }

  // Конструктор свидетельского пакета.
  const witnessForm = $("#witness-form");
  if (witnessForm) {
    let sourceHash = "", derivativeHash = "";
    const timeline = $("#witness-timeline"), provenance = $("#witness-provenance");
    const row = (kind) => {
      const wrap = document.createElement("div"); wrap.className = "repeat-row";
      if (kind === "event") wrap.innerHTML = '<label class="field"><span>Дата</span><input data-event-date type="date"></label><label class="field grow"><span>Событие</span><input data-event-text placeholder="Получение, копирование, проверка, публикация"></label><button class="button small ghost" data-row-remove type="button">Удалить</button>';
      else wrap.innerHTML = '<label class="field grow"><span>Утверждение</span><input data-claim-text></label><label class="field grow"><span>Источник или адрес</span><input data-claim-source></label><label class="field"><span>Условие опровержения</span><input data-claim-boundary></label><button class="button small ghost" data-row-remove type="button">Удалить</button>';
      $("[data-row-remove]", wrap).addEventListener("click", () => wrap.remove()); return wrap;
    };
    const gather = () => {
      const events = $$(".repeat-row", timeline).map((x) => ({ дата: clean($("[data-event-date]", x)?.value), событие: clean($("[data-event-text]", x)?.value) })).filter((x) => x.дата || x.событие);
      const claims = $$(".repeat-row", provenance).map((x) => ({ утверждение: clean($("[data-claim-text]", x)?.value), источник: clean($("[data-claim-source]", x)?.value), условие_опровержения: clean($("[data-claim-boundary]", x)?.value) })).filter((x) => x.утверждение || x.источник);
      return { схема: "война.com/свидетельский-пакет/1", редакция: "4.9.0-wip", дата_сборки: new Date().toISOString(), исходная_запись: { название: clean($("#witness-source-title")?.value), дата: clean($("#witness-source-date")?.value), происхождение: clean($("#witness-source-origin")?.value), имя_файла: $("#witness-source-file")?.files[0]?.name || "", отпечаток_ша_256_десятичный: sourceHash }, публикационная_производная: { название: clean($("#witness-derivative-title")?.value), описание_изменений: clean($("#witness-derivative-changes")?.value), имя_файла: $("#witness-derivative-file")?.files[0]?.name || "", отпечаток_ша_256_десятичный: derivativeHash }, хронология: events, минимизация_частных_данных: { удалены_избыточные_имена: Boolean($("#witness-private-names")?.checked), удалены_точные_адреса: Boolean($("#witness-private-addresses")?.checked), удалены_личные_контакты: Boolean($("#witness-private-contacts")?.checked), сохранены_необходимые_доказательства: Boolean($("#witness-evidence-kept")?.checked) }, карта_происхождения: claims, правило: "Исходник не изменяется. Публикационная копия маркируется как производная и получает отдельный отпечаток." };
    };
    const reportText = () => {
      const d = gather(); return ["война.com — СВИДЕТЕЛЬСКИЙ ПАКЕТ", `Редакция: 4.9.0-wip — незавершённый выпуск`, `Дата сборки: ${d.дата_сборки}`, "", "ИСХОДНАЯ ЗАПИСЬ", `Название: ${plain(d.исходная_запись.название)}`, `Дата: ${plain(d.исходная_запись.дата)}`, `Происхождение: ${plain(d.исходная_запись.происхождение)}`, `Файл: ${plain(d.исходная_запись.имя_файла)}`, `Отпечаток: ${plain(d.исходная_запись.отпечаток_ша_256_десятичный)}`, "", "ПУБЛИКАЦИОННАЯ ПРОИЗВОДНАЯ", `Название: ${plain(d.публикационная_производная.название)}`, `Изменения: ${plain(d.публикационная_производная.описание_изменений)}`, `Файл: ${plain(d.публикационная_производная.имя_файла)}`, `Отпечаток: ${plain(d.публикационная_производная.отпечаток_ша_256_десятичный)}`, "", "ХРОНОЛОГИЯ", ...d.хронология.map((x) => `${plain(x.дата)} — ${plain(x.событие)}`), "", "МИНИМИЗАЦИЯ ЧАСТНЫХ ДАННЫХ", ...Object.entries(d.минимизация_частных_данных).map(([k, v]) => `${k.replaceAll("_", " ")}: ${v ? "да" : "нет"}`), "", "КАРТА ПРОИСХОЖДЕНИЯ", ...d.карта_происхождения.flatMap((x, i) => [`${i + 1}. ${plain(x.утверждение)}`, `Источник: ${plain(x.источник)}`, `Условие опровержения: ${plain(x.условие_опровержения)}`]), "", "ПРЕДЕЛ", "Отпечаток доказывает тождество байтов, а не истинность содержания. Производная не является исходником.", "", "война.com · РЕБЕЛ ОР ДАЙ"].join("\n");
    };
    const render = () => {
      const d = gather(); $("#witness-report").value = reportText(); $("#witness-cover-title").textContent = d.исходная_запись.название || "Название исходной записи";
      $("#witness-cover-data").innerHTML = `<div><dt>Редакция</dt><dd>4.9.0-wip — незавершённый выпуск</dd></div><div><dt>Исходный отпечаток</dt><dd class="break-all">${safe(sourceHash)}</dd></div><div><dt>Производная</dt><dd>${safe(d.публикационная_производная.название)}</dd></div><div><dt>Отпечаток производной</dt><dd class="break-all">${safe(derivativeHash)}</dd></div><div><dt>Хронология</dt><dd>${d.хронология.length} записей</dd></div><div><dt>Утверждения</dt><dd>${d.карта_происхождения.length}</dd></div>`;
    };
    $("#witness-source-file")?.addEventListener("change", async (event) => { const file = event.target.files[0]; sourceHash = file ? await fingerprintFile(file) : ""; $("#witness-source-hash").textContent = sourceHash ? `SHA-256, десятичная запись: ${sourceHash}` : "Отпечаток ещё не рассчитан."; render(); });
    $("#witness-derivative-file")?.addEventListener("change", async (event) => { const file = event.target.files[0]; derivativeHash = file ? await fingerprintFile(file) : ""; $("#witness-derivative-hash").textContent = derivativeHash ? `SHA-256, десятичная запись: ${derivativeHash}` : "Отпечаток ещё не рассчитан."; render(); });
    $("#witness-add-event")?.addEventListener("click", () => timeline.appendChild(row("event")));
    $("#witness-add-claim")?.addEventListener("click", () => provenance.appendChild(row("claim")));
    witnessForm.addEventListener("input", render); witnessForm.addEventListener("change", render); witnessForm.addEventListener("submit", (event) => { event.preventDefault(); render(); $("#witness-cover")?.scrollIntoView({ behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" }); });
    const assembleWitnessZip = async (publicOnly) => {
      render();
      const data = gather(), source = $("#witness-source-file")?.files[0], derivative = $("#witness-derivative-file")?.files[0];
      if (!derivative) { alert("Добавьте файл публикационной производной."); return; }
      if (!publicOnly && !source) { alert("Для полного пакета добавьте исходный файл."); return; }
      const entries = [
        { name: "ведомость/свидетельский-пакет.json", bytes: JSON.stringify(data, null, 2) + "\n" },
        { name: "ведомость/свидетельский-пакет.txt", bytes: reportText() + "\n" },
        { name: "ПРОЧТИ-МЕНЯ.txt", bytes: publicOnly ? "ПУБЛИКАЦИОННАЯ КОПИЯ. ИСХОДНЫЙ ФАЙЛ НАМЕРЕННО ИСКЛЮЧЁН. ПРОИЗВОДНАЯ НЕ ЯВЛЯЕТСЯ ИСХОДНИКОМ.\n" : "ПОЛНЫЙ СВИДЕТЕЛЬСКИЙ ПАКЕТ. ИСХОДНИК СОХРАНЁН БЕЗ ИЗМЕНЕНИЯ. ЛЮБАЯ ПУБЛИКАЦИОННАЯ КОПИЯ ЯВЛЯЕТСЯ ОТДЕЛЬНОЙ ПРОИЗВОДНОЙ.\n" },
        { name: `производная/${zipName(derivative.name, "производная.bin")}`, bytes: new Uint8Array(await derivative.arrayBuffer()) },
      ];
      if (!publicOnly) entries.push({ name: `исходник/${zipName(source.name, "исходник.bin")}`, bytes: new Uint8Array(await source.arrayBuffer()) });
      const blob = buildZip(entries);
      saveBlob(publicOnly ? "свидетельский-пакет-публикационная-копия.zip" : "свидетельский-пакет-полный.zip", blob);
    };
    $("#witness-full-package")?.addEventListener("click", () => assembleWitnessZip(false));
    $("#witness-public-package")?.addEventListener("click", () => assembleWitnessZip(true));
    $("#witness-export-json")?.addEventListener("click", () => save("свидетельский-пакет.json", JSON.stringify(gather(), null, 2), "application/json;charset=utf-8"));
    $("#witness-export-text")?.addEventListener("click", () => save("свидетельский-пакет.txt", reportText()));
    $("#witness-print")?.addEventListener("click", () => { document.body.classList.add("print-witness"); window.print(); setTimeout(() => document.body.classList.remove("print-witness"), 300); });
    witnessForm.addEventListener("reset", () => setTimeout(() => { sourceHash = ""; derivativeHash = ""; timeline.replaceChildren(); provenance.replaceChildren(); timeline.appendChild(row("event")); provenance.appendChild(row("claim")); $("#witness-source-hash").textContent = "Отпечаток ещё не рассчитан."; $("#witness-derivative-hash").textContent = "Отпечаток ещё не рассчитан."; render(); }, 0));
    timeline.appendChild(row("event")); provenance.appendChild(row("claim")); render();
  }
})();
