"""Prepare a private GoldenEye 1.0.1 candidate from frozen 1.0.0 bytes.

This helper never starts a game/browser, reads user saves, publishes, changes
discovery, or writes the frozen source tree. A passing root A/B receipt is
mandatory before it creates a NEW ordinary-copy working directory.
"""
from __future__ import annotations

import argparse
from datetime import datetime, timezone
import hashlib
import html
import json
import os
from pathlib import Path, PurePosixPath
import re
import shutil
import stat
import sys
import zipfile
from zoneinfo import ZoneInfo

VERSION = '1.0.1'
DATE = '2026-10-31'
ZONE = 'America/New_York'
PROJECT = 'n64/goldeneye-usa'
BASE_URL = 'https://cosomenterprise.com/' + PROJECT + '/'
ARTICLE = 'dev-blog/2026-10-31-renderer-repair.html'
ROM_SHA = '33d4609661bd207d22146463e64edbcc5cc9a21668f4f8e450642d2a2f6d2de8'
MANIFEST_SHA = 'a5b7bccf85249c242a5798cd7bb2689122eb6ba3fdc7896b2be0c9b16b3fb094'
RELEASE_SHA = '73a555d074c0985d09fccf00df9a51ccd1dadd59ac9b85c937d51351ea9797d8'
OLD_ZIP_SHA = '65c49673652947988d4f7f3fad671b6252f9a042f8805597620950680f69f56f'
OLD_ARTICLE_SHA = 'f0571281472cea39e20ff8b116fac85c3eabae9cb1b554f80594a251a0160c7f'
OVERLAY_RECEIPT_SHA = '147f73bc451c6dddadbace026c9a1abf830a7ae6dd60307a3b1f17e699dbbefe'
OVERLAY_PLAYER_SHA = '07ff57e7234c88e7057bcd883c968b8c9f89c6c6894d6bfeb17573fc954cdd8b'
OVERLAY_CONFIG_SHA = 'aef50cc0199f46f3136a5ddda12f39874447e79f0ac6b74889279a41b944648f'
SAVE_PATCH_SHA = 'a455a07c01a1ead9611080214450460f5d38e60a62876082ada45cf3080b6e38'
SAVE_TEST_REPORT_SHA = 'be68750c06f70a9b7d276f1a803cc30262458c5e819dda787913ca44387472ec'
SHARED_PROVENANCE_SHA = '666053d72a26f89578b8a6b4d2825e06f9db165f363d4e50b819012d5c599964'
BASELINE_BODY = {
    'original/index.html': 'ee3c0336a194d7e74553e971159894fbb4f5a5424bbba3fe16cf2d8999bcbcd4',
    'player.js': 'bb2464fbe76a7c7b582b117cd669e5e5e10884f6305a5cb10c83285a8f0cabf6',
    'data/runtime-config.json': 'a2a903ca69089d8f8a05b4b6791d91306aa1dea765ea684b489c30057b047b93',
}
CANDIDATE_BODY = {**BASELINE_BODY, 'player.js': OVERLAY_PLAYER_SHA,
                  'data/runtime-config.json': OVERLAY_CONFIG_SHA}
CORE_ARCHIVE_SHA = '2da1cbce9fda395e3ae83ca5787353baa159142d45ef3ea90f108b92524f76cc'
REQUIRED_CHECKS = {'localBaselineCorruptionObserved', 'candidateStartupTextReadable',
                   'candidateMenuTextReadable', 'pinsMatched'}
UNVERIFIED = ('nativeSaveReload', 'fullCampaign', 'physicalController', 'cheatEffects')
REPLACE_ALLOWED = {'original/index.html', 'index.html', 'credits/index.html',
                   'dev-blog/index.html', 'dev-blog/history.json', 'release.json',
                   'source/README.txt'}
STATE_IDENTITIES = {
    'nativeDatabase': 'cosom-goldeneye-usa-v1-native',
    'playerLock': 'cosom-goldeneye-usa-v1-player',
    'stateDatabase': 'cosom-goldeneye-usa-v1-states',
    'stateBackupFormat': 'cosom-goldeneye-usa-state-v1',
    'virtualRom': 'goldeneye-usa-v1.n64', 'virtualSave': 'goldeneye-usa-v1.srm',
    'nativeContainerBytes': 296960,
    'notebook': 'cosom:goldeneye:usa:v1:mission-notebook',
}
HEX = re.compile(r'^[0-9a-f]{64}$')


