# SPDX-License-Identifier: MIT """Create or validate a Clark curator decision after the assessment bundle.""" from __future__ import annotations import argparse,json from datetime import date from hashlib import sha256 from pathlib import Path HASH=set("0123456789abcdef");DECISIONS={"accept_for_scientific_review","needs_changes","reject"} ASSESS_FIELDS=["schema_version","status","completion_sha256","assessment_file_sha256","role_identity_verified","role_independence_verified","domain_assessment_status","domain_reviewer_separately_designated","personal_evidence_published","submission_created","scientific_validation_claimed","discovery_claimed","curator_decision_required","receipt_sha256"] DECISION_FIELDS=["schema_version","status","assessment_receipt_sha256","decision","curator_reference_sha256","decision_evidence_sha256","decided_by","decided_on","curator_separate_from_assessed_roles_and_domain_reviewer","scope_reviewed","conflicts_reviewed","source_rights_reviewed","scientific_claims_reviewed","reason","personal_evidence_published","submission_created","scientific_validation_claimed","discovery_claimed"] def _hash(v):return isinstance(v,str) and len(v)==64 and not(set(v)-HASH) def _assessment(path): raw=Path(path).read_bytes();v=json.loads(raw.decode("utf-8")) if list(v)!=ASSESS_FIELDS or v["schema_version"]!="openlab-clark-assessment-receipt-v1" or v["status"] not in {"assessments_recorded_for_private_curator_decision_not_scientific_validation","assessment_blockers_remain"}:raise ValueError("assessment receipt identity or fields drift") for f in ["completion_sha256","assessment_file_sha256","receipt_sha256"]: if not _hash(v[f]):raise ValueError("assessment receipt hash invalid") expected=sha256(json.dumps({k:v[k] for k in ASSESS_FIELDS[:-1]},ensure_ascii=False,separators=(",",":"),sort_keys=True).encode()).hexdigest() if v["receipt_sha256"]!=expected:raise ValueError("assessment receipt seal mismatch") if set(v["role_identity_verified"])!={"first_source_reader","independent_reproducer"} or set(v["role_independence_verified"])!={"first_source_reader","independent_reproducer"}:raise ValueError("assessment roles drift") if v["domain_reviewer_separately_designated"] is not True or v["personal_evidence_published"] is not False or v["submission_created"] is not False or v["scientific_validation_claimed"] is not False or v["discovery_claimed"] is not False or v["curator_decision_required"] is not True:raise ValueError("assessment receipt boundary drift") return raw,v def template(assessment_path): _,a=_assessment(assessment_path);body={"schema_version":"openlab-clark-curator-decision-v1","status":"blank_private_curator_decision_not_recorded","assessment_receipt_sha256":a["receipt_sha256"],"allowed_decisions":sorted(DECISIONS) if a["status"].startswith("assessments_recorded") else ["needs_changes","reject"],"decision":None,"curator_reference_sha256":None,"decision_evidence_sha256":None,"decided_by":None,"decided_on":None,"curator_separate_from_assessed_roles_and_domain_reviewer":None,"scope_reviewed":None,"conflicts_reviewed":None,"source_rights_reviewed":None,"scientific_claims_reviewed":None,"reason":None,"personal_evidence_published":False,"submission_created":False,"scientific_validation_claimed":False,"discovery_claimed":False};body["template_sha256"]=sha256(json.dumps(body,ensure_ascii=False,separators=(",",":"),sort_keys=True).encode()).hexdigest();return body def decide(assessment_path,decision_path): raw,a=_assessment(assessment_path);v=json.loads(Path(decision_path).read_text(encoding="utf-8")) if list(v)!=DECISION_FIELDS or v["schema_version"]!="openlab-clark-curator-decision-v1" or v["status"]!="completed_private_curator_decision_not_scientific_validation" or v["assessment_receipt_sha256"]!=a["receipt_sha256"]:raise ValueError("decision identity or assessment binding drift") if v["decision"] not in DECISIONS or not _hash(v["curator_reference_sha256"]) or not _hash(v["decision_evidence_sha256"]):raise ValueError("decision or evidence hash invalid") if v["decision"]=="accept_for_scientific_review" and a["status"]!="assessments_recorded_for_private_curator_decision_not_scientific_validation":raise ValueError("assessment blockers prohibit scientific review acceptance") try:observed=date.fromisoformat(v["decided_on"]) except (TypeError,ValueError):raise ValueError("valid decision date required") from None if observed.isoformat()!=v["decided_on"] or not isinstance(v["decided_by"],str) or not 3<=len(v["decided_by"].strip())<=100 or not isinstance(v["reason"],str) or not 20<=len(v["reason"].strip())<=1200:raise ValueError("decision human record invalid") for f in ["curator_separate_from_assessed_roles_and_domain_reviewer","scope_reviewed","conflicts_reviewed","source_rights_reviewed","scientific_claims_reviewed"]: if v[f] is not True:raise ValueError("all curator attestations required") for f in ["personal_evidence_published","submission_created","scientific_validation_claimed","discovery_claimed"]: if v[f] is not False:raise ValueError("curator decision boundary drift") body={"schema_version":"openlab-clark-curator-decision-receipt-v1","status":"accepted_for_scientific_review_not_validated" if v["decision"]=="accept_for_scientific_review" else "curator_decision_recorded_with_no_scientific_review_acceptance","assessment_receipt_sha256":a["receipt_sha256"],"assessment_file_sha256":sha256(raw).hexdigest(),"decision_file_sha256":sha256(Path(decision_path).read_bytes()).hexdigest(),"decision":v["decision"],"decided_on":v["decided_on"],"curator_separation_attested":True,"scope_conflicts_rights_and_claims_reviewed":True,"personal_evidence_published":False,"submission_created":False,"scientific_review_authorized":v["decision"]=="accept_for_scientific_review","scientific_validation_claimed":False,"discovery_claimed":False,"public_report_authorized":False} body["receipt_sha256"]=sha256(json.dumps(body,ensure_ascii=False,separators=(",",":"),sort_keys=True).encode()).hexdigest();return body def main(): p=argparse.ArgumentParser();p.add_argument("assessment");g=p.add_mutually_exclusive_group(required=True);g.add_argument("--template-out");g.add_argument("--decision");p.add_argument("--out");a=p.parse_args() if a.template_out:Path(a.template_out).write_text(json.dumps(template(a.assessment),ensure_ascii=False,indent=2)+"\n",encoding="utf-8") else: if not a.out:raise ValueError("--out required with --decision") Path(a.out).write_text(json.dumps(decide(a.assessment,a.decision),ensure_ascii=False,indent=2)+"\n",encoding="utf-8") if __name__=="__main__":main()