#!/usr/bin/env python3 """Independently validate P01 register/coverage/frozen IDs, never game completion. Usage: python3 validate_atomic_requirements.py [--self-test] Negative checks mutate in-memory documents only and must be rejected. """ import argparse, copy, hashlib, json, re from pathlib import Path from datetime import datetime, timezone, timedelta ROOT=Path(__file__).resolve().parents[3]; HERE=Path(__file__).resolve().parent BASE='docs/traceability/original/BASELINE.txt' VIEWS=['source_condition','definition','runtime','normal_ui','save_migration','boundary_verification','assets_platform','evidence'] STATES={'unverified','not_started','partial','blocked','pass','fail','not_applicable'} FIELDS=['id','source_anchors','source_sha256','user_amendment_refs','condition','normal_entry','implementation_paths','save_contract','applicable_completion_views','view_results','evidence_refs','source_head','primary_package','responsible_role','reviewer','status','updated_at','acceptance','implementation_state','acceptance_state'] sha=lambda p:hashlib.sha256((ROOT/p).read_bytes()).hexdigest() def validate(reg,cov,freeze): errors=[] def require(ok,msg): if not ok:errors.append(msg) source_lines=(ROOT/BASE).read_text().splitlines() source_groups=json.loads((ROOT/'docs/traceability/original/requirements.json').read_text()) reqs=reg.get('requirements',[]); byid={r.get('id'):r for r in reqs} require(len(byid)==len(reqs),'duplicate requirement ID') require(reg.get('source_pins',{}).get(BASE)==sha(BASE),'baseline pin mismatch') for p,digest in reg.get('source_pins',{}).items():require((ROOT/p).is_file() and sha(p)==digest,'source pin mismatch: '+p) require(reg.get('final_denominator_established') is False,'final denominator claimed despite unresolved catalogs') require(reg.get('final_catalog_coverage_claimed') is False,'all catalog entries claimed despite unresolved catalogs') require(any(x.get('scope')=='additional_named_facilities_and_infrastructure_applicability' and len(x.get('source_examples',[]))>=9 for x in reg.get('open_expansion_register',[])), 'other named facilities/infrastructure applicability expansion missing') require(set(reg.get('completion_views',[]))==set(VIEWS),'eight views missing') inverse={n:set() for n in range(1,len(source_lines)+1)} packages={p['id'] for p in json.loads((HERE/'work_packages.json').read_text())} feature=set(); schedule=set() for r in reqs: rid=r.get('id','') require(bool(re.fullmatch(r'AR\.[A-Za-z0-9_.-]+',rid)),rid+': invalid semantic ID') for field in FIELDS:require(field in r,rid+': missing '+field) require(bool(r.get('condition')),rid+': no condition') require(r.get('mandatory') is True and r.get('equal_count_weight')==1,rid+': changed mandatory weight') require(r.get('primary_package') in packages,rid+': invalid primary package') require(set(r.get('view_results',{}))==set(VIEWS),rid+': eight result views missing') require(set(r.get('applicable_completion_views',[]))<=set(VIEWS),rid+': unknown applicability view') require(bool(r.get('source_anchors')),rid+': no source anchor') require(r.get('source_sha256')==sha(BASE),rid+': original SHA mismatch') for a in r.get('source_anchors',[]): p=a.get('path'); nums=a.get('lines',[]) require(p in reg.get('source_pins',{}),rid+': unpinned anchor') require(a.get('sha256')==reg.get('source_pins',{}).get(p),rid+': anchor SHA mismatch') require(bool(nums) and all(isinstance(n,int) and 1<=n<=len((ROOT/p).read_text().splitlines()) for n in nums),rid+': invalid source lines') if p==BASE: for n in nums: if n in inverse:inverse[n].add(rid) for v,result in r.get('view_results',{}).items(): state=result.get('state');require(state in STATES,rid+': unknown view state '+str(state)) if state=='pass': require(bool(result.get('evidence_refs')) and bool(result.get('reviewer')),rid+': unsupported PASS '+v) if state=='not_applicable': require(bool(result.get('not_applicable_reason')) and bool(result.get('reviewer')) and bool(result.get('source_or_explicit_user_decision')),rid+': unsupported N/A '+v) if r.get('implementation_state')=='implemented': require(all(r['view_results'].get(v,{}).get('state') in ['pass','not_applicable'] for v in VIEWS[:5]),rid+': incomplete implementation counted') if r.get('acceptance_state')=='accepted': require(all(r['view_results'].get(v,{}).get('state') in ['pass','not_applicable'] for v in r['applicable_completion_views']),rid+': incomplete acceptance counted') scope=r.get('scope') require(scope in ['feature_or_content','historical_schedule'],rid+': invalid scope') (schedule if scope=='historical_schedule' else feature).add(rid) if scope=='historical_schedule': require(all(g.startswith('B-14.') for g in r.get('source_group_ids',[])),rid+': functional group in schedule') for dep in r.get('decision_dependencies',[]): if dep['decision_id']=='D03':require(dep['kind']=='informational_explicitly_unset_optional_extension',rid+': explicit unset option treated as required decision') require(set(reg.get('denominator_ids',[]))==feature and len(reg.get('denominator_ids',[]))==len(feature),'register denominator changed/duplicated') require(set(reg.get('historical_schedule_ids',[]))==schedule,'schedule IDs mismatch') require(freeze.get('denominator_version')==reg.get('denominator_version'),'freeze version mismatch') require(freeze.get('source_head')==reg.get('source_head'),'freeze source HEAD mismatch') require(freeze.get('source_pins')==reg.get('source_pins'),'freeze source pins mismatch') require(freeze.get('denominator')==len(feature) and set(freeze.get('denominator_ids',[]))==feature,'frozen denominator mismatch') require(set(freeze.get('historical_schedule_ids',[]))==schedule and freeze.get('historical_schedule_count')==len(schedule),'frozen schedule mismatch') require(freeze.get('final_denominator_established') is False,'freeze falsely establishes final denominator') implemented={r['id'] for r in reqs if r.get('scope')=='feature_or_content' and r.get('implementation_state')=='implemented'} accepted={r['id'] for r in reqs if r.get('scope')=='feature_or_content' and r.get('acceptance_state')=='accepted'} require(set(freeze.get('implemented_ids',[]))==implemented,'implemented numerator IDs mismatch') require(set(freeze.get('accepted_ids',[]))==accepted,'accepted numerator IDs mismatch') require(freeze.get('implementation_percent')==100*len(implemented)/len(feature),'implementation percentage not recomputable') require(freeze.get('acceptance_percent')==100*len(accepted)/len(feature),'acceptance percentage not recomputable') clines=cov.get('source_lines',[]) require(cov.get('source_sha256')==sha(BASE) and cov.get('line_count')==len(source_lines),'coverage baseline mismatch') require([x.get('line') for x in clines]==list(range(1,len(source_lines)+1)),'coverage has missing/duplicate source lines') for row in clines: n=row.get('line');ids=set(row.get('atomic_ids',[])) require(bool(ids) and ids<=set(byid),f'line {n}: absent/unknown IDs') if isinstance(n,int) and 1<=n<=len(source_lines): require(row.get('line_sha256')==hashlib.sha256(source_lines[n-1].encode()).hexdigest(),f'line {n}: content hash mismatch') if row.get('kind')=='condition':require(ids==inverse[n],f'line {n}: condition anchor correspondence mismatch') cgroups={g.get('group_id'):g for g in cov.get('groups',[])} require(len(cgroups)==75 and set(cgroups)=={g['id'] for g in source_groups},'75 groups missing/duplicated') for g in source_groups: row=cgroups.get(g['id'],{});expected={r['id'] for r in reqs if g['id'] in r.get('source_group_ids',[])} require(row.get('source_lines')==g['source_lines'] and set(row.get('atomic_ids',[]))==expected and bool(expected),g['id']+': group coverage mismatch') facilities=cov.get('facility_ids',[]);heroes=cov.get('heroine_ids',[]) require(len(facilities)==30 and len({f.get('key') for f in facilities})==30,'original 30 facilities missing') require(len(heroes)==7 and len({h.get('key') for h in heroes})==7,'original 7 heroines missing') # Known facility fields and 360 finite, explicitly specified visual slots must exist. fields=['exists','footprint','construction_gold','upkeep_gold','upkeep_mana','service_price','role','operation_state','customer_capacity','staff_min','staff_max','related_facility','related_operating','service_attribute'] for f in facilities: key=f['key'] for field in fields:require('AR.facility.'+key+'.'+field in byid,key+': missing facility condition '+field) for lv in range(1,4): for view in ['front','back']: for part in ['exterior','interior']:require(f'AR.asset.facility.{key}.lv{lv}.{view}.{part}' in byid,key+': missing finished asset slot') for h in heroes: for route in ['love','ntr','corruption']:require(f'AR.heroine.{h["key"]}.route.{route}' in byid,h['key']+': route missing') for d in ['A01','A02','A03','A04','A05']: expected={r['id'] for r in reqs if d in r.get('user_amendment_refs',[])} got=[row for row in cov.get('approved_decisions',[]) if row.get('decision_id')==d] require(len(got)==1 and bool(expected) and set(got[0]['atomic_ids'])==expected,d+': approval coverage mismatch') for n in range(1,11): fid=f'F{n:02}';expected={r['id'] for r in reqs if fid in r.get('supplement_ids',[])} got=[row for row in cov.get('facility_supplements',[]) if row.get('supplement_id')==fid] require(len(got)==1 and bool(expected) and set(got[0]['atomic_ids'])==expected,fid+': supplement coverage mismatch') required_contracts=['AR.progression.all_endings','AR.progression.encyclopedia_complete','AR.platform.windows','AR.progression.main_duration','AR.progression.full_duration'] for rid in required_contracts:require(rid in feature,'missing release condition: '+rid) return errors def main(): parser=argparse.ArgumentParser();parser.add_argument('--self-test',action='store_true');args=parser.parse_args() reg=json.loads((HERE/'atomic_requirements.json').read_text());cov=json.loads((HERE/'source_atomic_coverage.json').read_text());freeze=json.loads((HERE/'atomic_denominator_freeze.json').read_text()) errors=validate(reg,cov,freeze);negative=[] if args.self_test: def mutate(label,callback): r,c,f=copy.deepcopy(reg),copy.deepcopy(cov),copy.deepcopy(freeze);callback(r,c,f) found=validate(r,c,f);negative.append({'case':label,'rejected':bool(found),'first_error':found[0] if found else None}) mutate('duplicate_id',lambda r,c,f:r['requirements'].append(copy.deepcopy(r['requirements'][0]))) mutate('source_line_deleted',lambda r,c,f:c['source_lines'].pop()) mutate('source_hash_changed',lambda r,c,f:r['source_pins'].__setitem__(BASE,'0'*64)) mutate('frozen_denominator_changed',lambda r,c,f:f['denominator_ids'].pop()) mutate('unsupported_pass',lambda r,c,f:r['requirements'][0]['view_results']['runtime'].__setitem__('state','pass')) mutate('unsupported_not_applicable',lambda r,c,f:r['requirements'][0]['view_results']['runtime'].__setitem__('state','not_applicable')) mutate('partial_implementation_counted',lambda r,c,f:r['requirements'][0].__setitem__('implementation_state','implemented')) mutate('unverified_acceptance_counted',lambda r,c,f:r['requirements'][0].__setitem__('acceptance_state','accepted')) mutate('all_content_final_claim',lambda r,c,f:r.__setitem__('final_denominator_established',True)) mutate('facility_removed',lambda r,c,f:r['requirements'].__setitem__(slice(None),[x for x in r['requirements'] if x['id']!='AR.facility.chapel.exists'])) mutate('heroine_route_removed',lambda r,c,f:r['requirements'].__setitem__(slice(None),[x for x in r['requirements'] if x['id']!='AR.heroine.anri.route.love'])) mutate('schedule_counted_as_function',lambda r,c,f:next(x for x in r['requirements'] if x['scope']=='historical_schedule').__setitem__('scope','feature_or_content')) mutate('altered_percentage',lambda r,c,f:f.__setitem__('implementation_percent',50.0)) ok=not errors and all(x['rejected'] for x in negative) result={'validator':'P01 register structure and source coverage, not game acceptance','passed':ok, 'checked_at_jst':datetime.now(timezone(timedelta(hours=9))).isoformat(timespec='seconds'), 'source_head':reg['source_head'],'known_condition_v1_denominator':len(reg['denominator_ids']), 'historical_schedule_atoms':len(reg['historical_schedule_ids']),'original_lines':cov['line_count'],'original_groups':75, 'original_facilities':len(cov['facility_ids']),'heroines':len(cov['heroine_ids']),'heroine_routes':21, 'implemented_whole_atoms':len(freeze['implemented_ids']),'accepted_whole_atoms':len(freeze['accepted_ids']), 'all_evidence_unverified':all(x['acceptance_state']=='unverified' for x in reg['requirements']), 'final_denominator_established':False,'errors':errors,'negative_checks':negative} (HERE/'atomic_requirements_validation.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n') print(json.dumps(result,ensure_ascii=False,indent=2)) raise SystemExit(0 if ok else 1) if __name__=='__main__':main()