#!/usr/bin/env python3
import hashlib,json,os,sys,xml.etree.ElementTree as ET
from pathlib import Path
E=Path(__file__).resolve().parent; A=E/'artifacts'; Q=E/'receipts'; D='4e9962be49b72951c3b9ed7e1a4fe36d0ca03e757872ae03364c1ab7a790f32b'; CORE='88caabf3dee923e720463d917d35b6dd32ecc31102ed3ab4df4c6a203df4a760'; W='33688875a0d1ce20955bd8272257ee34d84cb138f6a23a43e5d16682368ae5e5'; C=Path('/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-clarification-normalized-successor-'+D)
sha=lambda p:hashlib.sha256(Path(p).read_bytes()).hexdigest()
def junit(n):
 p=A/f'{n}.junit.xml';x=ET.parse(p).getroot();ss=[x] if x.tag=='testsuite' else x.findall('testsuite');r={k:sum(int(float(s.attrib.get(k,0))) for s in ss) for k in ('tests','failures','errors','skipped')};r['passed']=r['tests']-r['failures']-r['errors']-r['skipped'];r['sha256']=sha(p);r['path']=str(p.relative_to(E));return r
def cmd(n):
 d=A/n
 return {'name':n,'command':(d/'command.txt').read_text().strip(),'exit_code':int((d/'exit-code.txt').read_text()),'started_at_utc':(d/'started-at-utc.txt').read_text().strip(),'ended_at_utc':(d/'ended-at-utc.txt').read_text().strip(),'duration_ms':int((d/'duration-ms.txt').read_text()),'stdout_sha256':sha(d/'stdout.txt'),'stderr_sha256':sha(d/'stderr.txt')}
def dump(p,x):p.write_text(json.dumps(x,ensure_ascii=False,sort_keys=True,separators=(',',':'))+'\n')
bind={'full_candidate_digest':D,'core_candidate_digest':CORE,'wheel_sha256':W,'candidate_root':str(C),'candidate_manifest_sha256':sha(C/'candidate-manifest.json'),'inventory_sha256':sha(C/'hash-inventory.json'),'source_delta_binding_sha256':sha(C/'bindings/source-delta.json'),'source_delta_diff_sha256':'cb15bb8067e665eaaf5a5d0b0d70a8058e0986962a54917b708b9a5e5e9d9a87'}
inv={'git_status_unchanged':(A/'git-status-before.nul').read_bytes()==(A/'git-status-after.nul').read_bytes(),'candidate_pins_unchanged':(A/'candidate-pins-before.sha256').read_bytes()==(A/'candidate-pins-after.sha256').read_bytes(),'service_unchanged':(A/'service-before.txt').read_bytes()==(A/'service-after.txt').read_bytes(),'prior_receipts_byte_identical':(A/'prior-receipts-before.sha256').read_bytes()==(A/'prior-receipts-after.sha256').read_bytes(),'service_state':(A/'service-after.txt').read_text().splitlines()}
g18={'schema':'task26-gate18-receipt-v1','binding':bind,'verdict':'PASS','golden_path':junit('gate18-golden'),'golden_nodes':['canonical customer summary','stale advisory removal','exact clarification parser','customer-authorized legacy recovery','baseline-valid resubmission'],'source_wheel_parity':json.loads((A/'source-wheel-parity/stdout.txt').read_text()),'commands':[cmd('candidate-preverify'),cmd('source-wheel-parity'),cmd('gate18-golden')],'invariants':inv}
g19={'schema':'task26-gate19-receipt-v1','binding':bind,'verdict':'PASS','predecessor_matrix':junit('gate19-predecessor'),'predecessor_note':'Exact four-file predecessor overlay; predecessor collection contains 94 tests.','successor_focused_matrix':junit('gate19-successor'),'commands':[cmd('gate19-predecessor'),cmd('gate19-successor')],'invariants':inv}
typec=json.loads((A/'type-classification-final/stdout.txt').read_text()); full=junit('gate20-full-gateway'); installed=junit('installed-wheel-tests')
g20={'schema':'task26-gate20-receipt-v1','binding':bind,'verdict':'FAIL','blockers':[{'kind':'full_gateway_failures','count':22,'classification':{'discord_dns_isolation':12,'matrix_network_isolation':9,'runtime_footer_environment':1},'note':'The sole fresh full Gateway run was network-isolated and failed; it was not rerun because Gate 20 required exactly one execution.'}],'full_gateway':full,'installed_wheel_tests':installed,'installed_wheel_imports':json.loads((A/'installed-wheel-imports/stdout.txt').read_text()),'reproducible_wheel':{'expected_sha256':W,'actual_sha256':(A/'reproducible-build.sha256').read_text().split()[0],'match':(A/'reproducible-build.sha256').read_text().split()[0]==W},'ruff':'PASS','compile':'PASS','installed_wheel_cli':'PASS','type_baseline_classification':typec,'type_note':'Final authoritative comparison used candidate-bound strict configs; both 233 diagnostics are inherited. Earlier local-config attempts are retained as superseded invocation evidence.','commands':[cmd(n) for n in ['ruff','compile','reproducible-build','wheel-install','installed-wheel-tests','installed-wheel-imports','installed-wheel-cli','basedpyright-current-final','basedpyright-baseline-final','type-classification-final','gate20-full-gateway','candidate-postverify']],'invariants':inv}
for n,x in [('gate18-receipt.json',g18),('gate19-receipt.json',g19),('gate20-receipt.json',g20)]:dump(Q/n,x)
receipts=[{'path':f'receipts/{p.name}','sha256':sha(p)} for p in sorted(Q.glob('gate*-receipt.json'))]
artifact_rows=[]
for p in sorted(A.rglob('*')):
 if p.is_file():artifact_rows.append((str(p.relative_to(E)),sha(p),p.stat().st_size))
h=hashlib.sha256();
for p,s,z in artifact_rows:h.update(f'{s}  {z}  {p}\n'.encode())
idx={'schema':'task26-gates18-20-receipt-index-v1','binding':bind,'overall_verdict':'FAIL','gate_verdicts':{'gate18':'PASS','gate19':'PASS','gate20':'FAIL'},'receipts':receipts,'artifact_tree':{'file_count':len(artifact_rows),'entries_sha256':h.hexdigest(),'total_bytes':sum(x[2] for x in artifact_rows)},'prior_receipts_byte_identical':inv['prior_receipts_byte_identical'],'e2f_partial_reused':False}
dump(Q/'receipt-index.json',idx)
with (Q/'receipt-hashes.sha256').open('w') as f:
 for p in sorted(Q.glob('*.json')):f.write(f'{sha(p)}  {p.name}\n')
for p in Q.iterdir():p.chmod(0o400)
print(json.dumps({'gate18_receipt_sha256':sha(Q/'gate18-receipt.json'),'gate19_receipt_sha256':sha(Q/'gate19-receipt.json'),'gate20_receipt_sha256':sha(Q/'gate20-receipt.json'),'receipt_index_sha256':sha(Q/'receipt-index.json'),'artifact_count':len(artifact_rows),'artifact_entries_sha256':h.hexdigest(),'overall':'FAIL'},sort_keys=True))
