"""Bounded private 1.0.2 -> 1.0.3 preparation. Never publishes or runs a game.

init verifies accepted 1.0.2 and makes NEW ordinary copies. The human-facing
player/pages/notes are then edited in that candidate. freeze requires actual
source/local-Edge stage evidence, preserves all historical bytes and emits an exact
project-only archive, complete manifest and delta. Shared discovery is separate.
"""
from __future__ import annotations

import argparse
from datetime import datetime, timezone
import hashlib
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.3'
DATE = '2026-10-31'
ZONE = 'America/New_York'
PROJECT = 'n64/goldeneye-usa'
BASE = Path('L:/Game Preservation/GoldenEye-USA-Website/diagnostics/modern-controls-2026-10-31/candidate-1.0.2')
DIAGNOSTIC = Path(__file__).resolve().parent
BASE_MANIFEST_SHA = 'bf333e36f87557a773384a602785306b57b25e58a8b1215103015de82adf17da'
BASE_RELEASE_SHA = 'a4c22090deb520b0001c9e1dc274c2c9db41cacb54885c894453c8b7f3c1885e'
BASE_RELEASE_ID = 'goldeneye-usa-1.0.2-8a25c4439a29'
ARTICLE = 'dev-blog/2026-10-31-stage-startup.html'
VERIFY_FILE = 'data/stage-startup-verification-1.0.3.json'
PLAYER = 'player-1.0.3.js'
SPEED = 'original/speed-controls-1.0.3.js'
HEALTH = 'original/runtime-health-1.0.3.js'
MODERN = 'original/modern-controls-1.0.3.js'
HEX = re.compile(r'^[0-9a-f]{64}$')
REPLACE_ALLOWED = {'original/index.html', 'index.html', 'credits/index.html',
                   'dev-blog/index.html', 'dev-blog/history.json', 'release.json',
                   'source/README.txt'}
ADD_ALLOWED = {PLAYER, SPEED, HEALTH, MODERN, ARTICLE, VERIFY_FILE,
               'releases/1.0.2.json', 'source/player-1.0.3.js',
               'source/speed-controls-1.0.3.js', 'source/runtime-health-1.0.3.js',
               'source/modern-controls-1.0.3.js',
               'source/prepare-stage-startup-source.txt',
               'source/history/1.0.2/README.txt',
               'source/history/1.0.2/upload-manifest-public.json',
               'downloads/goldeneye-usa-website-1.0.3.zip',
               'downloads/goldeneye-usa-website-1.0.3.json'}
REQUIRED_CHECKS = {
    'syntheticRewindLifecycle': 'synthetic',
    'syntheticHealthMonitor': 'synthetic',
    'syntheticInputCompatibility': 'synthetic',
    'syntheticModernSetupGate': 'synthetic',
    'localEdgeStageContinuity': 'local-ui',
    'localEdgeNativeImageryChanges': 'local-ui',
    'localEdgeNativeWatchResponsive': 'local-ui',
}
NATIVE_CHECKS = {
    'localEdgeNativeImageryChanges': ('native-opening-or-world-imagery', 2),
    'localEdgeNativeWatchResponsive': ('native-watch-input-response', 3),
}
NATIVE_PHASES = ('world-start', 'world-end', 'watch-open', 'watch-close', 'world-resume')


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


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


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


def no_reparse(path):
    s = path.lstat()
    need(not stat.S_ISLNK(s.st_mode) and not (getattr(s, 'st_file_attributes', 0) & 0x400),
         'Reparse point/symlink refused: ' + str(path))


def rel(value):
    need(isinstance(value, str) and value and '\\' not in value and ':' not in value,
         'Expected relative POSIX path.')
    p = PurePosixPath(value)
    need(not p.is_absolute() and value == p.as_posix() and
         all(x not in ('', '.', '..') for x in p.parts), 'Unsafe relative path: ' + value)
    return value


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


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


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


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


def checked(path, expected, size=None):
    no_reparse(path)
    need(path.is_file() and digest(path) == expected and
         (size is None or path.stat().st_size == size), 'Pinned input changed: ' + str(path))


