"""Manifest-driven, fail-closed Phase 4 audit for OID v2 evidence."""
from __future__ import annotations
import argparse, hashlib, json
from pathlib import Path
from typing import Any, Mapping
import numpy as np
from .artifacts import atomic_write_json
from .manifest import sha256_file

SCENES=("karman_re100","illusion_1.0L")
OBSERVABLE_CHANNELS={"delta_forces":6,"delta_sensors":6}
REALIZATIONS=("r1","r2","r3")
EXPECTED_MASK_COUNTS={"karman_re100":{"fluid":5025,"solid":95},"illusion_1.0L":{"fluid":5024,"solid":96}}
MAX_CONDITION_NUMBER=1e12
MAX_DEFECT=1e-8
NEGATIVE_CONTROL_MARGIN=0.01
RCOND=1e-12
CODE_PATHS=(
 "src/OID_analysis/scripts/run_phase3_5_formal.py",
 "src/OID_analysis/v2/oid.py",
 "src/OID_analysis/v2/analysis.py",
)
OBSERVABLE_UNITS={
 "delta_forces":["LBM force units"]*6,
 "delta_sensors":["velocity normalized by U0"]*6,
}

def _gate(status,summary,**evidence):
 if status not in {"pass","conditional","fail"}: raise ValueError(status)
 return {"status":status,"summary":summary,"evidence":evidence}
def _json(path):
 value=json.loads(path.read_text(encoding="utf-8"))
 if not isinstance(value,dict): raise ValueError(f"JSON object required: {path}")
 return value
def _finite(value):
 if isinstance(value,Mapping): return all(_finite(x) for x in value.values())
 if isinstance(value,list): return all(_finite(x) for x in value)
 return not isinstance(value,float) or bool(np.isfinite(value))

def _canonical(root,active):
 records,errors={},[]
 from .oid import _hash
 for scene in SCENES:
  listed=active.get("scenes",{}).get(scene,{}).get("realizations",[])
  if [x.get("realization_id") for x in listed if isinstance(x,Mapping)]!=list(REALIZATIONS): errors.append(f"{scene}: ACTIVE_EVIDENCE realization registry is not r1,r2,r3")
  for rid in REALIZATIONS:
   directory=root/scene/rid; meta_path=directory/"paired_product.json"; product_path=directory/"paired_product.npz"; key=f"{scene}/{rid}"
   if not meta_path.is_file(): records[key]={"status":"missing"}; errors.append(f"{key}: metadata missing"); continue
   try:
    meta=_json(meta_path); arrays=meta.get("arrays",{}); source=meta.get("canonical_pair_sources",{}); timeline=meta.get("timeline_contract",{}); metric=meta.get("metric_metadata",{})
    required={"dq_ctl","delta_forces","delta_sensors","sample_ids","lattice_steps","times","fluid_mask","cell_metric","state_weights"}
    source_ok=len(source)==4 and all(isinstance(x,Mapping) and x.get("content_hash_validated") is True and x.get("computed_content_sha256")==x.get("declared_content_sha256") and len(str(x.get("file_sha256","")))==64 for x in source.values())
    timeline_ok=meta.get("pairing")=="exact field-level IDs, lattice_steps, and times" and timeline.get("pairing")=="controlled and baseline arrays exactly equal" and all(timeline.get(f"{x}_sha256")==arrays.get(x) for x in ("lattice_steps","sample_ids","times"))
    contract_ok=meta.get("schema_id")=="oid-canonical-paired/v1" and meta.get("realization_id")==rid and required<=set(arrays) and meta.get("field_shape")==[80,64] and meta.get("fluid_mask_sha256")==arrays.get("fluid_mask") and meta.get("cell_metric_sha256")==arrays.get("cell_metric") and meta.get("layout_contract",{}).get("component_order")==["ux","uy"] and metric.get("schema_id")=="legacy-cell-metric/v1" and metric.get("sampled_cell_area")==64.0 and metric.get("quadrature")=="uniform lattice-cell area, zero on actual solid mask cells"
    if not product_path.is_file():
     records[key]={"status":"missing","metadata_sha256":sha256_file(meta_path)}; errors.append(f"{key}: product missing"); continue
    with np.load(product_path,allow_pickle=False) as archive:
     missing=required.difference(archive.files)
     if missing: raise ValueError(f"missing arrays {sorted(missing)}")
     if any(_hash(archive[x])!=arrays[x] for x in required): raise ValueError("array hash mismatch")
     mask=np.asarray(archive["fluid_mask"],bool); weights=np.asarray(archive["cell_metric"]); counts={"fluid":int(mask.sum()),"solid":int((~mask).sum())}
     if counts!=EXPECTED_MASK_COUNTS[scene]: raise ValueError(f"mask counts {counts}")
     if not(np.all(weights[mask]==64.0) and np.all(weights[~mask]==0.0)): raise ValueError("metric/mask mismatch")
    if not source_ok: errors.append(f"{key}: source hashes")
    if not timeline_ok: errors.append(f"{key}: timeline")
    if not contract_ok: errors.append(f"{key}: mask/metric contract")
    records[key]={"status":"verified","metadata_sha256":sha256_file(meta_path),"product_sha256":sha256_file(product_path),"mask_counts":counts,"source_hashes_valid":source_ok,"timeline_valid":timeline_ok,"mask_metric_contract_valid":contract_ok}
   except Exception as exc: errors.append(f"{key}: {exc}"); records[key]={"status":"invalid","error":str(exc)}
 if errors:
  incomplete=all("missing" in x for x in errors)
  return _gate("conditional" if incomplete else "fail","canonical evidence incomplete" if incomplete else "canonical contract violation",errors=errors),records
 return _gate("pass","all six canonical products satisfy source, timeline, mask, metric, and array-hash contracts"),records