def need(condition, message):
    if not condition:
        raise RuntimeError(message)


def sha(data):
    return hashlib.sha256(data).hexdigest()


def digest_file(path):
    h = hashlib.sha256()
    with path.open('rb') as stream:
        for block in iter(lambda: stream.read(1024 * 1024), b''):
            h.update(block)
    return h.hexdigest()


def no_reparse(path):
    info = path.lstat()
    need(not stat.S_ISLNK(info.st_mode) and
         not (getattr(info, 'st_file_attributes', 0) & 0x400),
         'Symlinks/reparse points are not accepted: ' + str(path))


def under(root, path):
    resolved = path.resolve()
    need(resolved != root.resolve() and root.resolve() in resolved.parents,
         'Path is outside the intended bounded directory: ' + str(path))
    return resolved


def relative_path(value):
    need(isinstance(value, str) and '\\' not in value and ':' not in value,
         'Manifest paths must be relative POSIX paths.')
    parts = PurePosixPath(value).parts
    need(parts and not value.startswith('/') and all(x not in ('', '.', '..') for x in parts),
         'Unsafe manifest path: ' + value)
    return value


def read_json(path):
    no_reparse(path)
    return json.loads(path.read_text(encoding='utf-8-sig'))


def write_json(path, value):
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')


def json_bytes(value):
    return json.dumps(value, ensure_ascii=False, separators=(',', ':')).encode('utf-8')


def plain(value, field):
    need(isinstance(value, str) and 0 < len(value.strip()) <= 4000,
         field + ' must be nonempty reviewed text.')
    need(not re.search(r'[A-Za-z]:[\\/]|file://|/(?:Users|home|private)/|\\\\', value),
         field + ' includes a private/local path.')
    return value.strip()


def checked_file(path, expected):
    no_reparse(path)
    need(path.is_file() and digest_file(path) == expected,
         'Pinned input changed or missing: ' + str(path))


def records(project):
    result = []
    for directory, dirs, files in os.walk(project, followlinks=False):
        for name in dirs + files:
            no_reparse(Path(directory) / name)
        for name in files:
            p = Path(directory) / name
            result.append({'path': p.relative_to(project).as_posix(),
                           'bytes': p.stat().st_size, 'sha256': digest_file(p)})
    return sorted(result, key=lambda x: x['path'])


def frozen_records(work):
    manifest_path = work / 'upload-manifest.json'
    checked_file(manifest_path, MANIFEST_SHA)
    manifest = read_json(manifest_path)
    need(manifest.get('releaseId') == 'goldeneye-usa-1.0.0-a080cb592871',
         'The input must be the retained frozen 1.0.0 manifest.')
    source = work / 'site' / PROJECT
    no_reparse(source)
    expected = {}
    for record in manifest['files']:
        rel = relative_path(record['path'])
        need(rel.startswith(PROJECT + '/'), 'Baseline manifest contains another project.')
        project_rel = rel[len(PROJECT) + 1:]
        need(project_rel not in expected and HEX.fullmatch(record['sha256']),
             'Duplicate/invalid frozen record.')
        expected[project_rel] = {**record, 'path': project_rel}
    actual = records(source)
    need({x['path']: x for x in actual} == expected,
         'Frozen source tree differs from its complete manifest; nothing was prepared.')
    checked_file(source / 'release.json', RELEASE_SHA)
    checked_file(source / 'downloads/goldeneye-usa-website-1.0.0.zip', OLD_ZIP_SHA)
    checked_file(source / 'dev-blog/2026-10-30-original-edition.html', OLD_ARTICLE_SHA)
    return source, manifest, actual