def records(root):
    no_reparse(root)
    result = []
    for directory, dirs, files in os.walk(root, 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(root).as_posix(),
                           'bytes': p.stat().st_size, 'sha256': digest(p)})
    return sorted(result, key=lambda x: x['path'])


def baseline():
    path = BASE / 'upload-manifest-1.0.2.json'
    checked(path, BASE_MANIFEST_SHA)
    m = read_json(path)
    need(m.get('releaseId') == BASE_RELEASE_ID and m.get('version') == '1.0.2',
         'Baseline release ID/version changed.')
    expected = {}
    for record in m['files']:
        p = rel(record['path'])
        need(p.startswith(PROJECT + '/'), 'Baseline includes another project.')
        p = p[len(PROJECT) + 1:]
        need(p not in expected and HEX.fullmatch(record['sha256']), 'Invalid baseline record.')
        expected[p] = {'path': p, 'bytes': record['bytes'], 'sha256': record['sha256']}
    source = BASE / 'site' / PROJECT
    actual = records(source)
    need({x['path']: x for x in actual} == expected,
         'Accepted 1.0.2 tree differs from its complete manifest.')
    checked(source / 'release.json', BASE_RELEASE_SHA)
    need(len(actual) == m['filesCount'] and sum(x['bytes'] for x in actual) == m['bytes'],
         'Baseline inventory totals differ.')
    return source, m, actual


def candidate(name):
    need(re.fullmatch(r'candidate-1\.0\.3(?:-[A-Za-z0-9-]+)?', name), 'Bad candidate name.')
    return under(DIAGNOSTIC, DIAGNOSTIC / name)


def shared_assets():
    provenance = read_json(BASE / 'shared-assets/provenance.json')
    files = provenance['files']
    accepted_archive = read_json(BASE / 'site' / PROJECT / 'downloads/goldeneye-usa-website-1.0.2.json')
    need(files == accepted_archive['sharedDependencies'],
         'Offline shared provenance differs from pinned accepted archive metadata.')
    need(len(files) == 6, 'Expected six offline shared assets.')
    for item in files:
        need('/' not in rel(item['path']), 'Shared asset must be a single filename.')
        checked(BASE / 'shared-assets' / item['path'], item['sha256'], item['bytes'])
    return provenance, files


def init(output):
    source, manifest, old = baseline()
    provenance, assets = shared_assets()
    need(not output.exists(), 'Candidate exists; init never overwrites.')
    output.mkdir(parents=True, exist_ok=False)
    target = output / 'site' / PROJECT
    shutil.copytree(source, target, copy_function=shutil.copy2, symlinks=False)
    shutil.copytree(BASE / 'shared-assets', output / 'shared-assets',
                    copy_function=shutil.copy2, symlinks=False)
    (target / 'releases').mkdir(exist_ok=True)
    shutil.copy2(source / 'release.json', target / 'releases/1.0.2.json')
    history = target / 'source/history/1.0.2'
    history.mkdir(parents=True, exist_ok=False)
    shutil.copy2(source / 'source/README.txt', history / 'README.txt')
    public_manifest = {key: manifest[key] for key in
                       ('schemaVersion', 'project', 'releaseId', 'contentSha256',
                        'files', 'filesCount', 'bytes')}
    public_manifest['sourcePrivateManifestSha256'] = BASE_MANIFEST_SHA
    public_manifest['scope'] = 'Public file identities only; private root/chat/coordination fields omitted.'
    write_json(history / 'upload-manifest-public.json', public_manifest)
    copied = {x['path']: x for x in records(target)}
    need(all(copied[x['path']] == x for x in old), 'Ordinary-copy readback differs.')
    _, _, after = baseline()
    need(after == old, 'Baseline changed during ordinary-copy initialization.')
    receipt = {'schemaVersion': 1, 'kind': 'goldeneye-stage-startup-init-v1',
               'initializedAtUtc': datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
               'baselineManifestSha256': BASE_MANIFEST_SHA, 'sourceTreeUnchanged': True,
               'ordinaryCopiesUsed': True, 'hardlinksRequested': False,
               'publicWritesPerformed': False, 'browserOrNativeUiUsed': False,
               'userSavesRead': False, 'output': str(output),
               'baselineFilesCount': len(old), 'baselineBytes': sum(x['bytes'] for x in old)}
    write_json(output / 'initialization-receipt.json', receipt)
    print(json.dumps({'result': 'initialized-private-copy', 'output': str(output),
                      'projectRoot': str(target), 'published': False}))


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


