"""Descriptive fixed-cohort analysis; no probability model or invented waits. Usage: python analyze_confirmation_report.py report-30d.json output.json The collector creates its immutable report only after all 30 fixed days exist. """ import json,sys,hashlib from pathlib import Path def analyze(d): if d.get('protocol')!='lw-confirmations-2026-09-27-v2' or d.get('scheduled_seconds')!=30*86400 or len(d.get('daily',[]))!=30:raise ValueError('Protocol/period mismatch') counts=d['counts'] if any(type(n) is not int or n<0 for n in counts.values()):raise ValueError('Invalid counts') covered=d['covered_seconds'];total=sum(counts.values());confirmed=counts.get('confirmed',0) if not 0<=covered<=30*86400 or covered!=sum(r['covered_seconds'] for r in d['daily']):raise ValueError('Invalid coverage') if d['cohort_end']-d['cohort_start']!=30*86400:raise ValueError('Invalid cohort') return {'protocol':d['protocol'],'sampled_transactions':total,'matched_confirmations':confirmed,'coverage_fraction':covered/(30*86400),'outcomes':{s:{'count':n,'share':n/total if total else None} for s,n in counts.items()},'confirmed_only_median_seconds':d.get('confirmed_median_seconds') if confirmed>=20 else None,'confirmed_only_p90_seconds':d.get('confirmed_p90_seconds') if confirmed>=20 else None,'limits':'Conditional confirmed-only statistics, not deadline completion probabilities. Pending and censored outcomes remain in the sample denominator. Observation is not broadcast; a single node is not the whole network.'} if __name__=='__main__': raw=Path(sys.argv[1]).read_bytes();result=analyze(json.loads(raw));result['report_sha256']=hashlib.sha256(raw).hexdigest();Path(sys.argv[2]).write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result))