"""Build the website's deterministic, allowlisted source ZIP and text browser. Run from a complete monorepo checkout. The source archive deliberately excludes private runtime archives and separately licensed reference corpora. """ import argparse import hashlib import importlib.util import json from pathlib import Path import shutil import tempfile import zipfile SITE = Path(__file__).resolve().parents[1] LIMIT = 25 * 1024 * 1024 ARCHIVE = 'nichijoyaku-source.zip' PREFIX = 'nichijoyaku-source' BROWSER_SUFFIXES = {'.py': 'python', '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml', '.toml': 'toml', '.md': 'markdown'} def digest(data): return hashlib.sha256(data).hexdigest() def browser_language(name): path = Path(name) if name in ('README.md', 'CONTRIBUTING.md', 'LICENSE'): return 'markdown' if path.suffix == '.md' else 'text' if path.parts[0] not in ('software', 'tools'): return None if path.name == 'LICENSE': return 'text' return BROWSER_SUFFIXES.get(path.suffix) def archive(source, destination): """Freeze an already allowlisted directory; no unreviewed tree is read here.""" destination.mkdir(parents=True, exist_ok=True) code = destination / 'code' if code.exists(): shutil.rmtree(code) files = {} browser = [] with zipfile.ZipFile(destination / ARCHIVE, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as bundle: for path in sorted(source.rglob('*')): if path.is_symlink(): raise ValueError('Source package cannot contain symlinks') if not path.is_file(): continue name = path.relative_to(source).as_posix() data = path.read_bytes() files[name] = {'sha256': digest(data), 'bytes': len(data)} info = zipfile.ZipInfo(f'{PREFIX}/{name}', date_time=(2026, 10, 5, 0, 0, 0)) info.create_system = 3 info.external_attr = 0o100644 << 16 info.compress_type = zipfile.ZIP_DEFLATED bundle.writestr(info, data, compresslevel=9) language = browser_language(name) if language: data.decode('utf-8') # Fail rather than silently corrupt binary text. if len(data) >= LIMIT: raise ValueError('Browser source exceeds hosting file limit') raw = code / (name + '.txt') raw.parent.mkdir(parents=True, exist_ok=True) raw.write_bytes(data) browser.append({'file': name, 'url': '/downloads/source-v1/code/' + name + '.txt', 'language': language, **files[name]}) data = (destination / ARCHIVE).read_bytes() if len(data) >= LIMIT: raise ValueError('Source archive exceeds hosting file limit') record = {'schema_version': 1, 'archive': {'file': ARCHIVE, 'url': '/downloads/source-v1/' + ARCHIVE, 'sha256': digest(data), 'bytes': len(data), 'file_count': len(files)}, 'files': files, 'browser_files': browser, 'scope': 'Reviewed source allowlist, edition 1, and retained public provenance; no private prompts or reference Bible corpora.'} (destination / 'index.json').write_text(json.dumps(record, ensure_ascii=False, indent=2) + '\n') return record def build(destination): spec = importlib.util.spec_from_file_location('source_package', SITE / 'scripts/package-source.py') package = importlib.util.module_from_spec(spec) spec.loader.exec_module(package) with tempfile.TemporaryDirectory() as temporary: source = Path(temporary) / 'source' package.package(source) return archive(source, destination) if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--destination', type=Path, default=SITE / 'public/downloads/source-v1') record = build(parser.parse_args().destination) print(json.dumps(record['archive'])) print('Browsable files:', len(record['browser_files']))