def acceptance(path, diagnostic, before_identity, after_identity):
    path = under(diagnostic, path)
    value = read_json(path)
    need(value.get('schemaVersion') == 1 and
         value.get('kind') == 'goldeneye-renderer-ab-acceptance-v1' and
         value.get('decision') == 'pass', 'A root-authored passing A/B receipt is required.')
    need(value.get('timezone') == ZONE, 'Receipt must use America/New_York.')
    stamp = value.get('acceptedAtUtc', '')
    need(isinstance(stamp, str) and stamp.endswith('Z'), 'acceptedAtUtc must be a UTC Z timestamp.')
    accepted = datetime.fromisoformat(stamp[:-1] + '+00:00')
    need(accepted.astimezone(ZoneInfo(ZONE)).date().isoformat() == DATE,
         'Receipt acceptance is not on the actual October 31 release date.')
    need(accepted <= datetime.now(timezone.utc), 'Receipt acceptance is in the future.')
    need(datetime.now(ZoneInfo(ZONE)).date().isoformat() == DATE,
         'Preparation date changed; author fresh dated notes before another release.')
    for label, hashes, identity in [('baseline', BASELINE_BODY, before_identity),
                                     ('candidate', CANDIDATE_BODY, after_identity)]:
        part = value.get(label, {})
        need(part.get('bodySha256') == hashes and part.get('coreIdentity') == identity,
             label + ' body/core identity pins do not match the tested sources.')
    need(value.get('isolatedOrigin') is True and value.get('userSavesRead') is False,
         'Receipt must confirm an isolated local test and no user-save access.')
    checks = value.get('checks')
    need(isinstance(checks, list), 'Receipt checks must be a list.')
    seen, sanitized = set(), []
    for check in checks:
        identifier = check.get('id')
        need(isinstance(identifier, str) and re.fullmatch(r'[A-Za-z][A-Za-z0-9]{0,79}', identifier)
             and identifier not in seen, 'Receipt check ID is invalid/duplicate.')
        seen.add(identifier)
        status = check.get('status')
        need(status in ('pass', 'fail', 'unverified', 'not-tested'), 'Unknown observed-check status.')
        need(identifier not in UNVERIFIED or status in ('unverified', 'not-tested'),
             'This renderer-only update cannot assert ' + identifier + ' acceptance.')
        if identifier in REQUIRED_CHECKS:
            need(status == 'pass', 'Required A/B check did not pass: ' + identifier)
        evidence = check.get('evidence', [])
        need(status != 'pass' or isinstance(evidence, list) and evidence,
             'Passing checks require retained local evidence.')
        evidence_hashes = []
        for entry in evidence:
            expected = entry.get('sha256', '')
            need(isinstance(expected, str) and HEX.fullmatch(expected), 'Evidence hash is invalid.')
            artifact = under(diagnostic, Path(entry.get('path', '')))
            need(artifact.suffix.lower() in ('.json', '.png', '.jpg', '.jpeg', '.webp', '.txt', '.md', '.log'),
                 'Only diagnostic reports/screenshots may be receipt evidence.')
            checked_file(artifact, expected)
            evidence_hashes.append(expected)
        sanitized.append({'id': identifier, 'label': plain(check.get('label'), 'Check label'),
                          'status': status, 'evidenceSha256': evidence_hashes})
    need(REQUIRED_CHECKS <= seen, 'Receipt omits a required A/B check.')
    limits = value.get('limitations')
    need(isinstance(limits, list) and limits, 'Reviewed remaining limits are required.')
    summary = plain(value.get('publicSummary'), 'Public summary')
    public = {'schemaVersion': 1, 'kind': value['kind'], 'decision': 'pass',
              'acceptedAtUtc': stamp, 'timezone': ZONE,
              'receiptSha256': digest_file(path),
              'baseline': {'bodySha256': dict(BASELINE_BODY), 'coreIdentity': dict(before_identity)},
              'candidate': {'bodySha256': dict(CANDIDATE_BODY), 'coreIdentity': dict(after_identity)},
              'checks': sanitized,
              'publicSummary': summary,
              'limitations': [plain(x, 'Limitation') for x in limits],
              'verification': {key: 'unverified' for key in UNVERIFIED},
              'publicDelivery': 'Not established by this private preparation.'}
    return value, public


def replace_once(text, old, new, context):
    need(text.count(old) == 1, 'Expected one exact edit target in ' + context)
    return text.replace(old, new, 1)


def page_body(template, title, body):
    need(template.count('<main id="main">') == 1 and template.count('</main>') == 1,
         'The retained HTML template changed.')
    before, remainder = template.split('<main id="main">', 1)
    _, after = remainder.split('</main>', 1)
    nav = re.search(r'(<nav class="project-nav".*?</nav>)', template)
    need(nav is not None, 'Retained project navigation is missing.')
    before = re.sub(r'<title>.*?</title>', '<title>' + html.escape(title) + ' · COSOM</title>', before, count=1)
    return before + '<main id="main"><p class="eyebrow">Nintendo 64 · USA · 1997</p>' + nav[1] + body + '</main>' + after


def insert_before_main_end(text, body, context):
    need(text.count('</main>') == 1, 'Expected one main element in ' + context)
    return text.replace('</main>', body + '</main>', 1)


