# SPDX-License-Identifier: MIT """Bind two Clark admission receipts to separate manifests for curator pairing.""" from __future__ import annotations import argparse,json from hashlib import sha256 from pathlib import Path if __package__:from .prepare_clark_handoff import validate_manifest else:from prepare_clark_handoff import validate_manifest REQUEST="ab2a8887110cb058d496e0afe1563fe7e42ee260a63608efa2de03a62e58a534";HASH=set("0123456789abcdef");ROLES=("first_source_reader","independent_reproducer") FIELDS=["schema_version","status","request_sha256","intake_sha256","dispatch_sha256","role","worksheet_sha256","handoff_manifest_sha256","semantic_recheck_passed","raw_content_copied_to_receipt","production_database_used","submission_created","identity_verified","independence_verified","scientific_validation_claimed","manual_private_pairing_required","admission_sha256"] def _hash(v):return isinstance(v,str) and len(v)==64 and not(set(v)-HASH) def _admission(v): if list(v)!=FIELDS or v["schema_version"]!="openlab-clark-intake-admission-receipt-v1" or v["status"]!="admitted_for_private_role_pairing_not_submission" or v["request_sha256"]!=REQUEST:raise ValueError("admission identity or fields drift") for f in ["intake_sha256","dispatch_sha256","worksheet_sha256","handoff_manifest_sha256","admission_sha256"]: if not _hash(v[f]):raise ValueError("admission hash invalid") expected=sha256(json.dumps({k:v[k] for k in FIELDS[:-1]},ensure_ascii=False,separators=(",",":"),sort_keys=True).encode()).hexdigest() if v["admission_sha256"]!=expected:raise ValueError("admission seal mismatch") if v["role"] not in ROLES or v["semantic_recheck_passed"] is not True or v["manual_private_pairing_required"] is not True:raise ValueError("admission pairing boundary drift") for f in ["raw_content_copied_to_receipt","production_database_used","submission_created","identity_verified","independence_verified","scientific_validation_claimed"]: if v[f] is not False:raise ValueError("admission promotion refused") return v def pair(admission_paths,manifest_paths): if len(admission_paths)!=2 or len(manifest_paths)!=2:raise ValueError("exactly two admissions and two manifests required") admissions=[];manifests=[] for ap,mp in zip(map(Path,admission_paths),map(Path,manifest_paths)): a=_admission(json.loads(ap.read_text(encoding="utf-8")));raw=mp.read_bytes();m=validate_manifest(json.loads(raw.decode("utf-8"))) if sha256(raw).hexdigest()!=a["handoff_manifest_sha256"] or m["reviewer_role"]!=a["role"] or m["files"][0]["sha256"]!=a["worksheet_sha256"]:raise ValueError("admission to manifest binding drift") admissions.append(a);manifests.append(m) if {a["role"] for a in admissions}!=set(ROLES):raise ValueError("one admission per required role") for field,label in [("admission_sha256","admissions"),("intake_sha256","intakes"),("dispatch_sha256","dispatches"),("worksheet_sha256","worksheets")]: if len({a[field] for a in admissions})!=2:raise ValueError(f"separate {label} required") by_role={m["reviewer_role"]:m for m in manifests};first=by_role[ROLES[0]];second=by_role[ROLES[1]] for index,label in [(1,"validation drafts"),(2,"reviewer self-checks")]: if first["files"][index]["sha256"]==second["files"][index]["sha256"]:raise ValueError(f"separate {label} required") body={"schema_version":"openlab-clark-admission-pairing-v1","status":"paired_for_private_curator_conflict_review_not_submission","request_sha256":REQUEST,"roles":list(ROLES),"admission_sha256s":{a["role"]:a["admission_sha256"] for a in admissions},"worksheet_sha256s":{a["role"]:a["worksheet_sha256"] for a in admissions},"required_disclosures":sorted(set(first["required_disclosures"]+second["required_disclosures"])),"semantic_rechecks_passed":True,"distinct_role_artifacts":True,"manual_private_curator_review_required":True,"production_database_used":False,"submission_created":False,"identity_verified":False,"independence_verified":False,"scientific_validation_claimed":False,"discovery_claimed":False} body["pairing_sha256"]=sha256(json.dumps(body,ensure_ascii=False,separators=(",",":"),sort_keys=True).encode()).hexdigest();return body def main(): p=argparse.ArgumentParser();p.add_argument("admission",nargs=2);p.add_argument("--manifests",nargs=2,required=True);p.add_argument("--out",required=True);a=p.parse_args();r=pair(a.admission,a.manifests);Path(a.out).write_text(json.dumps(r,ensure_ascii=False,indent=2)+"\n",encoding="utf-8");print(json.dumps(r,ensure_ascii=False)) if __name__=="__main__":main()