2026-06-24 13:48:22 +09:00
#!/usr/bin/env python3
""" Lightweight regression guard for the buildless Tarinai app.
Checks static - file consistency , generated - load order , syntax , and the ActionSpec
registry contract . This intentionally avoids save - data migration checks ; save
2026-06-25 14:51:58 +09:00
compatibility is intentionally not retained for save format changes .
2026-06-24 13:48:22 +09:00
"""
from __future__ import annotations
from pathlib import Path
import json
import re
import subprocess
import sys
import tempfile
ROOT = Path ( __file__ ) . resolve ( ) . parents [ 1 ]
MANIFEST = ROOT / " app_manifest.json "
def fail ( message : str ) - > None :
print ( f " [FAIL] { message } " , file = sys . stderr )
raise SystemExit ( 1 )
def ok ( message : str ) - > None :
print ( f " [OK] { message } " )
def read_text ( path : Path ) - > str :
return path . read_text ( encoding = " utf-8 " )
def manifest ( ) - > dict :
try :
data = json . loads ( read_text ( MANIFEST ) )
except Exception as exc : # pragma: no cover - diagnostic path
fail ( f " cannot read app_manifest.json: { exc } " )
for key in ( " version " , " css " , " js " ) :
if key not in data :
fail ( f " manifest missing { key } " )
return data
def check_files_exist ( data : dict ) - > None :
missing = [ p for p in [ * data [ " css " ] , * data [ " js " ] ] if not ( ROOT / p ) . exists ( ) ]
if missing :
fail ( " manifest references missing files: " + " , " . join ( missing ) )
ok ( " manifest file references exist " )
def check_versions ( data : dict ) - > None :
version = str ( data [ " version " ] )
version_js = read_text ( ROOT / " js/version.js " )
service_worker = read_text ( ROOT / " service-worker.js " )
if f ' APP_VERSION = " { version } " ' not in version_js :
fail ( " js/version.js APP_VERSION does not match manifest " )
if f ' APP_VERSION = " { version } " ' not in service_worker :
fail ( " service-worker.js APP_VERSION does not match manifest " )
ok ( f " version synchronized: { version } " )
def check_index_order ( data : dict ) - > None :
index = read_text ( ROOT / " index.html " )
css = re . findall ( r ' <link rel= " stylesheet " href= " ([^ " ]+) " \ s*/> ' , index )
scripts = re . findall ( r ' <script src= " ([^ " ]+) " defer></script> ' , index )
css_clean = [ entry . split ( " ? " , 1 ) [ 0 ] for entry in css if entry . startswith ( " css/ " ) ]
script_clean = [ entry . split ( " ? " , 1 ) [ 0 ] for entry in scripts if entry . startswith ( " js/ " ) ]
if css_clean [ : len ( data [ " css " ] ) ] != data [ " css " ] :
fail ( " index.html CSS order differs from manifest " )
if script_clean [ : len ( data [ " js " ] ) ] != data [ " js " ] :
fail ( " index.html JS order differs from manifest " )
ok ( " index.html load order matches manifest " )
def check_service_worker_order ( data : dict ) - > None :
sw = read_text ( ROOT / " service-worker.js " )
match = re . search ( r " const coreScriptNames = \ [ \ s*(.*?) \ s* \ ]; " , sw , re . S )
if not match :
fail ( " service-worker.js coreScriptNames not found " )
names = re . findall ( r ' " ([^ " ]+) " ' , match . group ( 1 ) )
expected = [ Path ( p ) . stem for p in data [ " js " ] ]
if names != expected :
fail ( " service-worker.js coreScriptNames differs from manifest JS order " )
ok ( " service-worker.js core script order matches manifest " )
def node_check ( paths : list [ Path ] ) - > None :
node = " node "
for path in paths :
result = subprocess . run ( [ node , " --check " , str ( path ) ] , cwd = ROOT , text = True , capture_output = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( f " node --check failed: { path . relative_to ( ROOT ) } " )
ok ( f " node --check passed for { len ( paths ) } files " )
def action_spec_smoke ( ) - > None :
source = read_text ( ROOT / " js/tarinai_action_spec.js " )
js = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
vm . createContext ( context ) ;
vm . runInContext ( { source ! r } , context , { { filename : ' tarinai_action_spec.js ' } } ) ;
const action = context . registerTarinaiActionSpec ( { {
id : ' smoke_action ' ,
need : ' fulfill ' ,
label : ' smoke ' ,
selectTarget ( ) { { return { { id : ' target-1 ' } } ; } } ,
canStart ( t , world , ctx ) { { const target = ctx . target | | this . selectTarget ( t , world , ctx ) ; return target & & target . id == = ' target-1 ' ; } } ,
start ( t , world , ctx ) { { t . startedWith = ctx . target . id ; return true ; } } ,
tick ( ) { { return ' finished ' ; } } ,
finish ( t ) { { t . finished = true ; return true ; } } ,
} } ) ;
const subject = { { } } ;
if ( ! context . canStartTarinaiAction ( ' smoke_action ' , subject , { { } } ) ) throw new Error ( ' canStart failed ' ) ;
if ( ! context . startTarinaiAction ( action , subject , { { } } ) ) throw new Error ( ' start failed ' ) ;
if ( subject . startedWith != = ' target-1 ' ) throw new Error ( ' selectTarget/start context failed ' ) ;
if ( context . tickTarinaiAction ( ' smoke_action ' , subject , { { } } , 0.1 , { { } } ) != = ' finished ' ) throw new Error ( ' tick failed ' ) ;
if ( ! context . finishTarinaiAction ( ' smoke_action ' , subject , { { } } , { { phase : ' finished ' } } ) ) throw new Error ( ' finish failed ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " ActionSpec smoke test failed " )
ok ( " ActionSpec lifecycle smoke test passed " )
def behavior_state_smoke ( ) - > None :
source = read_text ( ROOT / " js/tarinai_behavior_state.js " )
js = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . Tarinai = class Tarinai { { } } ;
vm . createContext ( context ) ;
vm . runInContext ( { source ! r } , context , { { filename : ' tarinai_behavior_state.js ' } } ) ;
const t = new context . Tarinai ( ) ;
t . world = { { time : 12 } } ;
t . state = ' seek_food ' ;
2026-06-25 14:51:58 +09:00
context . setTarinaiBehavior ( t , { { actionId : ' eat_food ' , need : ' food ' , label : ' eat ' , reason : ' hungry ' , text : ' hungry ' , tiedNeeds : [ ' food ' , ' sleep ' ] , source : ' forced ' , forcedId : ' f1 ' , forcedRequest : { { uid : ' f1 ' , id : ' eat_food ' , source : ' test ' , priority : 99 , reasonText : ' forced hungry ' , status : ' active ' } } } } ) ;
2026-06-24 13:48:22 +09:00
if ( context . getTarinaiBehaviorId ( t ) != = ' eat_food ' ) throw new Error ( ' behavior id mismatch ' ) ;
if ( context . getTarinaiBehaviorNeed ( t ) != = ' food ' ) throw new Error ( ' behavior need mismatch ' ) ;
2026-06-25 14:51:58 +09:00
if ( ! context . isTarinaiBehaviorForced ( t ) ) throw new Error ( ' forced state not visible ' ) ;
const behavior = context . currentTarinaiBehavior ( t ) ;
if ( ! behavior | | behavior . actionId != = ' eat_food ' | | behavior . reason != = ' hungry ' | | behavior . source != = ' forced ' ) throw new Error ( ' canonical behavior fields not normalized ' ) ;
if ( ' intent ' in t | | ' currentAction ' in t | | ' activeBehavior ' in t | | ' activeForcedBehavior ' in t ) throw new Error ( ' legacy behavior accessors still present ' ) ;
2026-06-24 13:48:22 +09:00
const forced = context . getTarinaiForcedBehavior ( t ) ;
if ( ! forced | | forced . uid != = ' f1 ' | | forced . priority != = 99 ) throw new Error ( ' forced request not normalized ' ) ;
context . setTarinaiForcedBehavior ( t , null ) ;
2026-06-25 14:51:58 +09:00
if ( context . currentTarinaiBehavior ( t ) . source == = ' forced ' | | context . getTarinaiForcedBehavior ( t ) ) throw new Error ( ' forced source not cleared ' ) ;
2026-06-24 13:48:22 +09:00
context . clearTarinaiBehavior ( t ) ;
if ( context . currentTarinaiBehavior ( t ) != = null ) throw new Error ( ' behavior not cleared ' ) ;
"""
with tempfile . NamedTemporaryFile ( " w " , encoding = " utf-8 " , suffix = " .js " , delete = False ) as f :
f . write ( js )
tmp = Path ( f . name )
try :
result = subprocess . run ( [ " node " , str ( tmp ) ] , cwd = ROOT , text = True , capture_output = True )
finally :
tmp . unlink ( missing_ok = True )
if result . returncode != 0 :
sys . stderr . write ( result . stdout )
sys . stderr . write ( result . stderr )
fail ( " Behavior state smoke test failed " )
ok ( " behavior state smoke test passed " )
def check_legacy_behavior_refs ( ) - > None :
tokens = re . compile ( r " \ b(activeBehavior|currentAction|activeForcedBehavior|intent) \ b " )
offenders = [ ]
for path in ( ROOT / " js " ) . glob ( " *.js " ) :
for i , line in enumerate ( read_text ( path ) . splitlines ( ) , start = 1 ) :
2026-06-25 14:51:58 +09:00
stripped = line . strip ( )
if stripped . startswith ( " // " ) :
continue
if tokens . search ( line ) :
offenders . append ( f " { path . relative_to ( ROOT ) . as_posix ( ) } : { i } " )
2026-06-24 13:48:22 +09:00
if offenders :
2026-06-25 14:51:58 +09:00
fail ( " legacy behavior fields still present: " + " , " . join ( offenders [ : 20 ] ) )
ok ( " legacy behavior runtime fields removed " )
2026-06-24 13:48:22 +09:00
2026-06-25 14:51:58 +09:00
def check_basic_action_spec_runtime ( ) - > None :
source = " \n " . join ( read_text ( ROOT / rel ) for rel in [ " js/tarinai_action_definitions.js " , " js/tarinai_needs_items.js " ] )
2026-06-24 13:48:22 +09:00
required = {
" eat_food " : " createConsumableActionSpec " ,
" drink_water " : " createConsumableActionSpec " ,
" use_medicine " : " createConsumableActionSpec " ,
" sleep_in_bed " : " createSleepInBedActionSpec " ,
" sleep_build_grass_bed " : " createSleepBuildGrassBedActionSpec " ,
" sleep_anywhere " : " createSleepAnywhereActionSpec " ,
" rest_to_recover " : " createRestActionSpec " ,
" sunbath " : " createSunbathActionSpec " ,
" wander_lightly " : " createWanderActionSpec " ,
}
for action_id , factory in required . items ( ) :
if factory not in source :
fail ( f " basic ActionSpec factory missing for { action_id } : { factory } " )
forbidden_late_ticks = [
' set( " eat_food " , ' ,
' set( " drink_water " , ' ,
' set( " use_medicine " , ' ,
' set( " sunbath " , ' ,
]
offenders = [ token for token in forbidden_late_ticks if token in source ]
if offenders :
fail ( " basic actions still patched in configureTarinaiNeedActions: " + " , " . join ( offenders ) )
2026-06-25 14:51:58 +09:00
ok ( " basic need actions owned by ActionSpec factories " )
2026-06-24 13:48:22 +09:00
2026-06-25 14:51:58 +09:00
def check_social_action_spec_runtime ( ) - > None :
source = " \n " . join ( read_text ( ROOT / rel ) for rel in [ " js/tarinai_action_definitions.js " , " js/tarinai_needs_items.js " ] )
2026-06-24 13:48:22 +09:00
required_factories = [
" createPanicEscapeActionSpec " ,
" createFleeActionSpec " ,
" createApproachFriendActionSpec " ,
" createApproachFamilyActionSpec " ,
" createApproachMateActionSpec " ,
" createFightRivalActionSpec " ,
" createIntimidateEnemyActionSpec " ,
" createBirthRitualActionSpec " ,
]
for factory in required_factories :
if factory not in source :
fail ( f " social/combat ActionSpec factory missing: { factory } " )
definitions_match = re . search ( r " const TARINAI_ACTION_DEFINITIONS = \ [(.*?) \ ]; " , source , re . S )
if not definitions_match :
fail ( " TARINAI_ACTION_DEFINITIONS block missing " )
definitions = definitions_match . group ( 1 )
for factory in required_factories :
if f " { factory } () " not in definitions :
fail ( f " { factory } is not registered in TARINAI_ACTION_DEFINITIONS " )
if " function configureTarinaiNeedActions " in source :
2026-06-25 14:51:58 +09:00
fail ( " configureTarinaiNeedActions should not be needed after ActionSpec runtime consolidation " )
2026-06-24 13:48:22 +09:00
forbidden_late_ticks = [
' set( " panic_escape " , ' ,
' set( " flee " , ' ,
' set( " approach_friend " , ' ,
' set( " approach_parent_or_child " , ' ,
' set( " approach_mate " , ' ,
' set( " fight_rival " , ' ,
' set( " intimidate_enemy " , ' ,
' set( " sunbath " , ' ,
" birthRitualAction " ,
" TARINAI_ACTIONS.push " ,
]
offenders = [ token for token in forbidden_late_ticks if token in source ]
if offenders :
fail ( " actions still patched late: " + " , " . join ( offenders ) )
2026-06-25 14:51:58 +09:00
ok ( " social/fear/combat/mate/birth/sunbath actions owned by ActionSpec factories " )
def check_action_spec_no_legacy_hooks ( ) - > None :
source = read_text ( ROOT / " js/tarinai_action_spec.js " )
forbidden = [ " condition " , " originalRun " , " originalUpdate " , " Legacy hooks " ]
offenders = [ token for token in forbidden if token in source ]
if offenders :
fail ( " ActionSpec legacy hook support still present: " + " , " . join ( offenders ) )
needs = " \n " . join ( read_text ( ROOT / rel ) for rel in [ " js/tarinai_action_definitions.js " , " js/tarinai_consumable_behavior.js " , " js/tarinai_social_action_runtime.js " , " js/tarinai_building_behavior.js " , " js/tarinai_needs_items.js " ] )
if " .condition " in needs or " action.condition " in needs :
fail ( " need action runtime still falls back to action.condition " )
ok ( " ActionSpec legacy hooks removed " )
2026-06-24 13:48:22 +09:00
def check_runtime_diagnostics_hooks ( ) - > None :
spatial_budget = read_text ( ROOT / " js/world_spatial_budget.js " )
debug_tools = read_text ( ROOT / " js/debug_tools.js " )
world_update = read_text ( ROOT / " js/world_update.js " )
2026-06-25 14:51:58 +09:00
world_update_phases = read_text ( ROOT / " js/world_update_phases.js " )
2026-06-24 13:48:22 +09:00
if " runtimeDiagnostics() " not in spatial_budget :
fail ( " World.runtimeDiagnostics hook missing " )
2026-06-25 14:51:58 +09:00
if " lastRuntimeDiagnostics " not in ( world_update + world_update_phases ) :
fail ( " world update phases do not publish lastRuntimeDiagnostics " )
2026-06-24 13:48:22 +09:00
if " spatial rebuild " not in debug_tools or " behavior forced " not in debug_tools :
fail ( " debug overlay is missing runtime diagnostic lines " )
2026-06-25 14:51:58 +09:00
spatial_budget = read_text ( ROOT / " js/world_spatial_budget.js " )
debug_tools = read_text ( ROOT / " js/debug_tools.js " )
if " performance: " in spatial_budget or " diag.performance " in debug_tools or " load-check removed " in debug_tools :
fail ( " removed performance/load-check diagnostics residue remains " )
2026-06-24 13:48:22 +09:00
ok ( " runtime diagnostics hooks present " )
def check_social_balance_guards ( ) - > None :
2026-06-25 14:51:58 +09:00
needs = " \n " . join ( read_text ( ROOT / rel ) for rel in [
" js/tarinai_needs_core.js " ,
" js/tarinai_behavior_text.js " ,
" js/tarinai_forced_behavior.js " ,
" js/tarinai_item_targeting.js " ,
" js/tarinai_action_definitions.js " ,
" js/tarinai_consumable_behavior.js " ,
" js/tarinai_social_action_runtime.js " ,
" js/tarinai_building_behavior.js " ,
" js/tarinai_needs_items.js " ,
] )
2026-06-24 13:48:22 +09:00
world_social = read_text ( ROOT / " js/world_family_social.js " )
social_move = read_text ( ROOT / " js/tarinai_social_move_life.js " )
required_need_tokens = [
" fight_rival: 56 " ,
" intimidate_enemy: 42 " ,
" approach_mate: 68 " ,
" approach_friend: 66 " ,
" return ( { conflict: 34, mate: 24, family: 12, bond: 14 " ,
" if (subNeed !== \" conflict \" && subValue >= threshold) return true " ,
" subValue >= threshold && subValue >= socialPull * 0.82 " ,
]
for token in required_need_tokens :
if token not in needs :
fail ( f " social balance guard token missing: { token } " )
if " canStart: basicCanStartWithTarget, \n start(t, world, ctx = {} ) { \n const rival " in needs :
fail ( " intimidate_enemy reverted to ungated basicCanStart " )
required_world_tokens = [
" fightPairCooldowns " ,
" fightPairKey(a, b) " ,
" fightPairCooldownRemaining(a, b) " ,
" markFightPairCooldown(a, b " ,
" isAlreadyFightingPair(a, b) " ,
" if (this.isAlreadyFightingPair?.(a, b)) return true " ,
]
for token in required_world_tokens :
if token not in world_social :
fail ( f " fight re-entry guard missing: { token } " )
if " dt * (battleDrug ? 0.23 : 0.085) " not in social_move :
fail ( " ordinary proximity fight probability guard missing " )
ok ( " social balance and fight re-entry guards present " )
def check_item_bucket_polish ( ) - > None :
spatial_budget = read_text ( ROOT / " js/world_spatial_budget.js " )
required_tokens = [
" markItemBucketsDirty(reason " ,
" ensureItemBuckets(reason " ,
" addItem(item " ,
" itemById(id) " ,
" itemsOfType(type) " ,
" liveItemsOfType(type " ,
" this.itemIdMap = new Map() " ,
" bucketRebuildsTotal " ,
]
for token in required_tokens :
if token not in spatial_budget :
fail ( f " item bucket polish helper missing: { token } " )
if " return item && !item.dead ? item : null " not in spatial_budget :
fail ( " itemById must not return dead items " )
debug_tools = read_text ( ROOT / " js/debug_tools.js " )
if " bucket " not in debug_tools . lower ( ) and " id-map " not in debug_tools . lower ( ) :
fail ( " debug overlay is missing item bucket/id-map diagnostics " )
routed_files = {
" js/world_placement_log.js " : " addItem?.(item " ,
2026-06-25 14:51:58 +09:00
" js/world_update_phases.js " : " addItem?.(drop " ,
2026-06-24 13:48:22 +09:00
" js/world_combat_effects.js " : " addItem?.(it " ,
" js/ants.js " : " addItem?.(corpse " ,
" js/tarinai_social_move_life.js " : " addItem?.(new Item( \" trace \" " ,
2026-06-25 14:51:58 +09:00
" js/tarinai_building_behavior.js " : " addItem?.(structure " ,
2026-06-24 13:48:22 +09:00
}
for rel , token in routed_files . items ( ) :
if token not in read_text ( ROOT / rel ) :
fail ( f " runtime item creation path is not routed through addItem: { rel } " )
ok ( " runtime item bucket/id-map polish present " )
2026-06-25 14:51:58 +09:00
def check_current_save_format ( ) - > None :
2026-06-24 13:48:22 +09:00
snapshot = read_text ( ROOT / " js/snapshot_system.js " )
save = read_text ( ROOT / " js/save_system.js " )
2026-06-25 14:51:58 +09:00
save_codec = read_text ( ROOT / " js/save_codec.js " )
save_storage = read_text ( ROOT / " js/save_storage.js " )
restore_coordinator = read_text ( ROOT / " js/restore_coordinator.js " )
2026-06-24 13:48:22 +09:00
required_snapshot = [
2026-06-25 17:43:24 +09:00
" const SNAPSHOT_VERSION = 4 " ,
" a: \" tj1 \" " ,
2026-06-24 13:48:22 +09:00
" function compactWorld " ,
" function compactTarinai " ,
" function compactItem " ,
2026-06-25 17:43:24 +09:00
" worldRef.logs = [] " ,
2026-06-24 13:48:22 +09:00
" worldRef.family = {} " ,
" recordFamily " ,
]
for token in required_snapshot :
if token not in snapshot :
2026-06-25 17:43:24 +09:00
fail ( f " binary save snapshot token missing: { token } " )
2026-06-24 13:48:22 +09:00
forbidden_snapshot = [
2026-06-25 17:43:24 +09:00
" compactAnt " ,
2026-06-24 13:48:22 +09:00
" effects: (worldRef.effects " ,
2026-06-25 17:43:24 +09:00
" cameraX " ,
" cameraY " ,
" behavior: " ,
" deathReason " ,
2026-06-24 13:48:22 +09:00
" summary: " ,
" app: \" tarinai_colony_game \" " ,
]
offenders = [ token for token in forbidden_snapshot if token in snapshot ]
if offenders :
2026-06-25 17:43:24 +09:00
fail ( " broad snapshot fields still present after light-save conversion: " + " , " . join ( offenders ) )
2026-06-25 14:51:58 +09:00
required_codec = [
2026-06-25 17:43:24 +09:00
' EXPORT_PREFIX = " たり " ' ,
' RAW_CODEC = " 生 " ' ,
' DEFLATE_CODEC = " 縮 " ' ,
2026-06-24 13:48:22 +09:00
" SAVE_TEXT_ALPHABET " ,
2026-06-25 17:43:24 +09:00
" jp2048Encode " ,
" jp2048Decode " ,
2026-06-24 13:48:22 +09:00
' CompressionStream( " deflate-raw " ) ' ,
" DecompressionStream " ,
" async function encodeSnapshot " ,
" async function decodeSnapshot " ,
]
2026-06-25 14:51:58 +09:00
for token in required_codec :
if token not in save_codec :
2026-06-25 17:43:24 +09:00
fail ( f " binary save codec token missing: { token } " )
2026-06-25 14:51:58 +09:00
required_storage = [
2026-06-25 17:43:24 +09:00
' STORAGE_PREFIX = " tarinai_japanese_hash_slot_v1_ " ' ,
2026-06-25 14:51:58 +09:00
" function writeSlot " ,
" function readSlot " ,
" function requireSlot " ,
" function deleteSlot " ,
]
for token in required_storage :
if token not in save_storage :
2026-06-25 17:43:24 +09:00
fail ( f " binary save storage token missing: { token } " )
2026-06-25 14:51:58 +09:00
required_facade = [
" TarinaiSaveSystem " ,
" TarinaiSaveCodec " ,
" TarinaiSaveStorage " ,
" TarinaiRestoreCoordinator " ,
]
combined = save + save_codec + save_storage + restore_coordinator
for token in required_facade :
if token not in combined :
fail ( f " save/restore split facade token missing: { token } " )
2026-06-25 17:43:24 +09:00
for token in ( " TN2! " , " TN3! " , " tarinai_save_slot_v2_ " , " tarinai_save_slot_v3_ " , " legacyForcedColumn " ) :
2026-06-25 14:51:58 +09:00
if token in combined or token in snapshot :
fail ( f " save backward compatibility residue still present: { token } " )
if " renderStats " in snapshot or " TarinaiFreezeSystem " in snapshot or " renderArchive " in snapshot :
fail ( " snapshot restore must not call UI or freeze subsystems directly " )
freeze = read_text ( ROOT / " js/freeze_system.js " )
if " global.renderStats " in freeze or " global.render?.() " in freeze or " renderFrozenPanel(worldRef); " in freeze :
fail ( " freeze core still performs direct UI rendering instead of emitting freeze:changed " )
effects = read_text ( ROOT / " js/world_event_effects.js " )
if " freeze:changed " not in effects or " tool:placed " not in effects or " function playLogSoundForEntry " not in effects :
fail ( " event effects do not own freeze/tool/log side effects " )
placement = read_text ( ROOT / " js/world_placement_log.js " )
if " playLogSoundForEntry " in placement :
fail ( " log sound mapping still lives in world_placement_log.js " )
2026-06-25 17:43:24 +09:00
ok ( " binary hash save snapshot, codec, storage, and restore coordinator present " )
2026-06-25 14:51:58 +09:00
def check_unified_item_registry ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
if " js/item_registry.js " not in js_files :
fail ( " unified item registry missing from manifest " )
removed = [ " js/food_registry.js " , " js/effect_registry.js " , " js/item_visual_registry.js " ]
stale_manifest = [ rel for rel in removed if rel in js_files ]
if stale_manifest :
fail ( " old split item registry files still listed in manifest: " + " , " . join ( stale_manifest ) )
stale_files = [ rel for rel in removed if ( ROOT / rel ) . exists ( ) ]
if stale_files :
fail ( " old split item registry files still exist: " + " , " . join ( stale_files ) )
source = read_text ( ROOT / " js/item_registry.js " )
required = [
" global.TarinaiItemRegistry " ,
" const TOOL_DEFINITIONS " ,
" function itemDefinition " ,
" function itemVisualDefinition " ,
" class FoodRegistry " ,
" class EffectRegistry " ,
" food: FOOD_REGISTRY " ,
" effect: EFFECT_REGISTRY " ,
" visual: itemVisualDefinition " ,
]
for token in required :
if token not in source :
fail ( f " unified item registry token missing: { token } " )
forbidden_globals = [ " global.TarinaiFoodRegistry " , " global.TarinaiEffectRegistry " , " global.TARINAI_ITEM_VISUALS " , " global.TARINAI_FOOD_DEFINITIONS " , " global.TARINAI_EFFECT_DEFINITIONS " ]
offenders = [ token for token in forbidden_globals if token in source ]
if offenders :
fail ( " old split item registry globals still exported: " + " , " . join ( offenders ) )
data_source = read_text ( ROOT / " js/data.js " )
forbidden_data = [ " const TOOL_DEFINITIONS " , " const ITEM_TRAITS " , " class FoodRegistry " , " class EffectRegistry " , " TARINAI_ITEM_VISUALS " ]
offenders = [ token for token in forbidden_data if token in data_source ]
if offenders :
fail ( " item definitions still mixed into data.js: " + " , " . join ( offenders ) )
ok ( " item definitions centralized in item_registry.js " )
def check_docs_removed ( ) - > None :
docs = ROOT / " docs "
if docs . exists ( ) and any ( docs . iterdir ( ) ) :
fail ( " non-runtime docs directory still contains files " )
ok ( " non-runtime docs removed " )
def check_phase_and_input_split ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [
" js/world_update_phases.js " ,
" js/ui_input_shared.js " ,
" js/ui_input_touch.js " ,
" js/ui_input_mouse.js " ,
]
for rel in required :
if rel not in js_files :
fail ( f " split runtime module missing from manifest: { rel } " )
world_update = read_text ( ROOT / " js/world_update.js " )
phases = read_text ( ROOT / " js/world_update_phases.js " )
ui_bind = read_text ( ROOT / " js/ui_bind.js " )
touch = read_text ( ROOT / " js/ui_input_touch.js " )
mouse = read_text ( ROOT / " js/ui_input_mouse.js " )
shared = read_text ( ROOT / " js/ui_input_shared.js " )
if " TarinaiWorldUpdatePhases " not in world_update or " function update(worldRef, dt) " not in phases :
fail ( " World.update is not delegated to world_update_phases.js " )
for token in [ " updateClock " , " updateEnvironment " , " updateItemsAndAnts " , " updateCreatures " , " runMaintenance " , " spawnRainWater " ] :
if token not in phases :
fail ( f " world update phase missing: { token } " )
if " touchstart " in ui_bind or " mousedown " in ui_bind or " touchmove " in ui_bind :
fail ( " ui_bind.js still owns field pointer input handlers " )
for token in [ " TarinaiUIInputShared " , " createInputContext " ] :
if token not in shared :
fail ( f " shared input helper missing: { token } " )
if " touchstart " not in touch or " TarinaiTouchInput " not in touch :
fail ( " touch input module missing touch handlers " )
if " mousedown " not in mouse or " TarinaiMouseInput " not in mouse :
fail ( " mouse input module missing mouse handlers " )
if ui_bind . count ( " syncToolSizeBadges(); " ) != 1 :
fail ( " ui_bind.js should bind/sync tool size badges only once " )
phase_birth_direct = " audio.birth " in phases
placement_birth_direct = " audio.birth " in read_text ( ROOT / " js/world_placement_log.js " )
family_birth_direct = " audio.birth " in read_text ( ROOT / " js/world_family_social.js " )
if phase_birth_direct or placement_birth_direct or family_birth_direct :
fail ( " birth log sound can double-fire; direct audio.birth remains in log-emitting birth flows " )
ok ( " world phase split and UI input split present " )
def check_processing_split ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [
" js/item_type_initializers.js " ,
" js/item_lifecycle_runtime.js " ,
" js/item_render_runtime.js " ,
" js/tarinai_item_targeting.js " ,
" js/tarinai_action_definitions.js " ,
" js/tarinai_consumable_behavior.js " ,
" js/tarinai_social_action_runtime.js " ,
" js/tarinai_building_behavior.js " ,
]
for rel in required :
if rel not in js_files :
fail ( f " processing split module missing from manifest: { rel } " )
if not ( js_files . index ( " js/items.js " ) < js_files . index ( " js/item_type_initializers.js " ) < js_files . index ( " js/item_lifecycle_runtime.js " ) < js_files . index ( " js/item_render_runtime.js " ) < js_files . index ( " js/ants.js " ) ) :
fail ( " item init/runtime/render split load order is invalid " )
if not ( js_files . index ( " js/tarinai_forced_behavior.js " ) < js_files . index ( " js/tarinai_item_targeting.js " ) < js_files . index ( " js/tarinai_consumable_behavior.js " ) < js_files . index ( " js/tarinai_social_action_runtime.js " ) < js_files . index ( " js/tarinai_building_behavior.js " ) < js_files . index ( " js/tarinai_action_definitions.js " ) < js_files . index ( " js/tarinai_needs_items.js " ) ) :
fail ( " tarinai behavior split load order is invalid " )
items = read_text ( ROOT / " js/items.js " )
init = read_text ( ROOT / " js/item_type_initializers.js " )
lifecycle = read_text ( ROOT / " js/item_lifecycle_runtime.js " )
item_render = read_text ( ROOT / " js/item_render_runtime.js " )
if " class Item " not in items or " function initializeItemTypeState " not in init or " Object.assign(Item.prototype " not in lifecycle or " Object.assign(Item.prototype " not in item_render :
fail ( " item construction/init/lifecycle/render split is incomplete " )
if " updateDuplicator " in items or " \n draw(ctx " in items or " if (type === \" grass \" ) " in items :
fail ( " items.js still owns type init, runtime, or render methods " )
defs = read_text ( ROOT / " js/tarinai_action_definitions.js " )
consumable = read_text ( ROOT / " js/tarinai_consumable_behavior.js " )
social = read_text ( ROOT / " js/tarinai_social_action_runtime.js " )
building = read_text ( ROOT / " js/tarinai_building_behavior.js " )
needs = read_text ( ROOT / " js/tarinai_needs_items.js " )
required_pairs = [
( defs , " const TARINAI_ACTION_DEFINITIONS " ) ,
( defs , " const TARINAI_ACTIONS " ) ,
( consumable , " function updateConsumableBehavior " ) ,
( social , " function updateMateBehavior " ) ,
( social , " function updatePanicBehavior " ) ,
( building , " function continueBuildPlan " ) ,
( needs , " function chooseActionForNeed " ) ,
]
for source , token in required_pairs :
if token not in source :
fail ( f " processing split token missing: { token } " )
for token in [ " createConsumableActionSpec " , " function updateConsumableBehavior " , " function continueBuildPlan " ] :
if token in needs :
fail ( f " tarinai_needs_items.js still owns extracted processing: { token } " )
ok ( " item and Tarinai processing split present " )
2026-06-24 13:48:22 +09:00
def main ( ) - > None :
data = manifest ( )
check_files_exist ( data )
2026-06-25 14:51:58 +09:00
check_unified_item_registry ( )
check_docs_removed ( )
2026-06-24 13:48:22 +09:00
check_versions ( data )
check_index_order ( data )
check_service_worker_order ( data )
node_check ( [ * ( ROOT / " js " ) . glob ( " *.js " ) , ROOT / " service-worker.js " ] )
action_spec_smoke ( )
behavior_state_smoke ( )
2026-06-25 14:51:58 +09:00
check_basic_action_spec_runtime ( )
check_social_action_spec_runtime ( )
check_action_spec_no_legacy_hooks ( )
2026-06-24 13:48:22 +09:00
check_runtime_diagnostics_hooks ( )
check_social_balance_guards ( )
check_item_bucket_polish ( )
2026-06-25 14:51:58 +09:00
check_current_save_format ( )
check_phase_and_input_split ( )
check_processing_split ( )
2026-06-24 13:48:22 +09:00
check_legacy_behavior_refs ( )
if __name__ == " __main__ " :
main ( )