Save The Tree implementation and acceptance work in progress

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.
This commit is contained in:
Takeshi
2026-10-11 15:58:00 +09:00
parent 39f6068461
commit b84bade045
235 changed files with 495831 additions and 312 deletions
@@ -0,0 +1,155 @@
#!/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()