def native_timeline(value, accepted):
    need(isinstance(value, dict) and value.get('case') == 'candidate' and
         value.get('continuousForegroundWorldObserved') is True and
         value.get('pausedOrOccludedDuringWorldWindow') is False,
         'Native timeline must describe actual candidate foreground continuity, not a baseline or paused/occluded window.')
    rows = value.get('observations')
    need(isinstance(rows, list) and [x.get('phase') for x in rows] == list(NATIVE_PHASES),
         'Native timeline requires WORLD START, WORLD END, WATCH OPEN, WATCH CLOSE and WORLD RESUME in order.')
    times, images, hashes, public = {}, {}, set(), []
    for row in rows:
        stamp = row.get('observedAtUtc', '')
        need(isinstance(stamp, str) and stamp.endswith('Z'), 'Native timeline needs actual UTC Z timestamps.')
        observed = datetime.fromisoformat(stamp[:-1] + '+00:00')
        need(observed <= accepted and observed.astimezone(ZoneInfo(ZONE)).date().isoformat() == DATE,
             'Native timeline observations must precede acceptance on the actual release date.')
        artifact = row.get('image', {})
        need(isinstance(artifact, dict) and isinstance(artifact.get('sha256'), str) and
             HEX.fullmatch(artifact['sha256']), 'Native timeline image hash is invalid.')
        path = under(DIAGNOSTIC, Path(artifact.get('path', '')))
        need(path.suffix.lower() in ('.png', '.jpg', '.jpeg', '.webp'),
             'Native timeline phases require actual retained native image artifacts, not callback logs.')
        need(artifact['sha256'] not in hashes, 'Native timeline phases require distinct native image artifacts.')
        hashes.add(artifact['sha256'])
        checked(path, artifact['sha256'])
        times[row['phase']], images[row['phase']] = observed, artifact['sha256']
        public.append({'phase': row['phase'], 'observedAtUtc': stamp, 'imageSha256': artifact['sha256']})
    need(all(times[b] > times[a] for a, b in zip(NATIVE_PHASES, NATIVE_PHASES[1:])),
         'Native timeline must follow WORLD START → WORLD END → WATCH OPEN → WATCH CLOSE → WORLD RESUME.')
    seconds = (times['world-end'] - times['world-start']).total_seconds()
    need(seconds >= 30, 'Actual foreground native world continuity must last at least 30 seconds before the watch cycle.')
    return times, images, {'case': 'candidate', 'continuousForegroundWorldObserved': True,
                           'pausedOrOccludedDuringWorldWindow': False,
                           'observedWorldContinuitySeconds': seconds, 'observations': public}


