Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
156 lines
13 KiB
Python
156 lines
13 KiB
Python
#!/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','<missing>')
|
|
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()
|