# SPDX-License-Identifier: MIT """Create or validate the two-role identity/independence plus domain assessment.""" from __future__ import annotations import argparse,json from datetime import datetime,timezone from hashlib import sha256 from pathlib import Path ROLES=("first_source_reader","independent_reproducer");HASH=set("0123456789abcdef");METHODS={"government_id_private_review","institutional_email_private_confirmation","trusted_existing_relationship"} COMPLETION_FIELDS=["schema_version","status","kit_sha256","binding_sha256","pairing_sha256","source_review_receipt_sha256","source_review","identity_assessment_required","independence_assessment_required","domain_assessment_required","production_database_used","submission_created","identity_verified","independence_verified","scientific_validation_claimed","discovery_claimed","completion_sha256"] ASSESSMENT_FIELDS=["schema_version","status","completion_sha256","pairing_sha256","role_assessments","domain_assessment","operator_attestation","personal_evidence_published","submission_created","scientific_validation_claimed","discovery_claimed"] ROLE_FIELDS=["role","identity_status","independence_status","method","identity_evidence_sha256","independence_evidence_sha256","method_selected_without_unpublished_answer","unpublished_kiwerb_answer_accessed","relationship_status","conflicts_disclosed","assessed_by","assessed_at_utc","reason"] DOMAIN_FIELDS=["status","reviewer_reference_sha256","expertise_evidence_sha256","sources_and_methods_reviewed","conflicts_disclosed","assessed_by","assessed_at_utc","reason"] def _hash(v):return isinstance(v,str) and len(v)==64 and not(set(v)-HASH) def _time(v): try:d=datetime.fromisoformat(v.replace("Z","+00:00")) except (AttributeError,ValueError):return False return d.tzinfo is not None and d<=datetime.now(timezone.utc) def _completion(path): v=json.loads(Path(path).read_text(encoding="utf-8")) if list(v)!=COMPLETION_FIELDS or v["schema_version"]!="openlab-clark-review-kit-completion-v1" or v["status"]!="source_review_completed_for_private_identity_independence_and_domain_assessment_not_validation":raise ValueError("completion identity or fields drift") expected=sha256(json.dumps({k:v[k] for k in COMPLETION_FIELDS[:-1]},ensure_ascii=False,separators=(",",":"),sort_keys=True).encode()).hexdigest() if v["completion_sha256"]!=expected:raise ValueError("completion seal mismatch") if not all(v[x] is True for x in ["identity_assessment_required","independence_assessment_required","domain_assessment_required"]):raise ValueError("completion assessment requirements drift") for f in ["production_database_used","submission_created","identity_verified","independence_verified","scientific_validation_claimed","discovery_claimed"]: if v[f] is not False:raise ValueError("completion promotion refused") return v def template(completion_path): c=_completion(completion_path);roles=[] for role in ROLES:roles.append({"role":role,"identity_status":"not_assessed","independence_status":"not_assessed","method":None,"identity_evidence_sha256":None,"independence_evidence_sha256":None,"method_selected_without_unpublished_answer":None,"unpublished_kiwerb_answer_accessed":None,"relationship_status":None,"conflicts_disclosed":None,"assessed_by":None,"assessed_at_utc":None,"reason":None}) body={"schema_version":"openlab-clark-assessment-bundle-v1","status":"blank_private_assessment_bundle","completion_sha256":c["completion_sha256"],"pairing_sha256":c["pairing_sha256"],"role_assessments":roles,"domain_assessment":{"status":"not_assessed","reviewer_reference_sha256":None,"expertise_evidence_sha256":None,"sources_and_methods_reviewed":None,"conflicts_disclosed":None,"assessed_by":None,"assessed_at_utc":None,"reason":None},"operator_attestation":False,"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 assess(completion_path,assessment_path): c=_completion(completion_path);v=json.loads(Path(assessment_path).read_text(encoding="utf-8")) if list(v)!=ASSESSMENT_FIELDS or v["schema_version"]!="openlab-clark-assessment-bundle-v1" or v["status"]!="completed_private_assessment_bundle_not_scientific_validation" or v["completion_sha256"]!=c["completion_sha256"] or v["pairing_sha256"]!=c["pairing_sha256"]:raise ValueError("assessment identity or completion binding drift") if v["operator_attestation"] 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:raise ValueError("assessment boundary drift") roles=v["role_assessments"] if not isinstance(roles,list) or len(roles)!=2 or [x.get("role") for x in roles]!=list(ROLES):raise ValueError("two ordered role assessments required") verified=True;operators=[] for x in roles: if list(x)!=ROLE_FIELDS or x["identity_status"] not in {"verified","unable_to_verify"} or x["independence_status"] not in {"verified","not_verified"} or x["method"] not in METHODS or not _hash(x["identity_evidence_sha256"]) or not _hash(x["independence_evidence_sha256"]):raise ValueError("role assessment fields invalid") if not _time(x["assessed_at_utc"]) or not isinstance(x["assessed_by"],str) or not 3<=len(x["assessed_by"].strip())<=100 or not isinstance(x["reason"],str) or not 20<=len(x["reason"].strip())<=800:raise ValueError("role assessment human record invalid") clean=x["identity_status"]=="verified" and x["independence_status"]=="verified" and x["method_selected_without_unpublished_answer"] is True and x["unpublished_kiwerb_answer_accessed"] is False and x["relationship_status"]=="none_known" and x["conflicts_disclosed"] is True if x["independence_status"]=="verified" and not clean:raise ValueError("independence cannot be verified with unresolved declarations") verified&=clean;operators.append(x["assessed_by"].strip().casefold()) d=v["domain_assessment"] if list(d)!=DOMAIN_FIELDS or d["status"] not in {"suitable_for_curator_decision","needs_changes","reject"} or not _hash(d["reviewer_reference_sha256"]) or not _hash(d["expertise_evidence_sha256"]):raise ValueError("domain assessment fields invalid") if d["sources_and_methods_reviewed"] is not True or d["conflicts_disclosed"] is not True or not _time(d["assessed_at_utc"]) or not isinstance(d["assessed_by"],str) or not 3<=len(d["assessed_by"].strip())<=100 or not isinstance(d["reason"],str) or not 20<=len(d["reason"].strip())<=800:raise ValueError("domain assessment human record invalid") if d["assessed_by"].strip().casefold() in operators:raise ValueError("domain reviewer must be separately designated") body={"schema_version":"openlab-clark-assessment-receipt-v1","status":"assessments_recorded_for_private_curator_decision_not_scientific_validation" if verified and d["status"]=="suitable_for_curator_decision" else "assessment_blockers_remain","completion_sha256":c["completion_sha256"],"assessment_file_sha256":sha256(Path(assessment_path).read_bytes()).hexdigest(),"role_identity_verified":{x["role"]:x["identity_status"]=="verified" for x in roles},"role_independence_verified":{x["role"]:x["independence_status"]=="verified" for x in roles},"domain_assessment_status":d["status"],"domain_reviewer_separately_designated":True,"personal_evidence_published":False,"submission_created":False,"scientific_validation_claimed":False,"discovery_claimed":False,"curator_decision_required":True} 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("completion");g=p.add_mutually_exclusive_group(required=True);g.add_argument("--template-out");g.add_argument("--assessment");p.add_argument("--out");a=p.parse_args() if a.template_out:Path(a.template_out).write_text(json.dumps(template(a.completion),ensure_ascii=False,indent=2)+"\n",encoding="utf-8") else: if not a.out:raise ValueError("--out required with --assessment") Path(a.out).write_text(json.dumps(assess(a.completion,a.assessment),ensure_ascii=False,indent=2)+"\n",encoding="utf-8") if __name__=="__main__":main()