"""Import complete, text-bound editorial decisions into the alignment manifest.""" from __future__ import annotations from copy import deepcopy import hashlib import json from pathlib import Path from src.output.alignment import candidate_hashes, digest, validate_repair def indexed(rows): result = {row['target']: row for row in rows} if len(result) != len(rows): raise ValueError('Duplicate review targets') return result def import_bundle(review_path, bundle_path, out): review_path, bundle_path, out = Path(review_path), Path(bundle_path), Path(out).resolve() if out in (review_path.resolve(), bundle_path.resolve()): raise ValueError('Imported review must not overwrite its evidence inputs') original = review_path.read_bytes() evidence = bundle_path.read_bytes() bundle = json.loads(evidence) if hashlib.sha256(original).hexdigest() != bundle['input_review_sha256']: raise ValueError('Repair bundle belongs to a different input review') review = json.loads(original) result = merge_repairs(review, bundle['packets'], bundle['decisions'], bundle['verification'], hashlib.sha256(evidence).hexdigest()) result['repair_source_files'] = bundle['source_files'] result['usage'] = {'initial_alignment': review['usage'], 'repair_round': bundle['usage']} out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n') return len(result['target_repairs']) def merge_repairs(review, packets, decisions, verification, evidence_sha256): """Require a passing verification of each exact final text before import.""" packets, decisions, verification = map(indexed, (packets, decisions, verification)) if packets.keys() != decisions.keys() or packets.keys() != verification.keys(): raise ValueError('Incomplete or unexpected repair review targets') result = deepcopy(review) repairs = result.setdefault('target_repairs', {}) for target, packet in packets.items(): decision, check = decisions[target], verification[target] if check['status'] != 'pass' or check['text_sha256'] != digest(decision['text']): raise ValueError(f'Repair lacks passing verification of final text: {target}') if decision.get('boundary_issue') and not check.get('boundary_resolution'): raise ValueError(f'Unresolved source boundary: {target}') selected = decision['selected_sources'] choices = {item['source']: item['text'] for item in packet['candidates']} if selected and (len(set(selected)) != len(selected) or set(selected) - choices.keys() or ''.join(choices[source] for source in selected) != decision['text']): raise ValueError(f'Selected text does not match candidate sources: {target}') repair = {'text': decision['text'], 'expected_candidates': candidate_hashes(packet['candidates']), 'selected_sources': selected, 'reason': decision['reason'], 'evidence_sha256': evidence_sha256, 'verified_by': check['reviewer'], 'verification': check, 'original_sources': packet['source_spans']} validate_repair(target, repair, packet['candidates']) repairs[target] = repair for source in packet.get('held_sources', []): prior = result['records'][source] if prior['mapped_targets'] != [target] or prior['decision'] not in ('mixed', 'unclear'): raise ValueError(f'Held record mapping changed: {source}') prior['pre_repair_review'] = deepcopy(prior) prior.update(decision='mapped', reviewer=check['reviewer'], reason=f'Original ambiguous candidate retained in audit; verified target repair supplies {target}.') return result