def _analysis(directory,canonical_root,scene,observable):
 report_path=directory/"phase_3_5_report.json"; model_path=directory/"phase_3_5_models.npz"; verify_path=directory/"verification.json"
 claim=f"cross-realization {observable} association for {scene}"
 if not any(x.exists() for x in (report_path,model_path,verify_path)):
  return {**_gate("conditional","formal report has not been published",path=str(directory)),
          "integrity_status":"incomplete","scientific_claim_status":"not_evaluated",
          "failed_folds":[],"reconstruction_warning":None,
          "permitted_claim":None,"prohibited_claim":claim}
 missing=[x.name for x in (report_path,model_path,verify_path) if not x.is_file()]
 if missing:
  return {**_gate("fail","partially published analysis artifact set",missing=missing),
          "integrity_status":"fail","scientific_claim_status":"not_evaluated",
          "failed_folds":[],"reconstruction_warning":None,
          "permitted_claim":None,"prohibited_claim":claim}
 errors=[]; failed_folds=[]; reconstruction_folds=[]
 try:
  from .oid import _hash
  report,verification=_json(report_path),_json(verify_path); report_hash,model_hash=sha256_file(report_path),sha256_file(model_path)
  if report.get("schema_id")!="oid-phase3-5-loro/v2": errors.append("wrong report schema")
  if verification.get("schema_id")!="oid-phase3-5-verification/v1" or verification.get("report_json_verified") is not True or verification.get("model_npz_verified") is not True: errors.append("verification contract")
  if verification.get("report_sha256")!=report_hash or verification.get("model_sha256")!=model_hash: errors.append("verification byte hashes")
  artifact=report.get("model_artifact",{})
  if artifact.get("path")!=model_path.name or artifact.get("sha256")!=model_hash or artifact.get("verified_after_write") is not True: errors.append("model binding")
  with np.load(model_path,allow_pickle=False) as archive: hashes={x:_hash(archive[x]) for x in archive.files}
  if hashes!=artifact.get("array_sha256") or hashes!=verification.get("model_array_sha256"): errors.append("model array hashes")
  inputs=report.get("input_products",[])
  if len(inputs)!=3: errors.append("input count")
  for rid in REALIZATIONS:
   matches=[x for x in inputs if Path(str(x.get("directory",""))).name==rid]
   if len(matches)!=1: errors.append(f"input {rid}"); continue
   for filename,field in (("paired_product.json","metadata_sha256"),("paired_product.npz","product_sha256")):
    path=canonical_root/scene/rid/filename
    if not path.is_file() or matches[0].get(field)!=sha256_file(path): errors.append(f"input hash {rid}/{filename}")
  metrics=report.get("metrics",{}); metric=report.get("metric_contract",{}); identity=np.ones(6)
  if metrics.get("primary")!="metric_weighted_r2" or set(metrics.get("supplementary",[]))!={"per_channel_r2","unweighted_mean_channel_r2"}: errors.append("primary/supplementary metric declaration")
  if metric.get("observable_metric")!=identity.tolist() or metric.get("observable_metric_declaration")!="H=I" or metric.get("observable_metric_sha256")!=_hash(identity): errors.append("observable H=I metric/hash")
  if metric.get("observable_metric_coordinates")!="physical incremental observable channel coordinates" or metric.get("observable_coordinate_units")!=OBSERVABLE_UNITS[observable]: errors.append("observable metric coordinates/units")
  if not isinstance(metric.get("state_metric_sha256"),str) or len(metric["state_metric_sha256"])!=64: errors.append("state metric hash")
  null_contract=report.get("circular_time_shift_null_contract",{})
  if (null_contract.get("tested_statistic")!="metric_weighted_r2"
      or null_contract.get("selection_repeated")!="all training-only rank/sample-lag selection"
      or null_contract.get("periodic_equivalent_exclusions_applied") is not True
      or null_contract.get("admissible_offsets_fixed_before_null_scoring") is not True
      or null_contract.get("period_estimation_data")!="each training realization's complete multichannel observable only; held-out realization excluded"
      or null_contract.get("period_estimator")!="deterministic normalized multichannel rFFT power spectrum"):
   errors.append("selection-adjusted periodic-excluded null contract")
  width=null_contract.get("exclusion_width_samples")
  width_time=null_contract.get("exclusion_width_physical_time")
  if not isinstance(width,int) or width<2 or not isinstance(width_time,(int,float)) or width_time<=0:
   errors.append("periodic exclusion widths")
  execution=report.get("execution_metadata",{})
  if execution.get("selection_adjusted") is not True or execution.get("surrogate_count")!=999 or execution.get("tested_statistic")!="metric_weighted_r2": errors.append("formal execution selection/surrogate contract")
  if execution.get("negative_control_margin")!=NEGATIVE_CONTROL_MARGIN or execution.get("rcond")!=RCOND or execution.get("numerical_status")!="truncated" or execution.get("regularization") is not None or execution.get("observable_metric")!=identity.tolist() or execution.get("observable_coordinate_units")!=OBSERVABLE_UNITS[observable]: errors.append("formal execution metric/margin/numerical contract")
  code_hashes=execution.get("code_sha256",{})
  if set(code_hashes)!=set(CODE_PATHS): errors.append("full code provenance path set")
  for relative in CODE_PATHS:
   path=canonical_root.parents[4]/relative
   if not path.is_file() or code_hashes.get(relative)!=sha256_file(path): errors.append(f"current code hash {relative}")
  numerical=report.get("numerical_contract",{})
  if (numerical.get("status")!="truncated" or numerical.get("regularization") is not None
      or numerical.get("rcond")!=RCOND
      or set(numerical.get("applies_to",[]))!={
       "state covariance pseudoinverse","propagator generated rank",
       "LR Gram rank and pseudoinverse","LE Gram rank and pseudoinverse"}):
   errors.append("fixed-rcond numerical contract")
  if execution.get("cpu_only") is not True or execution.get("gpu_or_cfd_execution") is not False or execution.get("process_concurrency_limit")!=3: errors.append("execution resource policy")
  for name in ("analysis_wall_time_seconds","peak_rss_kib"):
   value=execution.get(name)
   if not isinstance(value,(int,float)) or not np.isfinite(value) or value<=0: errors.append(f"execution {name}")
  for count,bound in (("state_pod_fit_count","state_pod_fit_bound"),("state_field_transform_count","state_field_transform_bound"),("physical_mode_materialization_count","physical_mode_materialization_bound")):
   actual,limit=report.get(count),report.get(bound)
   if not isinstance(actual,int) or not isinstance(limit,int) or actual<0 or limit<0 or actual>limit: errors.append(f"resource counter {count}/{bound}")
  folds=report.get("folds",[])
  if report.get("fold_count")!=3 or len(folds)!=3 or report.get("realization_ids")!=list(REALIZATIONS): errors.append("three folds/realizations")
  for i,fold in enumerate(folds):
   rid=fold.get("held_out_realization",f"fold_{i}"); null=fold.get("circular_time_shift_surrogate_scores",[]); selections=fold.get("circular_time_shift_surrogate_selections",[])
   if len(null)!=999 or len(selections)!=999 or not _finite(null): errors.append(f"fold {rid}: 999 finite surrogates")
   p=fold.get("selection_adjusted_periodic_excluded_empirical_p_value"); resolution=1/1000
   if not isinstance(p,(int,float)) or not np.isfinite(p) or p<resolution or abs(p/resolution-round(p/resolution))>1e-9: errors.append(f"fold {rid}: p-value resolution")
   admissibility=fold.get("circular_time_shift_admissibility",[])
   if len(admissibility)!=2: errors.append(f"fold {rid}: training realization period records")
   for period in admissibility:
    excluded=period.get("excluded_offsets",[]); admissible=period.get("admissible_offsets",[])
    record_samples=period.get("record_samples")
    if (period.get("estimator")!="deterministic normalized multichannel rFFT power spectrum"
        or not isinstance(period.get("period_samples"),(int,float))
        or not np.isfinite(period.get("period_samples",np.nan))
        or not isinstance(period.get("peak_concentration"),(int,float))
        or not np.isfinite(period.get("peak_concentration",np.nan))
        or period.get("excluded_offset_count")!=len(excluded)
        or period.get("admissible_offset_count")!=len(admissible)
        or len(admissible)<2 or set(excluded)&set(admissible)
        or len(excluded)+len(admissible)!=record_samples
        or period.get("exclusion_width_samples")!=width
        or period.get("exclusion_width_physical_time")!=width_time
        or not isinstance(period.get("period_physical_time"),(int,float))):
     errors.append(f"fold {rid}: periodic admissibility record")
    elif (set(excluded)|set(admissible)!=set(range(record_samples))
          or any(offset not in excluded for offset in list(range(width+1))+list(range(record_samples-width,record_samples)))
          or any(abs(offset-location)<=width and offset not in excluded
                 for location in period.get("integer_period_multiple_locations_samples",[])
                 for offset in range(record_samples))):
     errors.append(f"fold {rid}: periodic exclusions not applied")
   for selection in selections:
    offsets=selection.get("offsets",[])
    if len(offsets)!=len(admissibility) or any(
        offset not in admissibility[index].get("admissible_offsets",[])
        for index,offset in enumerate(offsets)
    ):
     errors.append(f"fold {rid}: surrogate used excluded offset"); break
   channels=fold.get("per_channel_r2",[])
   if len(channels)!=6 or not _finite(channels): errors.append(f"fold {rid}: six channels")
   weighted,supplement=fold.get("metric_weighted_r2"),fold.get("unweighted_mean_channel_r2")
   if fold.get("tested_statistic")!="metric_weighted_r2" or not isinstance(weighted,(int,float)) or not np.isfinite(weighted): errors.append(f"fold {rid}: primary metric")
   if not isinstance(supplement,(int,float)) or not np.isfinite(supplement) or (len(channels)==6 and not np.isclose(supplement,np.mean(channels))): errors.append(f"fold {rid}: supplementary mean")
   if (fold.get("numerical_status")!="truncated" or fold.get("regularization") is not None
       or fold.get("rcond")!=RCOND): errors.append(f"fold {rid}: numerical status/rcond")
   requested,effective,generated=(fold.get("requested_observable_rank"),
                                  fold.get("effective_observable_rank"),
                                  fold.get("generated_rank"))
   if not all(isinstance(x,int) and x>=1 for x in (requested,effective,generated)) or not generated<=effective<=requested:
    errors.append(f"fold {rid}: requested/effective/generated ranks")
   for name in ("state_rank","state_covariance_rank"):
    if not isinstance(fold.get(name),int) or fold[name]<1: errors.append(f"fold {rid}: {name}")
   energy=fold.get("state_retained_energy")
   if not isinstance(energy,(int,float)) or not np.isfinite(energy) or not 0<energy<=1+1e-12: errors.append(f"fold {rid}: state retained energy")
   for name in ("propagator_threshold","propagator_smallest_retained_singular",
                "state_covariance_threshold","state_covariance_smallest_retained_singular"):
    value=fold.get(name)
    if not isinstance(value,(int,float)) or not np.isfinite(value) or value<0: errors.append(f"fold {rid}: {name}")
   cond=fold.get("propagator_condition_number")
   if not isinstance(cond,(int,float)) or not np.isfinite(cond) or cond>MAX_CONDITION_NUMBER: errors.append(f"fold {rid}: conditioning")
   for name in ("lr_identity_defect","le_identity_defect","lr_projection_defect","le_projection_defect"):
    value=fold.get(name)
    if not isinstance(value,(int,float)) or not np.isfinite(value) or value>MAX_DEFECT: errors.append(f"fold {rid}: {name}")
   control=fold.get("realization_derangement_negative_control_metric_weighted_r2")
   expected_pass=isinstance(weighted,(int,float)) and isinstance(control,(int,float)) and np.isfinite(weighted) and np.isfinite(control) and weighted>control+NEGATIVE_CONTROL_MARGIN
   if fold.get("negative_control_margin")!=NEGATIVE_CONTROL_MARGIN or fold.get("negative_control_pass") is not expected_pass: errors.append(f"fold {rid}: negative-control calculation")
   elif not expected_pass: failed_folds.append(rid)
   sensitivity=fold.get("realization_derangement_rank_lag_sensitivity",[])
   if (fold.get("realization_derangement_selection")!="complete training-only rank/sample-lag reselection on deranged training observables"
       or not isinstance(fold.get("realization_derangement_selected_rank"),int)
       or not isinstance(fold.get("realization_derangement_selected_sample_lag"),int)
       or not isinstance(sensitivity,list) or not sensitivity):
    errors.append(f"fold {rid}: reselected negative control")
   lr=fold.get("lr_state_coefficient_r2")
   if isinstance(lr,(int,float)) and np.isfinite(lr) and lr<0: reconstruction_folds.append(rid)
   if fold.get("observable")!=observable or fold.get("circular_time_shift_surrogate_statistic")!="metric_weighted_r2": errors.append(f"fold {rid}: observable/statistic")
  aggregate=report.get("negative_control_gate",{})
  if aggregate.get("tested_statistic")!="metric_weighted_r2" or aggregate.get("all_folds_pass")!=(not failed_folds): errors.append("negative-control aggregate consistency")
  if not _finite(report): errors.append("non-finite report")
  warning=(f"LR state-coefficient reconstruction R2 is negative for held-out folds {reconstruction_folds}; "
           "do not claim state reconstruction skill") if reconstruction_folds else None
  detail={"errors":errors,"report_sha256":report_hash,"model_sha256":model_hash,"fold_count":len(folds),
          "surrogate_count_per_fold":[len(x.get("circular_time_shift_surrogate_scores",[])) for x in folds],
          "p_value_resolution":1/1000,"negative_control_margin":NEGATIVE_CONTROL_MARGIN}
  if errors:
   return {**_gate("fail","formal artifact or mathematical integrity failure",**detail),
           "integrity_status":"fail","scientific_claim_status":"not_evaluated","failed_folds":failed_folds,
           "reconstruction_warning":warning,"permitted_claim":None,"prohibited_claim":claim}
  promoted=not failed_folds
  return {**_gate("pass" if promoted else "conditional",
                  "integrity verified; all-fold association promoted" if promoted else "integrity verified; scientific negative-control gate not met in every fold",**detail),
          "integrity_status":"pass","scientific_claim_status":"promoted" if promoted else "not_promoted",
          "failed_folds":failed_folds,"reconstruction_warning":warning,
          "permitted_claim":claim if promoted else f"report the valid negative-control result and descriptive {observable} metrics for {scene}",
          "prohibited_claim":None if promoted else claim}
 except Exception as exc:
  return {**_gate("fail","formal report could not be verified",error=str(exc)),
          "integrity_status":"fail","scientific_claim_status":"not_evaluated","failed_folds":failed_folds,
          "reconstruction_warning":None,"permitted_claim":None,"prohibited_claim":claim}

