#!/usr/bin/env python3 """Evidence inventory for P22; file presence never certifies production readiness.""" import hashlib import json from pathlib import Path import re import struct ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / 'docs/plans/original-completion-2026-09-29/execution/assets' OUT.mkdir(parents=True, exist_ok=True) def digest(p): return hashlib.sha256(p.read_bytes()).hexdigest() def save(name, data): (OUT/name).write_text(json.dumps(data, ensure_ascii=False, indent=2)+'\n') references = {} for folder in ('scripts', 'scenes', 'autoload', 'data', 'assets'): for source in (ROOT/folder).rglob('*'): if source.suffix not in ('.gd', '.tres', '.tscn') or not source.is_file(): continue for path in set(re.findall(r'res://([^"\s]+\.(?:png|svg|jpg|jpeg|webp|ogg|wav|mp3|ttf|otf))', source.read_text())): references.setdefault(path, []).append(str(source.relative_to(ROOT))) media = [] for p in sorted((ROOT/'assets').rglob('*')): if not p.is_file() or p.suffix.lower() not in ('.png','.svg','.jpg','.jpeg','.webp','.ogg','.wav','.mp3','.ttf','.otf'): continue path = str(p.relative_to(ROOT)) row = {'path':path, 'bytes':p.stat().st_size, 'sha256':digest(p), 'literal_references':references.get(path, []), 'acceptance':'not_reviewed', 'provenance':'unverified'} if p.suffix.lower() == '.png': header = p.read_bytes()[:24] if header[:8] == b'\x89PNG\r\n\x1a\n': row['width'],row['height'] = struct.unpack('>II',header[16:24]) if path.startswith('assets/mockups/'): row['purpose']='reference_mockup_not_shipping_acceptance' elif path.startswith('assets/kenney/'): pack = p.relative_to(ROOT/'assets/kenney').parts[0] license_path = ROOT/'assets/kenney'/pack/'License.txt' row.update(provenance='Kenney', creator='Kenney', license=str(license_path.relative_to(ROOT)), license_present=license_path.exists()) if license_path.exists(): row['license_sha256']=digest(license_path) row['license_declares_cc0']='CC0' in license_path.read_text() else: row['purpose']='existing_theme_or_icon_requires_source_and_quality_review' media.append(row) save('media_inventory.json', media) facility_mapping = ROOT/'docs/plans/original-completion-2026-09-29/execution/original_facility_mapping.json' facilities = json.loads(facility_mapping.read_text()) renderer = ROOT/'scripts/ui/pixel_tower_art.gd' text = renderer.read_text() slots = [] for index,f in enumerate(facilities,1): key = f['definition_id'] or f'original_facility_{index:02}' for level in range(1,4): for layer in ('front','back'): for surface in ('exterior','interior'): slots.append({'slot_id':f'{key}.lv{level}.{layer}.{surface}', 'requirement':['B-13.3',f"B-{f['section']}"], 'facility_name':f['name'], 'definition_id':f['definition_id'], 'level':level, 'layer':layer, 'surface':surface, 'original_placement':f['original_layer_zone'], 'creator':'unassigned', 'status':'unverified_required_slot', 'approved_asset':None, 'existing_candidate':str(renderer.relative_to(ROOT)) if f['definition_id'] and ('&"'+f['definition_id']+'"') in text else None, 'note':'Code token is only a search lead; it does not prove a dedicated level/layer/surface variant. These are usage slots, not a committed image count.'}) save('facility_usage_slots.json',slots) people = [('anri','アンリ'),('fine','フィーネ'),('tomari','トマリ'),('amelia','アメリア'),('liz','リズ'),('mimi','ミミ'),('satsuki','サツキ')] character_slots = [] for key,name in people: for kind in ('portrait_base','portrait_expression','portrait_costume','portrait_damage','portrait_dirt','sd_idle','sd_walk','sd_work','sd_collapsed','sd_carry','sd_rescue','sd_rest'): character_slots.append({'slot_id':f'heroine.{key}.{kind}', 'character':name, 'requirement':['B-6.2','B-13.1' if kind.startswith('portrait') else 'B-13.2'], 'kind':kind, 'required_variant_count':1 if kind=='portrait_base' else None, 'creator':'unassigned', 'status':'unverified_required_slot', 'approved_asset':None, 'candidate':'scripts/ui/pixel_people.gd' if kind.startswith('sd_') else None, 'note':'Generic procedural poses do not prove character-specific appearance or role-specific animation.'}) save('character_usage_slots.json',character_slots) other = [ ('background.front','B-13.3','scenes/main/world_preview.gd'),('background.back','B-13.3','scenes/main/world_preview.gd'), ('ui.front_skin','B-13.4','assets/themes/japanese_fantasy/visual-theme-v1.tres'),('ui.back_skin','B-13.4','assets/themes/japanese_fantasy/visual-theme-v1.tres'), ('ui.emotion_graph','B-13.4',None),('ui.camera_overlay','B-13.4',None), ('effect.shield','B-13.2',None),('effect.smoke','B-13.2',None),('effect.mana_light','B-13.2',None), ('audio.front_bgm','B-13.5',None),('audio.back_arrangement','B-13.5',None),('audio.front_ambience','B-13.5',None),('audio.back_ambience','B-13.5',None),('audio.ui','B-13.5',None),('audio.character_voice','B-13.5',None), ('other_characters','B-13.1',None),('restricted_content_unproduced','B-13.1',None)] save('other_usage_slots.json',[{'slot_id':k,'requirement':(['B-12.3','B-13.1','B-13.2','B-13.5'] if k=='restricted_content_unproduced' else [req]),'candidate':candidate,'status':'not_produced_outside_current_content_scope' if k=='restricted_content_unproduced' else 'unverified_required_slot','creator':'unassigned','required_variant_count':None,'approved_asset':None} for k,req,candidate in other]) summary = {'facility_mapping':str(facility_mapping.relative_to(ROOT)), 'facility_mapping_sha256':digest(facility_mapping), 'baseline_sha256':digest(ROOT/'docs/traceability/original/BASELINE.txt'), 'media_files':len(media), 'literal_referenced_media_files':sum(bool(x['literal_references']) for x in media), 'audio_files':sum(Path(x['path']).suffix in ('.wav','.ogg','.mp3') for x in media), 'facility_count':len(facilities), 'facility_usage_slots':len(slots), 'named_heroines':len(people), 'character_usage_slot_groups':len(character_slots), 'other_slot_groups':len(other), 'approved_assets':0, 'missing_literal_references':[p for p in references if not (ROOT/p).exists()], 'scope':'Inventory and candidate mappings only. No asset is promoted to complete by file existence, procedural fallback, or a passing gameplay test. Variant counts and production ownership remain unresolved.'} save('inventory_summary.json',summary) print(json.dumps(summary,ensure_ascii=False))