def acceptance(path, target):
    path = under(DIAGNOSTIC, path)
    r = read_json(path)
    need(r.get('schemaVersion') == 1 and r.get('kind') == 'goldeneye-stage-startup-acceptance-v1'
         and r.get('decision') == 'pass', 'Passing stage-startup acceptance receipt required.')
    need(r.get('baselineManifestSha256') == BASE_MANIFEST_SHA and r.get('timezone') == ZONE,
         'Wrong receipt baseline/timezone.')
    stamp = r.get('acceptedAtUtc', '')
    need(isinstance(stamp, str) and stamp.endswith('Z'), 'acceptedAtUtc must be UTC Z.')
    accepted = datetime.fromisoformat(stamp[:-1] + '+00:00')
    need(accepted <= datetime.now(timezone.utc) and
         accepted.astimezone(ZoneInfo(ZONE)).date().isoformat() == DATE and
         datetime.now(ZoneInfo(ZONE)).date().isoformat() == DATE,
         'Author fresh actual-date notes; acceptance/preparation date mismatch.')
    need(r.get('isolatedOrigin') is True and r.get('userSavesRead') is False and
         r.get('physicalControllerTested') is False and
         r.get('localBrowser') == 'Microsoft Edge' and
         r.get('actualGameExecuted') is True and
         r.get('localStageContinuityObserved') is True and
         r.get('reportedOtherPcFreezeReproduced') is False and
         r.get('rootCauseEstablished') is False,
         'Receipt must separate actual isolated Edge stage observations from the unreproduced other-PC report and unknown root cause.')
    native = r.get('nativeVisualObservations', {})
    need(isinstance(native, dict) and native.get('foregroundVisible') is True and
         native.get('openingOrWorldImageryChanged') is True and
         native.get('watchResponsiveAfterImagery') is True and
         native.get('callbackOnly') is False and
         native.get('blackOrOccluded') is False,
         'Native continuity requires foreground changing game imagery followed by responsive native watch input; callback-only, black, minimized or occluded observations cannot pass.')
    timeline_times, timeline_images, timeline_public = native_timeline(r.get('nativeContinuityTimeline'), accepted)
    tested, seen = [], set()
    for entry in r.get('candidateFiles', []):
        p = rel(entry['path'])
        need(p not in seen and p in {PLAYER, SPEED, HEALTH, MODERN, 'original/index.html'},
             'Unexpected/duplicate tested source path.')
        seen.add(p)
        checked(target / p, entry['sha256'], entry['bytes'])
        tested.append({'path': p, 'bytes': entry['bytes'], 'sha256': entry['sha256']})
    need(seen == {PLAYER, SPEED, HEALTH, MODERN, 'original/index.html'}, 'Receipt must pin player, speed module, health module, modern setup module and player HTML.')
    checks, seen, native_times = [], set(), {}
    for check in r.get('checks', []):
        key, kind, state = check.get('id'), check.get('kind'), check.get('status')
        need(isinstance(key, str) and re.fullmatch(r'[A-Za-z][A-Za-z0-9]{0,79}', key) and
             key not in seen and kind in ('synthetic', 'local-ui') and
             state in ('pass', 'unverified', 'not-tested'), 'Invalid/duplicate observed check.')
        seen.add(key)
        if key in REQUIRED_CHECKS:
            need(kind == REQUIRED_CHECKS[key] and state == 'pass', 'Required check did not pass: ' + key)
        evidence_hashes, image_hashes, evidence_paths = [], set(), set()
        has_json = False
        for item in check.get('evidence', []):
            p = under(DIAGNOSTIC, Path(item['path']))
            need(p not in evidence_paths, 'Duplicate evidence artifact in one check.')
            evidence_paths.add(p)
            need(p.suffix.lower() in ('.json', '.png', '.jpg', '.jpeg', '.webp', '.txt', '.md', '.log'),
                 'Only retained evidence artifacts accepted.')
            need(isinstance(item.get('sha256'), str) and HEX.fullmatch(item['sha256']), 'Evidence hash is invalid.')
            checked(p, item['sha256'])
            evidence_hashes.append(item['sha256'])
            if p.suffix.lower() in ('.png', '.jpg', '.jpeg', '.webp'):
                image_hashes.add(item['sha256'])
            has_json = has_json or p.suffix.lower() == '.json'
        need(state != 'pass' or evidence_hashes, 'Passing checks require retained evidence.')
        public_check = {'id': key, 'kind': kind, 'status': state,
                        'label': public_text(check.get('label'), 'Check label'),
                        'evidenceSha256': evidence_hashes}
        if key in NATIVE_CHECKS:
            basis, minimum_images = NATIVE_CHECKS[key]
            need(check.get('observationBasis') == basis,
                 'Native check must identify actual native imagery or native watch input, not callbacks.')
            need(has_json and len(image_hashes) >= minimum_images,
                 'Native visual evidence needs a retained JSON action/observation record and distinct native image artifacts.')
            observed_stamp = check.get('observedAtUtc', '')
            need(isinstance(observed_stamp, str) and observed_stamp.endswith('Z'),
                 'Native observation needs an actual UTC Z timestamp.')
            observed = datetime.fromisoformat(observed_stamp[:-1] + '+00:00')
            need(observed <= accepted and observed.astimezone(ZoneInfo(ZONE)).date().isoformat() == DATE,
                 'Native observation time must precede acceptance on the actual release date.')
            phases = ('world-start', 'world-end') if key == 'localEdgeNativeImageryChanges' else ('watch-open', 'watch-close', 'world-resume')
            need({timeline_images[x] for x in phases} <= image_hashes,
                 'Native check evidence must include its exact candidate timeline images.')
            ending = 'world-end' if key == 'localEdgeNativeImageryChanges' else 'world-resume'
            need(observed == timeline_times[ending],
                 'Native check timestamp must match the corresponding completed candidate timeline phase.')
            native_times[key] = observed
            public_check.update({'observationBasis': basis, 'observedAtUtc': observed_stamp,
                                 'nativeAction': public_text(check.get('nativeAction'), 'Native action'),
                                 'distinctImageEvidenceCount': len(image_hashes)})
        checks.append(public_check)
    need(set(REQUIRED_CHECKS) <= seen, 'Receipt omitted a required source/local-UI check.')
    need(native_times['localEdgeNativeWatchResponsive'] > native_times['localEdgeNativeImageryChanges'],
         'Native watch responsiveness must be observed after the native imagery change.')
    need(isinstance(r.get('limitations'), list) and r['limitations'], 'Remaining limits required.')
    return {'schemaVersion': 1, 'kind': r['kind'], 'decision': 'pass',
            'acceptedAtUtc': stamp, 'timezone': ZONE, 'receiptSha256': digest(path),
            'baselineManifestSha256': BASE_MANIFEST_SHA,
            'candidateFiles': sorted(tested, key=lambda x: x['path']), 'checks': checks,
            'publicSummary': public_text(r.get('publicSummary'), 'Public summary'),
            'limitations': [public_text(x, 'Limitation') for x in r['limitations']],
            'isolatedOrigin': True, 'userSavesRead': False,
            'physicalControllerTested': False,
            'localBrowser': 'Microsoft Edge',
            'localStageContinuityObserved': True,
            'nativeVisualObservations': {
                'foregroundVisible': True, 'openingOrWorldImageryChanged': True,
                'watchResponsiveAfterImagery': True, 'callbackOnly': False, 'blackOrOccluded': False},
            'nativeContinuityTimeline': timeline_public,
            'reportedOtherPcFreezeReproduced': False, 'rootCauseEstablished': False,
            'actualGameExecuted': r.get('actualGameExecuted') is True,
            'nativeSaveReload': 'unverified', 'fullCampaign': 'unverified', 'cheatEffects': 'unverified'}


