/* война.com. Единое ядро доказательной методики; браузер и проверочные сценарии.
   Никакой сетевой передачи пользовательских записей. Не является правовым заключением. */
(() => {
  'use strict';
  const RELEASE = '4.10.0-wip';
  const SOURCE_LIMIT = 100;
  const TOTAL_LIMIT = 25 * 1024 * 1024;
  // Непубличные метки происхождения объектов; не сериализуются и не заменяют отпечаток байтов.
  const sourceTrust = new WeakMap();
  const resolutionTrust = new WeakMap();
  const remember = (map, value) => { map.set(value, canonical(value)); return value; };
  const resolutionContext = new WeakMap();
  const revocationTrust = new WeakMap();
  const migrationTrust = new WeakSet();
  function freeze(value, seen=new WeakSet(), depth=0) {
    if(depth>40)throw Error('Превышена глубина фиксируемого объекта: 40.');
    if(value && typeof value==='object') {
      if(seen.has(value))return value;seen.add(value);
      for(const v of Object.values(value))freeze(v,seen,depth+1);
      Object.freeze(value);
    }
    return value;
  }
  const trusted = value => { freeze(value);sourceTrust.set(value,true);return value; };
  const STATES = ['не проверено', 'подтверждено', 'частично', 'провалено', 'не применимо'];
  const CLASSES = ['ДФ', 'НП', 'НО', 'АВ', 'ЗТ', 'ТВ', 'ИС', 'ПР'];
  const HASH = /^[a-f0-9]{64}$(?![\s\S])/;
  const LIMIT = 5 * 1024 * 1024;
  const cmp = (a,b) => a < b ? -1 : a > b ? 1 : 0;
  let schemas = {};
  const configureSchemas = value => { const next = structuredClone(value); for (const [name, schema] of Object.entries(next)) { const issues = schemaErrors(schema); if (issues.length) throw Error(name + ': ' + issues.join('\n')); } schemas = next; };
  const utf8 = x => new TextEncoder().encode(x);
  const text = x => typeof x === 'string' && x.trim().length > 0;
  const date = x => typeof x === 'string' && x.length===10 && /^(?!0000)\d{4}-\d{2}-\d{2}$/.test(x) && Number.isFinite(Date.parse(x)) && new Date(x).toISOString().slice(0, 10) === x;
  const time = x => typeof x === 'string' && !/[\r\n]$/.test(x) && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(x) && date(x.slice(0,10)) && Number.isFinite(Date.parse(x)) && x.slice(11,13) < '24' && x.slice(14,16) < '60' && x.slice(17,19) < '60';
  // Единая токенизация для строки, сравнения и SHA-256. Порядок 4.7.0 сохранён.
  // Промежуточные массивы символов и второй объектный граф не создаются.
  function* canonicalTokens(value) {
    let nodes=0; const active=new WeakSet();
    function* walk(v,depth,array=false){
      if(depth>40||++nodes>1000000)throw Error('Предел канонизации: вложенность 40, не более 1 000 000 значений.');
      if(v===null||typeof v!=='object'){
        if(typeof v==='number'&&!Number.isFinite(v))throw Error('Недопустимое число.');
        const token=JSON.stringify(v);yield token===undefined&&array?'null':token??'';return;
      }
      if(active.has(v))throw Error('Циклический объект не является JSON.');active.add(v);
      if(Array.isArray(v)){yield '[';for(let i=0;i<v.length;i++){if(i)yield ',';yield* walk(v[i],depth+1,true);}yield ']';}
      else {yield '{';let first=true;for(const k of Object.keys(v).sort((a,b)=>{const ai=String(a>>>0)===a&&a!=='4294967295',bi=String(b>>>0)===b&&b!=='4294967295';return ai&&bi?Number(a)-Number(b):ai?-1:bi?1:cmp(a,b);})){
        if(v[k]===undefined||typeof v[k]==='function'||typeof v[k]==='symbol')continue;
        if(!first)yield ',';first=false;yield JSON.stringify(k);yield ':';yield* walk(v[k],depth+1);
      }yield '}';}active.delete(v);
    }
    yield* walk(value,0);
  }
  function utf8Size(value,limit=Infinity){
    let n=0;
    for(let i=0;i<value.length;i++){
      const c=value.charCodeAt(i);
      if(c<128)n++;else if(c<2048)n+=2;
      else if(c>=0xd800&&c<=0xdbff&&i+1<value.length&&value.charCodeAt(i+1)>=0xdc00&&value.charCodeAt(i+1)<=0xdfff){n+=4;i++;}
      else n+=3;
      if(n>limit)return n;
    }
    return n;
  }
  function codePointLength(value,stop=Infinity){
    let n=0;for(let i=0;i<value.length;i++){if(value.codePointAt(i)>0xffff)i++;if(++n>stop)return n;}return n;
  }
  const canonical = value => {
    const chunks=[];let pending=[],size=0;
    for(const token of canonicalTokens(value)){pending.push(token);size+=token.length;if(size>=65536){chunks.push(pending.join(''));pending=[];size=0;}}
    if(pending.length)chunks.push(pending.join(''));return chunks.join('');
  };
  // Два ограниченных прохода дают один буфер digest без полной строки и её копии UTF-8.
  // 128 МиБ — предел представления, не гарантия RSS или мобильной пригодности.
  const CANONICAL_BYTES_LIMIT=128*1024*1024;
  function canonicalBytes(value,limit=CANONICAL_BYTES_LIMIT){
    let n=0;for(const t of canonicalTokens(value)){n+=utf8Size(t,limit-n);if(n>limit)throw Error('Каноническое представление превышает 128 МиБ.');}
    const bytes=new Uint8Array(n),encoder=new TextEncoder();let offset=0;
    for(const t of canonicalTokens(value)){const r=encoder.encodeInto(t,bytes.subarray(offset));if(r.read!==t.length)throw Error('Представление изменилось во время кодирования.');offset+=r.written;}
    if(offset!==n)throw Error('Размер представления изменился во время кодирования.');return bytes;
  }
  // Сравнение переданных байтов с записью тем же генератором; без второй полной строки.
  function canonicalBytesEqual(value,input){
    if(!(input instanceof ArrayBuffer)&&!ArrayBuffer.isView(input))return false;
    const bytes=input instanceof ArrayBuffer?new Uint8Array(input):new Uint8Array(input.buffer,input.byteOffset,input.byteLength),encoder=new TextEncoder();let offset=0;
    for(const token of canonicalTokens(value)){
      for(let pos=0;pos<token.length;){let end=Math.min(pos+16384,token.length);
        if(end<token.length&&token.charCodeAt(end-1)>=0xd800&&token.charCodeAt(end-1)<=0xdbff&&token.charCodeAt(end)>=0xdc00&&token.charCodeAt(end)<=0xdfff)end--;
        const chunk=encoder.encode(token.slice(pos,end));if(offset+chunk.length>bytes.length)return false;
        for(let j=0;j<chunk.length;j++)if(bytes[offset+j]!==chunk[j])return false;offset+=chunk.length;pos=end;
      }
    }
    return offset===bytes.length;
  }
  const hashCanonical=value=>hash(canonicalBytes(value));
  const hash = async x => {
    if (!globalThis.crypto?.subtle) throw Error('Контрольный отпечаток требует защищённого соединения HTTPS или локального сервера.');
    const b = await crypto.subtle.digest('SHA-256', typeof x === 'string' ? utf8(x) : x);
    return [...new Uint8Array(b)].map(n => n.toString(16).padStart(2, '0')).join('');
  };
  const payload = x => { const y = {...x}; delete y.ша256; return y; };
  function snapshotPayload(x){
    const body=payload(x);
    // Только прошедшие ingest и глубоко замороженные источники можно разделять.
    // Произвольный Object.freeze не является основанием для обхода снимка.
    if(Array.isArray(body.источники)&&body.источники.every(s=>sourceTrust.get(s)===true)){
      const sources=body.источники.slice();delete body.источники;return {...structuredClone(body),источники:sources};
    }
    return structuredClone(body);
  }
  const seal = async (x,options={}) => { if(options.signal?.aborted)throw new DOMException('Обработка отменена.','AbortError');const body=snapshotPayload(x);await stage(options,'снимок');const sha=await hashCanonical(body);await stage(options,'отпечаток');return {...body,ша256:sha}; };
  const base64 = bytes => {
    let s = ''; const a = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
    for (let i = 0; i < a.length; i += 32768) s += String.fromCharCode(...a.subarray(i, i + 32768));
    return btoa(s);
  };
  const unbase64 = (s, limit=LIMIT) => {
    if(typeof s!=='string'||!Number.isSafeInteger(limit)||limit<0||s.length%4!==0||s.length>Math.ceil(limit/3)*4||!/^[A-Za-z0-9+/]*={0,2}$/.test(s))throw Error('Недопустимое вложение в кодировке Base64.');
    const pad=s.endsWith('==')?2:s.endsWith('=')?1:0,n=s.length/4*3-pad;
    if(n>limit)throw Error('Превышен разрешённый размер Base64 до декодирования.');
    const alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    if(pad&&((alphabet.indexOf(s[s.length-pad-1])& (pad===2?15:3))!==0))throw Error('Неканоническая кодировка Base64.');
    const binary=atob(s),bytes=new Uint8Array(n);
    if(binary.length!==n)throw Error('Размер Base64 не совпал.');
    for(let i=0;i<n;i++)bytes[i]=binary.charCodeAt(i);
    return bytes;
  };
  // Эквивалентность канонического JSON без строкового представления всего объекта.
  // Порядок свойств незначим; порядок массива, пропуски/undefined и -0 — как в canonical.
  function same(a,b){
    let nodes=0;const left=new WeakSet(),right=new WeakSet();
    const keys=v=>Object.keys(v).filter(k=>v[k]!==undefined&&typeof v[k]!=='function'&&typeof v[k]!=='symbol');
    function eq(x,y,depth,array=false){
      if(depth>40||++nodes>1000000)throw Error('Предел сравнения JSON: вложенность 40, 1 000 000 значений.');
      const xo=x!==null&&typeof x==='object',yo=y!==null&&typeof y==='object';
      if(!xo||!yo){
        if(xo!==yo)return false;
        for(const v of [x,y])if(typeof v==='number'&&!Number.isFinite(v))throw Error('Недопустимое число.');
        const norm=v=>v===undefined||typeof v==='function'||typeof v==='symbol'?(array?null:undefined):v;
        return norm(x)===norm(y);
      }
      if(left.has(x)||right.has(y))throw Error('Циклический объект не является JSON.');
      if(Array.isArray(x)!==Array.isArray(y))return false;
      left.add(x);right.add(y);let ok=true;
      if(Array.isArray(x)){ok=x.length===y.length;for(let i=0;ok&&i<x.length;i++)ok=eq(x[i],y[i],depth+1,true);}
      else {const k=keys(x),l=keys(y);ok=k.length===l.length;for(const name of k){if(!ok)break;ok=Object.hasOwn(y,name)&&y[name]!==undefined&&typeof y[name]!=='function'&&typeof y[name]!=='symbol'&&eq(x[name],y[name],depth+1);}}
      left.delete(x);right.delete(y);return ok;
    }
    return eq(a,b,0);
  }
  const safeURL = value => { try { const u = new URL(value); return ['https:', 'http:'].includes(u.protocol) && !u.username && !u.password; } catch { return false; } };
  // Единый лексический контроль перед JSON.parse. Значения строк не декодируются
  // во временные объекты; только имена ключей нужны для проверки повторов.
  function inspectJSON(raw, limit=LIMIT, budget=null) {
    if(typeof raw!=='string'||!Number.isSafeInteger(limit)||limit<0||utf8Size(raw,limit)>limit)throw Error(`Размер JSON превышает ${limit} байт.`);
    let p=raw.charCodeAt(0)===0xfeff?1:0,values=0,keys=0,units=0,maximumDepth=0;
    const fail=message=>{let line=1;for(let i=0;i<p;i++)if(raw.charCodeAt(i)===10)line++;throw Error(`Строка ${line}, позиция ${p+1}: ${message}`);};
    const ws=()=>{while(p<raw.length&&[32,9,10,13].includes(raw.charCodeAt(p)))p++;};
    const charge=(n=0)=>{units+=n;if(budget&&(units>budget.stringUnits||values+keys>budget.nodes))fail('Ограничительный профиль: расширение JSON превышает число узлов или строк до полного разбора.');};
    const decimalIdentity=token=>{let [mantissa,exponent='0']=token.toLowerCase().split('e');let sign='';if(mantissa.startsWith('-')){sign='-';mantissa=mantissa.slice(1);}const [whole,fraction='']=mantissa.split('.');let digits=(whole+fraction).replace(/^0+/,'');let power=Number(exponent)-fraction.length;if(!digits)return '0';const zeros=/0*$/.exec(digits)[0].length;if(zeros){digits=digits.slice(0,-zeros);power+=zeros;}return sign+digits+'e'+power;};
    function string(key=false){
      if(raw.charCodeAt(p)!==34)fail('ожидается строка в двойных кавычках.');
      const start=p++;let high=false,length=0;
      while(p<raw.length){
        let c=raw.charCodeAt(p++);
        if(c===34){if(high)fail('непарный суррогат Unicode.');charge(length);return key?JSON.parse(raw.slice(start,p)):null;}
        if(c<32)fail('управляющий знак внутри строки JSON.');
        if(c===92){
          if(p>=raw.length)fail('незавершённая escape-последовательность.');
          const e=raw[p++];
          if(e==='u'){const hex=raw.slice(p,p+4);if(!/^[0-9a-fA-F]{4}$/.test(hex))fail('недопустимая escape-последовательность Unicode.');c=parseInt(hex,16);p+=4;}
          else {const escapes={'"':34,'\\':92,'/':47,b:8,f:12,n:10,r:13,t:9};if(!Object.hasOwn(escapes,e))fail('недопустимая escape-последовательность.');c=escapes[e];}
        }
        if(high){if(c<0xdc00||c>0xdfff)fail('непарный суррогат Unicode.');high=false;}
        else if(c>=0xd800&&c<=0xdbff)high=true;
        else if(c>=0xdc00&&c<=0xdfff)fail('непарный суррогат Unicode.');
        length++;if(budget&&units+length>budget.stringUnits)fail('Ограничительный профиль: строка превышает бюджет до декодирования.');
      }
      fail('незавершённая строка JSON.');
    }
    function walk(depth){
      if(depth>40||++values>1000000)fail('предел JSON: вложенность 40, не более 1 000 000 значений.');
      if(budget&&depth>budget.depth)fail('Ограничительный профиль: глубина JSON.');
      maximumDepth=Math.max(maximumDepth,depth);charge();ws();const c=raw[p];
      if(c==='{'){
        p++;ws();const seen=new Set();if(raw[p]==='}'){p++;return;}
        for(;;){ws();const k=string(true);keys++;charge();if(seen.has(k))fail(`повтор ключа «${k.slice(0,120)}».`);if(['__proto__','constructor','prototype'].includes(k))fail('недопустимый служебный ключ.');seen.add(k);ws();if(raw[p++]!==':')fail('ожидается двоеточие.');walk(depth+1);ws();const end=raw[p++];if(end==='}')return;if(end!==',')fail('ожидается запятая или закрывающая фигурная скобка.');}
      }
      if(c==='['){p++;ws();if(raw[p]===']'){p++;return;}for(;;){walk(depth+1);ws();const end=raw[p++];if(end===']')return;if(end!==',')fail('ожидается запятая или закрывающая квадратная скобка.');}}
      if(c==='"'){string();return;}
      for(const literal of ['true','false','null'])if(raw.startsWith(literal,p)){p+=literal.length;return;}
      const start=p;while(p<raw.length&&!/[\s,\]}]/.test(raw[p])){p++;if(p-start>256)fail('числовая лексема превышает 256 знаков.');}
      const token=raw.slice(start,p);if(!/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/.test(token))fail('недопустимое значение JSON.');
      const n=Number(token);if(Object.is(n,-0))fail('отрицательный ноль не входит в числовой профиль.');if(!Number.isFinite(n)||Math.abs(n)>Number.MAX_SAFE_INTEGER)fail('число вне безопасного диапазона IEEE-754.');if(decimalIdentity(token)!==decimalIdentity(String(n)))fail('десятичная запись потеряла бы значащие цифры; округление не принято.');
    }
    walk(0);ws();if(p!==raw.length)fail('посторонние данные после JSON.');
    return {значений:values,ключей:keys,единиц_строк:units,глубина:maximumDepth,граница:'Лексический допуск до полного разбора, не предметная схема и не гарантия RSS.'};
  }
  function parse(raw,limit=LIMIT,budget=null){inspectJSON(raw,limit,budget);return JSON.parse(raw.charCodeAt(0)===0xfeff?raw.slice(1):raw);}
  // Поддерживаемый профиль схем проверяется целиком, в том числе невыбранные ветви anyOf.
  const SCHEMA_KEYS = new Set(['$schema','$id','$comment','title','description','type','properties','required','additionalProperties','items','anyOf','const','enum','pattern','format','minLength','maxLength','minItems','maxItems','uniqueItems','minimum','maximum','minProperties','maxProperties']);
  const SCHEMA_PATTERNS = new Set(["Z$", "[^\\s\\u001c-\\u001f\\u0085\\ufeff]", "^(?!0000)[0-9]{4}-[0-9]{2}-[0-9]{2}$(?![\\s\\S])", "^(?!0000)[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{1,3})?Z$(?![\\s\\S])", "^[A-Za-z0-9А-Яа-яЁё_-]{1,100}$(?![\\s\\S])", "^[a-f0-9]{64}$", "^[a-f0-9]{64}$(?![\\s\\S])"]);
  SCHEMA_PATTERNS.add("^(?!0000)[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{1,3})?(?:Z|[+-][0-9]{2}:[0-9]{2})$(?![\\s\\S])");
  function schemaErrors(schema, path='$схема', depth=0, active=new WeakSet()) {
    if(depth>40)return [`${path}: превышена глубина схемы 40.`];
    if(typeof schema==='boolean')return [];
    if(!schema||Array.isArray(schema)||typeof schema!=='object')return [`${path}: требуется объект или логическая схема.`];
    if(active.has(schema))return [`${path}: циклическая схема не поддерживается.`];active.add(schema);
    const errors=[],plain=x=>x&&typeof x==='object'&&!Array.isArray(x);
    for(const key of Object.keys(schema))if(!SCHEMA_KEYS.has(key))errors.push(`${path}: неподдерживаемое правило схемы ${key}.`);
    for(const key of ['$schema','$id','$comment','title','description'])if(Object.hasOwn(schema,key)&&typeof schema[key]!=='string')errors.push(`${path}: ${key} должно быть строкой.`);
    if(Object.hasOwn(schema,'format')&&!['date','date-time'].includes(schema.format))errors.push(`${path}: неподдерживаемый формат ${String(schema.format)}.`);
    if(Object.hasOwn(schema,'type')){
      const types=Array.isArray(schema.type)?schema.type:[schema.type];
      if(!types.length||new Set(types).size!==types.length||!types.every(t=>['null','array','object','integer','number','string','boolean'].includes(t)))errors.push(`${path}: недопустимый набор типов.`);
    }
    if(Object.hasOwn(schema,'pattern')&&!SCHEMA_PATTERNS.has(schema.pattern))errors.push(`${path}: неподдерживаемое выражение pattern; нужен явный разбор и сопоставление валидаторов.`);
    for(const key of ['minLength','maxLength','minItems','maxItems','minProperties','maxProperties'])if(Object.hasOwn(schema,key)&&(!Number.isSafeInteger(schema[key])||schema[key]<0))errors.push(`${path}: ${key} требует неотрицательное безопасное целое.`);
    for(const key of ['minimum','maximum'])if(Object.hasOwn(schema,key)&&(typeof schema[key]!=='number'||!Number.isFinite(schema[key])))errors.push(`${path}: ${key} требует конечное число.`);
    if(Object.hasOwn(schema,'uniqueItems')&&typeof schema.uniqueItems!=='boolean')errors.push(`${path}: uniqueItems требует логическое значение.`);
    if(Object.hasOwn(schema,'required')&&(!Array.isArray(schema.required)||!schema.required.every(k=>typeof k==='string')||new Set(schema.required).size!==schema.required.length))errors.push(`${path}: required требует массив разных строк.`);
    if(Object.hasOwn(schema,'enum')&&(!Array.isArray(schema.enum)||!schema.enum.length||new Set(schema.enum.map(canonical)).size!==schema.enum.length))errors.push(`${path}: enum требует непустой массив разных значений.`);
    if(Object.hasOwn(schema,'properties')){
      if(!plain(schema.properties))errors.push(`${path}: properties требует объект.`);
      else for(const [k,v] of Object.entries(schema.properties))errors.push(...schemaErrors(v,path+'.properties.'+k,depth+1,active));
    }
    for(const k of ['additionalProperties','items'])if(Object.hasOwn(schema,k))errors.push(...schemaErrors(schema[k],path+'.'+k,depth+1,active));
    if(Object.hasOwn(schema,'anyOf')){
      if(!Array.isArray(schema.anyOf)||!schema.anyOf.length)errors.push(`${path}: anyOf требует непустой массив схем.`);
      else for(const v of schema.anyOf)errors.push(...schemaErrors(v,path+'.anyOf',depth+1,active));
    }
    active.delete(schema);return errors;
  }
  const formatTime = value => {
    const m=/^(\d{4}-\d{2}-\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/.exec(value);
    return !!m && !/[\r\n]$/.test(value) && date(m[1]) && +m[2]<24 && +m[3]<60 && +m[4]<60 && (!m[5] || (+m[6]<24 && +m[7]<60));
  };
  function validate(schema,value,path='$') {
    const errors=schemaErrors(schema);
    return errors.length?errors:validateValue(schema,value,path);
  }
  function validateValue(schema, value, path = '$') {
    const errors=[];
    if (schema === true) return errors;
    if (schema === false) return [`${path}: значение запрещено схемой.`];
    if(schema.anyOf && !schema.anyOf.some(s=>validateValue(s,value,path).length===0))errors.push(`${path}: ни одна допустимая форма значения не подошла.`);
    if('const' in schema && !same(value,schema.const))errors.push(`${path}: неверное фиксированное значение.`);
    if(schema.enum && !schema.enum.some(x=>same(x,value)))errors.push(`${path}: неизвестное значение словаря.`);
    const type=schema.type;
    const isType=t=>t==='null'?value===null:t==='array'?Array.isArray(value):t==='object'?value!==null && typeof value==='object' && !Array.isArray(value):t==='integer'?Number.isInteger(value):t==='number'?typeof value==='number' && Number.isFinite(value):typeof value===t;
    const ok=!type || (Array.isArray(type)?type.some(isType):isType(type));
    if(!ok)return errors.concat(`${path}: ожидается тип ${type}.`);
    if(typeof value==='string') {
      const length=(schema.minLength!==undefined||schema.maxLength!==undefined)?codePointLength(value,schema.maxLength??Infinity):0;
      if(schema.minLength!==undefined && length<schema.minLength)errors.push(`${path}: строка слишком короткая.`);
      if(schema.maxLength!==undefined && length>schema.maxLength)errors.push(`${path}: строка слишком длинная.`);
      if(schema.pattern && !new RegExp(schema.pattern,'u').test(value))errors.push(`${path}: строка не соответствует формату.`);
      if(schema.format==='date'&&!date(value))errors.push(`${path}: недействительная дата.`);
      if(schema.format==='date-time'&&!formatTime(value))errors.push(`${path}: недействительное время UTC.`);
    }
    if(typeof value==='number') {
      if(!Number.isFinite(value))errors.push(`${path}: недопустимое число.`);
      if(schema.minimum!==undefined && value<schema.minimum)errors.push(`${path}: число ниже минимума.`);
      if(schema.maximum!==undefined && value>schema.maximum)errors.push(`${path}: число выше максимума.`);
    }
    if(Array.isArray(value)) {
      if(schema.minItems!==undefined&&value.length<schema.minItems)errors.push(`${path}: недостаточно элементов.`);
      if(schema.maxItems!==undefined&&value.length>schema.maxItems)errors.push(`${path}: слишком много элементов.`);
      if(schema.uniqueItems&&new Set(value.map(canonical)).size!==value.length)errors.push(`${path}: повтор элемента.`);
      if(Object.hasOwn(schema,'items'))value.forEach((x,i)=>errors.push(...validateValue(schema.items,x,`${path}[${i}]`)));
    } else if(value && typeof value==='object') {
      for(const k of schema.required||[])if(!Object.hasOwn(value,k))errors.push(`${path}.${k}: обязательное поле отсутствует.`);
      if(schema.maxProperties!==undefined && Object.keys(value).length>schema.maxProperties)errors.push(`${path}: слишком много полей.`);
      if(schema.minProperties!==undefined && Object.keys(value).length<schema.minProperties)errors.push(`${path}: недостаточно полей.`);
      for(const [key,x] of Object.entries(value)) {
        if(schema.properties && Object.hasOwn(schema.properties,key))errors.push(...validateValue(schema.properties[key],x,`${path}.${key}`));
        else if(schema.additionalProperties===false)errors.push(`${path}.${key}: неизвестное поле.`);
        else if(typeof schema.additionalProperties==='object')errors.push(...validateValue(schema.additionalProperties,x,`${path}.${key}`));
      }
    }
    return errors;
  }
  function exactKeys(x, allowed, prefix, errors) {
    if (!x || Array.isArray(x) || typeof x !== 'object') { errors.push(`${prefix}: требуется объект.`); return false; }
    for (const k of Object.keys(x)) if (!allowed.includes(k)) errors.push(`${prefix}.${k}: неизвестное поле.`);
    return true;
  }
  function auditErrors(a, schema) {
    const e = [];
    if (!a || typeof a !== 'object' || Array.isArray(a)) return ['Корень записи: требуется объект.'];
    const old = a.редакция_схемы === '4.2.0-wip';
    const selected = a.редакция_схемы === '4.3.0-wip' ? schemas.audit430 : a.редакция_схемы === '4.4.0-wip' ? schemas.audit440 : a.редакция_схемы === '4.5.0-wip' ? schemas.audit450 : a.редакция_схемы === '4.6.0-wip' ? schemas.audit460 : a.редакция_схемы === '4.7.0-wip' ? schemas.audit470 : a.редакция_схемы === '4.8.0-wip' ? schemas.audit480 : a.редакция_схемы === '4.9.0-wip' ? schemas.audit490 : schemas.audit;
    if (!old && selected) e.push(...validate(selected,a));
    if (!old && !selected) e.push('Схема этой редакции не загружена; импорт заблокирован.');
    if (!old && ![RELEASE,'4.3.0-wip','4.4.0-wip','4.5.0-wip','4.6.0-wip','4.7.0-wip','4.8.0-wip','4.9.0-wip'].includes(a.редакция_схемы)) e.push('редакция_схемы: неподдерживаемая редакция.');
    if (!old) exactKeys(a, ['редакция_схемы','идентификатор','дата_создания','проверяемая_система','версия_системы','условия','дата_проверки','области','права','доказательства','критические_провалы','итог','расчётная_полнота','граница','серия','журналы','ша256'], 'запись', e);
    for (const k of ['дата_создания','проверяемая_система','версия_системы','условия','дата_проверки']) if (!text(a[k])) e.push(`${k}: обязательное непустое поле.`);
    if (!old && !text(a.идентификатор)) e.push('идентификатор: не указан.');
    if (!time(a.дата_создания)) e.push('дата_создания: требуется действительное время UTC.');
    if (!date(a.дата_проверки)) e.push('дата_проверки: требуется действительная дата.');
    if (date(a.дата_проверки) && time(a.дата_создания) && a.дата_проверки > a.дата_создания.slice(0,10)) e.push('дата_проверки: позже даты создания.');
    if (!HASH.test(a.ша256 || '')) e.push('ша256: требуется 64 строчных шестнадцатеричных знака.');
    for (const [kind, definitions] of [['области', schema.восемнадцать_областей], ['права', schema.двадцать_прав]]) {
      const allowed = new Set(definitions.map(x => x.код)); const used = new Set();
      if (!Array.isArray(a[kind])) { e.push(`${kind}: требуется массив.`); continue; }
      for (const [i, r] of a[kind].entries()) {
        const prefix = `${kind}[${i}]`;
        if (!exactKeys(r, ['код','состояние','класс_доказательства','доказательство'], prefix, e)) continue;
        if (!allowed.has(r.код)) e.push(`${prefix}.код: неизвестный код ${r.код}.`);
        if (used.has(r.код)) e.push(`${prefix}.код: повтор ${r.код}.`); used.add(r.код);
        if (!STATES.includes(r.состояние)) e.push(`${prefix}.состояние: неизвестное состояние.`);
        if (r.класс_доказательства !== '' && !CLASSES.includes(r.класс_доказательства)) e.push(`${prefix}.класс_доказательства: неизвестный класс.`);
        if (typeof r.доказательство !== 'string') e.push(`${prefix}.доказательство: требуется текст.`);
      }
      for (const id of allowed) if (!used.has(id)) e.push(`${kind}: отсутствует код ${id}.`);
    }
    if (!Array.isArray(a.доказательства)) e.push('доказательства: требуется массив.');
    else if (Array.isArray(a.области) && Array.isArray(a.права) && !same(a.доказательства, [...a.области,...a.права].filter(r => text(r?.доказательство)))) e.push('доказательства: расходятся с записями матрицы.');
    if (!['ПРОЙДЕНО','НЕ ПРОЙДЕНО','НЕ ЗАВЕРШЕНО'].includes(a.итог)) e.push('итог: неизвестное состояние.');
    if (!Number.isSafeInteger(a.расчётная_полнота) || a.расчётная_полнота<0 || a.расчётная_полнота>100) e.push('расчётная_полнота: требуется целое число от 0 до 100.');
    if (a.серия !== undefined && a.серия !== null) {
      const s = a.серия;
      if (exactKeys(s, ['идентификатор','наблюдатель','протокол','окно','начало','конец','условие','условия_серии','изменяемые_переменные','неизменяемые_условия','наблюдения','независимость'], 'серия', e)) {
        for (const k of ['идентификатор','наблюдатель','протокол','окно','условие']) if (!text(s[k])) e.push(`серия.${k}: обязательное поле.`);
        if (!date(s.начало) || !date(s.конец) || s.начало > s.конец || a.дата_проверки < s.начало || a.дата_проверки > s.конец) e.push('серия: дата аудита не входит в действительный интервал окна.');
        if (!Array.isArray(s.условия_серии) || s.условия_серии.length < 2 || !s.условия_серии.every(text) || new Set(s.условия_серии).size !== s.условия_серии.length || !s.условия_серии.includes(s.условие)) e.push('серия.условия_серии: нужны разные условия, включая текущее.');
        if (!Array.isArray(s.изменяемые_переменные) || !s.изменяемые_переменные.every(text)) e.push('серия.изменяемые_переменные: требуется массив названий.');
        if (!s.неизменяемые_условия || Array.isArray(s.неизменяемые_условия) || typeof s.неизменяемые_условия !== 'object' || !Object.keys(s.неизменяемые_условия).length || !Object.values(s.неизменяемые_условия).every(text)) e.push('серия.неизменяемые_условия: нужен непустой объект строк.');
        const independence=s.независимость;
        if(independence) {
          const history=independence.история_основания||[];
          for(let i=0;i<history.length;i++)if(Date.parse(history[i].дата_время)>Date.parse(a.дата_создания)||(i&&Date.parse(history[i].дата_время)<Date.parse(history[i-1].дата_время)))e.push('серия.независимость: история основания находится в будущем или нарушен её порядок.');
          if(['документирована','внешняя проверка заявлена'].includes(independence.состояние) && (!text(independence.основание)||!text(independence.проверяющий)))e.push('серия.независимость: отсутствуют основание и проверяющий.');
          if(independence.состояние==='внешняя проверка заявлена' && (!text(independence.идентификатор_группы)||!history.length||!safeURL(history.at(-1).источник)))e.push('серия.независимость: нет идентификатора группы или опубликованного основания. Это всё равно заявление, не автоматическая проверка полномочий.');
        }
        if (!Array.isArray(s.наблюдения) || s.наблюдения.length > 500) e.push('серия.наблюдения: требуется массив, не более 500 наблюдений.');
        else {
          const seen = new Set();
          s.наблюдения.forEach((r, i) => {
            const p = `серия.наблюдения[${i}]`;
            if (!exactKeys(r, ['идентификатор','дата_время','сеанс','запрос','ответ','класс_доказательства','доказательство_вызова'], p, e)) return;
            for (const k of ['идентификатор','сеанс','запрос','ответ']) if (!text(r[k])) e.push(`${p}.${k}: обязательное поле.`);
            if (!([RELEASE,'4.5.0-wip','4.6.0-wip','4.7.0-wip','4.8.0-wip','4.9.0-wip'].includes(a.редакция_схемы)?formatTime(r.дата_время):time(r.дата_время)) || new Date(r.дата_время).toISOString().slice(0,10) < s.начало || new Date(r.дата_время).toISOString().slice(0,10) > s.конец || Date.parse(r.дата_время) > Date.parse(a.дата_создания)) e.push(`${p}.дата_время: вне окна или позже создания записи.`);
            if(r.доказательство_вызова && Date.parse(r.доказательство_вызова.дата_время)!==Date.parse(r.дата_время))e.push(`${p}: заявленное время и время доказательства вызова расходятся.`);
            if (!CLASSES.includes(r.класс_доказательства)) e.push(`${p}: неизвестный класс доказательства.`);
            if (seen.has(r.идентификатор)) e.push(`${p}: повтор идентификатора.`); seen.add(r.идентификатор);
          });
        }
      }
    }
    if (a.журналы !== undefined && (!Array.isArray(a.журналы) || a.журналы.some(x => !x || !text(x.имя) || !HASH.test(x.ша256 || '') || !Number.isSafeInteger(x.байты) || x.байты < 0))) e.push('журналы: некорректный список отпечатков.');
    return e;
  }
  function score(a, schema) {
    const all = [...a.области, ...a.права];
    const support = r => text(r.доказательство) && CLASSES.includes(r.класс_доказательства);
    const checked = all.filter(r => r.состояние !== 'не проверено' && support(r));
    const unknown = all.filter(r => r.состояние === 'не проверено').length;
    const unsupported = all.filter(r => r.состояние !== 'не проверено' && !support(r)).length;
    const critical = a.области.filter(r => schema.четыре_критических_рубежа.includes(r.код) && r.состояние === 'провалено').map(r => r.код);
    const failure = all.some(r => r.состояние === 'провалено');
    const partial = all.some(r => r.состояние === 'частично');
    const applicable = all.filter(r => r.состояние !== 'не применимо');
    const evidenceObserved = r => ['ДФ','НП'].includes(r.класс_доказательства);
    const weak = applicable.filter(r => r.состояние === 'подтверждено' && !evidenceObserved(r)).length;
    return {полнота: Math.round(checked.length / all.length * 100), всего: all.length, заполнено: checked.length, неизвестно: unknown, без_опоры: unsupported, критические_провалы: critical, неподтверждённые_заявления: weak,
      итог: failure ? 'НЕ ПРОЙДЕНО' : unknown || unsupported || partial || weak || !applicable.length ? 'НЕ ЗАВЕРШЕНО' : 'ПРОЙДЕНО',
      граница: 'Полнота — доля заполненных позиций с классом и записью, не мера истинности. Непроверенное не считается исполнением.'};
  }
  async function ingest(bytes, name, schema, options={}) {
    if(!(bytes instanceof ArrayBuffer)&&!ArrayBuffer.isView(bytes))throw Error('Нужны исходные байты.');
    if(bytes.byteLength>LIMIT)throw Error(`${name}: файл превышает 5 МиБ.`);
    bytes = bytes instanceof ArrayBuffer ? new Uint8Array(bytes).slice() : new Uint8Array(bytes.buffer,bytes.byteOffset,bytes.byteLength).slice();
    if (bytes.byteLength > LIMIT) throw Error(`${name}: файл превышает 5 МиБ.`);
    const sourceHash = await hash(bytes);
    const source = {имя: name, байты: bytes.byteLength, отпечаток_исходного_файла: sourceHash, база64: base64(bytes), ошибки: [], предупреждения: []};
    try {
      const raw = new TextDecoder('utf-8', {fatal: true}).decode(bytes);
      const a = parse(raw,LIMIT,options.jsonBudget);
      source.ошибки = auditErrors(a, schema);
      source.редакция_схемы = a?.редакция_схемы || 'не указана';
      if (source.ошибки.length) return trusted(source);
      const legacy = a.редакция_схемы === '4.2.0-wip';
      const checksum = legacy ? await hash(JSON.stringify(payload(a))) : await hashCanonical(payload(a));
      source.отпечаток_исходной_записи = checksum;
      if (checksum !== a.ша256) { source.ошибки.push('ша256: контрольный отпечаток исходной записи не совпал.'); return trusted(source); }
      source.запись = a;
      source.пересчёт = score(a, schema);
      if (legacy) source.предупреждения.push('Историческая запись 4.2.0 прочитана без изменения исходных байтов; её итог пересчитан, отсутствие серии не восполняется.');
      if (a.итог !== source.пересчёт.итог || a.расчётная_полнота !== source.пересчёт.полнота) source.предупреждения.push('Заявленный итог или полнота отличаются от пересчёта 4.10.0. Исходный вывод сохранён отдельно.');
    } catch (e) { source.ошибки.push(e.message); }
    return trusted(source);
  }
  async function checkpoint(signal) {
    if(signal?.aborted)throw new DOMException('Обработка отменена; рабочие данные не заменены.','AbortError');
    await new Promise(resolve=>setTimeout(resolve,0));
    if(signal?.aborted)throw new DOMException('Обработка отменена; рабочие данные не заменены.','AbortError');
  }
  async function stage(options,name,detail={}){
    if(!options.signal&&!options.onStage)return;
    if(options.signal?.aborted)throw new DOMException('Обработка отменена; рабочие данные не заменены.','AbortError');
    if(options.onStage)options.onStage({этап:name,...detail});
    await checkpoint(options.signal);
  }
  const OWNED_ENVELOPE=Symbol('исключительное внутреннее владение разобранной оболочкой');
  async function ingestBatch(files,schema,{signal,onProgress}={}) {
    const list=Array.from(files);
    if(list.length>SOURCE_LIMIT)throw Error('За одну операцию принимается не более 100 файлов.');
    let total=0;for(const f of list){if(!Number.isSafeInteger(f.size)||f.size<0||f.size>LIMIT)throw Error('Недопустимый размер исходника.');total+=f.size;}
    if(total>TOTAL_LIMIT)throw Error('Суммарный размер исходников превышает 25 МиБ.');
    const out=[];
    for(const [i,f] of list.entries()){
      await checkpoint(signal);
      const bytes=await f.arrayBuffer();if(bytes.byteLength!==f.size)throw Error('Размер исходника изменился во время чтения.');
      out.push(await ingest(bytes,f.name,schema));await checkpoint(signal);
      if(onProgress)onProgress({обработано:i+1,всего:list.length,байты:out.reduce((n,x)=>n+x.байты,0)});
    }
    await checkpoint(signal);return out;
  }
  function* mergeBaseSteps(sources, schema, budget=null) {
    let stepCount=0;yield {этап:"индекс исходников"};
    const sorted = uniqueSources(sources);
    for(const source of sorted)yield {этап:'исходник',обработано:++stepCount};
    const valid = sorted.filter(s => !s.ошибки.length && s.запись);
    const conflicts = []; const blocked = new Set();
    const add = (kind, message, hashes, block = true) => { if(budget&&conflicts.length>=budget.conflicts)throw Error('Ограничительный профиль: более 2048 предметных конфликтов; расчёт прекращён до следующей записи.');conflicts.push({вид: kind, сообщение: message, источники: [...new Set(hashes)].sort(), исключает_из_сравнения: block}); if (block) hashes.forEach(h => blocked.add(h)); };
    const ids = new Map();
    for (const s of valid) {
      const id = s.запись.идентификатор || s.отпечаток_исходной_записи;
      if (!ids.has(id)) ids.set(id, []); ids.get(id).push(s);
    }
    for (const [id, list] of ids) if (new Set(list.map(s => s.отпечаток_исходной_записи)).size > 1) add('идентичность', `Несовместимые записи с одним идентификатором ${id}; победитель не выбран.`, list.map(s => s.отпечаток_исходного_файла));
    if (new Set(valid.map(s => s.редакция_схемы)).size > 1) add('редакция схемы', 'Смешаны редакции схемы. Исходные записи не переписаны; исторические записи без протокола не считаются серией.', valid.map(s => s.отпечаток_исходного_файла), false);
    for (const s of sorted.filter(x => x.ошибки.length)) for (const message of s.ошибки) add(/редакция/.test(message) ? 'редакция схемы' : /дата|окн/.test(message) ? 'дата' : /класс/.test(message) ? 'класс доказательства' : /код/.test(message) ? 'код права или области' : 'структура или целостность', message, [s.отпечаток_исходного_файла]);
    // Вызов существует до разбиения по сериям, наблюдателям и окнам.
    const calls=new Map(), observationIds=new Map();
    // Одиночная запись не требует отдельного массива на каждом индексе.
    // Список создаётся только при фактическом повторе; сами наблюдения не копируются.
    const indexCall=(map,key,item)=>{
      const old=map.get(key);
      if(!old)map.set(key,item);
      else if(Array.isArray(old))old.push(item);
      else map.set(key,[old,item]);
    };
    for(const source of valid)if(source.запись.серия){
      const series=source.запись.серия,placement=canonical([series.идентификатор,series.окно,series.условие]);
      for(const observation of series.наблюдения){
        if(++stepCount%250===0)yield {этап:"индекс вызовов",обработано:stepCount};
        const item={source,placement,call:canonical([observation.сеанс,Date.parse(observation.дата_время),observation.запрос])};
        indexCall(calls,item.call,item);indexCall(observationIds,observation.идентификатор,item);
      }
    }
    for(const list of calls.values())if(Array.isArray(list)&&new Set(list.map(x=>x.placement)).size>1)add('повтор вызова между группами','Один вызов внесён в разные серии, окна или условия. Он не становится независимым повтором; затронутые исходники исключены до рассмотрения.',list.map(x=>x.source.отпечаток_исходного_файла));
    for(const [id,list] of observationIds)if(Array.isArray(list)&&new Set(list.map(x=>x.call)).size>1)add('коллизия наблюдения',`Идентификатор наблюдения ${id} относится к разным вызовам; автоматического выбора нет.`,list.map(x=>x.source.отпечаток_исходного_файла));
    calls.clear();observationIds.clear();
    const groups = new Map();
    for (const s of valid) if (s.запись.серия) {
      const id = s.запись.серия.идентификатор;
      if (!groups.has(id)) groups.set(id, []); groups.get(id).push(s);
    }
    const series = [];
    for (const [id, list] of [...groups].sort(([a],[b]) => cmp(a,b))) {
      yield {этап:"сравнение серии",идентификатор:id};
      const hashes = list.map(s => s.отпечаток_исходного_файла);
      for (const [label, get] of [
        ['система', s => s.запись.проверяемая_система], ['версия системы', s => s.запись.версия_системы],
        ['условия', s => s.запись.условия], ['протокол', s => s.запись.серия.протокол],
        ['изменяемая переменная', s => s.запись.серия.изменяемые_переменные], ['неизменяемые условия', s => s.запись.серия.неизменяемые_условия],
        ['набор условий', s => [...s.запись.серия.условия_серии].sort()]]) {
        if (new Set(list.map(s => canonical(get(s)))).size > 1) add(label, `Серия ${id}: расходятся ${label}. Снимки сохранены, общий сравнительный вывод заблокирован.`, hashes);
      }
      const windowMap = new Map();
      for (const s of list) {
        const a = s.запись.серия;
        if (!windowMap.has(a.окно)) windowMap.set(a.окно, []); windowMap.get(a.окно).push(s);
      }
      const windows = [];
      for (const [w, wl] of windowMap) {
        yield {этап:"сравнение окна",окно:w};
        const definitions = new Set(wl.map(s => `${s.запись.серия.начало}/${s.запись.серия.конец}`));
        if (definitions.size > 1) add('дата', `Серия ${id}, окно ${w}: разные границы одного окна.`, wl.map(s => s.отпечаток_исходного_файла));
        const first = wl[0].запись.серия;
        const counters = Object.fromEntries(first.условия_серии.map(c => [c, 0]));
        const observations = new Map();
        for (const s of wl) for (const r of s.запись.серия.наблюдения) {
          if(++stepCount%250===0)yield {этап:"наблюдения окна",обработано:stepCount};
          const key=r.идентификатор,condition=s.запись.серия.условие,previous=observations.get(key);
          if(previous){
            // Полную нормализованную проекцию строим только для сравнения
            // совпавшего ID. Все поля, включая доказательство вызова, сохраняются.
            const view=(record,c)=>({условие:c,...record,дата_время:new Date(record.дата_время).toISOString()});
            if(!same(view(previous.record,previous.condition),view(r,condition)))add('наблюдение',`Серия ${id}: разные данные наблюдения ${key}.`,[s.отпечаток_исходного_файла,previous.hash]);
          }else observations.set(key,{record:r,condition,hash:s.отпечаток_исходного_файла});
        }
        const invocations = new Map(); let duplicated = 0; let synthetic = 0;
        for (const entry of observations.values()) {
          const {record:r,condition,hash:sourceHash}=entry;
          if(++stepCount%250===0)yield {этап:"повторы окна",обработано:stepCount};
          const invocation=canonical([r.сеанс,Date.parse(r.дата_время),r.запрос]);
          const previous=invocations.get(invocation);
          if(previous) {
            duplicated++;
            if(!same([previous.condition,previous.record.ответ,previous.record.класс_доказательства],[condition,r.ответ,r.класс_доказательства]))
              add('совпадающий вызов',`Серия ${id}: один вызов имеет разные ответы, условия или классы.`,[previous.hash,sourceHash]);
            continue;
          }
          invocations.set(invocation,entry);
          if(!['НП','ДФ'].includes(r.класс_доказательства)){synthetic++;continue;}
          counters[condition]=(counters[condition]||0)+1;
        }
        for (const condition of first.условия_серии) {
          const sameCondition = wl.filter(s => s.запись.серия.условие === condition);
          const rowCodes = new Set(sameCondition.flatMap(s => [...s.запись.права,...s.запись.области].map(r => r.код)));
          for (const code of [...rowCodes].sort()) {
            const rows = sameCondition.map(s => ({row:[...s.запись.права,...s.запись.области].find(r=>r.код===code), hash:s.отпечаток_исходного_файла})).filter(x=>x.row);
            for (const field of ['класс_доказательства','состояние']) if (new Set(rows.map(x=>x.row[field])).size>1)
              add(field==='класс_доказательства'?'класс доказательства':'состояние права или области', `Серия ${id}, окно ${w}, условие ${condition}, код ${code}: разные ${field.replaceAll('_',' ')}. Различие не сводится к единому решению.`, rows.map(x=>x.hash), false);
          }
        }
        const uniqueAudits = [...new Map(wl.map(s => [s.отпечаток_исходной_записи, s])).values()];
        const recalcs = uniqueAudits.map(s => score(s.запись, schema));
        const roster=[...new Set(wl.map(s=>s.запись.серия.наблюдатель))].sort();
        const observers=roster.map(name=>({наблюдатель:name,заявления:wl.filter(s=>s.запись.серия.наблюдатель===name).map(s=>({источник:s.отпечаток_исходного_файла,...{идентификатор_группы:null,история_основания:[],...(s.запись.серия.независимость||{состояние:'не проверено',группа_управления:'',основание:'',проверяющий:''})}})),
          покрытие_условий:first.условия_серии.map(c=>({условие:c,уникальных_вызовов:new Set(wl.filter(s=>s.запись.серия.наблюдатель===name&&s.запись.серия.условие===c).flatMap(s=>s.запись.серия.наблюдения).map(r=>canonical([r.сеанс,Date.parse(r.дата_время),r.запрос]))).size,граница:'Заявленные вызовы наблюдателя; общие вызовы не доказывают независимость.'})),полнота_по_исходникам:uniqueAudits.filter(s=>s.запись.серия.наблюдатель===name).map(s=>({условие:s.запись.серия.условие,источник:s.отпечаток_исходного_файла,полнота:score(s.запись,schema).полнота}))}));
        windows.push({наблюдатели:roster, состав_по_условиям:Object.fromEntries(first.условия_серии.map(c=>[c,[...new Set(wl.filter(s=>s.запись.серия.условие===c).map(s=>s.запись.серия.наблюдатель))].sort()])), независимость_наблюдателей:observers,
          окно:w, начало:first.начало, конец:first.конец, повторы:counters, дубли_наблюдений:duplicated, ненаблюдательные_записи:synthetic,
          полнота: Math.round(recalcs.reduce((n,s) => n + s.полнота, 0) / recalcs.length),
          состояния: [...new Set(recalcs.map(r => r.итог))].sort(),
          источники: wl.map(s => s.отпечаток_исходного_файла).sort(),
          аудит_по_условиям: Object.fromEntries(first.условия_серии.map(c => [c, uniqueAudits.filter(s => s.запись.серия.условие === c).map(s => ({источник: s.отпечаток_исходного_файла, права:s.запись.права, области:s.запись.области, итог:s.пересчёт.итог}))])),
          минимум_повторов: Object.values(counters).length >= 2 && Object.values(counters).every(n => n >= 5)});
      }
      windows.sort((a,b) => cmp(a.начало,b.начало) || cmp(a.окно,b.окно));
      for (let i=1; i<windows.length; i++) if (windows[i].начало <= windows[i-1].конец) add('дата', `Серия ${id}: временные окна пересекаются.`, hashes);
      const globalObservations = new Map();
      for (const s of list) for (const r of s.запись.серия.наблюдения) {
        if (globalObservations.has(r.идентификатор) && globalObservations.get(r.идентификатор) !== s.запись.серия.окно) add('наблюдение', `Серия ${id}: наблюдение ${r.идентификатор} использовано в разных окнах.`, hashes);
        globalObservations.set(r.идентификатор, s.запись.серия.окно);
      }
      for(const w of windows) {
        w.заявленные_повторы=structuredClone(w.повторы);
        w.есть_блокирующий_конфликт=w.источники.some(h=>blocked.has(h));
        if(w.есть_блокирующий_конфликт){w.повторы=Object.fromEntries(Object.keys(w.повторы).map(k=>[k,0]));w.минимум_повторов=false;}
      }
      const single = list.every(s => s.запись.серия.изменяемые_переменные.length === 1);
      const issues = [];
      if (!single) issues.push('Нужна ровно одна заявленная изменяемая переменная.');
      if (windows.length < 2) issues.push('Нужны не менее двух непересекающихся временных окон.');
      if (windows.some(w => !w.минимум_повторов)) issues.push('Нужны не менее пяти уникальных наблюдений на каждое условие в каждом окне.');
      if (hashes.some(h => blocked.has(h))) issues.push('Есть неразрешённый конфликт; сравнение заблокировано.');
      const snapshot = w => Object.fromEntries(Object.entries(w.аудит_по_условиям).map(([c,items]) => [c,Object.fromEntries([...new Set(items.flatMap(a=>[...a.права,...a.области].map(r=>r.код)))].sort().map(code=>[code,[...new Set(items.flatMap(a=>[...a.права,...a.области].filter(r=>r.код===code).map(r=>r.состояние)))].sort()]))]));
      const rendered = windows.map((w,i) => {
        const changes=[];
        if(i){const before=snapshot(windows[i-1]),after=snapshot(w);
          for(const c of [...new Set([...Object.keys(before),...Object.keys(after)])].sort())
            for(const code of [...new Set([...Object.keys(before[c]||{}),...Object.keys(after[c]||{})])].sort()){
              const a=before[c]?.[code]||[],b=after[c]?.[code]||[];
              if(!same(a,b))changes.push({условие:c,код:code,было:a,стало:b});
            }
        }
        const before=i?windows[i-1]:null;
        const added=before?w.наблюдатели.filter(n=>!before.наблюдатели.includes(n)):[];
        const removed=before?before.наблюдатели.filter(n=>!w.наблюдатели.includes(n)):[];
        const duration=x=>Math.round((Date.parse(x.конец)-Date.parse(x.начало))/86400000)+1;
        const independence=x=>x.независимость_наблюдателей.map(n=>({наблюдатель:n.наблюдатель,заявления:[...new Set(n.заявления.map(z=>canonical(Object.fromEntries(Object.entries(z).filter(([k])=>k!=='источник')))))].sort()}));
        const independenceChanged=before?!same(independence(w),independence(before)):false;
        const comparable=before?{сведения_о_независимости_изменились:independenceChanged,состав_изменился:!same(w.состав_по_условиям,before.состав_по_условиям),добавились:added,выбыли:removed,
          длительность_изменилась:duration(w)!==duration(before),дней_в_окне:duration(w),
          сопоставимо_по_заявленным_полям:same(w.состав_по_условиям,before.состав_по_условиям)&&!independenceChanged&&duration(w)===duration(before)&&!w.есть_блокирующий_конфликт&&!before.есть_блокирующий_конфликт,
          граница:'Сравнение состава, заявлений о независимости и длительности, не подтверждение независимости или причинности.'}:null;
        return {...w, сопоставимость_с_предыдущим:comparable, изменение_полноты:i ? w.полнота-windows[i-1].полнота:null,
          изменение_состояния:i ? (same(w.состояния,windows[i-1].состояния) ? 'набор итоговых состояний не изменился' : 'набор итоговых состояний изменился') : 'исходное окно', изменения_по_кодам:changes};
      });
      while (rendered.length < 3) rendered.push({окно: `Ожидаемое окно ${rendered.length+1}`, нет_данных: true});
      series.push({идентификатор:id, наблюдатели:[...new Set(list.map(s => s.запись.серия.наблюдатель))].sort(), окна:rendered, ошибок_полноты:issues.length,
        состояние: issues.length ? 'НЕ ЗАВЕРШЕНО' : 'СТРУКТУРНО ПОЛНАЯ СЕРИЯ', причины:issues,
        причинность:'Не присваивается. Заявленные наблюдатели, независимость, условия и подлинность ответов требуют отдельной проверки.'});
    }
    conflicts.sort((a,b) => cmp(canonical(a),canonical(b)));
    return {редакция_схемы:RELEASE, вид:'продольная панель', источники:sorted, конфликты:conflicts, серии:series,
      без_серии:valid.filter(s => !s.запись.серия).map(s => ({источник:s.отпечаток_исходного_файла, ...s.пересчёт})),
      правило:'Исходные байты и отпечатки сохранены отдельно. Совпадение контрольных отпечатков не доказывает истинность. Различие не доказывает причинность.'};
  }
  function assertSources(sources) {
    if(!Array.isArray(sources)||sources.length>200)throw Error('За операцию разрешены текущие 100 исходников и до 100 добавляемых.');
    const unique=new Map();
    for(const s of sources){
      if(!s || sourceTrust.get(s)!==true || !Object.isFrozen(s))throw Error('Источник не прошёл проверку исходных байтов или его метаданные подменены.');
      unique.set(s.отпечаток_исходного_файла,s);
    }
    if(unique.size>SOURCE_LIMIT || [...unique.values()].reduce((n,s)=>n+s.байты,0)>TOTAL_LIMIT)throw Error('Превышен предел: 100 исходников или 25 МиБ.');
  }
  function uniqueSources(sources){assertSources(sources);const byHash=new Map();for(const item of sources){const key=item.отпечаток_исходного_файла;if(!byHash.has(key)||item.имя<byHash.get(key).имя)byHash.set(key,item);}return [...byHash.values()].sort((a,b)=>cmp(a.отпечаток_исходного_файла,b.отпечаток_исходного_файла));}
  function* mergeSteps(sources,schema,resolutions=[],revocations=[],migrations=[],budget=null) {
    assertSources(sources);
    if(!Array.isArray(resolutions)||resolutions.length>100 || !Array.isArray(revocations)||revocations.length>100)throw Error('Не более 100 решений и 100 отзывов.');
    const known=new Set(sources.map(s=>s.отпечаток_исходного_файла)), ids=new Set();
    for(const r of resolutions){
      if(resolutionTrust.get(r)!==canonical(r))throw Error('Решение конфликта не прошло проверку.');
      if(ids.has(r.идентификатор)||r.источники.some(h=>!known.has(h)))throw Error('Повтор решения или потеря неизменяемого исходника конфликта.');ids.add(r.идентификатор);
    }
    for(const r of revocations) {
      if(revocationTrust.get(r)!==canonical(r)||ids.has(r.идентификатор))throw Error('Отзыв не проверен или его идентификатор повторён.');ids.add(r.идентификатор);
      if(!resolutions.some(d=>d.идентификатор===r.решение&&d.ша256===r.отпечаток_решения))throw Error('Отзыв потерял исходное решение.');
    }
    if(!Array.isArray(migrations)||migrations.length>10)throw Error('Не более десяти сохранённых переносов.');
    for(const m of migrations)if(!migrationTrust.has(m)||m.сохранённые_источники.some(h=>!known.has(h)))throw Error('Не проверено происхождение переноса или утрачен его исходник.');
    const raw=yield* mergeBaseSteps(sources,schema,budget), history=[];
    const eligible=resolutions.filter(r=>!revocations.some(v=>v.решение===r.идентификатор)&&raw.конфликты.some(c=>canonical(c)===resolutionContext.get(r)));
    const active=eligible.filter(r=>!raw.конфликты.some(c=>c.исключает_из_сравнения&&c.источники.some(h=>r.исключённые_из_сравнения.includes(h))&&!eligible.some(x=>resolutionContext.get(x)===canonical(c))));
    for(const r of resolutions)history.push({решение:r.идентификатор,состояние:revocations.some(v=>v.решение===r.идентификатор)?'отозвано':active.includes(r)?'применено':'приостановлено',основание:active.includes(r)?'Точный конфликт и все затронутые блокирующие конфликты рассмотрены.':'Отзыв, изменение состава конфликта или дополнительный нерассмотренный конфликт. История сохранена.'});
    const excluded=[...new Set(active.flatMap(r=>r.исключённые_из_сравнения))].sort();
    const projection=excluded.length?(yield* mergeBaseSteps(sources.filter(s=>!excluded.includes(s.отпечаток_исходного_файла)),schema,budget)):raw;
    return {...raw,серии:projection.серии,без_серии:projection.без_серии,
      разрешения:[...resolutions].sort((a,b)=>cmp(a.идентификатор,b.идентификатор)),отзывы:[...revocations].sort((a,b)=>cmp(a.идентификатор,b.идентификатор)),переносы:[...migrations].sort((a,b)=>cmp(a.отпечаток_файла,b.отпечаток_файла)),состояния_решений:history.sort((a,b)=>cmp(a.решение,b.решение)),исключённые_из_сравнения:excluded,конфликты_проекции:projection.конфликты};
  }
  function merge(...args){const steps=mergeSteps(...args);let x;do{x=steps.next();}while(!x.done);return x.value;}
  async function mergeAsync(sources,schema,resolutions=[],revocations=[],migrations=[],options={}){
    assertSources(sources);
    // Источники глубоко заморожены ingest; решения копируются с проверкой частных меток.
    const copy=(r,map)=>{if(map.get(r)!==canonical(r))throw Error('История решения не проверена.');const x=freeze(structuredClone(r));map.set(x,map.get(r));if(resolutionContext.has(r))resolutionContext.set(x,resolutionContext.get(r));return x;};
    const steps=mergeSteps(sources.slice(),structuredClone(schema),resolutions.map(r=>copy(r,resolutionTrust)),revocations.map(r=>copy(r,revocationTrust)),migrations.slice(),options.jsonBudget||null);
    await stage(options,'начало слияния');
    try{while(true){const x=steps.next();if(x.done){await stage(options,'слияние готово');return x.value;}await stage(options,x.value.этап,x.value);}}
    finally{steps.return();}
  }
  async function revokeResolution(resolutions,input) {
    const candidate=await seal(input), errors=validate(schemas.revocation,candidate);
    if(errors.length)throw Error(errors.join('\n'));
    const target=resolutions.find(r=>r.идентификатор===candidate.решение&&r.ша256===candidate.отпечаток_решения);
    if(!target||resolutionTrust.get(target)!==canonical(target))throw Error('Исходное решение отзыва не проверено.');
    if(Date.parse(candidate.дата_время)<Date.parse(target.дата_время)||Date.parse(candidate.дата_время)>Date.now())throw Error('Дата отзыва раньше решения или находится в будущем.');
    return remember(revocationTrust,candidate);
  }
  async function resolveConflict(sources,schema,input) {
    assertSources(sources);
    if(!schemas.resolution)throw Error('Схема решения конфликта не загружена.');
    const candidate=await seal(input);
    const errors=validate(schemas.resolution,candidate);
    if(errors.length)throw Error(errors.join('\n'));
    if(Date.parse(candidate.дата_время)>Date.now())throw Error('Дата решения находится в будущем.');
    const conflicts=merge(sources.filter(s=>candidate.источники.includes(s.отпечаток_исходного_файла)),schema).конфликты;
    let found;
    for(const c of conflicts)if(await hashCanonical(c)===candidate.отпечаток_конфликта)found=c;
    if(!found || !same([...candidate.источники].sort(),found.источники))throw Error('Решение не связано с точным конфликтом неизменённых источников.');
    if(candidate.исключённые_из_сравнения.some(h=>!found.источники.includes(h)))throw Error('Исключается исходник вне рассматриваемого конфликта.');
    if(!text(candidate.проверяющий)||!text(candidate.основание))throw Error('Нужны имя проверяющего и содержательное основание.');
    resolutionContext.set(candidate,canonical(found));return remember(resolutionTrust,candidate);
  }
  async function restorePanel(envelope, schema, options={},ownership=null) {
    if(options.signal?.aborted)throw new DOMException("Обработка отменена.","AbortError");
    if(ownership!==OWNED_ENVELOPE)envelope=structuredClone(envelope);
    await stage(options,"снимок панели");
    if (schemas.panel) { const errors=validate(schemas.panel,envelope); if(errors.length)throw Error(errors.join('\n')); }
    if (envelope?.вид !== 'продольная панель' || envelope.редакция_схемы !== RELEASE || !Array.isArray(envelope.источники) || envelope.источники.length > 100) throw Error('Неизвестный формат продольной панели.');
    await stage(options,'схема панели');
    if (await hashCanonical(payload(envelope)) !== envelope.ша256) throw Error('Не совпал отпечаток производной.');
    await stage(options,'отпечаток панели');
    const items = []; let total = 0;
    for (const x of envelope.источники) {
      await stage(options,"исходник панели",{обработано:items.length});
      const bytes = unbase64(x.база64); total += bytes.length;
      if (total > 25 * 1024 * 1024) throw Error('Суммарный размер исходников превышает 25 МиБ.');
      const s = await ingest(bytes, x.имя, schema,options);
      if (s.отпечаток_исходного_файла !== x.отпечаток_исходного_файла || s.байты !== x.байты) throw Error(`Исходник ${x.имя}: байты не совпали.`);
      if(!same(s,x))throw Error(`Исходник ${x.имя}: представление расходится с повторным приёмом байтов.`);
      // Здесь envelope принадлежит только восстановлению: клон публичного входа
      // либо частный результат разбора. Равная замена освобождает второй граф
      // источника; исходные байты, отпечаток и последующая проверка не меняются.
      envelope.источники[items.length]=s;
      items.push(s);
    }
    const decisions=[];
    for(const r of envelope.разрешения||[]){
      if(await hashCanonical(payload(r))!==r.ша256)throw Error('Не совпал отпечаток решения конфликта.');
      decisions.push(await resolveConflict(items,schema,payload(r)));
    }
    const revocations=[];for(const r of envelope.отзывы||[]) {
      if(await hashCanonical(payload(r))!==r.ша256)throw Error('Не совпал отпечаток отзыва.');
      revocations.push(await revokeResolution(decisions,payload(r)));
    }
    const migrations=[];for(const m of envelope.переносы||[])migrations.push(await restoreMigration(m,schema,'продольная панель'));
    const recomputed = await mergeAsync(items, schema,decisions,revocations,migrations,options);
    if (!same(recomputed, payload(envelope))) throw Error('Вычисляемая панель расходится с повторным расчётом исходников.');
    await stage(options,"панель проверена");
    Object.defineProperties(items,{разрешения:{value:decisions},отзывы:{value:revocations},переносы:{value:migrations}});
    return items;
  }
  const CHECKSTATES = ['не проверено','подтверждено','не выполнено','не применимо'];
  function correction(c) {
    const issues = [];
    const check = (label, value, allowNA=false) => {
      if (!value || !CHECKSTATES.includes(value.состояние)) { issues.push(`${label}: неизвестное состояние.`); return; }
      if (value.состояние === 'не применимо' && allowNA && text(value.основание)) return;
      if (value.состояние !== 'подтверждено' || !text(value.доказательство)) issues.push(`${label}: нет подтверждения с доказательной записью.`);
    };
    check('Исходная запись', c.исходная_запись);
    check('Пересчёт решения', c.решение);
    if (c.все_получатели_перечислены !== true) issues.push('Полнота перечня получателей не заявлена.');
    if (!Array.isArray(c.получатели) || (!c.получатели.length && !text(c.основание_отсутствия_получателей))) issues.push('Не указан ни один получатель и нет основания отсутствия получателей.');
    else {
      const names = new Set();
      for (const r of c.получатели) {
        if (!text(r.название) || names.has(r.название)) issues.push('Получатель не назван или повторён.'); names.add(r.название);
        check(`Уведомление: ${r.название}`, r.уведомление);
        check(`Производная запись: ${r.название}`, r.исправление);
      }
    }
    for (const k of ['доступ','положение','видимость','репутация']) check(`Восстановление: ${k}`, c.восстановление?.[k], true);
    return {состояние: issues.length ? 'НЕ ЗАВЕРШЕНО' : 'ЗАЯВЛЕНО ПОЛНОЕ ИСПРАВЛЕНИЕ', причины:issues,
      граница:'Проверена полнота заявлений и доказательных записей, а не факт внешнего исполнения.'};
  }
  const EVENT_LIMIT=2000;
  const EVENT_BOUNDARY='Заявленное лицо; подпись и полномочия не удостоверены. Событие не доказывает внешнее исполнение.';
  const CORRECTION_ACTIONS=['исправление источника','уведомление получателя','изменение производной','пересчёт решения','восстановление положения','отзыв исправления','повторное распространение','изменение состава исправления'];
  const eventSlot=e=>e.действие==='уведомление получателя'?'уведомление:'+e.цель:e.действие==='изменение производной'?'производная:'+e.цель:e.действие==='восстановление положения'?'восстановление:'+e.цель:e.действие==='пересчёт решения'?'решение':'источник';
  const SCOPE_ACTION='изменение состава исправления';
  function correctionScope(c,r,at,recorded=at){
    const end=Date.parse(at),seen=Date.parse(recorded);
    const history=(c.события_исправления||[]).filter(e=>e.исправление===r.идентификатор&&e.действие===SCOPE_ACTION&&Date.parse(e.момент_события)<=end&&Date.parse(e.внесено)<=seen)
      .slice().sort((a,b)=>Date.parse(a.момент_события)-Date.parse(b.момент_события)||Date.parse(a.внесено)-Date.parse(b.внесено)||cmp(a.идентификатор,b.идентификатор));
    if(!history.length)return {получатели:r.получатели.map(x=>x.название),области_восстановления:['доступ','положение','видимость','репутация'],все_получатели_перечислены:r.все_получатели_перечислены,
      основание_отсутствия_получателей:r.основание_отсутствия_получателей||'',история:'нет датированной записи',опоры:[],активация:new Map()};
    const groups=new Map();for(const e of history){const key=Date.parse(e.момент_события);if(!groups.has(key))groups.set(key,[]);groups.get(key).push(e);}
    let selected,active=new Map(),prior=new Set(),lastRows=[];
    for(const [stamp,rows] of groups){
      const values=[...new Map(rows.map(e=>[canonical(e.состав),e.состав])).values()];
      const recipients=[...new Set(values.flatMap(v=>v.получатели))].sort(),areas=[...new Set(values.flatMap(v=>v.области_восстановления))].sort();
      const keys=new Set([...recipients.flatMap(n=>['уведомление:'+n,'производная:'+n]),...areas.map(n=>'восстановление:'+n)]);
      for(const key of keys)if(!prior.has(key)||values.length>1)active.set(key,{событие:stamp,внесено:Math.max(...rows.map(e=>Date.parse(e.внесено)))});
      for(const key of prior)if(!keys.has(key))active.delete(key);
      selected={получатели:recipients,области_восстановления:areas,все_получатели_перечислены:values.length===1&&values[0].все_получатели_перечислены,
        основание_отсутствия_получателей:values.length===1?values[0].основание_отсутствия_получателей:'',история:values.length===1?'заявленный состав':'противоречивый состав'};
      prior=keys;lastRows=rows;
    }
    return {...selected,опоры:lastRows.map(e=>e.идентификатор),активация:active};
  }
  async function correctionEventErrors(c,options={}) {
    const rows=c.события_исправления||[],errors=[];
    if(!Array.isArray(rows)||rows.length>EVENT_LIMIT)return ['Не более 2000 событий исправления.'];
    if(!schemas.correctionEvent)return ['Схема событий исправления не загружена.'];
    const ids=new Set(),byId=new Map();let previous=null,recorded=-Infinity;
    for(const e of rows){
      await stage(options,"событие исправления",{обработано:ids.size});
      const structural=validate(schemas.correctionEvent,e);if(structural.length){errors.push(...structural);continue;}
      if(ids.has(e.идентификатор))errors.push('Повтор идентификатора события исправления.');ids.add(e.идентификатор);
      const target=(c.исправления||[]).find(r=>r.идентификатор===e.исправление);
      if(!target)errors.push('Событие потеряло исходную проверку исправления.');
      if(!(c.документы||[]).some(d=>d.идентификатор===e.документ))errors.push('Событие потеряло документ-источник.');
      if(Date.parse(e.момент_события)>Date.parse(e.внесено)||Date.parse(e.внесено)<recorded||Date.parse(e.внесено)>Date.now())errors.push('Время события или внесения недействительно: будущее, обратный порядок либо внесение раньше события.');
      if(e.предыдущий_ша256!==previous)errors.push('Нарушена цепочка внесения событий исправления.');
      if(await hashCanonical(payload(e))!==e.ша256)errors.push('Отпечаток события исправления не совпал.');
      const recipients=new Set(target?correctionScope(c,target,e.момент_события,e.внесено).получатели:[]);
      if(e.действие===SCOPE_ACTION){if(e.цель!=='состав'||!e.состав)errors.push('Изменению состава нужен полный датированный перечень.');if(e.состав&&!e.состав.получатели.length&&!text(e.состав.основание_отсутствия_получателей))errors.push('Пустому перечню получателей нужно основание.');}
      else if(e.состав!==undefined&&e.состав!==null)errors.push('Состав допустим только в событии изменения состава.');
      if(['уведомление получателя','изменение производной','повторное распространение'].includes(e.действие)&&!recipients.has(e.цель))errors.push('Событие относится к неперечисленному получателю.');
      if(e.действие==='исправление источника'&&e.цель!=='источник')errors.push('Для исправления источника нужна цель «источник».');
      if(e.действие==='пересчёт решения'&&e.цель!=='решение')errors.push('Для пересчёта нужна цель «решение».');
      if(e.действие==='восстановление положения'&&!['доступ','положение','видимость','репутация'].includes(e.цель))errors.push('Неизвестная область восстановления.');
      if(e.действие==='отзыв исправления'){
        const old=byId.get(e.отменяет);
        if(!old||old.исправление!==e.исправление||old.цель!==e.цель||['отзыв исправления','повторное распространение',SCOPE_ACTION].includes(old.действие)||Date.parse(e.момент_события)<Date.parse(old.момент_события))errors.push('Отзыв не связан с предшествующим событием той же цели.');
      }else if(e.отменяет!==null)errors.push('Ссылка отмены допустима только для отзыва.');
      previous=e.ша256;recorded=Date.parse(e.внесено);byId.set(e.идентификатор,e);
    }
    return errors;
  }
  function correctionTimeline(c,at=cutoff(c),details=null) {
    if(!time(at))throw Error('Для среза событий требуется действительное время UTC.');
    const bound=Date.parse(at),visible=(c.события_исправления||[]).filter(e=>Date.parse(e.момент_события)<=bound&&Date.parse(e.внесено)<=bound);
    return (c.исправления||[]).map(r=>{
      const scope=correctionScope(c,r,at),active=scope.активация;delete scope.активация;
      const rows=visible.filter(e=>e.исправление===r.идентификатор).slice().sort((a,b)=>Date.parse(a.момент_события)-Date.parse(b.момент_события)||cmp(a.идентификатор,b.идентификатор));
      const revoked=new Set(rows.filter(e=>e.действие==='отзыв исправления').map(e=>e.отменяет));
      const slots=new Map();let inactive=0;
      const required=['источник','решение',...scope.получатели.flatMap(p=>['уведомление:'+p,'производная:'+p]),...scope.области_восстановления.map(k=>'восстановление:'+k)];
      for(const e of rows){
        if(e.действие==='отзыв исправления'||e.действие===SCOPE_ACTION)continue;
        const name=e.действие==='повторное распространение'?'производная:'+e.цель:eventSlot(e);
        const activation=active.get(name);if(!required.includes(name)||(activation&&(Date.parse(e.момент_события)<activation.событие||Date.parse(e.внесено)<activation.внесено))){inactive++;continue;}
        if(!slots.has(name))slots.set(name,[]);slots.get(name).push(e);
      }
      const states=[...slots].map(([цель,events])=>{
        const stamp=Math.max(...events.map(e=>Date.parse(e.момент_события))),last=events.filter(e=>Date.parse(e.момент_события)===stamp);
        const facts=last.map(e=>revoked.has(e.идентификатор)?'отозвано':e.действие==='повторное распространение'?'ошибка распространена повторно':'заявлено исправление');
        const unique=[...new Set(facts)];
        return {цель,состояние:unique.length>1?'противоречие':unique[0],события:last.map(e=>e.идентификатор)};
      });
      if(details){
        const explain=rows.map(e=>{
          const goal=e.действие==='повторное распространение'?'производная:'+e.цель:e.действие==='отзыв исправления'?eventSlot(rows.find(x=>x.идентификатор===e.отменяет)||e):eventSlot(e);
          const reasons=[],add=(код,пояснение,опоры=[])=>reasons.push({код,пояснение,опоры});
          if(e.действие===SCOPE_ACTION)add(scope.опоры.includes(e.идентификатор)?'текущий состав':'прежний состав','Состав применяется только к своему временному срезу.',scope.опоры);
          else if(e.действие==='отзыв исправления')add('отзыв','Отзывает указанное событие, не удаляя исходную запись.',[e.отменяет]);
          else {
            const activation=active.get(goal),state=states.find(x=>x.цель===goal);
            if(!required.includes(goal))add('цель исключена из состава','Цель не входит в выбранный состав.',scope.опоры);
            if(activation&&Date.parse(e.момент_события)<activation.событие)add('прежний состав','Событие предшествует последнему включению или конфликту состава.',scope.опоры);
            if(activation&&Date.parse(e.внесено)<activation.внесено)add('внесено до действующего состава','На момент внесения события действующий состав ещё не был зафиксирован.',scope.опоры);
            if(revoked.has(e.идентификатор))add('отозвано','Событие отозвано; новое имя не восстанавливает его действие.',rows.filter(x=>x.действие==='отзыв исправления'&&x.отменяет===e.идентификатор).map(x=>x.идентификатор));
            if(state&&!state.события.includes(e.идентификатор)){
              const later=rows.filter(x=>state.события.includes(x.идентификатор));
              add(later.some(x=>x.действие==='повторное распространение')?'более позднее ухудшение':'заменено поздней записью','Последнее состояние цели определяется перечисленными событиями.',state.события);
            }
            if(state?.события.includes(e.идентификатор)&&state.состояние==='противоречие')add('конфликт одного момента','Несовместимые состояния одного момента не разрешаются порядком внесения.',state.события);
            if(!reasons.length)add(e.действие==='повторное распространение'?'повторное распространение':'действующая опора',e.действие==='повторное распространение'?'Ошибка вновь распространена; восстановление не завершено.':'Действующая заявленная опора, не внешнее удостоверение.',[e.идентификатор]);
          }
          return {идентификатор:e.идентификатор,цель:goal,причины:reasons};
        });
        details.set(r.идентификатор,explain);
      }
      const unresolved=required.filter(k=>!states.some(s=>s.цель===k&&s.состояние==='заявлено исправление'));
      if(!scope.все_получатели_перечислены)unresolved.push('полнота перечня получателей');
      if(scope.история!=='заявленный состав')unresolved.push('датированная история состава не установлена или противоречива');
      return {исправление:r.идентификатор,момент_среза:at,видимых_событий:rows.length,состав_среза:scope,неактуальных_событий:inactive,состояния:states,открытые_остатки:unresolved,
        итог:unresolved.length?'НЕ ЗАВЕРШЕНО':'ПОЛНОТА ЗАЯВЛЕННЫХ СОБЫТИЙ',граница:EVENT_BOUNDARY};
    });
  }
  async function appendCorrectionEvent(input,fields,{signal}={}) {
    if(signal?.aborted)throw new DOMException('Обработка отменена.','AbortError');
    const c=structuredClone(input);fields=structuredClone(fields);await checkpoint(signal);
    c.события_исправления??=[];
    if(c.события_исправления.length>=EVENT_LIMIT)throw Error('Достигнут предел 2000 событий.');
    if(Object.hasOwn(fields,'ша256')||Object.hasOwn(fields,'предыдущий_ша256'))throw Error('Отпечатки события формирует ядро, не форма.');
    const e=await seal({...fields,предыдущий_ша256:c.события_исправления.at(-1)?.ша256||null},{signal});
    c.события_исправления.push(e);const errors=await caseErrors(c);if(errors.length)throw Error(errors.join('\n'));
    await checkpoint(signal);return freeze(c);
  }
  const cutoff=c=>c.точное_время_проверки||c.дата_проверки+'T23:59:59.999Z';
  function documentAt(d,at) {
    const stamp=typeof at==='number'?at:Date.parse(at);if(!Number.isFinite(stamp))throw Error('Неоднозначный или отсутствующий момент среза.');
    const history=(d.история||[]).filter(h=>Date.parse(h.дата_время)<=stamp);
    const snapshots=(d.срезы||[]).filter(x=>Date.parse(x.дата_время)<=stamp);
    let view=snapshots.length?{...d,...snapshots.at(-1).содержание}:{...d};
    if(!snapshots.length && (d.история||[]).some(h=>Date.parse(h.дата_время)>stamp))view={...view,срок:'',основание_срока:'Историческое содержание неизвестно.',вопросы:[],ответы_на_вопросы:[],проверка_срока:null,получение:null};
    view.история=history;view.срезы=snapshots;
    view.состояние=history.at(-1)?.состояние||'не установлено';
    view.момент_состояния=history.at(-1)?.дата_время||null;
    return view;
  }
  function receivedAt(d,at) {
    if(d.состояние!=='получен адресатом'||!d.момент_состояния)return false;
    if(d.получение) return Date.parse(d.получение.дата_время)<=Date.parse(at)&&Date.parse(d.получение.внесено)<=Date.parse(at);
    return Date.parse(d.момент_состояния)<=Date.parse(at);
  }
  function silence(c) {
    if(!date(c.дата_проверки))return [];
    const at=cutoff(c),docs=c.документы.map(d=>documentAt(d,at));
    return docs.filter(d=>['обращение','исправление','пересмотр'].includes(d.вид)&&['отправлен','получен адресатом'].includes(d.состояние)&&date(d.срок)&&d.срок<c.дата_проверки &&
      !docs.some(r=>r.вид==='ответ'&&r.связанные_документы.includes(d.идентификатор)&&r.дата<=c.дата_проверки&&receivedAt(r,at)))
      .map(d=>({документ:d.идентификатор,срок:d.срок,дней_после_срока:Math.floor((Date.parse(c.дата_проверки)-Date.parse(d.срок))/86400000),основание_срока:d.основание_срока||'Применимость срока не подтверждена.',применимость_срока:deadlineState(d),состояние:'Ответ не зафиксирован к моменту среза; внешнее молчание автоматически не доказано.'}));
  }
  function deadlineState(d) {
    const b=d.проверка_срока;
    return b?.состояние==='проверено заявителем' && safeURL(b.источник) && text(b.проверяющий) && date(b.дата) && text(b.основание)
      ? 'Заявитель записал проверку основания; юридическая применимость не установлена программой.'
      : 'Основание срока не проверено; юридическая применимость не установлена.';
  }
  function responseCoverage(c) {
    const at=cutoff(c),docs=c.документы.map(d=>documentAt(d,at));
    return docs.filter(d=>['обращение','пересмотр','исправление'].includes(d.вид)&&d.дата<=c.дата_проверки).map(d=>{
      const replies=docs.filter(r=>r.вид==='ответ' && receivedAt(r,at) && r.дата<=c.дата_проверки && r.связанные_документы.includes(d.идентификатор));
      const questions=(d.вопросы||[]).map(q=>{
        const references=replies.flatMap(r=>(r.ответы_на_вопросы||[]).filter(a=>a.документ===d.идентификатор&&a.вопрос===q.код).map(a=>({ответ:r.идентификатор,дата:r.точное_время||r.дата,...a}))).sort((a,b)=>Date.parse(a.дата)-Date.parse(b.дата)||cmp(a.ответ,b.ответ));
        const last=references.at(-1);
        const sameDay=last?references.filter(x=>x.дата.slice(0,10)===last.дата.slice(0,10)):[];
        const uncertainDay=sameDay.some(x=>x.дата.length===10);
        const lastStates=last?[...new Set((uncertainDay?sameDay:references.filter(x=>Date.parse(x.дата)===Date.parse(last.дата))).map(x=>x.состояние))]:[];
        const state=lastStates.length>1?'спорный':last?.состояние||'нет ответа';
        return {вопрос:q.код,текст:q.текст,состояние:state,опоры:references,открыт:state!=='полный'};
      });
      const open=questions.filter(q=>q.открыт).map(q=>q.вопрос);
      return {документ:d.идентификатор,получено_ответов:replies.length,вопросы:questions,открытые_вопросы:open,
        полнота:!questions.length?'не проверено':!open.length?'полный':open.length===questions.length&&!replies.length?'нет ответа':'частичный',
        пояснение:!questions.length?'Вопросы не перечислены. Наличие связанного ответа не доказывает полноту.':'Полнота учитывает только полученные ответы со ссылками на вопросы. Моменты UTC нормализованы; ответ без точного времени не упорядочивается внутри дня по догадке. Это проверка записей, не внешних событий.',
        применимость_срока:deadlineState(d)};
    });
  }
  // Приложения не выполняются и не открываются встроенным просмотрщиком.
  // PDF, офисные форматы, HTML, SVG, архивы и исполняемые файлы здесь не принимаются.
  const crcTable=Uint32Array.from({length:256},(_,n)=>{let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;return c>>>0;});
  const crc32=bytes=>{let c=0xffffffff;for(const b of bytes)c=crcTable[(c^b)&255]^(c>>>8);return (c^0xffffffff)>>>0;};
  function imageStructure(bytes,kind) {
    const b=new Uint8Array(bytes),dv=new DataView(b.buffer,b.byteOffset,b.byteLength);
    if(kind==='png'){
      if(![137,80,78,71,13,10,26,10].every((n,i)=>b[i]===n))throw Error('Сигнатура PNG не совпала.');
      let p=8,header=null,ended=false,data=false,afterData=false,palette=false,transparency=false,paletteSize=0;const chunks=[];
      while(p<b.length){
        if(p+12>b.length)throw Error('Обрезан блок PNG.');
        const size=dv.getUint32(p),type=String.fromCharCode(...b.subarray(p+4,p+8));
        if(size>b.length-p-12)throw Error('Длина блока PNG выходит за исходные байты.');
        if(!['IHDR','PLTE','IDAT','IEND','tRNS'].includes(type))throw Error('Блок PNG вне консервативного профиля: '+type+'. Метаданные и анимация здесь не принимаются.');
        if(crc32(b.subarray(p+4,p+8+size))!==dv.getUint32(p+8+size))throw Error('CRC блока PNG не совпала.');
        if(!header && type!=='IHDR')throw Error('PNG начинается не с IHDR.');
        if(type==='IHDR'){
          if(header||p!==8||size!==13)throw Error('Неверный или повторный IHDR.');
          const width=dv.getUint32(p+8),height=dv.getUint32(p+12),depth=b[p+16],color=b[p+17];
          const channels={0:1,2:3,3:1,4:2,6:4}[color],allowed={0:[1,2,4,8,16],2:[8,16],3:[1,2,4,8],4:[8,16],6:[8,16]}[color];
          if(!width||!height||width>4096||height>4096||!allowed?.includes(depth)||b[p+18]!==0||b[p+19]!==0||b[p+20]!==0)throw Error('PNG вне разрешённых размеров, глубины или нечересстрочного профиля.');
          const row=1+Math.ceil(width*channels*depth/8);if(row*height>16*1024*1024)throw Error('Распакованный PNG превышает 16 МиБ.');header={width,height,row,depth,color,channels};
        }else if(type==='PLTE'){
          if(palette||data||transparency||[0,4].includes(header.color)||size<3||size>768||size%3||(header.color===3&&size/3>2**header.depth))throw Error('Непригодная палитра PNG: порядок, размер или цветовой профиль.');palette=true;paletteSize=size/3;
        }else if(type==='IDAT'){
          if(afterData||!size||(header.color===3&&!palette))throw Error('Пустой, несмежный IDAT или данные до обязательной палитры.');data=true;chunks.push(b.subarray(p+8,p+8+size));
        }else if(type==='IEND'){
          if(!data||size!==0||p+12!==b.length)throw Error('Неверный IEND или лишние байты после изображения.');ended=true;
        }else if(type==='tRNS'){
          if(transparency||data||[4,6].includes(header.color))throw Error('Повтор, неверный порядок или недопустимый цветовой тип tRNS.');
          if(header.color===3&&(!palette||size<1||size>paletteSize))throw Error('tRNS не соответствует палитре.');
          if(header.color===0&&(size!==2||dv.getUint16(p+8)>=2**header.depth))throw Error('tRNS не соответствует серому образцу.');
          if(header.color===2&&(size!==6||[0,2,4].some(i=>dv.getUint16(p+8+i)>=2**header.depth)))throw Error('tRNS не соответствует цветным образцам.');
          transparency=true;
        }
        p+=size+12;
      }
      if(!ended||!header||!data||(b[25]===3&&!palette))throw Error('Неполная структура PNG.');
      return {...header,chunks,paletteSize};
    }
    // Только структурный профиль JPEG. Полный декодер JPEG и антивирус не заявляются.
    if(b.length<12||b[0]!==255||b[1]!==216)throw Error('Неполная сигнатура JPEG.');
    let p=2,frame=false,scan=false,ended=false;
    while(p<b.length){
      if(b[p++]!==255)throw Error('Неверная граница сегмента JPEG.');while(b[p]===255)p++;const marker=b[p++];
      if(marker===217){ended=true;if(p!==b.length)throw Error('Лишние байты после JPEG.');break;}
      if(marker===216||marker===0||marker===undefined)throw Error('Неверный маркер JPEG.');
      if(p+2>b.length)throw Error('Обрезана длина JPEG.');const len=dv.getUint16(p);if(len<2||p+len>b.length)throw Error('Обрезан сегмент JPEG.');
      if(marker===192){if(frame||len<11||b[p+2]!==8||!dv.getUint16(p+3)||!dv.getUint16(p+5)||dv.getUint16(p+3)>4096||dv.getUint16(p+5)>4096)throw Error('Неверный базовый кадр JPEG.');frame=true;}
      else if(marker===218){if(!frame||scan)throw Error('JPEG вне однопроходного базового профиля.');scan=true;p+=len;let entropy=0;
        while(p<b.length){if(b[p]!==255){p++;entropy++;continue;}const next=b[p+1];if(next===0){p+=2;entropy++;continue;}if(next>=208&&next<=215){p+=2;continue;}break;}
        if(!entropy)throw Error('Пустые данные JPEG.');continue;
      }else if(![224,219,196,221].includes(marker))throw Error('Сегмент JPEG вне консервативного профиля; метаданные, комментарии и прогрессивные кадры не принимаются.');
      p+=len;
    }
    if(!frame||!scan||!ended)throw Error('Обрезан или неполон JPEG.');return null;
  }
  async function imagePayload(bytes,name) {
    if(!name.toLowerCase().endsWith('.png'))return;
    const info=imageStructure(bytes,'png');
    if(typeof DecompressionStream==='undefined')throw Error('Проверка распаковки PNG недоступна в этой среде; приложение не принято.');
    const stream=new Blob(info.chunks).stream().pipeThrough(new DecompressionStream('deflate')),reader=stream.getReader();let count=0,filter=0;
    // Для индексного PNG снимаются все пять фильтров. Только две строки, не целое изображение.
    let previous=new Uint8Array(info.row-1),current=new Uint8Array(info.row-1);
    const paeth=(a,b,c)=>{const p=a+b-c,pa=Math.abs(p-a),pb=Math.abs(p-b),pc=Math.abs(p-c);return pa<=pb&&pa<=pc?a:pb<=pc?b:c;};
    try{
      while(true){
        const {value,done}=await reader.read();if(done)break;
        if(count+value.length>info.row*info.height)throw Error('PNG превышает заявленную развёрнутую длину.');
        for(let i=0;i<value.length;i++){
          const offset=(count+i)%info.row;
          if(offset===0){filter=value[i];if(filter>4)throw Error('Недопустимый фильтр строки PNG.');continue;}
          if(info.color===3){
            const x=offset-1,left=x?current[x-1]:0,up=previous[x],corner=x?previous[x-1]:0;
            const predictor=filter===0?0:filter===1?left:filter===2?up:filter===3?Math.floor((left+up)/2):paeth(left,up,corner);
            const pixelByte=current[x]=(value[i]+predictor)&255,perByte=8/info.depth;
            for(let part=0;part<perByte&&x*perByte+part<info.width;part++){
              const index=(pixelByte>>(8-info.depth*(part+1)))&((1<<info.depth)-1);
              if(index>=info.paletteSize)throw Error('Индекс пикселя PNG выходит за сохранённую палитру после снятия фильтра.');
            }
            if(offset===info.row-1){const swap=previous;previous=current;current=swap;}
          }
        }
        count+=value.length;
      }
      if(count!==info.row*info.height)throw Error('Данные PNG усечены относительно IHDR.');
    }finally{try{await reader.cancel();}catch{}reader.releaseLock();}
  }

  function attachmentSafety(bytes,name,mime='') {
    const b=new Uint8Array(bytes), errors=[];
    if(!text(name)||!name.isWellFormed()||name!==name.normalize('NFC')||name.length>180||/[\\/\x00-\x1f\x7f\u200b-\u200f\u202a-\u202e\u2060-\u206f]/u.test(name)||name.startsWith('.')||/[. ]$/.test(name))return ['Недопустимое имя приложения: путь, управляющий знак или неоднозначное окончание.'];
    const ext=name.split('.').at(-1).toLowerCase();
    const mimes={txt:['','text/plain'],log:['','text/plain'],csv:['','text/plain','text/csv'],json:['','application/json','text/plain'],png:['','image/png'],jpg:['','image/jpeg'],jpeg:['','image/jpeg']};
    if(!Object.hasOwn(mimes,ext))return ['Этот вид приложения не принимается: разрешены TXT, LOG, CSV, JSON, PNG и JPEG. Активные и неподдерживаемые форматы сохраните отдельно; здесь укажите их отпечаток и происхождение.'];
    if(!mimes[ext].includes(mime))errors.push('Медиатип не соответствует разрешённому расширению приложения.');
    if(b.length>LIMIT)errors.push('Одно приложение не может превышать 5 МиБ.');
    if(['txt','log','csv','json'].includes(ext)){
      try{
        const t=new TextDecoder('utf-8',{fatal:true}).decode(b);
        if(t.includes('\u0000')||/^\s*(?:<!doctype\s+html|<html|<svg|<script|<\?xml|#!)/i.test(t)||/^\s*(?:MZ|\x7fELF|PK\x03\x04)/.test(t))errors.push('Приложение содержит признаки активного, двоичного или замаскированного формата.');
        if(ext==='json')parse(t);
      }catch(e){errors.push('Текстовое приложение не прошло проверку UTF-8/JSON: '+e.message);}
    }else {try{imageStructure(b,ext==='png'?'png':'jpeg');}catch(e){errors.push(e.message);}}
    return errors;
  }
  async function attachment(bytes, name, mime='') {
    if(!(bytes instanceof ArrayBuffer)&&!ArrayBuffer.isView(bytes))throw Error('Нужны исходные байты.');
    if(bytes.byteLength>LIMIT)throw Error(`${name}: файл превышает 5 МиБ.`);
    bytes = bytes instanceof ArrayBuffer ? new Uint8Array(bytes).slice() : new Uint8Array(bytes.buffer,bytes.byteOffset,bytes.byteLength).slice();
    if (bytes.byteLength > LIMIT) throw Error('Одно приложение не может превышать 5 МиБ.');
    const safety=attachmentSafety(bytes,name,mime);if(safety.length)throw Error(safety.join('\n'));
    await imagePayload(bytes,name);const h = await hash(bytes);
    return {идентификатор:`ПРИЛ-${h.slice(0,16)}`, имя:name, медиатип:mime, байты:bytes.byteLength, ша256:h, база64:base64(bytes)};
  }
  const DOCTYPES = ['обращение','приложение','ответ','исправление','заметка','пересмотр'];
  const DOCSTATES = ['черновик','готов к отправке','отправлен','получен адресатом','заменён','отозван'];
  async function caseErrors(c,options={}) {
    const errors=[];
    if (schemas.case) { const work=structuredClone(schemas.case); work.required=work.required.filter(k=>!['ша256','журнал_молчания','итоги_исправлений','остатки_ответов','срез_исправления','события_исправления'].includes(k)); errors.push(...validate(work,c)); if(errors.length)return errors; }
    if (!c || c.редакция_схемы !== RELEASE || c.вид !== 'папка дела' || !text(c.идентификатор) || !date(c.дата_проверки)) return ['Неизвестный формат дела, идентификатор или дата проверки.'];
    if (!Array.isArray(c.документы) || !Array.isArray(c.приложения) || !Array.isArray(c.хронология) || !Array.isArray(c.исправления)) return ['Документы, приложения, хронология и исправления должны быть массивами.'];
    if (c.документы.length > 500 || c.приложения.length > 100) return ['Превышен предел: 500 документов или 100 приложений.'];
    const ids = new Set(c.документы.map(d => d.идентификатор));
    const atts = new Set(c.приложения.map(a => a.идентификатор));
    if (ids.size !== c.документы.length || atts.size !== c.приложения.length) errors.push('Повтор идентификаторов документов или приложений.');
    for (const d of c.документы) {
      if (!text(d.идентификатор) || !DOCTYPES.includes(d.вид) || !DOCSTATES.includes(d.состояние) || !text(d.название) || !date(d.дата) || typeof d.текст !== 'string') errors.push(`Документ ${d.идентификатор}: недопустимое поле.`);
      // Более поздний документ сохраняется, но не попадает в более ранний срез.
      if(d.точное_время && d.точное_время.slice(0,10)!==d.дата)errors.push(`Документ ${d.идентификатор}: точное время расходится с датой.`);
      if(d.получение && (Date.parse(d.получение.внесено)<Date.parse(d.получение.дата_время)||d.получение.дата_время.slice(0,10)<d.дата))errors.push(`Документ ${d.идентификатор}: получение раньше документа или внесено раньше получения.`);
      for(let i=0;i<(d.срезы||[]).length;i++){
        const z=d.срезы[i];if(z.содержание.идентификатор!==d.идентификатор||(i&&Date.parse(z.дата_время)<=Date.parse(d.срезы[i-1].дата_время)))errors.push(`Документ ${d.идентификатор}: нарушен порядок или идентификатор срезов.`);
      }
      if(d.срезы?.length){const body={...d};delete body.история;delete body.срезы;if(!same(body,d.срезы.at(-1).содержание))errors.push(`Документ ${d.идентификатор}: текущие поля расходятся с последним срезом.`);}
      const questionIds=new Set((d.вопросы||[]).map(q=>q.код));
      if(questionIds.size!==(d.вопросы||[]).length)errors.push(`Документ ${d.идентификатор}: повтор кода вопроса.`);
      const links=new Set();
      for(const a of d.ответы_на_вопросы||[]){
        const target=c.документы.find(x=>x.идентификатор===a.документ);
        if(d.вид!=='ответ'||!target||!['обращение','исправление','пересмотр'].includes(target.вид)||!(target.вопросы||[]).some(q=>q.код===a.вопрос)||!d.связанные_документы.includes(a.документ)||target.дата>d.дата)
          errors.push(`Документ ${d.идентификатор}: ответ не связан с допустимым вопросом исходного обращения.`);
        const key=a.документ+'/'+a.вопрос;if(links.has(key))errors.push(`Документ ${d.идентификатор}: вопрос указан дважды.`);links.add(key);
      }
      if(d.проверка_срока?.состояние==='проверено заявителем' && (!safeURL(d.проверка_срока.источник)||!text(d.проверка_срока.проверяющий)||!date(d.проверка_срока.дата)||d.проверка_срока.дата>c.дата_проверки||!text(d.проверка_срока.основание)))errors.push(`Документ ${d.идентификатор}: проверка основания срока не подкреплена обязательными полями.`);
      for(let i=0;i<(d.история||[]).length;i++)if(d.история[i].дата_время.slice(0,10)<d.дата || (i&&Date.parse(d.история[i].дата_время)<Date.parse(d.история[i-1].дата_время)))errors.push(`Документ ${d.идентификатор}: нарушен порядок истории.`);
      if (d.срок && (!date(d.срок) || d.срок < d.дата)) errors.push(`Документ ${d.идентификатор}: срок раньше даты или недействителен.`);
      if (!Array.isArray(d.связанные_документы) || d.связанные_документы.some(id => !ids.has(id) || id === d.идентификатор)) errors.push(`Документ ${d.идентификатор}: неизвестная или циклическая собственная ссылка.`);
      if (!Array.isArray(d.приложения) || d.приложения.some(id => !atts.has(id))) errors.push(`Документ ${d.идентификатор}: неизвестное приложение.`);
      if (!Array.isArray(d.история) || !d.история.length || d.история.some(h => !time(h.дата_время) || !DOCSTATES.includes(h.состояние) || !text(h.основание)) || d.история.at(-1)?.состояние !== d.состояние) errors.push(`Документ ${d.идентификатор}: история не подтверждает текущее состояние.`);
    }
    let total=0;
    for (const a of c.приложения) {
      await stage(options,"приложение дела",{байты:total});
      try { const b=unbase64(a.база64); total+=b.byteLength; errors.push(...attachmentSafety(b,a.имя,a.медиатип));await imagePayload(b,a.имя); if (!text(a.имя) || b.byteLength !== a.байты || await hash(b) !== a.ша256 || a.идентификатор !== `ПРИЛ-${a.ша256.slice(0,16)}`) errors.push(`Приложение ${a.имя}: байты, имя, идентификатор или отпечаток не совпали.`); }
      catch(e) { errors.push(e.message); }
    }
    if (total > 25 * 1024 * 1024) errors.push('Суммарный размер приложений превышает 25 МиБ.');
    for (const h of c.хронология) if (!time(h.дата_время) || !text(h.событие) || (h.документ && !ids.has(h.документ))) errors.push('Хронология: неизвестный документ или некорректная запись.');
    if(new Set(c.исправления.map(r=>r.идентификатор)).size!==c.исправления.length)errors.push('Повтор идентификатора исправления.');
    for(const r of c.исправления)if(r.дата>c.дата_проверки)errors.push('Дата исправления позже проверки дела.');
    for (const r of c.исправления) if (!text(r.идентификатор) || !text(r.запись) || !date(r.дата) || !Array.isArray(r.получатели) || !r.восстановление) errors.push('Исправление: неполная структура.');
    errors.push(...await correctionEventErrors(c,options));
    return errors;
  }
  async function exportCase(c,options={}) {
    if(options.signal?.aborted)throw new DOMException("Обработка отменена.","AbortError");
    if(options.jsonBudget)operationBudget('case-export',c);
    c=structuredClone(c);await stage(options,"снимок дела");c.события_исправления??=[];const errors=await caseErrors(c,options);if(errors.length)throw Error(errors.join('\n'));
    // Только частная, уже проверенная копия. Прежние расчётные представления
    // будут пересчитаны; их удаление здесь не меняет объект вызывающего.
    for(const key of ['журнал_молчания','срез_исправления','итоги_исправлений','остатки_ответов'])delete c[key];
    for(const m of c.переносы||[])await restoreMigration(m,null,'папка дела');
    await stage(options,'проверка дела');
    const result=await seal({...payload(c),срез_исправления:correctionTimeline(c),журнал_молчания:silence(c),остатки_ответов:responseCoverage(c),итоги_исправлений:c.исправления.map(r=>({идентификатор:r.идентификатор,...correction(r)}))},options);await stage(options,'экспорт дела готов');return result;
  }
  async function restoreCase(input,options={},ownership=null) {
    if(ownership!==OWNED_ENVELOPE)input=structuredClone(input);await stage(options,"снимок восстанавливаемого дела");
    if(!schemas.case)throw Error('Схема папки дела не загружена.');
    const structural=validate(schemas.case,input);if(structural.length)throw Error(structural.join('\n'));
    if(await hashCanonical(payload(input))!==input.ша256)throw Error('Не совпал отпечаток папки дела.');
    const recomputed=await exportCase(input,options);
    if(!same(recomputed,input))throw Error('Производные итоги папки расходятся с повторным расчётом.');
    const c=payload(input);delete c.журнал_молчания;delete c.остатки_ответов;delete c.итоги_исправлений;delete c.срез_исправления;
    return c;
  }
  const REQUIREMENT_ID=/^[A-Za-z0-9А-Яа-яЁё_-]{1,100}$/u;
  function requirementGraph(records, original={узлы:[],связи:[]}, revision=RELEASE) {
    if(!Array.isArray(records)||records.length>500)throw Error('Реестр ограничен 500 записями.');
    const recordIds=new Set();
    for(const r of records){const id=String(r?.код||'');const folded=id.normalize('NFC').toLowerCase();if(!REQUIREMENT_ID.test(id)||recordIds.has(folded))throw Error('Недопустимый или повторный код требования.');recordIds.add(folded);}

    const nodes=structuredClone(original.узлы || []); const edges=structuredClone(original.связи || []);
    if(nodes.some(x=>!x||typeof x.идентификатор!=='string'||!x.идентификатор.trim()))throw Error('Узел базового графа без строкового идентификатора.');
    const names = new Set(nodes.map(x => x.идентификатор));
    if(new Set(nodes.map(x=>String(x.идентификатор).normalize('NFC').toLowerCase())).size!==nodes.length)throw Error('Коллизия идентификаторов базового графа.');
    for(const e of edges)if(!names.has(e.от)||!names.has(e.к))throw Error('Односторонняя утрата узла базового графа.'); const edgeKeys = new Set(edges.map(e => canonical(e)));
    const roles = [['требующая_сторона','требующая сторона'],['посредник_знания','посредник знания'],['версия','версия'],['политика','политика'],['источник','источник'],['испытание','испытание']];
    const add = x => { if([...names].some(n=>n.normalize('NFC').toLowerCase()===x.идентификатор.normalize('NFC').toLowerCase()))throw Error('Коллизия базового графа и локальной роли: '+x.идентификатор);nodes.push(x);names.add(x.идентификатор); };
    const link = (a,b,label) => { const e={от:a,к:b,связь:label}; if (!edgeKeys.has(canonical(e))) { edges.push(e); edgeKeys.add(canonical(e)); } };
    for (const r of records) {
      const center=`требование-${r.код}`;
      add({идентификатор:center,вид:'запись требования',название:r.код,путь:`/реестр-требований.html#${center}`,класс_доказательства:r.класс_доказательства});
      for (const [key,label] of roles) {
        const id=`${center}-${key}`;
        add({идентификатор:id,вид:label,название:r[key] || 'Не установлено',путь:`/реестр-требований.html#${id}`,установлено:text(r[key]) && r[key] !== 'Не установлено'});
        link(center,id,label); link(id,center,`связанная запись требования`);
      }
      if (names.has('сайт')) { link('сайт',center,'локальная запись'); link(center,'сайт','проект'); }
    }
    return {...original, редакция:revision,узлы:nodes,связи:edges,число_узлов:nodes.length,число_связей:edges.length};
  }
  async function registryErrors(records) {
    if(!schemas.registry)return ['Схема реестра не загружена.'];
    const recordSchema=schemas.registry.properties.производная.properties.записи;
    const errors=validate(recordSchema,records);if(errors.length)return errors;
    const ids=new Set();
    for(const r of records){
      const folded=r.код.normalize('NFC').toLowerCase();if(ids.has(folded))errors.push('Повтор кода требования.');ids.add(folded);
      if(!safeURL(r.источник))errors.push(`Требование ${r.код}: недопустимый адрес источника.`);
      if(!time(r.создано)||Date.parse(r.создано)>Date.now())errors.push(`Требование ${r.код}: недопустимое время создания.`);
      if(await hashCanonical(payload(r))!==r.ша256)errors.push(`Требование ${r.код}: не совпал отпечаток записи.`);
    }
    return errors;
  }
  const SIGNING_BOUNDARY='Локальный ключ сеанса, не ключ владельца и не независимое удостоверение личности.';
  async function exportRegistry(records,base,keyPair=null,migration=null) {
    records=structuredClone(records);base=structuredClone(base);const errors=await registryErrors(records);if(errors.length)throw Error(errors.join('\n'));
    let previous=null;
    if(migration){const old=await verifyRegistryMigration(migration);previous=old;for(const original of old.производная.записи){const current=records.find(x=>x.код===original.код);if(!current||!same(current,original))throw Error('Перенос не допускает удаления или подмены исходного требования. Сохраните отдельную новую запись.');}}
    if(!crypto?.subtle)throw Error('Подпись реестра требует настоящего Web Crypto API в защищённом контексте.');
    const work=await seal({редакция_схемы:RELEASE,вид:'реестр требований',учёт_записей:recordOrigins(records,previous),записи:structuredClone(records),отпечаток_базового_графа:await hashCanonical(base),граф:requirementGraph(records,base),граница:'Локальная производная. Связь и подпись не доказывают реальность требования или истинность источника.',перенос:migration});
    const keys=keyPair||await crypto.subtle.generateKey({name:'Ed25519'},false,['sign','verify']);
    const publicBytes=await crypto.subtle.exportKey('raw',keys.publicKey);
    const signature=await crypto.subtle.sign('Ed25519',keys.privateKey,canonicalBytes(work));
    return {редакция_схемы:RELEASE,вид:'подписанный реестр требований',производная:work,подпись:{алгоритм:'Ed25519',открытый_ключ_Base64:base64(publicBytes),значение_Base64:base64(signature),граница:SIGNING_BOUNDARY}};
  }
  async function restoreRegistry(envelope,base) {
    envelope=structuredClone(envelope);base=structuredClone(base);
    if(!schemas.registry)throw Error('Схема подписанного реестра не загружена.');
    const errors=validate(schemas.registry,envelope);if(errors.length)throw Error(errors.join('\n'));
    const body=envelope.производная;let previous=null;if(body.перенос){const old=await verifyRegistryMigration(body.перенос);previous=old;for(const original of old.производная.записи)if(!body.записи.some(x=>same(x,original)))throw Error('Исходное требование утрачено или изменено после переноса.');}const pub=unbase64(envelope.подпись.открытый_ключ_Base64);const sig=unbase64(envelope.подпись.значение_Base64);
    if(pub.length!==32||sig.length!==64)throw Error('Неверная длина открытого ключа или подписи Ed25519.');
    const key=await crypto.subtle.importKey('raw',pub,'Ed25519',false,['verify']);
    if(!await crypto.subtle.verify('Ed25519',key,sig,canonicalBytes(body)))throw Error('Подпись локального реестра не совпала.');
    if(await hashCanonical(payload(body))!==body.ша256)throw Error('Отпечаток производной реестра не совпал.');
    if(!same(recordOrigins(body.записи,previous),body.учёт_записей))throw Error('Подменён учёт происхождения записей.');
    const recordErrors=await registryErrors(body.записи);if(recordErrors.length)throw Error(recordErrors.join('\n'));
    if(await hashCanonical(base)!==body.отпечаток_базового_графа)throw Error('Иная версия базового графа: автоматическая подмена основания запрещена. Сохраните исходный реестр; требуется документированная миграция.');
    if(!same(requirementGraph(body.записи,base),body.граф))throw Error('Граф расходится с повторным расчётом подписанных записей.');
    return {записи:structuredClone(body.записи),отпечаток_производной:body.ша256,отпечаток_ключа:await hash(pub),граница:SIGNING_BOUNDARY,перенос:body.перенос||null};
  }
  // Односторонний перенос — данные того же ядра, а не второй исполняемый движок.
  const ENVELOPE_LIMIT=128*1024*1024;
  const snapshotBytes=(bytes,limit=ENVELOPE_LIMIT)=>{
    if(!(bytes instanceof ArrayBuffer)&&!ArrayBuffer.isView(bytes))throw Error('Нужен ArrayBuffer или его представление.');
    if(bytes.byteLength>limit)throw Error('Исходные байты превышают допустимый размер до копирования.');
    return bytes instanceof ArrayBuffer?new Uint8Array(bytes).slice():new Uint8Array(bytes.buffer,bytes.byteOffset,bytes.byteLength).slice();
  };
  async function originalEnvelope(bytes,kind) {
    if(bytes.length>ENVELOPE_LIMIT)throw Error('Исходная оболочка превышает 128 МиБ.');
    const env=parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes),ENVELOPE_LIMIT);
    const version=env.редакция_схемы;
    const suffix=version==='4.3.0-wip'?'430':version==='4.4.0-wip'?'440':version==='4.5.0-wip'?'450':version==='4.6.0-wip'?'460':version==='4.7.0-wip'?'470':version==='4.8.0-wip'?'480':version==='4.9.0-wip'?'490':null;
    if(!suffix || env.вид!==kind)throw Error('Для переноса требуется исходная оболочка 4.3.0, 4.4.0, 4.5.0, 4.6.0, 4.7.0, 4.8.0 или 4.9.0 нужного вида.');
    const selected=schemas[(kind==='продольная панель'?'panel':'case')+suffix];
    if(!selected)throw Error('Прежняя схема не загружена; подмена её текущей схемой запрещена.');
    const errors=validate(selected,env);if(errors.length)throw Error(errors.join('\n'));
    if(await hashCanonical(payload(env))!==env.ша256)throw Error('Не совпал отпечаток прежней оболочки.');
    return env;
  }
  async function rawRecord(bytes,name) {
    return {имя:name,байты:bytes.length,ша256:await hash(bytes),база64:base64(bytes)};
  }
  async function migrationRecord(bytes,name,env,unknown,algorithm='война/односторонний-перенос/4.10.0-1') {
    const source=await rawRecord(bytes,name);
    const result={исходная_редакция:env.редакция_схемы,отпечаток_файла:source.ша256,отпечаток_записи:env.ша256||env.производная?.ша256,
      алгоритм:algorithm,добавленные_неизвестные:unknown,исходник:source,
      сохранённые_источники:[...new Set((env.источники||[]).map(x=>x.отпечаток_исходного_файла))].sort(),
      прежние_решения_не_активированы:(env.разрешения||[]).map(x=>x.идентификатор).sort(),
      граница:'Сохранены точные байты и прежняя схема. Итоги пересчитаны; прежние решения не активированы автоматически. SHA-256 и локальная подпись не устанавливают истинность или полномочия.'};
    freeze(result);migrationTrust.add(result);return result;
  }
  const unknownFields=kind=>kind==='продольная панель'
    ? ['идентификатор группы управления при его отсутствии','история основания независимости при её отсутствии','доказательство фактического вызова при его отсутствии']
    : ['точное время проверки','точное время документа','отдельное удостоверение получения','неизвестные прежние вопросы','неизвестные прежние основания срока','исторические срезы содержания до переноса'];
  async function restoreMigration(m,schema,kind) {
    if(schemas.migration){const errors=validate(schemas.migration,m);if(errors.length)throw Error(errors.join('\n'));}
    const bytes=unbase64(m.исходник.база64,ENVELOPE_LIMIT),env=await originalEnvelope(bytes,kind);
    const computed=await migrationRecord(bytes,m.исходник.имя,env,unknownFields(kind),m.алгоритм);
    if(!same(m,computed))throw Error('Подменена запись происхождения или исходные байты переноса.');
    return computed;
  }
  async function recoverOriginal(m) {
    const bytes=unbase64(m.исходник.база64,ENVELOPE_LIMIT);
    if(bytes.length!==m.исходник.байты||await hash(bytes)!==m.отпечаток_файла||m.исходник.ша256!==m.отпечаток_файла)throw Error('Исходные байты происхождения подменены.');
    return bytes;
  }
  async function migratePanelBytes(input,name,schema,options={}) {
    if(options.signal?.aborted)throw new DOMException('Обработка отменена.','AbortError');
    const bytes=snapshotBytes(input),env=await originalEnvelope(bytes,'продольная панель'),items=[];let total=0;
    for(const x of env.источники){
      await stage(options,"перенос исходника",{обработано:items.length});
      const raw=unbase64(x.база64);total+=raw.length;if(total>TOTAL_LIMIT)throw Error('Исходники переноса превышают 25 МиБ.');
      const item=await ingest(raw,x.имя,schema,options);
      if(item.байты!==x.байты||item.отпечаток_исходного_файла!==x.отпечаток_исходного_файла)throw Error('Байты исходника прежней панели не совпали.');
      items.push(item);
    }
    const origin=await migrationRecord(bytes,name,env,unknownFields('продольная панель'));
    // История старых исключений целиком остаётся внутри исходных байтов, не заменяет новый разбор.
    const migrations=[origin];await mergeAsync(items,schema,[],[],migrations,options);
    Object.defineProperties(items,{разрешения:{value:[]},отзывы:{value:[]},переносы:{value:migrations}});
    return items;
  }
  async function importPanelBytes(input,name,schema,options={}) {
    if(options.signal?.aborted)throw new DOMException('Обработка отменена.','AbortError');
    const bytes=snapshotBytes(input),env=parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes),ENVELOPE_LIMIT,options.jsonBudget);
    await stage(options,"разбор панели");return env.редакция_схемы===RELEASE?restorePanel(env,schema,options,OWNED_ENVELOPE):migratePanelBytes(bytes,name,schema,options);
  }
  async function migrateCaseBytes(input,name,options={}) {
    if(options.signal?.aborted)throw new DOMException('Обработка отменена.','AbortError');
    const bytes=snapshotBytes(input),env=await originalEnvelope(bytes,'папка дела'),origin=await migrationRecord(bytes,name,env,unknownFields('папка дела'));
    const c=structuredClone(payload(env));for(const key of ['журнал_молчания','остатки_ответов','итоги_исправлений'])delete c[key];
    c.редакция_схемы=RELEASE;c.точное_время_проверки??=null;c.переносы=[origin];c.события_исправления=structuredClone(env.события_исправления||[]);delete c.срез_исправления;
    c.происхождение={исходная_редакция:env.редакция_схемы,отпечаток_файла:origin.отпечаток_файла,отпечаток_записи:env.ша256,описание:'Односторонний перенос с исходными байтами; неизвестные поля не заполнены догадками.'};
    for(const d of c.документы) {
      d.вопросы??=[];d.ответы_на_вопросы??=[];d.проверка_срока??={состояние:'не проверено',источник:'',проверяющий:'',дата:'',основание:''};
      d.точное_время??=null;d.получение??=null;d.срезы??=[];
    }
    const errors=await caseErrors(c,options);if(errors.length)throw Error(errors.join('\n'));
    await stage(options,'перенос дела готов');return c;
  }
  async function importCaseBytes(input,name,options={}) {
    if(options.signal?.aborted)throw new DOMException('Обработка отменена.','AbortError');
    const bytes=snapshotBytes(input),env=parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes),ENVELOPE_LIMIT,options.jsonBudget);
    await stage(options,"разбор дела");return env.редакция_схемы===RELEASE?restoreCase(env,options,OWNED_ENVELOPE):migrateCaseBytes(bytes,name,options);
  }
  function documentSnapshot(d,at) {const content=structuredClone(d);delete content.история;delete content.срезы;return {дата_время:at,содержание:content};}
  async function reviseDocument(input,id,changes,reason,at=new Date().toISOString()) {
    if(!text(reason)||!time(at)||Date.parse(at)>Date.now())throw Error('Изменению нужны основание и действительное время внесения, не из будущего.');
    const c=structuredClone(input),d=c.документы.find(x=>x.идентификатор===id);if(!d)throw Error('Документ не найден.');
    if(Object.keys(changes).some(k=>['идентификатор','история','срезы'].includes(k)))throw Error('Идентификатор и историю нельзя переписать изменением полей.');
    if(Date.parse(at)<=Date.parse(d.история.at(-1).дата_время))throw Error('Внесение должно быть позже последнего события; заднее число не принято.');
    if(changes.вопросы && (d.вопросы||[]).some(q=>!changes.вопросы.some(x=>x.код===q.код)))throw Error('Прежний вопрос нельзя удалить: сохраните его код и оформите изменение содержания с основанием.');
    Object.assign(d,structuredClone(changes));
    d.история.push({дата_время:at,состояние:d.состояние,основание:reason});d.срезы??=[];d.срезы.push(documentSnapshot(d,at));
    const errors=await caseErrors(c);if(errors.length)throw Error(errors.join('\n'));return c;
  }
  // Прежнее локальное значение читается лишь после явного согласия и никогда здесь не удаляется.
  async function inspectPreviousStorage(storage,key,consent) {
    if(consent!==true)throw Error('Требуется явное согласие на чтение прежнего хранилища.');
    const raw=storage.getItem(key);if(raw===null)throw Error('Прежнее значение не найдено.');
    const bytes=utf8(raw);if(bytes.length>40*1024*1024)throw Error('Прежнее локальное значение превышает 40 МиБ.');
    const value=parse(raw,40*1024*1024);return {ключ:key,исходник:await rawRecord(bytes,key+'.json'),значение:value,граница:'Исходное локальное значение не изменено. Чтение не означает проверку схемы, подписи или успешную миграцию.'};
  }
  const draftTrust=new WeakMap();
  async function previewStorageMigration(storage,key,consent){
    const kind=['война-аудит-когнитивной-свободы-4-5','война-аудит-когнитивной-свободы-4-6','война-аудит-когнитивной-свободы-4-7','война-аудит-когнитивной-свободы-4-8','война-аудит-когнитивной-свободы-4-9'].includes(key)?'localAudit450':['война-реестр-требований-4-5','война-реестр-требований-4-6','война-реестр-требований-4-7','война-реестр-требований-4-8','война-реестр-требований-4-9'].includes(key)?'localRegistry450':null;
    if(!kind)throw Error('Точная прежняя схема этого ключа не поддерживается. Исходник можно сохранить отдельно.');
    const old=await inspectPreviousStorage(storage,key,consent),selected=schemas[kind];
    if(!selected)throw Error('Точная схема прежнего локального черновика не загружена.');
    const errors=validate(selected,old.значение);if(errors.length)throw Error(errors.join('\n'));
    if(kind==='localRegistry450'){
      const recordErrors=await registryErrors(old.значение.записи);if(recordErrors.length)throw Error(recordErrors.join('\n'));
      if(old.значение.перенос){const previous=await verifyRegistryMigration(old.значение.перенос);preserveRecords(previous,old.значение.записи);}
    }
    const target=key.replace(/4-[56789]$/,'4-10'),prior=storage.getItem(target);
    const result=freeze({исходный_ключ:key,целевой_ключ:target,исходник:old.исходник,производная:structuredClone(old.значение),
      неизвестные_поля:kind==='localAudit450'?['Система, версия, условия, журналы и серия не сохранялись этим локальным черновиком.']:['Подпись локального черновика отсутствует; подписанная производная создаётся только отдельным экспортом.'],
      целевой_ключ_занят:prior!==null,граница:'Предварительный просмотр точной прежней формы. Не перенос неопределённого формата, не внешнее подтверждение. Исходный ключ не удаляется.'});
    draftTrust.set(result,{raw:new TextDecoder('utf-8',{ignoreBOM:true}).decode(unbase64(old.исходник.база64,40*1024*1024)),prior});return result;
  }
  function commitStorageMigration(storage,preview,consent){
    if(consent!==true)throw Error('Запись нового ключа требует отдельного подтверждения пользователя.');
    const trusted=draftTrust.get(preview);if(!trusted)throw Error('Предварительный просмотр не прошёл проверку этого сеанса.');
    if(storage.getItem(preview.исходный_ключ)!==trusted.raw||storage.getItem(preview.целевой_ключ)!==trusted.prior)throw Error('Исходный или целевой черновик изменился; сформируйте новый предварительный просмотр.');
    storage.setItem(preview.целевой_ключ,canonical(preview.производная));draftTrust.delete(preview);
    return {ключ:preview.целевой_ключ,исходный_ключ_сохранён:true,граница:'Запись выполнена в локальном хранилище; межвкладочная атомарность и сохранность на диске не удостоверены.'};
  }
  const REGISTRY_DEPTH=4,REGISTRY_BYTES=32*1024*1024;
  function chainContext(options={}){return {depth:0,total:0,seen:new Set(),rows:[],options};}
  function recordOrigins(records,previous=null){
    return records.map(r=>({код:r.код,вид:previous?.производная.записи.some(x=>x.код===r.код)?'сохранённая запись':'новая запись',
      отпечаток_исходной_записи:previous?.производная.записи.find(x=>x.код===r.код)?.ша256||null}));
  }
  function preserveRecords(previous,records){
    for(const original of previous.производная.записи)if(!records.some(x=>same(x,original)))throw Error('Перенос не допускает удаления или подмены исходного требования. Сохраните отдельную новую запись.');
  }
  async function verifyOldRegistry(bytes,baseBytes,ctx=chainContext()) {
    await stage(ctx.options,'звено реестра',{глубина:ctx.depth+1,учтённые_байты:ctx.total});
    if(++ctx.depth>REGISTRY_DEPTH)throw Error('Цепочка реестра превышает четыре исходные оболочки.');
    ctx.total+=bytes.length+baseBytes.length;
    if(ctx.total>REGISTRY_BYTES)throw Error('Суммарный объём цепочки реестра превышает 32 МиБ.');
    const fileHash=await hash(bytes);if(ctx.seen.has(fileHash))throw Error('Цикл или повтор исходной оболочки реестра.');ctx.seen.add(fileHash);
    const env=parse(new TextDecoder('utf-8',{fatal:true}).decode(bytes),REGISTRY_BYTES);
    const base=parse(new TextDecoder('utf-8',{fatal:true}).decode(baseBytes),25*1024*1024);
    const version=env.редакция_схемы,selected=version==='4.4.0-wip'?schemas.registry440:version==='4.5.0-wip'?schemas.registry450:version==='4.6.0-wip'?schemas.registry460:version==='4.7.0-wip'?schemas.registry470:version==='4.8.0-wip'?schemas.registry480:version==='4.9.0-wip'?schemas.registry490:version===RELEASE?schemas.registry:null;
    if(!selected)throw Error('Нет точной схемы прежнего подписанного реестра.');
    const errors=validate(selected,env);if(errors.length)throw Error(errors.join('\n'));
    const body=env.производная;
    if(await hashCanonical(payload(body))!==body.ша256)throw Error('Подменён отпечаток прежней производной.');
    const pub=unbase64(env.подпись.открытый_ключ_Base64),sig=unbase64(env.подпись.значение_Base64);
    if(pub.length!==32||sig.length!==64)throw Error('Некорректная длина Ed25519.');
    const key=await crypto.subtle.importKey('raw',pub,'Ed25519',false,['verify']);
    if(!await crypto.subtle.verify('Ed25519',key,sig,canonicalBytes(body)))throw Error('Прежняя подпись реестра не совпала.');
    await stage(ctx.options,'подпись предка',{глубина:ctx.depth});
    if(await hashCanonical(base)!==body.отпечаток_базового_графа)throw Error('Предоставлен другой прежний базовый граф.');
    const issues=await registryErrors(body.записи);if(issues.length)throw Error(issues.join('\n'));
    if(!same(requirementGraph(body.записи,base,version),body.граф))throw Error('Прежний граф не совпадает с расчётом семи ролей и обратных связей.');
    let previous=null;
    if(body.перенос){previous=await verifyRegistryMigration(body.перенос,ctx);preserveRecords(previous,body.записи);}
    if([RELEASE,'4.6.0-wip','4.7.0-wip','4.8.0-wip','4.9.0-wip'].includes(version)&&!same(recordOrigins(body.записи,previous),body.учёт_записей))throw Error('Подменён расчёт происхождения новых и сохранённых записей.');
    ctx.rows.push({редакция:version,отпечаток_файла:fileHash,отпечаток_ключа:await hash(pub),записей:body.записи.length});
    return {env,base,pub};
  }
  async function registryMigration(bytes,name,baseBytes,baseName) {
    const old=await verifyOldRegistry(bytes,baseBytes);
    return {происхождение:await migrationRecord(bytes,name,old.env,[]),исходный_граф:await rawRecord(baseBytes,baseName),отпечаток_старого_ключа:await hash(old.pub),отпечаток_старого_графа:old.env.производная.отпечаток_базового_графа};
  }
  async function verifyRegistryMigration(m,ctx=chainContext()) {
    if(!['война/односторонний-перенос/4.5.0-1','война/односторонний-перенос/4.6.0-1','война/односторонний-перенос/4.7.0-1','война/односторонний-перенос/4.8.0-1','война/односторонний-перенос/4.9.0-1','война/односторонний-перенос/4.10.0-1'].includes(m?.происхождение?.алгоритм))throw Error('Неподдерживаемый алгоритм переноса реестра.');
    // Check declared and encoded byte budgets before allocating decoded buffers.
    for(const raw of [m.происхождение.исходник,m.исходный_граф])if(!raw||!Number.isSafeInteger(raw.байты)||raw.байты<0||raw.байты>REGISTRY_BYTES||typeof raw.база64!=='string'||raw.база64.length>Math.ceil(REGISTRY_BYTES/3)*4)throw Error('Превышен бюджет исходника цепочки.');
    const rawBytes=m.происхождение.исходник.байты+m.исходный_граф.байты;
    if(ctx.total+rawBytes>REGISTRY_BYTES)throw Error('Суммарный бюджет цепочки превышен до декодирования.');
    await stage(ctx.options,'до декодирования звена',{глубина:ctx.depth+1});
    const bytes=await recoverOriginal(m.происхождение),baseBytes=unbase64(m.исходный_граф.база64,REGISTRY_BYTES);
    const old=await verifyOldRegistry(bytes,baseBytes,ctx);
    const expected={происхождение:await migrationRecord(bytes,m.происхождение.исходник.имя,old.env,[],m.происхождение.алгоритм),исходный_граф:await rawRecord(baseBytes,m.исходный_граф.имя),отпечаток_старого_ключа:await hash(old.pub),отпечаток_старого_графа:old.env.производная.отпечаток_базового_графа};
    if(!same(expected,m))throw Error('Подменено происхождение, исходный граф или старый ключ цепочки.');
    return old.env;
  }
  async function registryHistory(migration,options={}){
    if(options.signal?.aborted)throw new DOMException('Обработка отменена.','AbortError');
    if(!migration)return [];
    migration=structuredClone(migration);const ctx=chainContext(options);await verifyRegistryMigration(migration,ctx);await stage(options,"цепочка проверена");return ctx.rows;
  }
  async function recoverRegistryLink(migration,index,options={}){
    if(options.signal?.aborted)throw new DOMException('Обработка отменена.','AbortError');
    migration=structuredClone(migration);
    if(!Number.isSafeInteger(index)||index<0||index>=REGISTRY_DEPTH)throw Error('Неверный номер звена цепочки.');
    await verifyRegistryMigration(migration,chainContext(options));let m=migration;
    for(let i=0;i<index;i++){const env=parse(new TextDecoder().decode(await recoverOriginal(m.происхождение)),REGISTRY_BYTES);m=env.производная.перенос;if(!m)throw Error('Звено цепочки не найдено.');}
    const result={имя:m.происхождение.исходник.имя,байты:await recoverOriginal(m.происхождение),граф:unbase64(m.исходный_граф.база64,REGISTRY_BYTES),имя_графа:m.исходный_граф.имя};await stage(options,"звено подготовлено");return result;
  }
  async function migrateRegistryBytes(input,name,newBase,oldBaseInput,baseName='прежний-базовый-граф.json',{signal}={}) {
    const bytes=snapshotBytes(input),baseBytes=snapshotBytes(oldBaseInput),base=structuredClone(newBase);
    await checkpoint(signal);const old=await verifyOldRegistry(bytes,baseBytes),origin=await registryMigration(bytes,name,baseBytes,baseName);
    await checkpoint(signal);const out=await exportRegistry(old.env.производная.записи,base,null,origin);await checkpoint(signal);
    return {записи:structuredClone(out.производная.записи),перенос:origin,подписанный_реестр:out,граница:SIGNING_BOUNDARY};
  }

  // Квитанция — отдельная модель происхождения. Она не изменяет счётчик наблюдений.
  async function verifyCallReceipt(input,request,response,{pinnedKey=null,signal,onStage}={}) {
    if(signal?.aborted)throw new DOMException('Обработка отменена.','AbortError');
    const candidate=structuredClone(input),req=snapshotBytes(request,LIMIT),res=snapshotBytes(response,LIMIT),options={signal,onStage};
    await stage(options,'снимок квитанции');
    if(!schemas.callReceipt)throw Error('Схема квитанции не загружена.');
    const selectedReceipt=candidate?.запись?.редакция_схемы==='4.7.0-wip'?schemas.callReceipt470:candidate?.запись?.редакция_схемы==='4.8.0-wip'?schemas.callReceipt480:candidate?.запись?.редакция_схемы==='4.9.0-wip'?schemas.callReceipt490:schemas.callReceipt;
    if(!selectedReceipt)throw Error('Точная схема квитанции не загружена.');
    const errors=validate(selectedReceipt,candidate);if(errors.length)throw Error(errors.join('\n'));
    const body=candidate.запись;
    if(Date.parse(body.мгновение)>Date.now())throw Error('Время квитанции находится в будущем.');
    if(!safeURL(body.издатель)||!body.область_идентификатора.trim()||!body.идентификатор_вызова.trim())throw Error('Не задана область уникальности квитанции.');
    const pub=unbase64(candidate.открытый_ключ,32),signature=unbase64(candidate.подпись,64);
    if(pub.length!==32||signature.length!==64)throw Error('Неверная длина ключа или подписи квитанции.');
    const fingerprint=await hash(pub);
    if(pinnedKey!==null&&fingerprint!==pinnedKey)throw Error('Ключ квитанции не совпадает с отдельно заданным отпечатком.');
    await stage(options,'ключ квитанции');
    const key=await crypto.subtle.importKey('raw',pub,{name:'Ed25519'},false,['verify']);
    if(!await crypto.subtle.verify('Ed25519',key,signature,canonicalBytes(body)))throw Error('Подпись квитанции не совпала.');
    await stage(options,'подпись квитанции');
    if(await hash(req)!==body.запрос_ша256||await hash(res)!==body.ответ_ша256)throw Error('Квитанция не связана с точными байтами запроса и ответа.');
    await stage(options,'байты квитанции');
    return {подпись_проверена:true,байты_связаны:true,отпечаток_ключа:fingerprint,ключ_сопоставлен:pinnedKey!==null,
      заявленная_область:[body.издатель,body.область_идентификатора,body.идентификатор_вызова],
      полномочия:'не установлены',независимость:'не установлена',действительный_вызов:'не установлен',
      граница:'Подпись подтверждает согласованность квитанции относительно ключа. Издатель, область уникальности, время и вызов заявлены; подпись и совпадение байтов не устанавливают истинность, полномочия или независимость. Счётчик повторов не изменяется.'};
  }


  // Ограничительный профиль пользовательского потока. Это предел входа,
  // не обещание верхней границы RSS и не автоматический мобильный допуск.
  const INTERACTIVE_PROFILE=Object.freeze({id:'война/настольный-ограниченный/4.10-1',
    files:25,fileBytes:2*1024*1024,totalFileBytes:8*1024*1024,envelopeBytes:32*1024*1024,
    stringUnits:24*1024*1024,nodes:250000,depth:40,documents:100,events:512,recipients:64,questions:4096,conflicts:2048,questionReferences:32768,conflictReferences:16384,expansionBytes:96*1024*1024});
  function resourceCheck(value,{files=false,envelope=false,representation=false}={}) {
    const p=INTERACTIVE_PROFILE;let nodes=0,units=0,binary=0,questions=0,conflicts=0;
    const active=new WeakSet();
    function visit(v,depth=0){
      if(++nodes>p.nodes||depth>p.depth)throw Error('Ограничительный профиль: превышены число узлов или глубина.');
      if(typeof v==='string'){units+=v.length;if(units>p.stringUnits)throw Error('Ограничительный профиль: слишком много строковых данных.');return;}
      if(typeof v==='number'&&!Number.isFinite(v))throw Error('Неконечное число не входит в профиль снимка.');
      if(v===null||typeof v==='boolean'||typeof v==='number'||v===undefined)return;
      if(typeof v!=='object')throw Error('Ограничительный профиль: допускаются только данные, не исполняемые значения.');
      if(typeof SharedArrayBuffer!=='undefined'&&(v instanceof SharedArrayBuffer||(ArrayBuffer.isView(v)&&v.buffer instanceof SharedArrayBuffer)))throw Error('Общая изменяемая память не принимается.');
      if(v instanceof ArrayBuffer||ArrayBuffer.isView(v)){binary+=v.byteLength;if(binary>p.envelopeBytes)throw Error('Ограничительный профиль: превышен объём байтов.');return;}
      if(typeof Blob!=='undefined'&&v instanceof Blob){binary+=v.size;if(v.size>(envelope?p.envelopeBytes:p.fileBytes)||binary>(envelope?p.envelopeBytes:p.totalFileBytes))throw Error('Ограничительный профиль: файл слишком велик.');return;}
      if(active.has(v))throw Error('Циклическая структура не принимается.');
      if(!Array.isArray(v)&&Object.getPrototypeOf(v)!==Object.prototype&&Object.getPrototypeOf(v)!==null)throw Error('Неизвестный вид объекта снимка.');
      if(Object.getOwnPropertySymbols(v).length)throw Error('Символьные поля снимка не принимаются.');
      active.add(v);
      if(Array.isArray(v)&&v.length>p.nodes)throw Error('Ограничительный профиль: слишком длинный массив.');
      for(const key of Object.keys(v)){
        const d=Object.getOwnPropertyDescriptor(v,key);if(!d||!Object.hasOwn(d,'value'))throw Error('Вычисляемые поля снимка не принимаются.');
        const item=d.value;
        if(Array.isArray(item)&&['вопросы','ответы_на_вопросы'].includes(key))questions+=item.length;
        if(Array.isArray(item)&&['конфликты','конфликты_проекции'].includes(key))conflicts+=item.length;
        if(questions>(representation?p.questionReferences:p.questions)||conflicts>(representation?p.conflictReferences:p.conflicts))throw Error('Ограничительный профиль: превышен суммарный предел вопросов или конфликтов.');
        if(Array.isArray(item)&&((key==='документы'&&item.length>p.documents)||(key==='события_исправления'&&item.length>p.events)||(key==='получатели'&&item.length>p.recipients)))throw Error('Ограничительный профиль: превышен предметный предел '+key+'.');
        visit(key,depth+1);visit(item,depth+1);
      }
      active.delete(v);
    }
    if(files&&(!Array.isArray(value)||value.length>p.files))throw Error('Ограничительный профиль: не более 25 файлов за операцию.');
    visit(value);const estimate=nodes*192+units*2+binary*2;
    if(estimate>p.expansionBytes)throw Error('Ограничительный профиль: оценка расширения снимка превышает 96 МиБ.');
    return {профиль:p.id,узлы:nodes,единицы_строк:units,байты:binary,вопросов:questions,конфликтов:conflicts,оценка_расширения:estimate,
      граница:'Проверка выполняется до копирования в рабочий поток. Это ограничение входа, а не измерение или гарантия потребления памяти.'};
  }

  function canonicalMeasure(value,limit=CANONICAL_BYTES_LIMIT,details=false){
    if(!Number.isSafeInteger(limit)||limit<0)throw Error('Некорректный предел канонического размера.');
    let bytes=0,units=0,nodes=0;const active=new WeakSet();
    const add=(n,u=n)=>{bytes+=n;units+=u;if(bytes>limit)throw Error('Ограничительный профиль: канонические байты превысят '+limit+' до сериализации.');};
    function stringSize(str){
      add(2);
      for(let i=0;i<str.length;i++){
        const c=str.charCodeAt(i);
        if(c===34||c===92||[8,9,10,12,13].includes(c))add(2);
        else if(c<32)add(6);
        else if(c<128)add(1);
        else if(c<2048)add(2,1);
        else if(c>=0xd800&&c<=0xdbff){
          const lo=str.charCodeAt(i+1);if(lo>=0xdc00&&lo<=0xdfff){add(4,2);i++;}else add(6);
        }else if(c>=0xdc00&&c<=0xdfff)add(6);else add(3,1);
      }
    }
    function visit(v,depth=0,array=false){
      if(depth>40||++nodes>1000000)throw Error('Предел измерения канонизации: глубина или число значений.');
      if(typeof v==='string'){stringSize(v);return;}
      if(v===null){add(4);return;}
      if(typeof v!=='object'){
        if(typeof v==='number'&&!Number.isFinite(v))throw Error('Недопустимое число.');
        const token=JSON.stringify(v);add(token===undefined?(array?4:0):token.length);return;
      }
      if(active.has(v))throw Error('Циклический объект не является JSON.');active.add(v);
      if(Array.isArray(v)){add(2);for(let i=0;i<v.length;i++){if(i)add(1);visit(v[i],depth+1,true);}}
      else {add(2);let first=true;for(const key of Object.keys(v)){
        const descriptor=Object.getOwnPropertyDescriptor(v,key);
        if(!descriptor||!Object.hasOwn(descriptor,'value'))throw Error('Вычисляемое поле не допускается при измерении.');
        const item=descriptor.value;if(item===undefined||typeof item==='function'||typeof item==='symbol')continue;
        if(!first)add(1);first=false;stringSize(key);add(1);visit(item,depth+1);
      }}active.delete(v);
    }
    visit(value);return details?{байты_UTF8:bytes,единицы_UTF16:units,узлы:nodes}:bytes;
  }
  // Разделяет сущности дела, повторные представления и транспорт. Оценка
  // расширения консервативна для перечисленных форм, но не является пределом RSS.
  function operationBudget(command,data){
    const transport=resourceCheck(data,{envelope:true,representation:command!=='canonical'}),p=INTERACTIVE_PROFILE;
    if(command==='case-explanation')data=explanationRequest(data).case;
    let logicalQuestions=0,outputBytes=null,extra=0;
    const caseCommand=['case-export','case-transfer','case-explanation'].includes(command);
    if(caseCommand){
      if(!Array.isArray(data?.документы)||!Array.isArray(data?.исправления))throw Error('Для оценки дела нужны документы и исправления.');
      for(const d of data.документы)logicalQuestions+=(d.вопросы?.length||0)+(d.ответы_на_вопросы?.length||0);
      if(logicalQuestions>p.questions)throw Error('Ограничительный профиль: более 4096 текущих вопросов и связей ответов.');
      const view=Object.fromEntries(Object.entries(data).filter(([key])=>!['ша256','журнал_молчания','срез_исправления','итоги_исправлений','остатки_ответов'].includes(key)));
      const logical=resourceCheck(view,{envelope:true,representation:true}),inputBytes=canonicalMeasure(view,p.envelopeBytes);
      // История вопроса в срезах и повтор в остатках не являются новыми вопросами.
      // Шесть представлений и фиксированный запас на поля итогов ограничивают
      // вычисление до clone/coverage; схемы, подпись и выходная проверка остаются.
      outputBytes=inputBytes*6+data.документы.length*4096;
      extra=transport.оценка_расширения+logical.оценка_расширения*2+data.документы.length*4096;
      if(command==='case-explanation'){
        let references=0;
        for(const r of data.исправления){
          const events=(data.события_исправления||[]).filter(e=>e.исправление===r.идентификатор);
          const recipients=new Set((r.получатели||[]).map(x=>x.название));
          for(const e of events)for(const name of e.состав?.получатели||[])recipients.add(name);
          references+=(6+2*recipients.size)*events.length;
        }
        // Каждая опора несёт причины и ссылки; это явный консервативный запас.
        extra+=references*4096;outputBytes+=references*2048;
      }
    }else if(command==='canonical'){const size=canonicalMeasure(data,p.envelopeBytes,true);outputBytes=size.байты_UTF8;if(size.единицы_UTF16>p.stringUnits)throw Error('Ограничительный профиль: строка канонического результата превысит предел UTF-16; сериализация не начата.');}
    else if(command==='seal'||command==='panel-transfer')outputBytes=canonicalMeasure(data,p.envelopeBytes)+256;
    if(outputBytes!==null&&outputBytes>p.envelopeBytes)throw Error('Ограничительный профиль: ранняя оценка производного представления превышает 32 МиБ; вычисление не начато.');
    const estimate=transport.оценка_расширения+extra+(outputBytes||0);
    if(estimate>p.expansionBytes)throw Error('Ограничительный профиль: ранняя оценка рабочей операции превышает 96 МиБ; вычисление не начато.');
    return {профиль:p.id,команда:command,текущих_вопросов_и_ответов:logicalQuestions,
      повторных_представлений_вопросов:transport.вопросов,предварительные_байты_выхода:outputBytes,
      оценка_рабочих_представлений:estimate,граница:'Числа логических полей, точных канонических байтов и консервативного расширения различимы. Это не RSS и не гарантия устройства; выход проверяется повторно.'};
  }

  function correctionExplanation(c,at=undefined){
    at=at??cutoff(c);
    const details=new Map(),views=correctionTimeline(c,at,details),events=c.события_исправления||[];
    return {редакция:RELEASE,вид:'объяснение открытых остатков',срез:at,
      исправления:views.map(v=>({исправление:v.исправление,итог:v.итог,состав:v.состав_среза,
        остатки:v.открытые_остатки.map(goal=>{
          const scopeIds=v.состав_среза?.опоры||[];
          const slot=e=>e.действие==='повторное распространение'?'производная:'+e.цель:e.действие===SCOPE_ACTION?'состав':eventSlot(e);
          const matches=e=>e.действие==='отзыв исправления'?events.some(old=>old.идентификатор===e.отменяет&&slot(old)===goal):slot(e)===goal;
          const relevant=events.filter(e=>e.исправление===v.исправление&&(matches(e)||scopeIds.includes(e.идентификатор))&&Date.parse(e.момент_события)<=Date.parse(at)&&Date.parse(e.внесено)<=Date.parse(at));
          return {цель:goal,причина:goal.includes('состав')?'Перечень не даёт однозначной полноты.':'Нет однозначного действующего события для цели в выбранном составе и срезе.',
            опоры_состава:scopeIds,связанные_события:relevant.map(e=>({идентификатор:e.идентификатор,действие:e.действие,документ:e.документ,момент_события:e.момент_события,внесено:e.внесено,ша256:e.ша256,причины:(details.get(v.исправление)||[]).find(x=>x.идентификатор===e.идентификатор)?.причины||[]})),
            предел:'Причины вычислены тем же срезом. События с более поздним временем или внесением не раскрываются в раннем отчёте; их позднее появление не переписывает этот отчёт. История не удостоверяет внешних действий.'};
        })})),граница:EVENT_BOUNDARY};
  }

  function explanationRequest(input){
    if(input&&Object.hasOwn(input,'дело')){
      if(Object.keys(input).sort().join(',')!=='дело,момент_знания,момент_среза'||!time(input.момент_среза)||!time(input.момент_знания)||Date.parse(input.момент_знания)<Date.parse(input.момент_среза))throw Error('Для отдельного объяснения нужны точные моменты среза и более позднего знания, без лишних полей.');
      return {case:input.дело,at:input.момент_среза,knowledge:input.момент_знания};
    }
    return {case:input,at:cutoff(input),knowledge:null};
  }
  function explainLateKnowledge(c,at,knownAt){
    if(!time(at)||!time(knownAt)||Date.parse(knownAt)<Date.parse(at))throw Error('Неверные границы отдельного объяснения знания.');
    const early=correctionExplanation(c,at),a=Date.parse(at),b=Date.parse(knownAt);
    const late=(c.события_исправления||[]).filter(e=>Date.parse(e.момент_события)<=a&&Date.parse(e.внесено)>a&&Date.parse(e.внесено)<=b).map(e=>{
      const raw=(c.документы||[]).find(d=>d.идентификатор===e.документ),doc=raw?documentAt(raw,knownAt):null;
      return {идентификатор:e.идентификатор,исправление:e.исправление,действие:e.действие,цель:e.цель,момент_события:e.момент_события,внесено:e.внесено,
        источник:e.источник,документ:e.документ,документ_в_позднем_контексте:doc?{идентификатор:doc.идентификатор,название:doc.название,состояние:doc.состояние}:null,
        проверяющий:e.проверяющий,отпечаток_события:e.ша256,причина:'Событие заявлено не позже раннего среза, но внесено позже. Оно не было доступно в раннем контексте знания.',
        предел:'Поздняя запись не доказывает, что событие действительно произошло. Ранний отчёт не пересчитан и не исправлен задним числом.'};
    });
    return {...early,позднее_внесение:{вид:'отдельно запрошенное объяснение двух времён',ранний_срез:at,момент_знания:knownAt,ранний_срез_не_изменён:true,события:late,
      граница:'Это добавочный поздний контекст, не новое содержимое раннего отчёта. Записи после момента знания не раскрыты. Перечень не удостоверяет внешние действия или неизменяемость истории.'}};
  }

  const WORKER_PROTOCOL='война/поток/2';
  const WORKER_COMMANDS=Object.freeze(['canonical','seal','case-import','case-export','case-transfer','panel-transfer','case-explanation','registry-history','registry-link','receipt','panel-import']);
  function workerResultCheck(command,value){
    resourceCheck(value,{envelope:true,representation:true});
    const obj=x=>x!==null&&typeof x==='object'&&!Array.isArray(x);
    const sealed=x=>obj(x)&&HASH.test(x.ша256||'');
    let ok=false;
    switch(command){
      case 'canonical':ok=typeof value==='string';if(ok)inspectJSON(value,INTERACTIVE_PROFILE.envelopeBytes,INTERACTIVE_PROFILE);break;
      case 'seal':ok=sealed(value);break;
      case 'panel-import':ok=obj(value)&&value.вид==='продольная панель'&&value.редакция_схемы===RELEASE&&['источники','серии','конфликты','разрешения','отзывы','переносы'].every(k=>Array.isArray(value[k]));break;
      case 'case-import':ok=obj(value)&&value.вид==='папка дела'&&value.редакция_схемы===RELEASE&&['документы','приложения','исправления'].every(k=>Array.isArray(value[k]));break;
      case 'case-export':ok=sealed(value)&&value.вид==='папка дела'&&value.редакция_схемы===RELEASE;break;
      case 'case-transfer':case 'panel-transfer':ok=obj(value)&&Object.keys(value).sort().join(',')==='bytes,record'&&sealed(value.record)&&value.record.редакция_схемы===RELEASE&&value.record.вид===(command==='case-transfer'?'папка дела':'продольная панель')&&value.bytes instanceof ArrayBuffer&&value.bytes.byteLength>0&&canonicalBytesEqual(value.record,value.bytes);break;
      case 'case-explanation':ok=sealed(value)&&value.редакция===RELEASE&&value.вид==='объяснение открытых остатков'&&Array.isArray(value.исправления)&&HASH.test(value.отпечаток_папки||'');break;
      case 'registry-history':ok=Array.isArray(value)&&value.length<=REGISTRY_DEPTH&&value.every(x=>obj(x)&&HASH.test(x.отпечаток_файла||'')&&HASH.test(x.отпечаток_ключа||''));break;
      case 'registry-link':ok=obj(value)&&text(value.имя)&&text(value.имя_графа)&&value.байты instanceof Uint8Array&&value.граф instanceof Uint8Array;break;
      case 'receipt':ok=obj(value)&&value.редакция===RELEASE&&value.вид==='локальная проверка квитанции'&&obj(value.результат)&&value.результат.подпись_проверена===true&&value.счётчик_повторов==='не изменён'&&HASH.test(value.отпечаток_квитанции||'');break;
    }
    if(!ok)throw Error('Форма результата не соответствует операции рабочего потока.');
    return true;
  }
  const API = {explanationRequest,explainLateKnowledge,canonicalMeasure,operationBudget,canonicalBytesEqual,WORKER_PROTOCOL,WORKER_COMMANDS,workerResultCheck,uniqueSources,inspectJSON,INTERACTIVE_PROFILE,resourceCheck,correctionExplanation,verifyCallReceipt,mergeAsync,canonicalBytes,hashCanonical,utf8Size,codePointLength,previewStorageMigration,commitStorageMigration,REGISTRY_DEPTH,REGISTRY_BYTES,registryHistory,recoverRegistryLink,EVENT_LIMIT,EVENT_BOUNDARY,CORRECTION_ACTIONS,correctionEventErrors,correctionTimeline,appendCorrectionEvent,checkpoint,ingestBatch,migratePanelBytes,importPanelBytes,migrateCaseBytes,importCaseBytes,migrateRegistryBytes,recoverOriginal,documentAt,documentSnapshot,reviseDocument,inspectPreviousStorage,revokeResolution,configureSchemas, schemaErrors, validate, SOURCE_LIMIT, TOTAL_LIMIT, resolveConflict, deadlineState, responseCoverage, attachmentSafety, exportCase, restoreCase, registryErrors, exportRegistry, restoreRegistry, RELEASE, STATES, CLASSES, LIMIT, DOCTYPES, DOCSTATES, canonical, hash, seal, payload, parse, base64, unbase64, date, time, text, safeURL, score, auditErrors, ingest, merge, restorePanel, attachment, caseErrors, correction, silence, requirementGraph};
  globalThis.ВойнаМетодика = Object.freeze(API);
  if (typeof module !== 'undefined' && module.exports) module.exports=API;
})();