def prepare(work, diagnostic, output, receipt_path):
    source, baseline_manifest, baseline = frozen_records(work)
    overlay = diagnostic / 'webgl2-overlay' / PROJECT
    checked_file(diagnostic / 'webgl2-overlay-preparation.json', OVERLAY_RECEIPT_SHA)
    checked_file(overlay / 'player.js', OVERLAY_PLAYER_SHA)
    checked_file(overlay / 'data/runtime-config.json', OVERLAY_CONFIG_SHA)
    checked_file(source / 'original/runtime/cores/mupen64plus_next-wasm.data', CORE_ARCHIVE_SHA)
    patch = diagnostic / 'save-compatibility/saves.js'
    report_path = diagnostic / 'save-compatibility/source-tests-report.json'
    checked_file(patch, SAVE_PATCH_SHA)
    checked_file(report_path, SAVE_TEST_REPORT_SHA)
    report = read_json(report_path)
    need(report.get('checks') == report.get('passed') == 12 and report.get('failed') == 0 and
         report.get('privatePatchedSha256') == SAVE_PATCH_SHA and
         report.get('actualGameExecuted') is False and report.get('userSavesRead') is False,
         'Reviewed save patch source-test receipt is not the retained synthetic pass.')
    old_config = read_json(source / 'data/runtime-config.json')
    config = read_json(overlay / 'data/runtime-config.json')
    need(config['identity']['variant'] == 'standard-nonthread' and
         config['identity']['romSha256'] == ROM_SHA and config['nativeSave'] == old_config['nativeSave'],
         'Renderer overlay changed the cartridge/save contract.')
    _, public = acceptance(receipt_path, diagnostic, old_config['identity'], config['identity'])
    checked_file(work / 'shared-assets/provenance.json', SHARED_PROVENANCE_SHA)
    shared = read_json(work / 'shared-assets/provenance.json')
    assets = []
    for entry in shared['files']:
        relative_path(entry['path'])
        need('/' not in entry['path'], 'Shared dependency must be one retained file.')
        checked_file(work / 'shared-assets' / entry['path'], entry['sha256'])
        assets.append(entry)
    need(len(assets) == 6, 'Expected six retained offline shared dependencies.')
    need(not output.exists(), 'Candidate output already exists; choose a new candidate name.')

    # No output directory exists before all acceptance, privacy and input gates.
    output.mkdir(parents=True, exist_ok=False)
    target = output / 'site' / PROJECT
    shutil.copytree(source, target, copy_function=shutil.copy2, symlinks=False)
    for entry in assets:
        dest = output / 'shared-assets' / entry['path']
        dest.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(work / 'shared-assets' / entry['path'], dest)
    write_json(output / 'shared-assets/provenance.json', shared)
    preserved_release = target / 'releases/1.0.0.json'
    preserved_release.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(source / 'release.json', preserved_release)
    history_source = target / 'source/history/1.0.0'
    history_source.mkdir(parents=True, exist_ok=True)
    shutil.copy2(source / 'source/README.txt', history_source / 'README.txt')
    historical_manifest = {key: baseline_manifest[key] for key in
                           ['schemaVersion', 'project', 'releaseId', 'contentSha256',
                            'files', 'filesCount', 'bytes']}
    historical_manifest['sourcePrivateManifestSha256'] = MANIFEST_SHA
    historical_manifest['scope'] = 'Public file identities only; private root, chat and coordination fields omitted.'
    write_json(history_source / 'upload-manifest-public.json', historical_manifest)

    player = (overlay / 'player.js').read_text(encoding='utf-8')
    player = replace_once(player, "'./saves.js?v=1'", "'./saves-1.0.1.js'", 'new player save import')
    player = replace_once(player, "'../data/runtime-config.json'", "'../data/runtime-config-1.0.1.json'", 'new player config URL')
    player = replace_once(player, 'the diagnostic does not fall back to the legacy renderer.',
                          'this release requires the standard renderer and does not fall back to the legacy renderer.',
                          'new player WebGL2 error copy')
    (target / 'player-1.0.1.js').write_text(player, encoding='utf-8')
    shutil.copy2(patch, target / 'saves-1.0.1.js')
    shutil.copy2(overlay / 'data/runtime-config.json', target / 'data/runtime-config-1.0.1.json')
    for name in ['player-1.0.1.js', 'saves-1.0.1.js']:
        shutil.copy2(target / name, target / 'source' / name)
    shutil.copy2(target / 'data/runtime-config-1.0.1.json', target / 'source/runtime-config-1.0.1.json')
    shutil.copy2(Path(__file__).resolve(), target / 'source/prepare-renderer-repair-source.txt')
    write_json(target / 'data/renderer-verification-1.0.1.json', public)
    save_report = {key: report[key] for key in ['schemaVersion', 'checks', 'passed', 'failed',
                    'fixtures', 'actualBrowserExecuted', 'actualCoreExecuted', 'actualGameExecuted',
                    'userSavesRead', 'canonicalSourceModified', 'results']}
    save_report.update({'patchSha256': SAVE_PATCH_SHA, 'sourceReportSha256': SAVE_TEST_REPORT_SHA})
    write_json(target / 'data/save-compatibility-1.0.1.json', save_report)

    original = (target / 'original/index.html').read_text(encoding='utf-8')
    original = replace_once(original, 'src="../player.js?v=1"', 'src="../player-1.0.1.js"', 'original page module')
    original = insert_before_main_end(original,
        '<section class="panel"><h2>Renderer update · October 31</h2><p>' + html.escape(public['publicSummary']) +
        '</p><p>This edition requires WebGL2. Existing cartridge-save names and browser storage stay the same. '
        'Legacy renderer snapshots remain in their slots and can be exported after checksum validation; '
        'they require their matching old runtime to load. Cartridge save/reload remains unverified.</p>'
        '<p><a href="../' + ARTICLE + '">Read the exact verification scope and remaining limits</a></p></section>', 'original page')
    (target / 'original/index.html').write_text(original, encoding='utf-8')

    index = (target / 'index.html').read_text(encoding='utf-8')
    index = replace_once(index, 'href="dev-blog/2026-10-30-original-edition.html"',
                         'href="' + ARTICLE + '"', 'project latest journal link')
    index = replace_once(index,
        'Gameplay and save/reload testing were skipped at the owner’s request. Published file checks establish transfer integrity, not campaign completion or save durability.',
        'The initial October 30 edition skipped gameplay testing at the owner’s request. '
        'The October 31 renderer comparison is recorded separately below. Native save/reload, a complete campaign, '
        'physical-controller behavior and cheat effects remain unverified. File checks establish delivery integrity.',
        'latest project status')
    index = replace_once(index,
        'href="downloads/goldeneye-usa-website-1.0.0.zip">Download website edition 1.0.0</a>',
        'href="downloads/goldeneye-usa-website-1.0.1.zip">Download website edition 1.0.1</a> · '
        '<a href="downloads/goldeneye-usa-website-1.0.0.zip">Retained 1.0.0 download</a>', 'latest download')
    status_panel = '<section class="panel"><h2>Renderer repair · 1.0.1</h2><p>' + html.escape(public['publicSummary']) + '</p><p><a href="' + ARTICLE + '">Dated comparison results and limits</a> · <a href="data/renderer-verification-1.0.1.json">Rendering evidence record</a></p></section>'
    index = insert_before_main_end(index, status_panel, 'project landing')
    (target / 'index.html').write_text(index, encoding='utf-8')

    credits = (target / 'credits/index.html').read_text(encoding='utf-8')
    credits = replace_once(credits,
        'Gameplay, physical controller, performance, full-campaign and save/reload testing were skipped at the owner’s request. No claim of successful original EEPROM persistence is made. Check the release record for uploaded-file evidence.',
        'The original October 30 release skipped gameplay testing at the owner’s request. '
        'A separate October 31 local renderer comparison checked only the observations recorded in '
        '<a href="../data/renderer-verification-1.0.1.json">the rendering receipt</a>. '
        'Native save/reload, complete-campaign, physical-controller and cheat-effect acceptance remain unverified. '
        'This release requires the pinned standard nonthreaded WebGL2 core; the original ROM and both retained core archives are unchanged.',
        'latest credits verification')
    credits = insert_before_main_end(credits, status_panel.replace('href="' + ARTICLE, 'href="../' + ARTICLE).replace('href="data/', 'href="../data/'), 'credits evidence panel')
    (target / 'credits/index.html').write_text(credits, encoding='utf-8')

    passed = [x for x in public['checks'] if x['status'] == 'pass']
    observed = ''.join('<li>' + html.escape(x['label']) + '</li>' for x in passed)
    limits = ''.join('<li>' + html.escape(x) + '</li>' for x in public['limitations'])
    article_body = '<p class="eyebrow"><time datetime="2026-10-31">October 31, 2026</time> · Renderer repair · 1.0.1</p><h1>Repairing GoldenEye’s startup lettering.</h1><p class="lead">' + html.escape(public['publicSummary']) + '</p><section class="panel"><h2>The renderer change</h2><p>The original player forced the legacy WebGL1 renderer. This update selects the already retained standard nonthreaded WebGL2 core with its exact JS/WASM hashes. It requires actual WebGL2 support and does not silently fall back. Original cartridge bytes, core archives, recovered media, journey, controls and optional assistance are retained.</p><h2>What the local comparison observed</h2><ul>' + observed + '</ul><p>These observations apply to the isolated local comparison. They do not establish public delivery or a complete campaign.</p><h2>Existing saves and snapshots</h2><p>Native cartridge-save identity, filename and browser database remain unchanged; native save/reload durability remains unverified. Eight state slots stay in the same database. Recognized legacy renderer snapshots can be exported after checksum validation, but cross-renderer state loading/import remains blocked. Keep the matching <a href="../downloads/goldeneye-usa-website-1.0.0.zip">retained 1.0.0 runtime</a> for those backups. The compatibility helper passed 12 synthetic source checks; those are separate from browser/core/save acceptance.</p><h2>Remaining limits</h2><ul>' + limits + '</ul><p>Native save/reload, full-campaign completion, physical-controller behavior and cheat effects remain unverified. Source-only interface edits version the module URLs and add the reviewed legacy-export compatibility helper.</p><p><a href="../data/renderer-verification-1.0.1.json">Rendering evidence and source pins</a> · <a href="../data/save-compatibility-1.0.1.json">Synthetic compatibility check scope</a> · <a href="../downloads/goldeneye-usa-website-1.0.1.zip">Download 1.0.1</a> · <a href="2026-10-30-original-edition.html">Original October 30 article</a></p></section>'
    template = (source / 'dev-blog/2026-10-30-original-edition.html').read_text(encoding='utf-8')
    article = page_body(template, 'GoldenEye renderer repair — October 31, 2026', article_body)
    (target / ARTICLE).write_text(article, encoding='utf-8')
    journal = (target / 'dev-blog/index.html').read_text(encoding='utf-8')
    journal = replace_once(journal, '<h1>Development journal</h1>',
        '<h1>Development journal</h1><article class="panel"><time datetime="2026-10-31">October 31, 2026</time><h2><a href="2026-10-31-renderer-repair.html">Repairing GoldenEye’s startup lettering</a></h2><p>' + html.escape(public['publicSummary']) + '</p></article>', 'journal index')
    (target / 'dev-blog/index.html').write_text(journal, encoding='utf-8')
    history = read_json(target / 'dev-blog/history.json')
    need(not any(x.get('version') == VERSION for x in history['entries']), 'History already contains 1.0.1.')
    for entry in history['entries']:
        checked_file(target / 'dev-blog' / relative_path(entry['article']), entry['sha256'])
    history['entries'].append({'date': DATE, 'writtenAtTimezone': ZONE, 'version': VERSION,
        'title': 'Repairing GoldenEye’s startup lettering', 'article': ARTICLE.split('/')[-1],
        'sha256': sha(article.encode('utf-8')), 'verification': 'Scoped local A/B rendering comparison',
        'saveReloadTested': False, 'fullCampaignTested': False,
        'evidence': '../data/renderer-verification-1.0.1.json'})
    write_json(target / 'dev-blog/history.json', history)

    readme = '''GoldenEye USA website edition 1.0.1 — October 31, 2026
Current editable modules: player-1.0.1.js, saves-1.0.1.js and data/runtime-config-1.0.1.json.
The public player imports the versioned save helper and requires the exact standard nonthreaded WebGL2 runtime identity. Original ROM/core bytes are unchanged.
prepare-renderer-repair-source.txt contains the exact Python updater. Download/copy it and rename it prepare.py locally. Its specific 1.0.0-to-1.0.1 recipe requires the retained frozen authoring tree, pinned diagnostic overlay and source-test report, plus an actual root-authored passing local A/B receipt. It refuses before output creation without acceptance. The private authoring work root is selected with GOLDENEYE_WORK. It creates a new candidate ordinary-copy tree; it does not publish or change canonical source.
build.mjs, freeze.mjs and package.py retained here are HISTORICAL 1.0.0 tools. They hardcode that old version/date/legacy renderer and overwrite history. Do not run them as a routine 1.0.1 rebuild. The earlier README and an allowlisted historical public file manifest are preserved under history/1.0.0/. The exact private manifest stays in the original authoring tree; its hash is recorded without copying private root/chat fields into public files.
Persistent identities remain: cosom-goldeneye-usa-v1-native; cosom-goldeneye-usa-v1-player; cosom-goldeneye-usa-v1-states; cosom-goldeneye-usa-state-v1; goldeneye-usa-v1.n64; goldeneye-usa-v1.srm (296960 bytes); cosom:goldeneye:usa:v1:mission-notebook.
Recognized legacy snapshots remain preserved/exportable after integrity validation; loading/import requires the matching exact runtime. Do not rewrite, migrate, delete or relabel legacy state payloads as standard-core snapshots. Normal cartridge saves are separate.
The rendering receipt covers only its actual local observations. Native save/reload, full campaign, physical controller and cheat effects remain unverified. Twelve synthetic save-helper checks do not establish browser/core/persistence acceptance. No private receipts/screenshots, user saves, credentials or session data belong in public packages.
Serve the offline tree through HTTP, with /n64/goldeneye-usa/ as the project route and supplied /assets/ dependencies. Use localhost or HTTPS for browser storage. Wider COSOM links require the live site.
Future changes need their own actual dated article, preserved history, content hashes, new archive and guarded publication; this narrow helper is not a generic future-release builder.
'''
    (target / 'source/README.txt').write_text(readme, encoding='utf-8')
    old_release = read_json(source / 'release.json')
    content = [x for x in records(target) if x['path'] != 'release.json' and not x['path'].startswith('downloads/')]
    content_hash = sha(json_bytes(content))
    release = {**{key: old_release[key] for key in ['schemaVersion', 'title', 'projectUrl', 'edition',
                 'originalRomSha256', 'privacy']},
        'releaseId': 'goldeneye-usa-1.0.1-' + content_hash[:12], 'version': VERSION, 'date': DATE,
        'writtenAtTimezone': ZONE, 'previousReleaseId': old_release['releaseId'],
        'previousReleaseManifestSha256': RELEASE_SHA, 'notes': ARTICLE,
        'contentSha256': content_hash,
        'contentHashRule': 'SHA256 of compact UTF-8 JSON sorted by POSIX path, path/bytes/sha256 records; release.json and downloads/ excluded to avoid self-reference',
        'verification': {'rendering': public['publicSummary'], 'renderingReceipt': 'data/renderer-verification-1.0.1.json',
                         **{key: 'unverified' for key in UNVERIFIED},
                         'saveCompatibility': '12 synthetic source checks; browser/core persistence unverified',
                         'publicDelivery': 'Not established by private preparation; requires separate HTTPS receipt'},
        'runtimeIdentity': config['identity'], 'preservedStorageIdentities': STATE_IDENTITIES,
        'files': content}
    write_json(target / 'release.json', release)

    archive = target / 'downloads/goldeneye-usa-website-1.0.1.zip'
    need(not archive.exists(), 'New offline ZIP already exists.')
    with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED, compresslevel=6) as z:
        for entry in records(target):
            if not entry['path'].startswith('downloads/'):
                z.write(target / entry['path'], PROJECT + '/' + entry['path'])
        for entry in assets:
            z.write(output / 'shared-assets' / entry['path'], 'assets/' + entry['path'])
        z.writestr('index.html', '<!doctype html><meta charset="utf-8"><title>GoldenEye USA 1.0.1</title><meta http-equiv="refresh" content="0;url=/n64/goldeneye-usa/"><a href="/n64/goldeneye-usa/">Open GoldenEye USA</a>')
        z.writestr('README.txt', readme)
    archive_record = {'schemaVersion': 1, 'version': VERSION, 'date': DATE,
        'archive': archive.name, 'bytes': archive.stat().st_size, 'sha256': digest_file(archive),
        'releaseId': release['releaseId'], 'contentSha256': content_hash,
        'releaseJsonSha256': digest_file(target / 'release.json'),
        'excludes': 'downloads/ to avoid recursive old/new ZIP envelopes',
        'sharedDependencies': assets, 'verification': release['verification']}
    write_json(target / 'downloads/goldeneye-usa-website-1.0.1.json', archive_record)

    final = records(target)
    old_map, new_map = {x['path']: x for x in baseline}, {x['path']: x for x in final}
    removed = sorted(set(old_map) - set(new_map))
    need(not removed, 'A baseline file disappeared; candidate is incomplete.')
    changed = sorted(p for p in old_map if new_map[p] != old_map[p])
    need(set(changed) <= REPLACE_ALLOWED, 'An unrelated baseline file changed: ' + repr(set(changed) - REPLACE_ALLOWED))
    for protected, expected in [('releases/1.0.0.json', RELEASE_SHA),
                                ('downloads/goldeneye-usa-website-1.0.0.zip', OLD_ZIP_SHA),
                                ('dev-blog/2026-10-30-original-edition.html', OLD_ARTICLE_SHA)]:
        checked_file(target / protected, expected)
    for immutable in ['player.js', 'saves.js', 'data/runtime-config.json']:
        need(new_map[immutable] == old_map[immutable], 'An old named module/config changed.')
    manifest = {'schemaVersion': 1, 'project': 'GoldenEye USA', 'version': VERSION,
        'canonicalPrefixes': ['/' + PROJECT + '/'], 'root': str(output / 'site'),
        'releaseId': release['releaseId'], 'contentSha256': content_hash,
        'files': [{**x, 'path': PROJECT + '/' + x['path']} for x in final],
        'filesCount': len(final), 'bytes': sum(x['bytes'] for x in final),
        'sharedDiscoveryIncluded': False, 'publicDeliveryVerified': False}
    write_json(output / 'upload-manifest-1.0.1.json', manifest)
    added = sorted(set(new_map) - set(old_map))
    delta = {'schemaVersion': 1, 'project': 'GoldenEye USA', 'version': VERSION,
        'root': str(output / 'site'), 'canonicalPrefixes': ['/' + PROJECT + '/'],
        'previousManifestSha256': MANIFEST_SHA,
        'completeManifestSha256': digest_file(output / 'upload-manifest-1.0.1.json'),
        'releaseId': release['releaseId'], 'added': [PROJECT + '/' + p for p in added],
        'changed': [PROJECT + '/' + p for p in changed], 'removed': [],
        'files': [{**new_map[p], 'path': PROJECT + '/' + p} for p in sorted(added + changed)],
        'unchangedCount': len(old_map) - len(changed), 'sharedDiscoveryIncluded': False}
    write_json(output / 'delta-manifest-1.0.1.json', delta)
    # Final read-only checks leave both old authoring tree and historical bytes frozen.
    _, _, after_source = frozen_records(work)
    need(after_source == baseline, 'The frozen input changed during preparation.')
    proof = {'schemaVersion': 1, 'preparedAtUtc': datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
        'result': 'private-candidate-prepared', 'sourceManifestSha256': MANIFEST_SHA,
        'acceptanceReceiptSha256': public['receiptSha256'], 'releaseId': release['releaseId'],
        'output': str(output), 'ordinaryCopiesUsed': True, 'hardlinksRequested': False,
        'sourceTreeUnchanged': True, 'oldZipUnchanged': True, 'oldArticleUnchanged': True,
        'removed': [], 'changedExistingPaths': changed, 'addedPaths': added,
        'archive': archive_record, 'publicWritesPerformed': False,
        'canonicalSourceModified': False, 'browserOrNativeUiUsed': False, 'userSavesRead': False}
    write_json(output / 'preparation-receipt.json', proof)
    print(json.dumps({'output': str(output), 'releaseId': release['releaseId'],
                      'deltaFiles': len(delta['files']), 'removed': [], 'published': False}))


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--receipt', type=Path, required=True,
                        help='Root-authored passing local A/B acceptance JSON under the diagnostic directory')
    parser.add_argument('--output-name', default='candidate-1.0.1',
                        help='NEW child directory below diagnostic release-prep; never an existing directory')
    args = parser.parse_args()
    need(re.fullmatch(r'candidate-1\.0\.1(?:-[A-Za-z0-9-]+)?', args.output_name) is not None,
         'Output name must be candidate-1.0.1 or a bounded suffixed candidate name.')
    work = Path(os.environ.get('GOLDENEYE_WORK', 'L:/Game Preservation/GoldenEye-USA-Website')).resolve()
    diagnostic = work / 'diagnostics/render-2026-10-31'
    output = under(diagnostic / 'release-prep', diagnostic / 'release-prep' / args.output_name)
    prepare(work, diagnostic, output, args.receipt)


if __name__ == '__main__':
    try:
        main()
    except Exception as error:
        print('REFUSED/INCOMPLETE: ' + str(error), file=sys.stderr)
        sys.exit(1)