def check_candidate(target, old):
    current = records(target)
    oldmap, newmap = {x['path']: x for x in old}, {x['path']: x for x in current}
    need(set(oldmap) <= set(newmap), 'Baseline files were removed.')
    changed = {p for p in oldmap if oldmap[p] != newmap[p]}
    added = set(newmap) - set(oldmap)
    need(changed <= REPLACE_ALLOWED, 'Unrelated baseline file changed: ' + repr(changed - REPLACE_ALLOWED))
    need(added <= ADD_ALLOWED, 'Unreviewed new file: ' + repr(added - ADD_ALLOWED))
    checked(target / 'releases/1.0.2.json', BASE_RELEASE_SHA)
    return current


def freeze(output, receipt_path):
    source, oldmanifest, old = baseline()
    need(output.is_dir(), 'Initialize private copy first.')
    no_reparse(output)
    need(not (output / 'preparation-receipt.json').exists(), 'Candidate already frozen; do not overwrite.')
    target = output / 'site' / PROJECT
    check_candidate(target, old)
    public = acceptance(receipt_path, target)
    need((target / ARTICLE).is_file(), 'New dated stage-startup article missing.')
    history = read_json(target / 'dev-blog/history.json')
    old_history = read_json(source / 'dev-blog/history.json')
    need(history.get('entries', [])[:len(old_history['entries'])] == old_history['entries'],
         'Existing journal entries changed/reordered.')
    newer = history['entries'][len(old_history['entries']):]
    need(len(newer) == 1 and newer[0].get('version') == VERSION and newer[0].get('date') == DATE and
         newer[0].get('article') == ARTICLE.split('/')[-1], 'Expected one actual-date 1.0.3 journal entry.')
    for entry in history['entries']:
        checked(target / 'dev-blog' / rel(entry['article']), entry['sha256'])
    need('player-1.0.3.js' in (target / 'original/index.html').read_text(encoding='utf-8'),
         'Public player does not select new versioned entry module.')
    for publicpath, sourcepath in [(PLAYER, 'source/player-1.0.3.js'),
                                   (SPEED, 'source/speed-controls-1.0.3.js'),
                                   (HEALTH, 'source/runtime-health-1.0.3.js'),
                                   (MODERN, 'source/modern-controls-1.0.3.js')]:
        need((target / publicpath).is_file(), 'New input missing: ' + publicpath)
        shutil.copy2(target / publicpath, target / sourcepath)
    shutil.copy2(Path(__file__).resolve(), target / 'source/prepare-stage-startup-source.txt')
    write_json(target / VERIFY_FILE, public)
    provenance, assets = shared_assets()
    for item in assets:
        checked(output / 'shared-assets' / item['path'], item['sha256'], item['bytes'])
    config = read_json(target / 'data/runtime-config-1.0.1.json')
    old_release = read_json(source / 'release.json')
    need(config['identity'] == old_release['runtimeIdentity'], 'Runtime/state identity changed.')
    content = [x for x in records(target) if x['path'] != 'release.json' and not x['path'].startswith('downloads/')]
    content_hash = raw_sha(compact(content))
    verification = dict(old_release['verification'])
    verification.update({'stageStartup': public['publicSummary'], 'stageStartupReceipt': VERIFY_FILE,
                         'modernSetupGate': 'The new versioned modern input wrapper requires explicit native-setup acknowledgement and Apply; it does not automatically inspect or change native watch preferences.',
                         'reportedOtherPcFreezeReproduced': False, 'rootCauseEstablished': False,
                         'physicalController': 'unverified',
                         'publicDelivery': 'Not established by private preparation; requires separate HTTPS receipt'})
    release = {**{key: old_release[key] for key in ('schemaVersion', 'title', 'projectUrl', 'edition',
                 'originalRomSha256', 'privacy')},
               'releaseId': 'goldeneye-usa-1.0.3-' + content_hash[:12], 'version': VERSION, 'date': DATE,
               'writtenAtTimezone': ZONE, 'previousReleaseId': old_release['releaseId'],
               'previousReleaseManifestSha256': BASE_RELEASE_SHA, 'notes': ARTICLE,
               'contentSha256': content_hash, 'contentHashRule': old_release['contentHashRule'],
               'verification': verification, 'runtimeIdentity': old_release['runtimeIdentity'],
               'preservedStorageIdentities': old_release['preservedStorageIdentities'], 'files': content}
    write_json(target / 'release.json', release)
    archive = target / 'downloads/goldeneye-usa-website-1.0.3.zip'
    need(not archive.exists(), 'New ZIP already exists; freeze never overwrites.')
    zip_expected = {}
    root_index = '<!doctype html><meta charset="utf-8"><title>GoldenEye USA 1.0.3</title><meta http-equiv="refresh" content="0;url=/n64/goldeneye-usa/"><a href="/n64/goldeneye-usa/">Open GoldenEye USA</a>'
    with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED, compresslevel=6) as z:
        for item in records(target):
            if not item['path'].startswith('downloads/'):
                name = PROJECT + '/' + item['path']
                z.write(target / item['path'], name)
                zip_expected[name] = (item['bytes'], item['sha256'])
        for item in assets:
            name = 'assets/' + item['path']
            z.write(output / 'shared-assets' / item['path'], name)
            zip_expected[name] = (item['bytes'], item['sha256'])
        z.writestr('index.html', root_index)
        zip_expected['index.html'] = (len(root_index.encode('utf-8')), raw_sha(root_index.encode('utf-8')))
        z.write(target / 'source/README.txt', 'README.txt')
        zip_expected['README.txt'] = ((target / 'source/README.txt').stat().st_size, digest(target / 'source/README.txt'))
    # Re-open and read actual decompressed bytes, rather than treating successful
    # archive construction as evidence that every member matches the manifest.
    with zipfile.ZipFile(archive, 'r') as z:
        need(len(z.namelist()) == len(set(z.namelist())) and set(z.namelist()) == set(zip_expected),
             'Offline ZIP entry inventory differs or contains duplicate members.')
        for item in z.infolist():
            h = hashlib.sha256()
            with z.open(item) as stream:
                for chunk in iter(lambda: stream.read(1024 * 1024), b''):
                    h.update(chunk)
            need((item.file_size, h.hexdigest()) == zip_expected[item.filename],
                 'Offline ZIP member mismatch: ' + item.filename)
    archive_record = {'schemaVersion': 1, 'version': VERSION, 'date': DATE,
                      'archive': archive.name, 'bytes': archive.stat().st_size, 'sha256': digest(archive),
                      'releaseId': release['releaseId'], 'contentSha256': content_hash,
                      'releaseJsonSha256': digest(target / 'release.json'),
                      'excludes': 'downloads/ to avoid recursive old/new ZIP envelopes',
                      'sharedDependencies': assets, 'verification': verification}
    write_json(target / 'downloads/goldeneye-usa-website-1.0.3.json', archive_record)
    final = check_candidate(target, old)
    oldmap, newmap = {x['path']: x for x in old}, {x['path']: x for x in final}
    added = sorted(set(newmap) - set(oldmap))
    changed = sorted(p for p in oldmap if oldmap[p] != newmap[p])
    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}
    complete_path = output / 'upload-manifest-1.0.3.json'
    write_json(complete_path, manifest)
    delta = {'schemaVersion': 1, 'project': 'GoldenEye USA', 'version': VERSION,
             'root': str(output / 'site'), 'canonicalPrefixes': ['/' + PROJECT + '/'],
             'previousManifestSha256': BASE_MANIFEST_SHA,
             'completeManifestSha256': digest(complete_path), 'releaseId': release['releaseId'],
             'added': [PROJECT + '/' + p for p in added], 'changed': [PROJECT + '/' + p for p in changed],
             'removed': [], 'files': [{**newmap[p], 'path': PROJECT + '/' + p} for p in sorted(added + changed)],
             'unchangedCount': len(oldmap) - len(changed), 'sharedDiscoveryIncluded': False}
    write_json(output / 'delta-manifest-1.0.3.json', delta)
    _, _, after = baseline()
    need(after == old, 'Frozen baseline changed during preparation.')
    proof = {'schemaVersion': 1, 'kind': 'goldeneye-stage-startup-preparation-v1',
             'preparedAtUtc': datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'),
             'baselineManifestSha256': BASE_MANIFEST_SHA, 'sourceTreeUnchanged': True,
             'acceptanceReceiptSha256': public['receiptSha256'],
             'releaseId': release['releaseId'], 'contentSha256': content_hash,
             'completeManifestSha256': digest(complete_path),
             'deltaManifestSha256': digest(output / 'delta-manifest-1.0.3.json'),
             'ordinaryCopiesUsed': True, 'hardlinksRequested': False, 'removed': [],
             'addedPaths': added, 'changedExistingPaths': changed, 'archive': archive_record,
             'offlineArchiveEntriesVerified': len(zip_expected), 'offlineArchiveEntriesMatch': True,
             'publicWritesPerformed': False, 'browserOrNativeUiUsed': False, 'userSavesRead': False}
    write_json(output / 'preparation-receipt.json', proof)
    print(json.dumps({'result': 'private-candidate-frozen', 'releaseId': release['releaseId'],
                      'files': len(final), 'deltaFiles': len(delta['files']), 'published': False}))


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('action', choices=('init', 'freeze'))
    parser.add_argument('--candidate', default='candidate-1.0.3')
    parser.add_argument('--receipt', type=Path)
    args = parser.parse_args()
    output = candidate(args.candidate)
    if args.action == 'init':
        need(args.receipt is None, 'init does not consume an acceptance receipt.')
        init(output)
    else:
        need(args.receipt is not None, 'freeze requires --receipt.')
        freeze(output, args.receipt)


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