"""Export hash-matched historical outputs and an allowlisted input inventory. Never export user prompts: they embed separately licensed reference translations. System prompts are project-authored instructions, deduplicated by SHA-256. """ import argparse from collections import Counter import hashlib import json from pathlib import Path def digest(raw): return hashlib.sha256(raw).hexdigest() def collect(source, audit, destination, recovered=None): destination.mkdir(parents=True, exist_ok=True) records, systems, chapters = [], {}, [] source_names = Counter() with_prompts = 0 unavailable = [] for name, expected in sorted(audit['input_files'].items()): path = source / name origin = 'current source output' if (not path.is_file() or digest(path.read_bytes()) != expected) and recovered is not None: candidate = recovered / name if candidate.is_file() and digest(candidate.read_bytes()) == expected: path = candidate origin = 'recovered historical source output; exact audit hash match' if not path.is_file() or digest(path.read_bytes()) != expected: unavailable.append({'file': name, 'expected_sha256': expected, 'status': 'changed' if path.is_file() else 'missing'}) continue chapter = json.loads(path.read_bytes()) chapters.append({'file': name, 'sha256': expected, 'origin': origin}) for verse, saved in sorted(chapter['verses'].items(), key=lambda item: int(item[0])): row = {'reference': f"{chapter['book']}:{chapter['chapter']}:{verse}", 'source_file': name, 'output_kind': 'saved parsed translation, not raw model response'} for field in ('text', 'text_with_furigana', 'model', 'timestamp', 'prompt_version', 'sources_used'): row[field] = saved.get(field) provenance = saved.get('provenance') if provenance: system = provenance.get('system_prompt', '') user = provenance.get('user_prompt', '') systems[digest(system.encode())] = system row['input'] = {'system_prompt_sha256': digest(system.encode()), 'user_prompt_sha256': digest(user.encode()), 'user_prompt_characters': len(user), 'user_prompt_status': 'retained privately; embeds separately licensed Bible text', 'source_text_sha256': {k: digest(v.encode()) for k, v in provenance.get('source_texts', {}).items()}, 'reference_labels_in_prompt': [label for label in ('口語訳 (1955)', 'KJV', 'ASV', '明治訳', '大正訳') if label in user]} for field in ('started_at', 'completed_at', 'attempt_number', 'style_config_hash', 'glossary_hash'): row['input'][field] = provenance.get(field) with_prompts += bool(system and user) source_names.update(row['input']['reference_labels_in_prompt']) else: row['input'] = None records.append(row) for name, value in [('outputs.jsonl', ''.join(json.dumps(r, ensure_ascii=False) + '\n' for r in records)), ('system-prompts.json', json.dumps(systems, ensure_ascii=False, indent=2)+'\n')]: (destination/name).write_text(value) report = {'schema': 1, 'edition': 'nichijo-v1', 'chapters': len(chapters), 'source_records': len(records), 'records_with_retained_prompts': with_prompts, 'records_without_retained_prompts': len(records)-with_prompts, 'reference_labels_in_retained_prompts': dict(source_names), 'system_prompts': len(systems), 'limits': ['Only chapter files matching the immutable release audit are included.', 'Saved parsed output is not the complete raw model response.', 'Input records are absent for chunk translations; absence is not proof that reference text was excluded.', 'Historical prompts include reference translations; a no-reference-input claim is contradicted by these records.', 'Model and source labels are saved local metadata, not independently verified provider records.', 'Exact user prompts remain private because they embed separately licensed Bible text. Hashes permit later verification.'], 'source_files': chapters, 'unavailable_files': unavailable, 'files': {p.name: digest(p.read_bytes()) for p in (destination/'outputs.jsonl', destination/'system-prompts.json')}} (destination/'index.json').write_text(json.dumps(report, ensure_ascii=False, indent=2)+'\n') return report if __name__ == '__main__': parser=argparse.ArgumentParser() parser.add_argument('--source-output', type=Path, required=True) parser.add_argument('--audit', type=Path, required=True) parser.add_argument('--destination', type=Path, required=True) parser.add_argument('--recovered-output', type=Path) args=parser.parse_args() report=collect(args.source_output,json.loads(args.audit.read_text()),args.destination,args.recovered_output) print(json.dumps({k:v for k,v in report.items() if k not in ('source_files','files')},ensure_ascii=False,indent=2))