def _legacy(repo):
 path=repo/"src/OID_analysis/provenance/contradictions/steady_reanalysis.json"
 if not path.is_file(): return {"status":"fail","usable_for_claims":False,"reason":"contradiction record missing"}
 value=_json(path)
 return {"status":"preserved_contradiction","record_sha256":sha256_file(path),"contains_null_metrics":not _finite(value),"full_field_enstrophy_reduction":value.get("full-field",{}).get("enstrophy_reduction"),"force_fx_reduction":value.get("force",{}).get("Fx_reduction"),"usable_for_claims":False,"reason":"legacy steady evidence remains contradictory and excluded"}

def build_audit(repo):
 repo=Path(repo).resolve(); runs=repo/"src/OID_analysis/runs"; evidence_path=runs/"ACTIVE_EVIDENCE.json"; gates={}
 try:
  active=_json(evidence_path); valid=active.get("schema_id")=="oid-active-evidence/v1" and set(active.get("scenes",{}))==set(SCENES)
  gates["active_evidence_manifest"]=_gate("pass" if valid else "fail","active evidence registry verified" if valid else "active evidence registry contract failed",sha256=sha256_file(evidence_path))
 except Exception as exc: active={}; gates["active_evidence_manifest"]=_gate("fail","active evidence registry unavailable",error=str(exc))
 canonical_root=runs/"active-two-scene-v2/canonical"; gates["canonical_products"],canonical=_canonical(canonical_root,active)
 jobs={f"{s}/{o}":_analysis(runs/"active-two-scene-v2/analysis"/s/o,canonical_root,s,o) for s in SCENES for o in OBSERVABLE_CHANNELS}
 integrity_failures=[name for name,job in jobs.items() if job["integrity_status"]=="fail"]
 incomplete=[name for name,job in jobs.items() if job["integrity_status"]=="incomplete"]
 not_promoted=[name for name,job in jobs.items() if job["scientific_claim_status"]=="not_promoted"]
 promoted=[name for name,job in jobs.items() if job["scientific_claim_status"]=="promoted"]
 if integrity_failures:
  analysis_status,analysis_summary="fail","one or more formal jobs have artifact or mathematical integrity failures"
 elif incomplete or not_promoted:
  analysis_status,analysis_summary="conditional","formal jobs are valid but incomplete or not all scientific claims pass negative controls"
 else:
  analysis_status,analysis_summary="pass","all formal jobs have integrity and promoted all-fold associations"
 gates["formal_analyses"]=_gate(analysis_status,analysis_summary,jobs=jobs,integrity_failures=integrity_failures,
                                 incomplete_jobs=incomplete,not_promoted_jobs=not_promoted,promoted_jobs=promoted)
 formal=active.get("formal_complete",{}); complete=formal.get("status")=="complete"
 gates["formal_complete"]=_gate("pass" if complete else "conditional","formal COMPLETE verified" if complete else "formal COMPLETE remains fail-closed",declared_status=formal.get("status"),blocker=formal.get("blocker"),complete_markers_created=formal.get("complete_markers_created"))
 legacy=_legacy(repo); gates["legacy_steady_exclusion"]=_gate("pass" if not legacy["usable_for_claims"] else "fail","legacy steady contradiction remains excluded",record_status=legacy["status"])
 states=[x["status"] for x in gates.values()]; overall="fail" if "fail" in states else ("conditional" if "conditional" in states else "pass")
 allowed=["describe manifest-bound canonical provenance and exact geometry/mask/metric contracts"]
 allowed.extend(job["permitted_claim"] for job in jobs.values() if job["permitted_claim"])
 if all(job["integrity_status"]=="pass" for job in jobs.values()):
  allowed.append("report audited primary metric-weighted and supplementary per-channel results, including truthful negative scientific gates")
 if not_promoted:
  allowed.append("state that the negative-control association gate was not met for "+", ".join(not_promoted))
 if not integrity_failures and not incomplete and complete: allowed.append("treat the active evidence set as formally COMPLETE")
 prohibited=["use legacy steady outputs as evidence","claim causal, universal, or mode-unique interpretation beyond audited contracts"]
 prohibited.extend(job["prohibited_claim"] for job in jobs.values() if job["prohibited_claim"])
 if integrity_failures: prohibited.append("use jobs with artifact or mathematical integrity failures as evidence")
 if incomplete: prohibited.append("report formal Phase 3-5 findings for unpublished jobs")
 if not complete: prohibited.append("claim formal COMPLETE status or publication-ready immutable promotion")
 return {"schema_id":"oid-phase4-audit/v3","overall":overall,"active_evidence":{"path":str(evidence_path.relative_to(repo)),"sha256":sha256_file(evidence_path) if evidence_path.is_file() else None},"gates":gates,"canonical_products":canonical,"legacy_steady":legacy,"allowed_claims":allowed,"prohibited_claims":prohibited,"limitations":["Scientific negative-control failures are conditional non-promotion, not integrity failures.","Formal COMPLETE remains fail-closed as declared by ACTIVE_EVIDENCE.json.","Audit policy thresholds: condition number <= 1e12; identity/projection defects <= 1e-8."]}

def run(repo,output):
 destination=Path(output).resolve()
 if destination.exists(): raise FileExistsError(f"refusing to overwrite audit output: {destination}")
 report=build_audit(repo); atomic_write_json(destination,report); return report

def main(argv=None):
 parser=argparse.ArgumentParser(description="Manifest-driven OID Phase 4 audit"); parser.add_argument("--repo",type=Path,default=Path.cwd()); parser.add_argument("--output",type=Path,required=True,help="fresh explicit JSON destination"); args=parser.parse_args(argv)
 report=run(args.repo,args.output); print(json.dumps({"overall":report["overall"],"gates":report["gates"],"allowed_claims":report["allowed_claims"],"prohibited_claims":report["prohibited_claims"]},indent=2,allow_nan=False)); return 1 if report["overall"]=="fail" else 0
if __name__=="__main__": raise SystemExit(main())
