#!/usr/bin/env python3 """Fail-closed contract for purpose-classifier human review artifacts.""" from __future__ import annotations import csv import hashlib import json import math from collections import Counter, defaultdict from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Sequence from purpose_data import ( LABEL_SET, SLICES, DataError, SourceRecord, file_sha256, normalize_prompt, prompt_hash, validate_source_record, write_json, ) REVIEW_CSV_FIELDS = ( "promptHash", "source", "line", "prompt", "generatedPurpose", "generatedSecondary", "generatedMixed", "generatedDifficulty", "generatedSlice", "generatedLanguage", "reviewedPurpose", "reviewedSecondary", "reviewedDifficulty", "reviewedSlice", "reviewStatus", "reviewNotes", ) REVIEW_STATUSES = frozenset({"accept", "relabel", "reject"}) NONE_SENTINELS = frozenset({"", "none", "null"}) CHANGE_FIELDS = frozenset({"purpose", "secondary", "difficulty", "slice"}) DEFAULT_SAMPLE_SEED = 0xA11D17 DEFAULT_SAMPLE_FRACTION = 0.10 @dataclass(frozen=True) class ReviewProgress: records: int accepted: int relabeled: int rejected: int incomplete: int decisions: list[dict[str, Any]] @property def completed(self) -> int: return self.records - self.incomplete @dataclass(frozen=True) class HumanReviewResult: records: list[SourceRecord] summary: dict[str, Any] def _stable_rank(seed: int, record: SourceRecord) -> str: material = f"{seed}\0{prompt_hash(record.value['prompt'])}".encode("utf-8") return hashlib.sha256(material).hexdigest() def _review_stratum(record: SourceRecord) -> tuple[str, str, str]: language = record.value["lang"].split("-", 1)[0].casefold() return record.value["purpose"], record.value["slice"], language def stratified_review_sample( records: Sequence[SourceRecord], *, fraction: float, seed: int, ) -> list[SourceRecord]: """Choose exactly round(N*fraction), apportioned by purpose/slice/language.""" if not records: raise DataError("cannot sample an empty review population") if not 0.0 < fraction <= 1.0: raise DataError("review fraction must be in (0, 1]") target = round(len(records) * fraction) groups: dict[tuple[str, str, str], list[SourceRecord]] = defaultdict(list) for record in records: groups[_review_stratum(record)].append(record) allocations: dict[tuple[str, str, str], int] = {} remainders: list[tuple[float, str, tuple[str, str, str]]] = [] allocated = 0 for key in sorted(groups): quota = len(groups[key]) * target / len(records) base = math.floor(quota) allocations[key] = base allocated += base tie_break = hashlib.sha256(f"{seed}\0{key}".encode("utf-8")).hexdigest() remainders.append((quota - base, tie_break, key)) for _, _, key in sorted(remainders, reverse=True)[: target - allocated]: allocations[key] += 1 selected: list[SourceRecord] = [] for key in sorted(groups): ordered = sorted(groups[key], key=lambda record: _stable_rank(seed, record)) selected.extend(ordered[: allocations[key]]) return sorted(selected, key=lambda record: _stable_rank(seed + 1, record)) def _review_row( record: SourceRecord, source_formatter: Callable[[Path], str], ) -> dict[str, str]: value = record.value return { "promptHash": prompt_hash(value["prompt"]), "source": source_formatter(record.source), "line": str(record.line), "prompt": normalize_prompt(value["prompt"]), "generatedPurpose": value["purpose"], "generatedSecondary": value["secondary"] or "", "generatedMixed": str(value["mixed"]).lower(), "generatedDifficulty": str(value["difficulty"]), "generatedSlice": value["slice"], "generatedLanguage": value["lang"], "reviewedPurpose": "", "reviewedSecondary": "", "reviewedDifficulty": "", "reviewedSlice": "", "reviewStatus": "", "reviewNotes": "", } def write_review_csv( path: Path, records: Sequence[SourceRecord], *, source_formatter: Callable[[Path], str] = str, ) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=REVIEW_CSV_FIELDS) writer.writeheader() for record in records: writer.writerow(_review_row(record, source_formatter)) def _parse_review_changes( row: dict[str, str], record: SourceRecord, *, row_number: int, ) -> dict[str, Any]: value = record.value changes: dict[str, Any] = {} purpose = row["reviewedPurpose"].strip() if purpose: if purpose not in LABEL_SET: raise DataError(f"review CSV row {row_number}: invalid reviewed purpose") if purpose != value["purpose"]: changes["purpose"] = purpose secondary_text = row["reviewedSecondary"].strip() secondary = value["secondary"] if secondary_text: if secondary_text.casefold() in NONE_SENTINELS: secondary = None elif secondary_text in LABEL_SET: secondary = secondary_text else: raise DataError(f"review CSV row {row_number}: invalid reviewed secondary") if secondary != value["secondary"]: changes["secondary"] = secondary difficulty_text = row["reviewedDifficulty"].strip() if difficulty_text: try: difficulty = float(difficulty_text) except ValueError as exc: raise DataError( f"review CSV row {row_number}: invalid reviewed difficulty" ) from exc if not math.isfinite(difficulty) or not 0.0 <= difficulty <= 1.0: raise DataError( f"review CSV row {row_number}: reviewed difficulty must be in [0, 1]" ) if difficulty != float(value["difficulty"]): changes["difficulty"] = difficulty slice_name = row["reviewedSlice"].strip() if slice_name: if slice_name not in SLICES: raise DataError(f"review CSV row {row_number}: invalid reviewed slice") if slice_name != value["slice"]: changes["slice"] = slice_name candidate = dict(value) candidate.update(changes) candidate["mixed"] = candidate["secondary"] is not None validate_source_record(candidate, f"review CSV row {row_number}") return changes def inspect_review_csv( path: Path, expected_sample: Sequence[SourceRecord], *, source_formatter: Callable[[Path], str] = str, ) -> ReviewProgress: try: with path.open("r", encoding="utf-8", newline="") as handle: reader = csv.DictReader(handle) if tuple(reader.fieldnames or ()) != REVIEW_CSV_FIELDS: raise DataError( f"{path}: review CSV fields do not match the current schema; " "regenerate the blank artifact before reviewing" ) rows = list(reader) except (OSError, UnicodeError, csv.Error) as exc: raise DataError(f"{path}: cannot read review CSV: {exc}") from exc expected_by_hash = { prompt_hash(record.value["prompt"]): record for record in expected_sample } if len(expected_by_hash) != len(expected_sample): raise DataError("human-review sample contains duplicate prompt hashes") if len(rows) != len(expected_sample): raise DataError( f"{path}: expected {len(expected_sample)} review rows, found {len(rows)}" ) seen: set[str] = set() decisions: list[dict[str, Any]] = [] counts: Counter[str] = Counter() incomplete = 0 for row_number, row in enumerate(rows, 2): if None in row or any(row.get(field) is None for field in REVIEW_CSV_FIELDS): raise DataError(f"{path}:{row_number}: malformed review CSV column count") row_hash = row["promptHash"].strip() if row_hash in seen: raise DataError(f"{path}:{row_number}: duplicate promptHash {row_hash}") seen.add(row_hash) record = expected_by_hash.get(row_hash) if record is None: raise DataError( f"{path}:{row_number}: promptHash is not in the deterministic sample" ) expected = _review_row(record, source_formatter) for field in REVIEW_CSV_FIELDS[:10]: actual = row[field] if field == "generatedDifficulty": try: matches = float(actual) == float(expected[field]) except ValueError: matches = False else: matches = actual == expected[field] if not matches: raise DataError( f"{path}:{row_number}: generated field {field} no longer " "matches the curated corpus" ) status = row["reviewStatus"].strip().casefold() reviewer_values = [row[field].strip() for field in REVIEW_CSV_FIELDS[10:14]] notes = row["reviewNotes"].strip() if not status: if any(reviewer_values) or notes: raise DataError( f"{path}:{row_number}: reviewer fields require a reviewStatus" ) incomplete += 1 continue if status not in REVIEW_STATUSES: raise DataError(f"{path}:{row_number}: invalid reviewStatus {status!r}") changes = _parse_review_changes(row, record, row_number=row_number) if status == "accept": if any(reviewer_values): raise DataError( f"{path}:{row_number}: accept must leave reviewed fields blank" ) elif status == "reject": if any(reviewer_values): raise DataError( f"{path}:{row_number}: reject must leave reviewed fields blank" ) if not notes: raise DataError(f"{path}:{row_number}: reject requires reviewNotes") else: if not changes: raise DataError( f"{path}:{row_number}: relabel must change at least one field" ) if not notes: raise DataError(f"{path}:{row_number}: relabel requires reviewNotes") decision: dict[str, Any] = { "promptHash": row_hash, "status": status, } if changes: decision["changes"] = changes if notes: decision["notes"] = notes decisions.append(decision) counts[status] += 1 missing = sorted(set(expected_by_hash) - seen) if missing: raise DataError(f"{path}: deterministic sample rows are missing") return ReviewProgress( records=len(rows), accepted=counts["accept"], relabeled=counts["relabel"], rejected=counts["reject"], incomplete=incomplete, decisions=decisions, ) def finalize_human_review( csv_path: Path, output_path: Path, population: Sequence[SourceRecord], *, dataset_version: str, fraction: float, seed: int, source_formatter: Callable[[Path], str] = str, ) -> dict[str, Any]: sample = stratified_review_sample(population, fraction=fraction, seed=seed) progress = inspect_review_csv( csv_path, sample, source_formatter=source_formatter, ) if progress.incomplete: raise DataError( f"{csv_path}: human review is incomplete: {progress.completed}/" f"{progress.records} rows completed" ) summary = { "accepted": progress.accepted, "relabeled": progress.relabeled, "rejected": progress.rejected, } artifact = { "schemaVersion": 1, "datasetVersion": dataset_version, "status": "complete", "populationRecords": len(population), "sampleFraction": fraction, "sampleRecords": len(sample), "seed": seed, "sourceCSVSha256": file_sha256(csv_path), "summary": summary, "decisions": progress.decisions, } if output_path.exists(): try: existing = json.loads(output_path.read_text(encoding="utf-8")) except (OSError, UnicodeError, json.JSONDecodeError) as exc: raise DataError( f"{output_path}: cannot verify existing human-review ledger: {exc}" ) from exc if existing != artifact: raise DataError( f"{output_path}: refusing to replace a different human-review ledger" ) return artifact write_json(output_path, artifact) return artifact def apply_completed_human_review( path: Path, population: Sequence[SourceRecord], *, dataset_version: str, ) -> HumanReviewResult: try: artifact = json.loads(path.read_text(encoding="utf-8")) if artifact["schemaVersion"] != 1: raise DataError(f"{path}: unsupported human-review schema") if artifact["datasetVersion"] != dataset_version: raise DataError(f"{path}: human-review dataset version does not match") if artifact["status"] != "complete": raise DataError(f"{path}: human review is not complete") fraction = float(artifact["sampleFraction"]) seed = int(artifact["seed"]) decisions = artifact["decisions"] except DataError: raise except (OSError, UnicodeError, json.JSONDecodeError, KeyError, TypeError, ValueError) as exc: raise DataError(f"{path}: cannot read completed human review: {exc}") from exc if not isinstance(decisions, list): raise DataError(f"{path}: human-review decisions must be an array") if artifact.get("populationRecords") != len(population): raise DataError(f"{path}: reviewed population no longer matches the corpus") sample = stratified_review_sample(population, fraction=fraction, seed=seed) expected_hashes = {prompt_hash(record.value["prompt"]) for record in sample} if artifact.get("sampleRecords") != len(sample): raise DataError(f"{path}: reviewed sample size no longer matches the corpus") by_hash: dict[str, dict[str, Any]] = {} counts: Counter[str] = Counter() for index, decision in enumerate(decisions, 1): if not isinstance(decision, dict): raise DataError(f"{path}: human-review decision {index} must be an object") row_hash = decision.get("promptHash") status = decision.get("status") changes = decision.get("changes", {}) if not isinstance(row_hash, str) or not isinstance(status, str): raise DataError(f"{path}: invalid human-review decision {index}") if row_hash in by_hash: raise DataError(f"{path}: duplicate human-review promptHash") if row_hash not in expected_hashes or status not in REVIEW_STATUSES: raise DataError(f"{path}: invalid human-review decision {index}") if not isinstance(changes, dict) or not set(changes) <= CHANGE_FIELDS: raise DataError(f"{path}: invalid changes in human-review decision {index}") if status != "relabel" and changes: raise DataError(f"{path}: only relabel decisions may contain changes") if status == "relabel" and not changes: raise DataError(f"{path}: relabel decision {index} has no changes") if "purpose" in changes and ( not isinstance(changes["purpose"], str) or changes["purpose"] not in LABEL_SET ): raise DataError(f"{path}: invalid purpose in human-review decision {index}") if "secondary" in changes and ( changes["secondary"] is not None and ( not isinstance(changes["secondary"], str) or changes["secondary"] not in LABEL_SET ) ): raise DataError(f"{path}: invalid secondary in human-review decision {index}") if "slice" in changes and ( not isinstance(changes["slice"], str) or changes["slice"] not in SLICES ): raise DataError(f"{path}: invalid slice in human-review decision {index}") by_hash[row_hash] = decision counts[status] += 1 if set(by_hash) != expected_hashes: raise DataError(f"{path}: decisions do not exactly cover the deterministic sample") expected_summary = { "accepted": counts["accept"], "relabeled": counts["relabel"], "rejected": counts["reject"], } if artifact.get("summary") != expected_summary: raise DataError(f"{path}: human-review summary does not match its decisions") result: list[SourceRecord] = [] for record in population: decision = by_hash.get(prompt_hash(record.value["prompt"])) if decision is None or decision["status"] == "accept": result.append(record) continue if decision["status"] == "reject": continue value = dict(record.value) value.update(decision["changes"]) value["mixed"] = value["secondary"] is not None validate_source_record(value, f"{path}: {decision['promptHash']}") result.append(SourceRecord(value=value, source=record.source, line=record.line)) return HumanReviewResult( records=result, summary={ **expected_summary, "sampleRecords": len(sample), "retainedRecords": len(result), }, )