#!/usr/bin/env python3
"""Сборка четырёх полных наборов 4.11.0-wip с независимыми упаковочными манифестами.
Создаётся одноразовый ключ сборки только в памяти; это НЕ ключ владельца.
Запуск изменяет подготовленный исходный корень. Нужна отдельная рабочая копия.
"""
from pathlib import Path,PurePosixPath
from functools import partial
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding,PublicFormat
from datetime import datetime,timezone
import argparse,base64,hashlib,http.server,json,os,shutil,stat,subprocess,tempfile,threading,unicodedata,urllib.request,urllib.parse,zipfile
V='4.11.0-wip';EX={'МАНИФЕСТ-ВЫПУСКА.json','ПОДПИСЬ-МАНИФЕСТА.txt'}
def sha(b):return hashlib.sha256(b).hexdigest()
def dump(p,d):p.write_text(json.dumps(d,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
def files(root):return sorted((p for p in root.rglob('*') if p.is_file()),key=lambda p:p.relative_to(root).as_posix())
def clean(root):
 for p in list(root.rglob('__pycache__')):shutil.rmtree(p)
 for p in list(root.rglob('*.pyc')):p.unlink()
def copy(src,dst,paths):
 for p in paths:
  rel=p.relative_to(src);q=dst/rel;q.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(p,q)
def seal(root,key,scope):
 clean(root)
 pub=key.public_key().public_bytes(Encoding.Raw,PublicFormat.Raw)
 (root/'ПУБЛИЧНЫЙ-КЛЮЧ.txt').write_text('война.com · '+V+'\nОДНОРАЗОВЫЙ КЛЮЧ СБОРКИ, НЕ УСТАНОВЛЕННЫЙ КЛЮЧ ВЛАДЕЛЬЦА\nКЛЮЧ В КОДИРОВКЕ Base64:\n'+base64.b64encode(pub).decode()+'\nSHA-256 открытого ключа:\n'+sha(pub)+'\n',encoding='utf-8')
 records=[]
 for p in files(root):
  n=p.relative_to(root).as_posix()
  if n in EX:continue
  if p.is_symlink():raise ValueError('Символическая ссылка: '+n)
  records.append({'путь':n,'байты':p.stat().st_size,'ша256':sha(p.read_bytes())})
 d={'публичное_имя':'война.com','канонический_адрес':'https://xn--80advqd.com/','редакция':V,'состав':scope,'число_файлов':len(records),'исключены_из_перечня':sorted(EX),'алгоритм_подписи':'Ed25519','алгоритм_отпечатка':'SHA-256','граница':'Ключ из этого же пакета не устанавливает личность владельца или истинность содержания. Внешние рубежи открыты.','файлы':records}
 dump(root/'МАНИФЕСТ-ВЫПУСКА.json',d)
 sig=key.sign((root/'МАНИФЕСТ-ВЫПУСКА.json').read_bytes())
 (root/'ПОДПИСЬ-МАНИФЕСТА.txt').write_text('война.com · '+V+'\nПОДПИСЬ ТОЧНЫХ БАЙТОВ МАНИФЕСТА\nПОДПИСЬ В КОДИРОВКЕ Base64:\n'+base64.b64encode(sig).decode()+'\nГраница: подпись локального сборщика, не свидетельство владельца.\n',encoding='utf-8')
def check(root):
 r=subprocess.run(['python',str(root/'ПРОВЕРИТЬ-ПОДПИСЬ.py')],capture_output=True,text=True,env={**os.environ,'PYTHONDONTWRITEBYTECODE':'1'},timeout=35)
 if r.returncode:raise RuntimeError(r.stdout+r.stderr)
 return json.loads(r.stdout)
def pack(root,target):
 names=set()
 with zipfile.ZipFile(target,'w',compression=zipfile.ZIP_DEFLATED,compresslevel=6) as z:
  for p in files(root):
   n=p.relative_to(root).as_posix();fold=unicodedata.normalize('NFC',n).casefold()
   if fold in names:raise ValueError('Неоднозначные имена в архиве: '+n)
   names.add(fold)
   if any(ord(c)<32 or c in '\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069' for c in n):raise ValueError('Управляющий символ в имени: '+repr(n))
   if p.suffix.lower() in ['.woff','.woff2','.ttf','.otf','.ttc','.eot']:raise ValueError('Файлы шрифтов не включаются: '+n)
   i=zipfile.ZipInfo(n,date_time=(2026,9,6,0,0,0));i.compress_type=zipfile.ZIP_DEFLATED;i.external_attr=(stat.S_IFREG|0o644)<<16;z.writestr(i,p.read_bytes())
def extract_compare(zip_path,staged,output):
 output.mkdir()
 with zipfile.ZipFile(zip_path) as z:
  names=set();normalized=set()
  for i in z.infolist():
   n=i.filename;p=PurePosixPath(n)
   if p.is_absolute() or '..' in p.parts or '\\' in n or ':' in p.parts[0] or str(p)!=n or n in names or stat.S_ISLNK(i.external_attr>>16):raise ValueError('Небезопасный ZIP: '+n)
   fold=unicodedata.normalize('NFC',n).casefold()
   if fold in normalized or '\x00' in n or any(ord(c)<32 for c in n):raise ValueError('Неоднозначное имя ZIP: '+n)
   normalized.add(fold);names.add(n)
  bad=z.testzip()
  if bad:raise ValueError('Ошибка CRC: '+bad)
  z.extractall(output)
 left={p.relative_to(staged).as_posix():sha(p.read_bytes()) for p in files(staged)}
 right={p.relative_to(output).as_posix():sha(p.read_bytes()) for p in files(output)}
 if left!=right:raise ValueError('Побайтовая сверка не пройдена: '+zip_path.name)
 return {'архив':zip_path.name,'байты':zip_path.stat().st_size,'ша256':sha(zip_path.read_bytes()),'файлов':len(right),'чистое_извлечение':True,'побайтовая_сверка':True,'подпись_и_состав':check(output)}

def secret_scan(root):
 import re
 patterns=[('закрытый ключ PEM',re.compile(rb'-----BEGIN (?:RSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----\s*\n[A-Za-z0-9+/=\r\n]{32,}-----END (?:RSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----')),('ключ доступа AWS',re.compile(rb'\bAKIA[0-9A-Z]{16}\b')),('ключ с префиксом sk-',re.compile(rb'\bsk-(?:proj-)?[A-Za-z0-9_-]{40,}\b'))]
 hits=[];count=0
 for p in files(root):
  if p.suffix.lower() in ['.zip','.pdf','.png','.jpg','.jpeg','.webp']:continue
  raw=p.read_bytes();count+=1
  for label,pattern in patterns:
   if pattern.search(raw):hits.append({'путь':p.relative_to(root).as_posix(),'тип':label})
 if hits:raise ValueError('Обнаружен потенциальный секрет; значения не выводятся: '+json.dumps(hits,ensure_ascii=False))
 return {'проверено_файлов':count,'совпадения':hits,'предел':'Проверены известные форматы, а не все возможные секреты. Закрытый ключ сборки генерируется только в памяти процесса.'}
def adversarial(root,tmp):
 rows=[];target=root/'index.html';original=target.read_bytes()
 def expect(name,apply,restore):
  try:
   apply()
   try:check(root);rejected=False
   except Exception:rejected=True
   rows.append({'проверка':name,'отклонено':rejected})
   if not rejected:raise ValueError('Подмена не отклонена: '+name)
  finally:restore()
 expect('Изменение байтов страницы',lambda:target.write_bytes(original+b'\n'),lambda:target.write_bytes(original))
 expect('Отсутствующая страница',lambda:target.unlink(),lambda:target.write_bytes(original))
 extra=root/'необъявленный-файл.txt';expect('Лишний файл',lambda:extra.write_text('Искусственный ввод'),lambda:extra.unlink(missing_ok=True))
 manifest=root/'МАНИФЕСТ-ВЫПУСКА.json';raw=manifest.read_bytes();expect('Изменённые байты подписанного манифеста',lambda:manifest.write_bytes(raw+b'\n'),lambda:manifest.write_bytes(raw))
 def link():target.unlink();target.symlink_to(tmp/'несуществующая-цель')
 def unlink():target.unlink(missing_ok=True);target.write_bytes(original)
 expect('Символическая ссылка вместо страницы',link,unlink)
 # Validation must happen before extraction, even for an intentionally hostile archive.
 cases=[('../вне-корня.txt',None),('/абсолютный.txt',None),('каталог\\файл.txt',None),('ссылка',stat.S_IFLNK|0o777),('collision',None)]
 for number,(name,mode) in enumerate(cases):
  zpath=tmp/f'опасный-{number}.zip';out=tmp/f'отказ-{number}'
  with zipfile.ZipFile(zpath,'w') as z:
   if name=='collision':z.writestr('Ё.txt','a');z.writestr('Е\u0308.txt','b')
   else:
    info=zipfile.ZipInfo(name)
    if mode:info.external_attr=mode<<16
    z.writestr(info,'Искусственный ввод')
  try:extract_compare(zpath,root,out);rejected=False
  except ValueError:rejected=True
  rows.append({'проверка':'Опасное имя или вид ZIP: '+name,'отклонено':rejected})
  if not rejected:raise ValueError('Опасный ZIP принят')
 check(root)
 return {'проверок':len(rows),'результаты':rows,'команда':'python инструменты/собрать-пакеты.py --исходник /корень --выход /выход','предел':'Локальные искусственные подмены, не аудит всех реализаций ZIP или системных файлов.'}

def close_memory(root,stage):
 import re
 # Only existing same-root documents/data are closure dependencies. HTML/ZIP are full-site context.
 rx=re.compile(r'/(?:[\w.\-А-Яа-яЁё]+/)*[\w.\-А-Яа-яЁё]+\.(?:uai|md|json|jsonld|txt)')
 seen=set()
 while True:
  todo=[p for p in files(stage) if p.relative_to(stage).as_posix() not in seen and p.suffix in ['.uai','.md','.json','.txt','.jsonld']]
  if not todo:break
  for p in todo:
   seen.add(p.relative_to(stage).as_posix())
   try:text=p.read_text()
   except UnicodeError:continue
   for ref in rx.findall(text):
    rel=ref.lstrip('/');src=(root/rel).resolve();dst=stage/rel
    if src.is_relative_to(root) and src.is_file() and not dst.exists():dst.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(src,dst)

def scan_archive_members(root):
 import io,re
 count=0;total=0;archives=0;fonts=[];suspicious=[]
 pattern=re.compile(rb'-----BEGIN (?:RSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----\s*\n[A-Za-z0-9+/=\r\n]{32,}-----END (?:RSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----')
 def inspect(data,name,depth=0):
  nonlocal count,total,archives
  if depth>12:raise ValueError('Не проверена чрезмерная вложенность ZIP: '+name)
  archives+=1
  with zipfile.ZipFile(io.BytesIO(data)) as z:
   pathnames=set()
   for item in z.infolist():
    name_raw=item.filename;normal=name_raw.rstrip('/') if item.is_dir() else name_raw;parts=PurePosixPath(normal);fold=unicodedata.normalize('NFC',normal).casefold()
    if not normal or parts.is_absolute() or '..' in parts.parts or '\\' in normal or ':' in parts.parts[0] or str(parts)!=normal or fold in pathnames or stat.S_ISLNK(item.external_attr>>16) or any(ord(c)<32 or c in '\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069' for c in normal):raise ValueError('Небезопасный путь вложенного ZIP: '+name+'!'+name_raw)
    pathnames.add(fold)
    if item.is_dir():continue
    if item.file_size>256*1024*1024:raise ValueError('Слишком большой вложенный файл: '+name+'!'+item.filename)
    raw=z.read(item);count+=1;total+=len(raw)
    if total>6*1024*1024*1024:raise ValueError('Превышен бюджет проверки вложенных архивов')
    label=name+'!'+item.filename;suffix=Path(item.filename).suffix.lower()
    if suffix in ['.woff','.woff2','.ttf','.otf','.ttc','.eot']:fonts.append(label)
    if suffix=='.zip':inspect(raw,label,depth+1)
    elif suffix not in ['.png','.jpg','.jpeg','.webp','.pdf'] and pattern.search(raw):suspicious.append(label)
 if any(p.suffix.lower() in ['.woff','.woff2','.ttf','.otf','.ttc','.eot'] for p in files(root)):raise ValueError('Файл шрифта в корне пакета')
 for p in files(root):
  if p.suffix.lower()=='.zip':inspect(p.read_bytes(),p.relative_to(root).as_posix())
 if fonts or suspicious:raise ValueError('Недопустимые вложения; значения секретов не выводятся: '+str({'шрифты':fonts,'потенциальные_секреты':suspicious}))
 return {'архивов_с_учётом_вложений':archives,'записей':count,'распакованных_байтов':total,'шрифтов':fonts,'потенциальные_закрытые_ключи':suspicious,'предел':'Все встретившиеся ZIP прочитаны в пределах установленного бюджета. Шаблон PEM не доказывает отсутствия всех секретов; PDF/изображения не подвергались распознаванию текста.'}

def confirm_matrix(root):
 matrix=json.loads((root/'доказательства/редакция-4.11.0/офлайн-матрица-часть-000-086.json').read_text())
 pages={p.relative_to(root).as_posix() for p in root.rglob('*.html')}
 if pages!={r['страница'] for r in matrix['проверки']} or len(pages)!=86:raise ValueError('Состав матрицы не совпал')
 for r in matrix['проверки']:
  if r.get('ошибка_стенда') or r.get('ошибки_сценариев') or r.get('недоступные_данные') or {m['ширина'] for m in r['режимы']}!={320,390,768,1280,1920} or any(m.get('переполнение',0)>2 or m.get('без_имени') for m in r['режимы']):raise ValueError('Неуспешная матрица: '+r['страница'])
  if sha((root/r['страница']).read_bytes())!=r['исходный_SHA256']:raise ValueError('Страница изменилась после проверки: '+r['страница'])
  for d in r.get('прочитанные_данные',[]):
   n=urllib.parse.unquote(urllib.parse.urlsplit(d['путь']).path);p=root/n.lstrip('/') if n.startswith('/') else (root/r['страница']).parent/n
   if d['состояние']!=200 or sha(p.read_bytes())!=d['SHA256']:raise ValueError('Прочитанные данные изменились: '+str(p))
 for r in matrix['ресурсы_проверки']:
  if sha((root/r['путь']).read_bytes())!=r['SHA256']:raise ValueError('Ресурс изменился после матрицы: '+r['путь'])
 return {'страниц':86,'сочетаний':430,'точные_байты_страниц_ресурсов_и_прочитанных_данных':True,'граница':'Изолированный DOM, не настоящий браузерный допуск.'}
def main():
 parser=argparse.ArgumentParser();parser.add_argument('--исходник',type=Path,required=True);parser.add_argument('--выход',type=Path,required=True);ns=parser.parse_args();root=ns.исходник.resolve();out=ns.выход.resolve();out.mkdir(parents=True,exist_ok=True)
 if not (root/'index.html').is_file():raise ValueError('Нет корневого index.html')
 start=datetime.now(timezone.utc).isoformat();clean(root);E=root/'доказательства/редакция-4.11.0'
 for script in ['проверить-набор-исполнения-4-11.py','проверить-сохранность-4-11.py']:
  p=subprocess.run(['python',str(root/'инструменты'/script)],capture_output=True,text=True,env={**os.environ,'PYTHONDONTWRITEBYTECODE':'1'},timeout=45)
  if p.returncode:raise RuntimeError(p.stdout+p.stderr)
 matrix=confirm_matrix(root);u=json.loads((root/'uaix.uai').read_text());assert sum(p.is_file() for p in root.rglob('*.uai'))==u['всего_файлов_уаикс']
 assert (root/'ЖУРНАЛ-КАЧЕСТВА.md').read_bytes()==(root/'docs/ЖУРНАЛ-КАЧЕСТВА.md').read_bytes()
 scan=secret_scan(root);key=Ed25519PrivateKey.generate()
 scopes={'память-уаикс':'Самостоятельная корневая, горячая, холодная и историческая память; связанные текстовые документы и данные. Контекстные HTML/ZIP-ссылки относятся к полному сайту, не сетевой дельте.',
 'документы-и-доказательства':'Документы, схемы, точные источники, искусственные корпуса, отчёты и проверочные инструменты. Это переносной корпус, не второй вариант архитектуры сайта.',
 'набор-владельца':'Схемы, неподписанный черновик, инструкции и инструменты. Реальные подтверждения 0/4. Сборочный ключ не устанавливает владельца. Для свидетельства нужен манифест полного сайта, а не этого поднабора.'}
 nested={'память-уаикс':'uai/память-уаикс.zip','документы-и-доказательства':'docs/корпус-документов-4.11.0-wip.zip','набор-владельца':'набор-владельца-4.11.0-wip.zip'}
 common={'ПРОВЕРИТЬ-ПОДПИСЬ.py','СЛЕДУЮЩИЙ-ШАГ.md','СЛЕДУЮЩИЙ-РЕКУРСИВНЫЙ-ПРОМПТ.md','ЖУРНАЛ-КАЧЕСТВА.md'}
 with tempfile.TemporaryDirectory(prefix='война-4-11-') as tmp:
  tmp=Path(tmp);stages={};archives={};allfiles=files(root)
  for kind,scope in scopes.items():
   stage=tmp/kind;stage.mkdir();stages[kind]=stage;selected=[]
   for p in allfiles:
    rel=p.relative_to(root);top=rel.parts[0]
    if p.suffix=='.zip' or rel.as_posix() in EX or rel.as_posix() in ['ПУБЛИЧНЫЙ-КЛЮЧ.txt','СОСТАВ-ПАКЕТА.md']:continue
    if kind=='память-уаикс':include=p.suffix=='.uai' or (top in ['uai','.uai','.well-known','данные','сущности','docs','доказательства'] and p.suffix in ['.json','.jsonld','.md','.txt'])
    elif kind=='документы-и-доказательства':include=top in ['docs','доказательства','данные','инструменты','ресурсы','сущности']
    else:include=top=='владелец' or (top in ['docs','данные','инструменты'] and any(x in p.name for x in ['владельц','СВИДЕТЕЛЬСТВ','СВИДЕТЕЛЬСТВА','ВЛАДЕЛЬЦА','ОТКРЫТОГО-КЛЮЧА','ВНЕШНИХ-РУБЕЖЕЙ','закрепление-DNS','развёртывание','проверить-сеть'])) or (p.name in ['реестр-внешних-рубежей.json','решение-по-внешним-рубежам.py','схема-свидетельства-владельца.json','ПРОИСХОЖДЕНИЕ-КОРНЯ-4.11.0.md','РЕДАКЦИЯ-4.11.0.md','ВОСПРОИЗВЕДЕНИЕ-4.11.0.md','ДОВЕРИЕ-КЛЮЧУ-4.11.0.md']) or (rel.as_posix() in ['доказательства/редакция-4.11.0/приёмник-владельца.json','доказательства/редакция-4.11.0/закрепление-DNS.json'])
    if include or rel.as_posix() in common:selected.append(p)
   copy(root,stage,selected)
   if kind=='память-уаикс':close_memory(root,stage)
   # Exact common copies are mandatory even if a selector changes.
   for n in common:shutil.copyfile(root/n,stage/n)
   if kind=='набор-владельца':
    (stage/'docs').mkdir(exist_ok=True);shutil.copyfile(root/'docs/ЖУРНАЛ-КАЧЕСТВА.md',stage/'docs/ЖУРНАЛ-КАЧЕСТВА.md')
   (stage/'СОСТАВ-ПАКЕТА.md').write_text('# война.com · '+V+'\n\n'+scope+'\n\nСборочная подпись не устанавливает владельца. Все внешние рубежи открыты.\n')
   seal(stage,key,scope);check(stage);path=out/(kind+'-'+V+'.zip');pack(stage,path);archives[kind]=path;print('СОБРАН',kind,path.stat().st_size,flush=True)
  for kind,n in nested.items():shutil.copyfile(archives[kind],root/n)
  nestedscan=scan_archive_members(root)
  static=out/('война-'+V+'-статическая-проверка.json');p=subprocess.run(['python',str(root/'инструменты/проверить-статический-сайт-4-11.py'),str(static)],capture_output=True,text=True,env={**os.environ,'PYTHONDONTWRITEBYTECODE':'1'},timeout=45)
  if p.returncode or json.loads(static.read_text())['ожидают_сборки_архивов']:raise RuntimeError(p.stdout+p.stderr)
  scope='Полный новый корневой сайт война.com 4.11.0-wip, не дельта: 86 публичных страниц, память, документы, доказательства и три вложенных набора. Проверены три набора 4.10.0 и полный корень 4.8.0; полного ZIP 4.10.0 нет. Семь точных HTML 4.10.0 не восстановлены, текущие — обозначенные производные. Отдельные пробелы 4.9.0 и 4.7.0 открыты. Новая подпись не восстанавливает отсутствующие байты.'
  (root/'СОСТАВ-ПАКЕТА.md').write_text('# война.com · '+V+'\n\n'+scope+'\n\nindex.html непосредственно в корне. Одноразовый ключ сборки не является владельческим. Нормализованные даты ZIP — технические даты упаковки, не времена исходных исследований.\n')
  seal(root,key,scope);check(root);full=out/('война-'+V+'.zip');pack(root,full);archives['война']=full;stages['война']=root;print('СОБРАН полный корень',full.stat().st_size,flush=True)
  report={'редакция':V,'начало_UTC':start,'сборщик_SHA256':sha(Path(__file__).read_bytes()),'архивы':[],'граница':'Конечная локальная проверка передачи. Не внешний выпуск или владельческое удостоверение. Ключ Ed25519 только в памяти; свежий ключ меняет подписи следующей сборки.'}
  for kind in ['война',*scopes]:
   row=extract_compare(archives[kind],stages[kind],tmp/('извлечено-'+kind));report['архивы'].append(row);print('ПРОВЕРЕН',kind,row['файлов'],flush=True)
  extract=tmp/'извлечено-война'
  report['вложенные_наборы_тождественны_отдельным']=all(sha((extract/n).read_bytes())==sha(archives[k].read_bytes()) for k,n in nested.items());assert report['вложенные_наборы_тождественны_отдельным']
  class Quiet(http.server.SimpleHTTPRequestHandler):
   def log_message(self,*a):pass
  server=http.server.ThreadingHTTPServer(('127.0.0.1',0),partial(Quiet,directory=str(extract)));threading.Thread(target=server.serve_forever,daemon=True).start();http=[]
  try:
   for p in files(extract):
    n=p.relative_to(extract).as_posix()
    with urllib.request.urlopen('http://127.0.0.1:'+str(server.server_port)+'/'+urllib.parse.quote(n),timeout=20) as resp:
     raw=resp.read();ok=resp.status==200 and raw==p.read_bytes()
     if not ok:raise ValueError('Локальная выдача '+n)
     http.append({'путь':n,'код':resp.status,'точные_байты':ok})
  finally:server.shutdown();server.server_close()
  report['локальная_HTTP_выдача']={'проверено':len(http),'проверки':http,'предел':'Локальный Python HTTP из чистого извлечения; не действующий домен, TLS, Apache/CSP, редиректы или кэш.'};print('HTTP',len(http),flush=True)
  report['отрицательные_упаковочные_проверки']=adversarial(extract,tmp)
  neg=[]
  for kind in scopes:
   sr=tmp/('извлечено-'+kind);extra=sr/'необъявленный-файл.txt';extra.write_text('Искусственный ввод')
   try:
    try:check(sr);rejected=False
    except Exception:rejected=True
   finally:extra.unlink()
   if not rejected:raise ValueError('Лишний файл принят: '+kind)
   check(sr);neg.append({'набор':kind,'лишний_файл_отклонён':True,'после_восстановления':True})
  report['отрицательные_проверки_переносных_наборов']=neg;report['проверка_шаблонов_секретов']=scan;report['вложенные_архивы_проверены']=nestedscan;report['соответствие_окончательной_DOM_матрице']=confirm_matrix(extract)
  report['опись_памяти']={'всего_файлов_уаикс':u['всего_файлов_уаикс'],'тематических':u['число_тематических_модулей'],'служебных':u['число_служебных_модулей']}
  preservation=json.loads((extract/'доказательства/редакция-4.11.0/сохранность-и-происхождение.json').read_text());assert not preservation['ошибки'];report['сохранность']=preservation
  peaks=[json.loads((E/f'ресурсы-4110-{i}-рабочий.json').read_text())['пиковый_RSS_КиБ']/1024 for i in [1,2,3]];report['ресурсы']={'пики_RSS_МиБ':peaks,'контрольные_768_не_превышены_во_всех':all(x<=768 for x in peaks),'допуск_устройства':'Открыт; не физический предел и не мобильный допуск'}
  report['открытые_рубежи']=u['открытые_рубежи'];report['внешние_рубежи']={'развёртывание':'Не выполнялось','независимая_машина':'Не предоставлена','настоящий_браузер':'Навигация запрещена администратором','владельческие_свидетельства':{'подтверждено':0,'всего':4},'решение':'wip'}
  report['статическая_проверка']={'файл':static.name,'SHA256':sha(static.read_bytes()),'ошибок':0,'отсутствующих_архивов':0};report['окончание_UTC']=datetime.now(timezone.utc).isoformat();dump(out/('война-'+V+'-проверка-передачи.json'),report)
  print(json.dumps({'архивы':[{k:x[k] for k in ['архив','байты','файлов','ша256']} for x in report['архивы']],'HTTP':len(http),'решение':'wip'},ensure_ascii=False,indent=2))
if __name__=='__main__':main()
