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 ' } } ) ;
2026-06-29 16:21:58 +09:00
const action = context . registerTarinaiActionSpecs ( [ { {
2026-06-24 13:48:22 +09:00
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 ; } } ,
2026-06-29 16:21:58 +09:00
} } ] ) [ 0 ] ;
2026-06-24 13:48:22 +09:00
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 " )
2026-07-09 16:08:11 +09:00
def need_action_runtime_bridge_smoke ( ) - > None :
items_source = read_text ( ROOT / " js/tarinai_needs_items.js " )
core_source = read_text ( ROOT / " js/tarinai_needs_core.js " )
forced_source = read_text ( ROOT / " js/tarinai_forced_behavior.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 ( { items_source ! r } , context , { { filename : ' tarinai_needs_items.js ' } } ) ;
if ( ! context . TarinaiNeedsRuntime ) throw new Error ( ' TarinaiNeedsRuntime missing ' ) ;
if ( typeof context . TarinaiNeedsRuntime . startNeedAction != = ' function ' ) throw new Error ( ' startNeedAction bridge missing ' ) ;
"""
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 ( " need-action runtime bridge smoke test failed " )
qualified_call = " globalThis.TarinaiNeedsRuntime?.startNeedAction?.( "
if core_source . count ( qualified_call ) != 2 :
fail ( " tarinai_needs_core.js must route both emergency starts through TarinaiNeedsRuntime " )
if forced_source . count ( qualified_call ) != 1 :
fail ( " tarinai_forced_behavior.js must route forced starts through TarinaiNeedsRuntime " )
if re . search ( r " (?<![. \ w])startNeedAction \ s* \ ( " , core_source + " \n " + forced_source ) :
fail ( " cross-file bare startNeedAction call remains " )
ok ( " need-action runtime bridge is exported and all cross-file calls are qualified " )
2026-06-24 13:48:22 +09:00
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-27 19:22:38 +09:00
def check_legacy_physics_props_removed ( ) - > None :
terms = [
" rotatorThickness " , " rotatorSegments " , " rotatorPowered " , " rotatorSpeed " , " rotatorAngularVelocity " ,
" poisonCollisionEnabled " , " poisonDamage " , " poisonMass " , " poisonInertia " ,
" reciprocatorPowered " , " reciprocatorSpeed " , " reciprocatorTravel " , " reciprocatorPhase " ,
" reciprocatorDirection " , " reciprocatorAxisAngle " , " reciprocatorVelocity " , " reciprocatorAnchorX " , " reciprocatorAnchorY " ,
" linkA " , " linkB " , " linkLength " , " linkMidX " , " linkMidY " , " linkMidVX " , " linkMidVY " ,
" ropeParticles " , " _physicsAwakeUntil " , " _linkAwakeUntil " ,
]
offenders = [ ]
for path in ( ROOT / " js " ) . glob ( " *.js " ) :
text = read_text ( path )
for term in terms :
if term in text :
offenders . append ( f " { path . relative_to ( ROOT ) . as_posix ( ) } : { term } " )
if offenders :
fail ( " legacy physics property names still present: " + " , " . join ( offenders [ : 20 ] ) )
ok ( " legacy physics property names removed " )
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 " ,
" 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( " 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-26 12:46:32 +09:00
simulation_systems = read_text ( ROOT / " js/simulation_systems.js " )
2026-06-24 13:48:22 +09:00
if " runtimeDiagnostics() " not in spatial_budget :
fail ( " World.runtimeDiagnostics hook missing " )
2026-06-29 16:21:58 +09:00
if " lastRuntimeDiagnostics " not in ( world_update + simulation_systems ) :
fail ( " world update systems 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 = [
2026-06-30 10:57:06 +09:00
" fight_rival: 72 " ,
" intimidate_enemy: 0 " ,
" approach_mate: 72 " ,
2026-06-24 13:48:22 +09:00
" approach_friend: 66 " ,
2026-06-30 13:40:36 +09:00
" return ( { conflict: 18, mate: 24, family: 12, bond: 14 " ,
" const relationDrive = Math.max(Number(parts.mate || 0) " ,
" drugBoosted || relationDrive >= Math.min " ,
" social-fight-vs-mate-weighted " ,
2026-06-24 13:48:22 +09:00
]
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 } " )
2026-06-30 13:40:36 +09:00
if " social-weighted-fight-mate-start " not in social_move or " pickSocialFightMate " not in social_move :
fail ( " ordinary proximity weighted fight/mate guard missing " )
2026-06-24 13:48:22 +09:00
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) " ,
" 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-26 12:46:32 +09:00
" js/simulation_ambient_system.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-26 19:04:32 +09:00
" const SNAPSHOT_VERSION = SaveSchema.SNAPSHOT_VERSION " ,
2026-06-25 17:43:24 +09:00
" 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-30 22:30:37 +09:00
' EXPORT_PREFIX = " \\ u305f " ' ,
' SAVE_TEXT_BITS = 12 ' ,
2026-07-05 18:01:36 +09:00
' SAVE_HEADER_CHARS = " \\ u3089 \\ u308A \\ u308B " ' ,
2026-06-30 22:30:37 +09:00
" writeStringTable " ,
2026-06-24 13:48:22 +09:00
" SAVE_TEXT_ALPHABET " ,
2026-06-30 22:30:37 +09:00
" jp4096Encode " ,
" jp4096Decode " ,
2026-07-05 18:01:36 +09:00
" writeCurrentItemExtra " ,
" readCurrentItemExtra " ,
2026-06-24 13:48:22 +09:00
" 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-30 22:30:37 +09:00
' hash.startsWith( " \\ u305f " ) ' ,
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 " )
effects = read_text ( ROOT / " js/world_event_effects.js " )
2026-06-26 19:04:32 +09:00
if " tool:placed " not in effects or " function playLogSoundForEntry " not in effects :
fail ( " event effects do not own tool/log side effects " )
for rel in ( " js/freeze_system.js " , " js/freeze_store.js " , " js/freeze_snapshot_adapter.js " , " js/freeze_panel.js " ) :
if ( ROOT / rel ) . exists ( ) :
fail ( f " freezer subsystem file still exists after freezer removal: { rel } " )
2026-06-25 14:51:58 +09:00
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-28 23:07:40 +09:00
if " player_item_placement " in placement :
fail ( " player item placement should not create placement log entries " )
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 " ]
2026-06-28 23:07:40 +09:00
required_files = [
" js/item_tool_definitions.js " ,
" js/item_visual_definitions.js " ,
" js/item_food_definitions.js " ,
" js/item_effect_definitions.js " ,
" js/item_registry.js " ,
]
missing = [ rel for rel in required_files if rel not in js_files or not ( ROOT / rel ) . exists ( ) ]
if missing :
fail ( " split item registry files missing: " + " , " . join ( missing ) )
order = [ js_files . index ( rel ) for rel in required_files ]
if order != sorted ( order ) :
fail ( " split item registry files are not loaded before the facade in manifest order " )
stale_files = [ rel for rel in [ " js/food_registry.js " , " js/effect_registry.js " , " js/item_visual_registry.js " ] if ( ROOT / rel ) . exists ( ) or rel in js_files ]
2026-06-25 14:51:58 +09:00
if stale_files :
2026-06-28 23:07:40 +09:00
fail ( " obsolete item registry split files remain: " + " , " . join ( stale_files ) )
tool_source = read_text ( ROOT / " js/item_tool_definitions.js " )
visual_source = read_text ( ROOT / " js/item_visual_definitions.js " )
food_source = read_text ( ROOT / " js/item_food_definitions.js " )
effect_source = read_text ( ROOT / " js/item_effect_definitions.js " )
facade_source = read_text ( ROOT / " js/item_registry.js " )
required_pairs = [
( tool_source , " const TOOL_DEFINITIONS " ) ,
( visual_source , " function itemVisualDefinition " ) ,
( food_source , " class FoodRegistry " ) ,
( food_source , " global.TarinaiFoodRegistry " ) ,
( effect_source , " class EffectRegistry " ) ,
( effect_source , " global.TarinaiEffectRegistry " ) ,
( facade_source , " global.TarinaiItemRegistry " ) ,
2026-06-29 16:21:58 +09:00
( facade_source , " food: global.TarinaiFoodRegistry " ) ,
( facade_source , " effect: global.TarinaiEffectRegistry " ) ,
2026-06-25 14:51:58 +09:00
]
2026-06-28 23:07:40 +09:00
for source , token in required_pairs :
2026-06-25 14:51:58 +09:00
if token not in source :
2026-06-28 23:07:40 +09:00
fail ( f " split item registry token missing: { token } " )
2026-06-25 14:51:58 +09:00
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 ) )
2026-06-28 23:07:40 +09:00
ok ( " item registry responsibilities split behind facade " )
2026-06-25 14:51:58 +09:00
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 = [
2026-06-26 12:46:32 +09:00
" js/simulation_runtime_helpers.js " ,
" js/simulation_environment_system.js " ,
" js/simulation_item_ant_system.js " ,
" js/simulation_effects_system.js " ,
" js/simulation_creature_system.js " ,
" js/simulation_maintenance_system.js " ,
" js/simulation_ambient_system.js " ,
" js/simulation_systems.js " ,
2026-06-25 14:51:58 +09:00
" 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 " )
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 " )
2026-06-26 12:46:32 +09:00
systems = read_text ( ROOT / " js/simulation_systems.js " )
2026-06-29 16:21:58 +09:00
if " TarinaiSimulation " not in world_update :
fail ( " World.update is not delegated to TarinaiSimulation " )
2026-06-25 14:51:58 +09:00
for token in [ " updateClock " , " updateEnvironment " , " updateItemsAndAnts " , " updateCreatures " , " runMaintenance " , " spawnRainWater " ] :
2026-06-29 16:21:58 +09:00
if token not in systems :
2026-06-25 14:51:58 +09:00
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 " )
2026-06-29 16:21:58 +09:00
phase_birth_direct = False
2026-06-25 14:51:58 +09:00
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_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 } " )
2026-06-29 16:21:58 +09:00
if not ( js_files . index ( " js/items.js " ) < js_files . index ( " js/item_type_initializers.js " ) < js_files . index ( " js/item_render_runtime.js " ) < js_files . index ( " js/ants.js " ) ) :
fail ( " item init/render split load order is invalid " )
2026-06-25 14:51:58 +09:00
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 " )
item_render = read_text ( ROOT / " js/item_render_runtime.js " )
2026-06-29 16:21:58 +09:00
if " class Item " not in items or " function initializeItemTypeState " not in init or " Object.assign(Item.prototype " not in item_render :
fail ( " item construction/init/render split is incomplete " )
2026-06-25 14:51:58 +09:00
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 " ) ,
]
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
2026-06-26 12:46:32 +09:00
def command_dispatcher_smoke ( ) - > None :
source = read_text ( ROOT / " js/command_dispatcher.js " )
js = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . World = class World { { } } ;
context . scalableToolIds = ( ) = > [ ' grass ' , ' sweet ' ] ;
context . normalizedToolSizeFor = ( world , tool ) = > ( world . toolSizes & & world . toolSizes [ tool ] ) | | world . toolSize | | ' medium ' ;
vm . createContext ( context ) ;
vm . runInContext ( { source ! r } , context , { { filename : ' command_dispatcher.js ' } } ) ;
const events = [ ] ;
const world = new context . World ( ) ;
world . tool = ' observe ' ;
world . toolSize = ' medium ' ;
world . toolSizes = { { } } ;
world . speed = 1 ;
world . paused = false ;
world . w = 1000 ;
world . h = 700 ;
world . viewportW = 500 ;
world . viewportH = 300 ;
world . clampCamera = ( ) = > { { world . clamped = true ; } } ;
world . emit = ( type , payload ) = > events . push ( [ type , payload ] ) ;
world . handleClick = ( x , y ) = > { { world . clicked = [ x , y ] ; } } ;
const target = { { x : 420 , y : 260 , dead : false } } ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' tool.select ' , toolId : ' grass ' } } ) . ok | | world . tool != = ' grass ' ) throw new Error ( ' tool select failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' tool.size.cycle ' , toolId : ' grass ' , order : [ ' small ' , ' medium ' , ' large ' ] } } ) . ok | | world . toolSize != = ' large ' ) throw new Error ( ' tool size failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' selection.focus ' , target } } ) . ok | | world . selected != = target | | world . cameraX != = 170 | | world . cameraY != = 110 ) throw new Error ( ' focus failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' selection.favorite.toggle ' } } ) . ok | | target . favorite != = true ) throw new Error ( ' favorite failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' simulation.paused.toggle ' } } ) . ok | | world . paused != = true ) throw new Error ( ' pause failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' simulation.speed.cycle ' , speeds : [ 1 , 2 , 4 ] } } ) . ok | | world . speed != = 2 ) throw new Error ( ' speed failed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' pointer.primary ' , x : 7 , y : 9 } } ) . ok | | world . clicked [ 0 ] != = 7 | | world . clicked [ 1 ] != = 9 ) throw new Error ( ' pointer failed ' ) ;
2026-06-29 16:21:58 +09:00
if ( typeof world [ ' dispatch ' + ' Command ' ] != = ' undefined ' ) throw new Error ( ' World facade should not be installed ' ) ;
if ( ! context . TarinaiCommands . dispatch ( world , { { type : ' selection.clear ' } } ) . ok | | world . selected != = null ) throw new Error ( ' selection clear failed ' ) ;
2026-06-26 12:46:32 +09:00
if ( ! events . some ( ( [ type ] ) = > type == = ' selection:changed ' ) ) throw new Error ( ' selection event missing ' ) ;
"""
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 ( " command dispatcher smoke test failed " )
ok ( " command dispatcher smoke test passed " )
def check_system_order_boundary ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [
" js/simulation_runtime_helpers.js " ,
" js/simulation_environment_system.js " ,
" js/simulation_item_ant_system.js " ,
" js/simulation_effects_system.js " ,
" js/simulation_creature_system.js " ,
" js/simulation_maintenance_system.js " ,
" js/simulation_ambient_system.js " ,
" js/simulation_systems.js " ,
" js/system_order.js " ,
" js/simulation.js " ,
" js/world_update.js " ,
]
for rel in required :
if rel not in js_files :
fail ( f " simulation boundary module missing from manifest: { rel } " )
split_order = [
" js/simulation_runtime_helpers.js " ,
" js/simulation_environment_system.js " ,
" js/simulation_item_ant_system.js " ,
" js/simulation_effects_system.js " ,
" js/simulation_creature_system.js " ,
" js/simulation_maintenance_system.js " ,
" js/simulation_ambient_system.js " ,
" js/simulation_systems.js " ,
" js/system_order.js " ,
" js/simulation.js " ,
" js/world_update.js " ,
]
if [ rel for rel in js_files if rel in split_order ] != split_order :
fail ( " simulation boundary load order is invalid " )
system_order = read_text ( ROOT / " js/system_order.js " )
simulation = read_text ( ROOT / " js/simulation.js " )
systems_source = read_text ( ROOT / " js/simulation_systems.js " )
if " TarinaiSimulationSystems " not in systems_source or " const phases = Object.freeze " not in systems_source :
fail ( " concrete simulation systems facade missing exported phase registry " )
concrete_modules = {
" js/simulation_runtime_helpers.js " : " TarinaiSimulationRuntime " ,
" js/simulation_environment_system.js " : " TarinaiEnvironmentSimulationSystem " ,
" js/simulation_item_ant_system.js " : " TarinaiItemAntSimulationSystem " ,
" js/simulation_effects_system.js " : " TarinaiEffectsSimulationSystem " ,
" js/simulation_creature_system.js " : " TarinaiCreatureSimulationSystem " ,
" js/simulation_maintenance_system.js " : " TarinaiMaintenanceSimulationSystem " ,
" js/simulation_ambient_system.js " : " TarinaiAmbientSimulationSystem " ,
}
for rel , token in concrete_modules . items ( ) :
if token not in read_text ( ROOT / rel ) :
fail ( f " concrete simulation split module missing export: { rel } " )
for token in [ " const SYSTEM_ORDER " , " clock " , " environment " , " items_and_ants " , " creatures " , " maintenance " , " diagnostics " ] :
if token not in system_order :
fail ( f " system order token missing: { token } " )
2026-06-29 16:21:58 +09:00
if " TarinaiSystemOrder " not in simulation or " Object.freeze( { update }) " not in simulation :
fail ( " simulation.js does not expose the update boundary " )
2026-06-26 12:46:32 +09:00
ok ( " named simulation system order present " )
def simulation_boundary_smoke ( ) - > None :
sources = { rel : read_text ( ROOT / rel ) for rel in [
" js/simulation_runtime_helpers.js " ,
" js/simulation_environment_system.js " ,
" js/simulation_item_ant_system.js " ,
" js/simulation_effects_system.js " ,
" js/simulation_creature_system.js " ,
" js/simulation_maintenance_system.js " ,
" js/simulation_ambient_system.js " ,
" js/simulation_systems.js " ,
" js/system_order.js " ,
" js/simulation.js " ,
] }
js = f """
const vm = require ( ' vm ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
vm . createContext ( context ) ;
for ( const [ filename , source ] of Object . entries ( { sources ! r } ) ) { {
vm . runInContext ( source , context , { { filename } } ) ;
} }
if ( ! context . TarinaiSimulationSystems ? . phases ? . updateClock ) throw new Error ( ' simulation systems missing ' ) ;
2026-06-29 16:21:58 +09:00
if ( ! context . TarinaiSystemOrder ? . update ) throw new Error ( ' system order missing ' ) ;
if ( ! context . TarinaiSimulation ? . update ) throw new Error ( ' simulation facade missing ' ) ;
2026-06-26 12:46:32 +09:00
"""
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 ( " simulation boundary smoke test failed " )
ok ( " simulation boundary smoke test passed " )
def check_update_policy_boundaries ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [ " js/item_update_policy.js " , " js/tarinai_update_policy.js " ]
for rel in required :
if rel not in js_files :
fail ( f " update policy boundary module missing from manifest: { rel } " )
2026-06-29 16:21:58 +09:00
if not ( js_files . index ( " js/item_lifecycle_support.js " ) < js_files . index ( " js/item_update_policy.js " ) < js_files . index ( " js/item_runtime.js " ) ) :
2026-06-26 12:46:32 +09:00
fail ( " item update policy load order is invalid " )
if not ( js_files . index ( " js/simulation_runtime_helpers.js " ) < js_files . index ( " js/tarinai_update_policy.js " ) < js_files . index ( " js/simulation_creature_system.js " ) ) :
fail ( " Tarinai update policy load order is invalid " )
item_policy = read_text ( ROOT / " js/item_update_policy.js " )
tarinai_policy = read_text ( ROOT / " js/tarinai_update_policy.js " )
scheduler = read_text ( ROOT / " js/item_update_scheduler.js " )
sim_helpers = read_text ( ROOT / " js/simulation_runtime_helpers.js " )
creature_system = read_text ( ROOT / " js/simulation_creature_system.js " )
required_tokens = [
( item_policy , " global.TarinaiItemUpdatePolicy " ) ,
( item_policy , " itemUpdateInterval " ) ,
( item_policy , " forScheduledItems " ) ,
( tarinai_policy , " global.TarinaiCreatureUpdatePolicy " ) ,
2026-06-29 16:21:58 +09:00
( tarinai_policy , " updateCadence " ) ,
2026-06-26 12:46:32 +09:00
( scheduler , " TarinaiItemUpdatePolicy " ) ,
( sim_helpers , " TarinaiItemUpdatePolicy " ) ,
( creature_system , " TarinaiCreatureUpdatePolicy " ) ,
]
for source , token in required_tokens :
if token not in source :
fail ( f " update policy boundary token missing: { token } " )
duplicated_item_policy_sources = [ scheduler , sim_helpers ]
for source in duplicated_item_policy_sources :
if " const REALTIME_ITEM_TYPES " in source or " const PIN_ITEM_TYPES " in source or " SCHEDULED_ITEM_TYPES = " in source :
fail ( " item update policy constants are duplicated outside item_update_policy.js " )
if " function tarinaiUpdateInterval " in creature_system :
fail ( " Tarinai update cadence still lives inside simulation_creature_system.js " )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . isServingFoodType = ( type ) = > type == = ' food ' ;
context . TarinaiItemRuntime = { {
updateOne ( item , dt ) { { item . runs = ( item . runs | | 0 ) + 1 ; item . lastDt = dt ; return true ; } }
} } ;
2026-06-29 16:21:58 +09:00
context . TarinaiPerf = { { begin ( ) { { return ( ) = > undefined ; } } , renderQualityTier ( ) { { return " normal " ; } } } } ;
context . TarinaiPhysicsWorldSystem = { { updateWorld ( ) { { return { { mechanical : 0 , constraintRan : 0 , postConstraintPairs : 0 } } ; } } } } ;
context . TarinaiInputMode = { { snapshot ( ) { { return { { touchFirst : false } } ; } } } } ;
2026-06-26 12:46:32 +09:00
vm . createContext ( context ) ;
for ( const file of [ ' js/item_update_policy.js ' , ' js/item_update_scheduler.js ' ] ) { {
vm . runInContext ( fs . readFileSync ( file , ' utf8 ' ) , context , { { filename : file } } ) ;
} }
const world = { { time : 0 , items : [ { { type : ' ball ' } } , { { type : ' food ' } } , { { type : ' grass ' } } ] , ensureItemBuckets ( ) { { } } , itemBucketRebuildsTotal : 0 } } ;
if ( context . TarinaiItemUpdateScheduler . run ( world , 0.1 ) < 1 ) throw new Error ( ' realtime item did not run ' ) ;
world . time = 5.1 ;
if ( context . TarinaiItemUpdateScheduler . run ( world , 0.1 ) < 1 ) throw new Error ( ' scheduled item did not run ' ) ;
"""
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 ( " update policy scheduler smoke test failed " )
ok ( " update policy boundaries present " )
def check_entity_runtime_boundary ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [ " js/item_runtime.js " , " js/tarinai_runtime.js " ]
for rel in required :
if rel not in js_files :
fail ( f " entity runtime boundary module missing from manifest: { rel } " )
2026-06-29 16:21:58 +09:00
if not ( js_files . index ( " js/item_lifecycle_pipeline.js " ) < js_files . index ( " js/item_runtime.js " ) < js_files . index ( " js/structure_lifecycle.js " ) ) :
2026-06-26 12:46:32 +09:00
fail ( " item runtime boundary load order is invalid " )
if not ( js_files . index ( " js/tarinai_needs_items.js " ) < js_files . index ( " js/tarinai_social_move_life.js " ) < js_files . index ( " js/tarinai_runtime.js " ) < js_files . index ( " js/tarinai_render.js " ) ) :
fail ( " Tarinai runtime boundary load order is invalid " )
item_runtime = read_text ( ROOT / " js/item_runtime.js " )
tarinai_runtime = read_text ( ROOT / " js/tarinai_runtime.js " )
item_scheduler = read_text ( ROOT / " js/item_update_scheduler.js " )
sim_helpers = read_text ( ROOT / " js/simulation_runtime_helpers.js " )
creature_system = read_text ( ROOT / " js/simulation_creature_system.js " )
required_tokens = [
( item_runtime , " global.TarinaiItemRuntime " ) ,
( item_runtime , " installPrototypeBoundary " ) ,
( item_runtime , " updateScheduled " ) ,
( tarinai_runtime , " global.TarinaiCreatureRuntime " ) ,
2026-06-29 16:21:58 +09:00
( item_scheduler , " TarinaiItemRuntime.updateOne " ) ,
( sim_helpers , " TarinaiItemRuntime.updateScheduled " ) ,
( creature_system , " TarinaiCreatureRuntime.updateOne " ) ,
2026-06-26 12:46:32 +09:00
]
for source , token in required_tokens :
if token not in source :
fail ( f " entity runtime boundary token missing: { token } " )
ok ( " entity update runtime boundary present " )
def check_tarinai_update_pipeline_boundary ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
required = [
" js/tarinai_sunbath_system.js " ,
" js/tarinai_cursor_care_system.js " ,
" js/tarinai_local_environment_system.js " ,
" js/tarinai_need_planner_system.js " ,
" js/tarinai_item_interaction_context.js " ,
" js/tarinai_food_interaction_system.js " ,
" js/tarinai_contact_item_system.js " ,
" js/tarinai_item_interaction_system.js " ,
" js/tarinai_update_step_frame.js " ,
" js/tarinai_update_step_ai.js " ,
" js/tarinai_update_step_environment.js " ,
" js/tarinai_update_step_movement.js " ,
" js/tarinai_update_step_health.js " ,
2026-07-01 14:41:05 +09:00
" js/update_step_pipeline_runner.js " ,
2026-06-26 12:46:32 +09:00
" js/tarinai_update_pipeline.js " ,
]
for rel in required :
if rel not in js_files :
fail ( f " Tarinai update pipeline module missing from manifest: { rel } " )
expected_order = [
" js/tarinai_needs_items.js " ,
" js/tarinai_need_planner_system.js " ,
" js/tarinai_item_interaction_context.js " ,
" js/tarinai_food_interaction_system.js " ,
" js/tarinai_contact_item_system.js " ,
" js/tarinai_item_interaction_system.js " ,
" js/tarinai_sunbath_system.js " ,
" js/tarinai_cursor_care_system.js " ,
" js/tarinai_local_environment_system.js " ,
" js/tarinai_social_move_life.js " ,
" js/tarinai_update_step_frame.js " ,
" js/tarinai_update_step_ai.js " ,
" js/tarinai_update_step_environment.js " ,
" js/tarinai_update_step_movement.js " ,
" js/tarinai_update_step_health.js " ,
" js/tarinai_update_pipeline.js " ,
" js/tarinai_runtime.js " ,
]
if [ rel for rel in js_files if rel in expected_order ] != expected_order :
fail ( " Tarinai update pipeline load order is invalid " )
exports = {
" js/tarinai_sunbath_system.js " : " global.TarinaiSunbathSystem " ,
" js/tarinai_cursor_care_system.js " : " global.TarinaiCursorCareSystem " ,
" js/tarinai_local_environment_system.js " : " global.TarinaiLocalEnvironmentSystem " ,
" js/tarinai_need_planner_system.js " : " global.TarinaiNeedPlannerSystem " ,
" js/tarinai_item_interaction_context.js " : " global.TarinaiItemInteractionContext " ,
" js/tarinai_food_interaction_system.js " : " global.TarinaiFoodInteractionSystem " ,
" js/tarinai_contact_item_system.js " : " global.TarinaiContactItemSystem " ,
" js/tarinai_item_interaction_system.js " : " global.TarinaiItemInteractionSystem " ,
" js/tarinai_update_step_frame.js " : " global.TarinaiFrameUpdateStep " ,
" js/tarinai_update_step_ai.js " : " global.TarinaiAiUpdateStep " ,
" js/tarinai_update_step_environment.js " : " global.TarinaiEnvironmentUpdateStep " ,
" js/tarinai_update_step_movement.js " : " global.TarinaiMovementUpdateStep " ,
" js/tarinai_update_step_health.js " : " global.TarinaiHealthUpdateStep " ,
2026-07-01 14:41:05 +09:00
" js/update_step_pipeline_runner.js " : " global.TarinaiUpdateStepPipelineRunner " ,
2026-06-26 12:46:32 +09:00
" js/tarinai_update_pipeline.js " : " global.TarinaiUpdatePipeline " ,
}
for rel , token in exports . items ( ) :
if token not in read_text ( ROOT / rel ) :
fail ( f " Tarinai update pipeline export missing: { rel } " )
runtime = read_text ( ROOT / " js/tarinai_runtime.js " )
2026-06-26 19:04:32 +09:00
if " TarinaiUpdatePipeline?.updateOne " not in runtime and " TarinaiUpdatePipeline.updateOne " not in runtime :
fail ( " Tarinai runtime does not route through TarinaiUpdatePipeline " )
2026-06-26 12:46:32 +09:00
needs_items = read_text ( ROOT / " js/tarinai_needs_items.js " )
sunbath_system = read_text ( ROOT / " js/tarinai_sunbath_system.js " )
cursor_system = read_text ( ROOT / " js/tarinai_cursor_care_system.js " )
local_env_system = read_text ( ROOT / " js/tarinai_local_environment_system.js " )
interaction_system = read_text ( ROOT / " js/tarinai_item_interaction_system.js " )
2026-06-29 16:21:58 +09:00
if " update(dt) " in needs_items or " const minute = dt / 60 " in needs_items :
fail ( " Tarinai.prototype.update residue remains in needs mixin " )
if " TarinaiItemInteractionSystem.updateOne " not in needs_items or " const foodNeed = Number(this.needRaw?.food " in needs_items :
2026-06-26 12:46:32 +09:00
fail ( " Tarinai.prototype.interactWithItems is not a thin facade over the interaction system " )
2026-07-01 14:41:05 +09:00
# These subsystems are now routed by the split update steps rather than the prototype needs mixin.
2026-06-26 12:46:32 +09:00
interaction_context = read_text ( ROOT / " js/tarinai_item_interaction_context.js " )
food_interaction = read_text ( ROOT / " js/tarinai_food_interaction_system.js " )
contact_interaction = read_text ( ROOT / " js/tarinai_contact_item_system.js " )
for source , token in [
( sunbath_system , " this.sunbathFrameTimer " ) ,
( cursor_system , " this.nextCursorHeartAt " ) ,
( local_env_system , " grassComfort " ) ,
( interaction_context , " const foodNeed = Number(tarinai.needRaw?.food " ) ,
( food_interaction , " tarinai.applyParamItemEffect " ) ,
( contact_interaction , " tarinai.applyWaterEffect(dt, it.type) " ) ,
] :
if token not in source :
fail ( f " Tarinai update subsystem missing extracted token: { token } " )
2026-06-29 16:21:58 +09:00
if " TarinaiItemInteractionContext.create " not in interaction_system or " TarinaiFoodInteractionSystem.updateCandidate " not in interaction_system or " TarinaiContactItemSystem.updateCandidate " not in interaction_system :
2026-06-26 12:46:32 +09:00
fail ( " Tarinai item interaction orchestrator does not delegate to context/food/contact candidate subsystems " )
if " for (const item of ctx.foodCandidates " not in interaction_system :
fail ( " Tarinai item interaction orchestrator does not preserve per-candidate interaction order " )
2026-06-29 16:21:58 +09:00
if " TarinaiNeedPlannerSystem.updateOne " not in needs_items or " function resolveNeedsCore " not in needs_items :
2026-06-26 19:04:32 +09:00
fail ( " Tarinai resolveNeeds is not routed through planner system " )
if " TarinaiNeedPlannerCoreSystem " not in needs_items :
fail ( " Tarinai need planner core system export missing " )
2026-06-26 12:46:32 +09:00
if " TarinaiItemInteractionSystem.updateOne " not in read_text ( ROOT / " js/tarinai_update_step_ai.js " ) :
fail ( " Tarinai AI step does not delegate item interactions to subsystem " )
if " TarinaiCursorCareSystem.updateOne " not in read_text ( ROOT / " js/tarinai_update_step_frame.js " ) :
fail ( " Tarinai frame step does not delegate cursor care to subsystem " )
if " TarinaiLocalEnvironmentSystem.updateOne " not in read_text ( ROOT / " js/tarinai_update_step_environment.js " ) :
fail ( " Tarinai environment step does not delegate local environment to subsystem " )
if " TarinaiSunbathSystem.updateOne " not in read_text ( ROOT / " js/tarinai_update_step_movement.js " ) :
fail ( " Tarinai movement step does not delegate sunbath to subsystem " )
pipeline = read_text ( ROOT / " js/tarinai_update_pipeline.js " )
2026-06-26 19:04:32 +09:00
for token in [ " TarinaiFrameUpdateStep " , " TarinaiAiUpdateStep " , " TarinaiEnvironmentUpdateStep " , " TarinaiMovementUpdateStep " , " TarinaiHealthUpdateStep " ] :
2026-06-26 12:46:32 +09:00
if token not in pipeline :
fail ( f " Tarinai update pipeline missing step token: { token } " )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . CONFIG = { { hungerPerMinute : 60 , lonelinessPerMinute : 60 , energyPerMinute : 60 } } ;
context . clamp = ( v , a , b ) = > Math . max ( a , Math . min ( b , v ) ) ;
context . rand = ( a , b ) = > ( a + b ) / 2 ;
context . pick = ( arr ) = > arr [ 0 ] ;
context . stableUnit = ( ) = > 0.5 ;
2026-06-29 16:21:58 +09:00
context . deterministicRange = ( world , salt , min , max ) = > ( min + max ) / 2 ;
context . deterministicSigned = ( ) = > 0 ;
context . deterministicChance = ( ) = > false ;
2026-06-26 12:46:32 +09:00
context . applyNeedShock = ( ) = > undefined ;
context . applyNeedRelief = ( ) = > undefined ;
context . calculateStressFromNeeds = ( ) = > 0 ;
context . applyGroundStressModifier = ( t , v ) = > v ;
context . createDefaultNeeds = ( ) = > ( { { food : 0 , sleep : 0 , health : 0 , safety : 0 , social : 0 , fulfill : 0 } } ) ;
context . tarinaiMaxEnergy = ( ) = > 100 ;
context . tarinaiMetabolicHungerScale = ( ) = > 1 ;
vm . createContext ( context ) ;
for ( const file of { required ! r } ) { {
vm . runInContext ( fs . readFileSync ( file , ' utf8 ' ) , context , { { filename : file } } ) ;
} }
const calls = [ ] ;
const world = { {
time : 1 ,
groundType : ' soil ' ,
weather : ' cloudy ' ,
colonyMood : { { id : ' relaxed ' } } ,
selected : null ,
spendScheduledWork ( ) { { return true ; } } ,
temperatureAt ( ) { { return 0.62 ; } } ,
lightLevel ( ) { { return 0.5 ; } } ,
spawnBleedEffect ( ) { { calls . push ( ' bleed ' ) ; } } ,
resolveFenceCollision ( ) { { calls . push ( ' fence ' ) ; } } ,
} } ;
const t = { {
world , dead : false , x : 10 , y : 12 , vx : 0 , vy : 0 , radius : 20 ,
2026-06-29 16:21:58 +09:00
age : 0 , reproductionTimer : 10 , eatTimer : 0 , eatCooldown : 0 , foodReactTimer : 0 ,
2026-06-26 12:46:32 +09:00
surpriseTimer : 0 , fearTimer : 0 , intimidateTimer : 0 , intimidatedTimer : 0 , stress : 0 ,
hpBarTimer : 0 , stressBarTimer : 0 , loveMochiTimer : 0 , fightMochiTimer : 0 ,
birthRitualTimer : 0 , pokeFlashTimer : 0 , cursorPetting : 0 , pinchThoughtTimer : 0 ,
sunbathCooldown : 0 , fightTimer : 0 , fightCooldown : 0 , defeatedTimer : 0 , stretchTimer : 0 ,
grassEatTimer : 0 , hurtTimer : 0 , fallTimer : 0 , tempTimer : 0 , tempComfort : 0.62 ,
hunger : 10 , loneliness : 0 , energy : 100 , trait : { { hunger : 1 , social : 1 , sleep : 1 } } ,
state : ' idle ' , aiTimer : 0 , envTimer : 0 , needs : context . createDefaultNeeds ( ) , needRaw : context . createDefaultNeeds ( ) ,
updateZunchiDisease ( ) { { calls . push ( ' zunchiDisease ' ) ; } } ,
updateSpecialDiseases ( ) { { calls . push ( ' specialDisease ' ) ; } } ,
freezeSleepDiseaseMotion ( ) { { return false ; } } ,
personalityProfile ( ) { { return { { fear : 1 } } ; } } ,
updateFacing ( ) { { calls . push ( ' facing ' ) ; } } ,
maintainTargetProgress ( ) { { calls . push ( ' targetProgress ' ) ; } } ,
move ( ) { { calls . push ( ' move ' ) ; } } ,
updateNestBoxPresence ( ) { { calls . push ( ' nestBox ' ) ; } } ,
checkLife ( ) { { calls . push ( ' checkLife ' ) ; } } ,
} } ;
if ( ! context . TarinaiUpdatePipeline . updateOne ( t , 0.016 , { { legacyUpdate ( ) { { throw new Error ( ' legacy should not run ' ) ; } } } } ) ) throw new Error ( ' pipeline update failed ' ) ;
for ( const expected of [ ' zunchiDisease ' , ' specialDisease ' , ' facing ' , ' targetProgress ' , ' move ' , ' nestBox ' , ' fence ' , ' checkLife ' ] ) { {
if ( ! calls . includes ( expected ) ) throw new Error ( ' missing pipeline call: ' + expected ) ;
} }
"""
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 ( " Tarinai update pipeline smoke test failed " )
ok ( " Tarinai update pipeline boundary present " )
def check_item_lifecycle_pipeline_boundary ( ) - > None :
data = manifest ( )
js_files = data [ " js " ]
dynamic_required = [
" js/item_dynamic_tool_system.js " ,
" js/item_dynamic_ball_system.js " ,
" js/item_dynamic_duplicator_system.js " ,
" js/item_dynamic_pin_system.js " ,
" js/item_dynamic_zunchi_system.js " ,
" js/item_dynamic_system.js " ,
]
required = [
" js/item_lifecycle_support.js " ,
2026-06-29 16:21:58 +09:00
" js/physics_helpers.js " ,
" js/pin_attachment_system.js " ,
2026-06-26 12:46:32 +09:00
* dynamic_required ,
" js/item_lifecycle_decay_system.js " ,
" js/item_lifecycle_growth_system.js " ,
" js/item_lifecycle_step_frame.js " ,
" js/item_lifecycle_step_decay.js " ,
" js/item_lifecycle_step_dynamic.js " ,
" js/item_lifecycle_step_growth.js " ,
2026-07-01 14:41:05 +09:00
" js/update_step_pipeline_runner.js " ,
2026-06-26 12:46:32 +09:00
" js/item_lifecycle_pipeline.js " ,
]
for rel in required :
if rel not in js_files :
fail ( f " Item lifecycle pipeline module missing from manifest: { rel } " )
expected_order = [
2026-06-29 16:21:58 +09:00
" js/physics_helpers.js " ,
" js/pin_attachment_system.js " ,
2026-06-26 12:46:32 +09:00
" js/item_lifecycle_support.js " ,
" js/item_update_policy.js " ,
* dynamic_required ,
" js/item_lifecycle_decay_system.js " ,
" js/item_lifecycle_growth_system.js " ,
" js/item_lifecycle_step_frame.js " ,
" js/item_lifecycle_step_decay.js " ,
" js/item_lifecycle_step_dynamic.js " ,
" js/item_lifecycle_step_growth.js " ,
2026-07-01 14:41:05 +09:00
" js/update_step_pipeline_runner.js " ,
2026-06-26 12:46:32 +09:00
" js/item_lifecycle_pipeline.js " ,
" js/item_runtime.js " ,
]
if [ rel for rel in js_files if rel in expected_order ] != expected_order :
fail ( " Item lifecycle pipeline load order is invalid " )
exports = {
" js/item_lifecycle_support.js " : " global.TarinaiItemLifecycleRuntimeSupport " ,
" js/item_dynamic_tool_system.js " : " global.TarinaiItemDynamicToolSystem " ,
" js/item_dynamic_ball_system.js " : " global.TarinaiItemDynamicBallSystem " ,
" js/item_dynamic_duplicator_system.js " : " global.TarinaiItemDynamicDuplicatorSystem " ,
" js/item_dynamic_pin_system.js " : " global.TarinaiItemDynamicPinSystem " ,
" js/item_dynamic_zunchi_system.js " : " global.TarinaiItemDynamicZunchiSystem " ,
" js/item_dynamic_system.js " : " global.TarinaiItemDynamicSystem " ,
" js/item_lifecycle_decay_system.js " : " global.TarinaiItemDecaySystem " ,
" js/item_lifecycle_growth_system.js " : " global.TarinaiItemGrowthSystem " ,
" js/item_lifecycle_step_frame.js " : " global.TarinaiItemFrameLifecycleStep " ,
" js/item_lifecycle_step_decay.js " : " global.TarinaiItemDecayLifecycleStep " ,
" js/item_lifecycle_step_dynamic.js " : " global.TarinaiItemDynamicLifecycleStep " ,
" js/item_lifecycle_step_growth.js " : " global.TarinaiItemGrowthLifecycleStep " ,
2026-07-01 14:41:05 +09:00
" js/update_step_pipeline_runner.js " : " global.TarinaiUpdateStepPipelineRunner " ,
2026-06-26 12:46:32 +09:00
" js/item_lifecycle_pipeline.js " : " global.TarinaiItemLifecyclePipeline " ,
}
for rel , token in exports . items ( ) :
if token not in read_text ( ROOT / rel ) :
fail ( f " Item lifecycle pipeline export missing: { rel } " )
runtime = read_text ( ROOT / " js/item_runtime.js " )
2026-06-26 19:04:32 +09:00
if " TarinaiItemLifecyclePipeline?.updateOne " not in runtime and " TarinaiItemLifecyclePipeline.updateOne " not in runtime :
fail ( " item runtime does not route through TarinaiItemLifecyclePipeline " )
2026-06-26 12:46:32 +09:00
pipeline = read_text ( ROOT / " js/item_lifecycle_pipeline.js " )
2026-06-26 19:04:32 +09:00
for token in [ " TarinaiItemFrameLifecycleStep " , " TarinaiItemDecayLifecycleStep " , " TarinaiItemDynamicLifecycleStep " , " TarinaiItemGrowthLifecycleStep " ] :
2026-06-26 12:46:32 +09:00
if token not in pipeline :
fail ( f " Item lifecycle pipeline missing step token: { token } " )
dynamic_step = read_text ( ROOT / " js/item_lifecycle_step_dynamic.js " )
dynamic_system = read_text ( ROOT / " js/item_dynamic_system.js " )
2026-06-29 16:21:58 +09:00
if " TarinaiItemDynamicSystem.update " not in dynamic_step :
2026-06-26 12:46:32 +09:00
fail ( " Item dynamic lifecycle step does not delegate to TarinaiItemDynamicSystem " )
for token in [ " TarinaiItemDynamicToolSystem " , " TarinaiItemDynamicBallSystem " , " TarinaiItemDynamicDuplicatorSystem " , " TarinaiItemDynamicPinSystem " , " TarinaiItemDynamicZunchiSystem " ] :
if token not in dynamic_system :
fail ( f " Item dynamic system missing subsystem token: { token } " )
for rel in [ " js/item_dynamic_tool_system.js " , " js/item_dynamic_ball_system.js " , " js/item_dynamic_duplicator_system.js " , " js/item_dynamic_pin_system.js " , " js/item_dynamic_zunchi_system.js " ] :
body = read_text ( ROOT / rel )
2026-06-26 19:04:32 +09:00
for token in [ " item.updateFan " , " item.updateRotator " , " item.updateBall " , " item.updateDuplicator " , " item.updatePushpin " , " item.updateZunchiMotion " , " item.updateZunchi " ] :
2026-06-26 12:46:32 +09:00
if token in body :
fail ( f " dynamic subsystem still delegates to prototype method { token } in { rel } " )
lifecycle_support = read_text ( ROOT / " js/item_lifecycle_support.js " )
lifecycle_decay = read_text ( ROOT / " js/item_lifecycle_step_decay.js " )
lifecycle_growth = read_text ( ROOT / " js/item_lifecycle_step_growth.js " )
2026-06-29 16:21:58 +09:00
if " TarinaiItemLifecycleRuntimeSupport " not in lifecycle_support :
2026-06-26 12:46:32 +09:00
fail ( " item lifecycle support export missing " )
2026-06-29 16:21:58 +09:00
if " TarinaiDuplicatorRuntime " in lifecycle_support :
fail ( " duplicate duplicator runtime export still present " )
if " TarinaiItemDecaySystem.update " not in lifecycle_decay or " TarinaiItemGrowthSystem.update " not in lifecycle_growth :
2026-06-26 12:46:32 +09:00
fail ( " item lifecycle decay/growth steps are not thin system adapters " )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . isServingFoodType = ( type ) = > type == = ' food ' ;
context . passiveFoodDecayInterval = ( ) = > 0.5 ;
context . passiveFoodDecayRate = ( ) = > 0.1 ;
2026-06-29 16:21:58 +09:00
context . TarinaiItemRegistry = { { food : { { } } } } ;
2026-06-26 12:46:32 +09:00
context . isPinType = ( type ) = > type == = ' pushpin ' ;
context . normalizeGrassStage = ( item ) = > { { item . normalized = true ; } } ;
context . stableUnit = ( ) = > 0.5 ;
vm . createContext ( context ) ;
for ( const file of { required ! r } ) { {
vm . runInContext ( fs . readFileSync ( file , ' utf8 ' ) , context , { { filename : file } } ) ;
} }
const events = [ ] ;
const world = { {
itemDropImpact ( item ) { { events . push ( [ ' drop ' , item . type ] ) ; } } ,
markTerrainDirty ( reason ) { { events . push ( [ ' terrain ' , reason ] ) ; } } ,
emit ( type , payload ) { { events . push ( [ ' emit ' , type , payload . type ] ) ; } } ,
} } ;
const food = { { type : ' food ' , dead : false , age : 0 , dropTimer : 0.1 , dropImpactDone : false , foodServingsRemaining : 1 , amount : 1 , passiveFoodDecayTimer : 0 } } ;
if ( ! context . TarinaiItemLifecyclePipeline . updateOne ( food , 0.6 , world , { { legacyUpdate ( ) { { throw new Error ( ' legacy should not run ' ) ; } } } } ) ) throw new Error ( ' food update failed ' ) ;
2026-06-29 16:21:58 +09:00
if ( food . age != = 0.6 | | ! food . dropImpactDone | | ! ( food . foodServingsRemaining < 1 ) | | ! events . some ( ( [ kind , value ] ) = > kind == = ' terrain ' & & value == = ' food-passive-decay ' ) ) throw new Error ( ' frame/decay step did not run ' ) ;
2026-06-26 12:46:32 +09:00
context . CONFIG = { { worldPadding : 30 } } ;
context . clamp = ( v , min , max ) = > Math . max ( min , Math . min ( max , v ) ) ;
context . distXY = ( ax , ay , bx , by ) = > Math . hypot ( ax - bx , ay - by ) ;
const dynamic = { {
type : ' ball ' , dead : false , age : 0 , x : 80 , y : 80 , prevX : 80 , prevY : 80 ,
vx : 12 , vy : 0 , r : 18 , spin : 0 , spinVelocity : 0
} } ;
const ballWorld = { { . . . world , w : 500 , h : 500 , nearbyItems ( ) { { return [ ] ; } } } } ;
context . TarinaiItemLifecyclePipeline . updateOne ( dynamic , 0.25 , ballWorld , { { legacyUpdate ( ) { { throw new Error ( ' legacy should not run ' ) ; } } } } ) ;
if ( ! ( dynamic . x > 80 ) | | ! ( dynamic . spin > 0 ) ) throw new Error ( ' dynamic step did not run ' ) ;
const grass = { { type : ' grass ' , dead : false , age : 0 } } ;
context . TarinaiItemLifecyclePipeline . updateOne ( grass , 0.1 , world , { { legacyUpdate ( ) { { throw new Error ( ' legacy should not run ' ) ; } } } } ) ;
2026-06-29 16:21:58 +09:00
if ( grass . normalized ) throw new Error ( ' grass should not be normalized by per-item growth step ' ) ;
2026-06-26 12:46:32 +09:00
"""
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 ( " Item lifecycle pipeline smoke test failed " )
ok ( " Item lifecycle pipeline boundary present " )
2026-07-09 16:08:11 +09:00
def mechanical_collision_and_editor_smoke ( ) - > None :
footprints = read_text ( ROOT / " js/collision_footprint_system.js " )
editor_source = read_text ( ROOT / " js/physics_shape_editor_system.js " )
placement_source = read_text ( ROOT / " js/world_placement_log.js " )
for token in ( " rectCorners, " , " rectAxes, " , " projectPoints, " , " aabbOverlap, " , " rectOverlapInfo, " , " rectsOverlap, " ) :
if token not in footprints :
fail ( f " mechanical SAT primitive is not exported: { token } " )
if " const DEFAULT_GRID_SIZE = 20; " not in editor_source :
fail ( " mechanical shape editor grid must use coarse 20px snapping " )
if editor_source . count ( " snapPoint( " ) < 2 or " gridSize: DEFAULT_GRID_SIZE " not in editor_source :
fail ( " mechanical shape editor does not route pointer coordinates through grid snapping " )
if placement_source . count ( " 20px \\ u9593 \\ u9694 " ) < 2 :
fail ( " mechanical editor hints do not disclose 20px grid snapping " )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . TarinaiPerf = { { begin ( ) { { return null ; } } } } ;
context . TarinaiConstraintSystem = { { updateWorld ( ) { { return 0 ; } } } } ;
context . normalizedItemAngle = angle = > angle ;
vm . createContext ( context ) ;
for ( const path of { repr ( [ str ( ROOT / rel ) for rel in [
' js/math.js ' , ' js/geometry_helpers.js ' , ' js/collision_footprint_system.js ' ,
' js/physics_world_system.js ' , ' js/mechanical_system.js ' , ' js/physics_shape_editor_system.js '
] ] ) } ) vm . runInContext ( fs . readFileSync ( path , ' utf8 ' ) , context , { { filename : path } } ) ;
const fp = context . TarinaiCollisionFootprints ;
for ( const key of [ ' rectCorners ' , ' rectAxes ' , ' projectPoints ' , ' aabbOverlap ' , ' rectOverlapInfo ' , ' rectsOverlap ' ] ) { {
if ( typeof fp [ key ] != = ' function ' ) throw new Error ( ` missing footprint primitive : $ { { key } } ` ) ;
} }
const editor = context . TarinaiPhysicsShapeEditorSystem ;
if ( editor . gridSize != = 20 ) throw new Error ( ' editor grid size mismatch ' ) ;
const snapped = editor . snapPoint ( { { x : 13 , y : 31 } } ) ;
if ( snapped . x != = 20 | | snapped . y != = 40 ) throw new Error ( ' pointer did not snap to grid ' ) ;
const cleaned = editor . cleanSegments ( [ [ 13 , 31 , 91 , 44 ] , [ - 52 , - 20 , 52 , - 20 ] ] , { { fallback : [ - 80 , 0 , 80 , 0 ] } } ) ;
if ( ! cleaned . length | | cleaned . some ( seg = > seg . some ( value = > Math . abs ( value % 20 ) > 1e-9 ) ) ) throw new Error ( ' segment endpoints are not grid aligned ' ) ;
if ( cleaned . some ( seg = > Math . hypot ( seg [ 2 ] - seg [ 0 ] , seg [ 3 ] - seg [ 1 ] ) < 20 ) ) throw new Error ( ' editor retained sub-grid segments ' ) ;
let serial = 0 ;
function makeItem ( type , x , y ) { {
return { { id : ` m $ { { + + serial } } ` , type , x , y , angle : 0 , r : 64 , amount : 999 , dead : false } } ;
} }
function activate ( item ) { {
const api = context . TarinaiPhysicsBodySystem ;
api . setSegments ( item , [ [ - 80 , 0 , 80 , 0 ] ] , ' test-shape ' ) ;
api . setScalar ( item , ' solid ' , true , ' test-solid ' ) ;
if ( item . type == = ' rotator ' ) { { api . setScalar ( item , ' motorOn ' , true , ' test ' ) ; api . setScalar ( item , ' motorSpeed ' , 1.4 , ' test ' ) ; } }
if ( item . type == = ' reciprocator ' ) { { api . setScalar ( item , ' railOn ' , true , ' test ' ) ; api . setScalar ( item , ' railAxis ' , 0 , ' test ' ) ; api . setScalar ( item , ' railMotorSpeed ' , 80 , ' test ' ) ; } }
if ( item . type == = ' poison_block ' ) { { api . setScalar ( item , ' xv ' , 24 , ' test ' ) ; api . setScalar ( item , ' awakeUntil ' , 10 , ' test ' ) ; } }
} }
function pairResult ( typeA , typeB , disableB = false ) { {
const a = makeItem ( typeA , 120 , 120 ) ;
const b = makeItem ( typeB , 120 , 128 ) ;
const items = [ a , b ] ;
const world = { {
items , tarinai : [ ] , time : 1 , frameCount : 1 , w : 900 , h : 700 , drawListDirty : false ,
itemCounts : { { rotator : 0 , poison_block : 0 , reciprocator : 0 } } ,
ensureItemBuckets ( ) { { } } , markSpatialDirty ( ) { { } } , markItemBucketsDirty ( ) { { } } ,
nearbyObstacles ( ) { { return items ; } } , nearbyItems ( ) { { return items ; } } ,
itemsOfType ( type ) { { return items . filter ( item = > item . type == = type & & ! item . dead ) ; } } ,
} } ;
for ( const item of items ) { { item . world = world ; world . itemCounts [ item . type ] + = 1 ; context . TarinaiPhysicsBodySystem . ensureBody ( item , world ) ; activate ( item ) ; } }
if ( disableB ) context . TarinaiPhysicsBodySystem . setScalar ( b , ' solid ' , false , ' test-disabled ' ) ;
const solved = context . TarinaiMechanicalSystem . resolveMechanicalPairs ( world , 0.016 , { { maxPairs : 40 } } ) ;
return { { solved , a , b } } ;
} }
for ( const [ a , b ] of [ [ ' rotator ' , ' rotator ' ] , [ ' rotator ' , ' reciprocator ' ] , [ ' rotator ' , ' poison_block ' ] , [ ' reciprocator ' , ' reciprocator ' ] , [ ' reciprocator ' , ' poison_block ' ] , [ ' poison_block ' , ' poison_block ' ] ] ) { {
const result = pairResult ( a , b , false ) ;
if ( ! ( result . solved > 0 ) ) throw new Error ( ` $ { { a } } / $ { { b } } collision - on pair did not interact ` ) ;
const disabled = pairResult ( a , b , true ) ;
if ( disabled . solved != = 0 ) throw new Error ( ` $ { { a } } / $ { { b } } collision - off pair still interacted ` ) ;
} }
"""
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 ( " mechanical collision/editor smoke test failed " )
ok ( " collision-enabled mechanical pairs interact and editor geometry snaps to the 20px lattice " )
def colony_limits_and_defaults_smoke ( ) - > None :
tool_defs = read_text ( ROOT / " js/item_tool_definitions.js " )
initializer = read_text ( ROOT / " js/item_type_initializers.js " )
world_budget = read_text ( ROOT / " js/world_spatial_budget.js " )
world_family = read_text ( ROOT / " js/world_family_social.js " )
needs = read_text ( ROOT / " js/tarinai_needs_items.js " )
social = read_text ( ROOT / " js/tarinai_social_action_runtime.js " )
snapshot = read_text ( ROOT / " js/snapshot_system.js " )
codec = read_text ( ROOT / " js/save_codec.js " )
index = read_text ( ROOT / " index.html " )
if ' t === " reciprocator " ) return Math.PI / 2 ' not in tool_defs :
fail ( " reciprocator default angle is not 90 degrees " )
small = " [[-52, -20, 52, -20], [52, -20, 52, 20], [52, 20, -52, 20], [-52, 20, -52, -20]] "
poison_files = [
" js/item_type_initializers.js " , " js/physics_world_system.js " , " js/item_render_runtime.js " ,
" js/mechanical_system.js " , " js/world_placement_log.js " ,
]
for rel in poison_files :
body = read_text ( ROOT / rel )
if not all ( token in body for token in ( " -52 " , " -20 " , " 52 " , " 20 " ) ) :
fail ( f " small poison-block shape missing from { rel } " )
if " [-82, -28, 82, -28] " in body :
fail ( f " legacy large poison-block shape remains in { rel } " )
if ' radius: 44 ' not in tool_defs or ' thickness: 11 ' not in initializer :
fail ( " small poison-block radius/thickness defaults are not synchronized " )
for token in ( " canAddTarinai(count = 1) " , " canAddObjects(count = 1) " , " setTarinaiPopulationLimit " , " setObjectLimit " ) :
if token not in world_budget :
fail ( f " colony limit runtime missing: { token } " )
if " if (!(this.canAddTarinai?.(1) ?? true)) return null; " not in world_family :
fail ( " tarinai creation/birth is not unconditionally guarded by population limit " )
if " if (!this.canAddObjects(1)) return null; " not in world_budget :
fail ( " object creation is not unconditionally guarded by object limit " )
if " ignoreColonyLimit " in world_family or " ignoreColonyLimit " in world_budget :
fail ( " colony limit bypass option remains in central creation APIs " )
unsafe_fallback = re . compile ( r " addItem \ ? \ .[^ \ n;]+ \ | \ |[^ \ n;]*items \ .push " )
for rel in ( ROOT / " js " ) . glob ( " *.js " ) :
if unsafe_fallback . search ( read_text ( rel ) ) :
fail ( f " object-limit bypass fallback remains in { rel . name } " )
direct_item_pushes = [ ]
direct_tarinai_pushes = [ ]
for rel in ( ROOT / " js " ) . glob ( " *.js " ) :
body = read_text ( rel )
if re . search ( r " \ b(?:this|worldRef|world) \ .items \ .push \ ( " , body ) :
direct_item_pushes . append ( rel . name )
if re . search ( r " \ b(?:this|worldRef|world) \ .tarinai \ .push \ ( " , body ) :
direct_tarinai_pushes . append ( rel . name )
if sorted ( direct_item_pushes ) != [ " snapshot_system.js " , " world_spatial_budget.js " ] :
fail ( " unexpected direct world item insertion can bypass object limit: " + " , " . join ( sorted ( direct_item_pushes ) ) )
if sorted ( direct_tarinai_pushes ) != [ " snapshot_system.js " , " world_family_social.js " ] :
fail ( " unexpected direct tarinai insertion can bypass population limit: " + " , " . join ( sorted ( direct_tarinai_pushes ) ) )
guarded_sources = {
" js/item_dynamic_ball_system.js " : " if (!addedDrop) continue; " ,
" js/fire_runtime_system.js " : " if (!addedFire) return null; " ,
" js/tarinai_building_behavior.js " : " canAddObjects?.(1) " ,
" js/world_combat_effects.js " : " if (!spawned) return 0; " ,
}
for rel , token in guarded_sources . items ( ) :
if token not in read_text ( ROOT / rel ) :
fail ( f " object-limit guard missing from { rel } : { token } " )
if " championMate " not in needs or " championPair " not in social or " mateWeight *= 5.0 " not in social :
fail ( " champion-pair breeding preference is incomplete " )
for token in ( " tarinaiPopulationLimit " , " objectLimit " ) :
if token not in snapshot or token not in codec :
fail ( f " colony limit persistence missing: { token } " )
for element_id in ( " tarinaiPopulationLimitInput " , " objectLimitInput " , " colonyLimitStatus " ) :
if f ' id= " { element_id } " ' not in index :
fail ( f " colony limit UI missing: { element_id } " )
js = f """
const vm = require ( ' vm ' ) ;
const fs = require ( ' fs ' ) ;
class World { {
constructor ( ) { {
this . items = [ ] ;
this . tarinai = [ ] ;
this . itemCounts = { { } } ;
this . tarinaiPopulationLimit = 0 ;
this . objectLimit = 0 ;
} }
markItemBucketsDirty ( ) { { } }
markSpatialDirty ( ) { { } }
markTerrainDirty ( ) { { } }
markTerrainDirtyAt ( ) { { } }
} }
const context = { { console , World , CONFIG : { { grassLimit : Infinity } } , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . TarinaiGround = { { grassLimit ( ) { { return Infinity ; } } } } ;
context . TarinaiPerf = { { snapshot ( ) { { return null ; } } , begin ( ) { { return null ; } } } } ;
vm . createContext ( context ) ;
vm . runInContext ( fs . readFileSync ( { str ( ROOT / ' js/world_spatial_budget.js ' ) ! r } , ' utf8 ' ) , context ) ;
const world = new context . World ( ) ;
if ( world . tarinaiPopulationLimit != = 0 | | world . objectLimit != = 0 ) throw new Error ( ' limits must default to unlimited ' ) ;
world . setObjectLimit ( 2.9 ) ;
if ( world . objectLimit != = 2 ) throw new Error ( ' object limit must normalize to a positive integer ' ) ;
if ( ! world . canAddObjects ( 2 ) | | world . canAddObjects ( 3 ) ) throw new Error ( ' object capacity query is incorrect ' ) ;
if ( ! world . addItem ( { { type : ' stone ' } } , ' test-1 ' ) ) throw new Error ( ' first object rejected ' ) ;
if ( ! world . addItem ( { { type : ' stone ' } } , ' test-2 ' ) ) throw new Error ( ' second object rejected ' ) ;
if ( world . addItem ( { { type : ' stone ' } } , ' test-3 ' ) != = null ) throw new Error ( ' object limit not enforced ' ) ;
world . items [ 0 ] . dead = true ;
if ( ! world . addItem ( { { type : ' stone ' } } , ' test-4 ' ) ) throw new Error ( ' dead object should free capacity ' ) ;
world . setObjectLimit ( 1 ) ;
if ( world . addItem ( { { type : ' stone ' } } , ' over-limit-existing ' ) != = null ) throw new Error ( ' existing over-limit world must reject new objects ' ) ;
for ( const item of world . items ) item . dead = true ;
if ( ! world . addItem ( { { type : ' stone ' } } , ' capacity-restored ' ) ) throw new Error ( ' capacity was not restored after all live objects died ' ) ;
world . setTarinaiPopulationLimit ( 1.8 ) ;
if ( world . tarinaiPopulationLimit != = 1 ) throw new Error ( ' tarinai limit must normalize to a positive integer ' ) ;
world . tarinai . push ( { { dead : false } } ) ;
if ( world . canAddTarinai ( 1 ) ) throw new Error ( ' tarinai limit not enforced ' ) ;
world . tarinai [ 0 ] . dead = true ;
if ( ! world . canAddTarinai ( 1 ) ) throw new Error ( ' dead tarinai should free capacity ' ) ;
world . setObjectLimit ( ' ' ) ;
if ( world . objectLimit != = 0 | | ! world . canAddObjects ( 1000 ) ) throw new Error ( ' blank/zero must mean unlimited ' ) ;
context . TarinaiSnapshot = { { version : 23 } } ;
context . TarinaiSaveSchema = { { BINARY_SCHEMA_VERSION : 28 , FIELD_IDS : [ ' garden ' ] } } ;
context . CONFIG . dayLength = 120 ;
context . TextEncoder = TextEncoder ;
context . TextDecoder = TextDecoder ;
vm . runInContext ( fs . readFileSync ( { str ( ROOT / ' js/save_codec.js ' ) ! r } , ' utf8 ' ) , context ) ;
( async ( ) = > { {
const source = { { v : 23 , a : ' tj1 ' , m : [ 1 , 0 , ' garden ' ] , w : [ 0 , 0 , 0 , 120 , 0 , - 9990 , 1 , 0 , ' seed ' , 0 , 12 , 345 ] , t : [ ] , i : [ ] } } ;
const encoded = await context . TarinaiSaveCodec . encodeSnapshot ( source ) ;
const decoded = await context . TarinaiSaveCodec . decodeSnapshot ( encoded ) ;
if ( decoded . w [ 10 ] != = 12 | | decoded . w [ 11 ] != = 345 ) throw new Error ( ' colony limits did not survive save roundtrip ' ) ;
} } ) ( ) . catch ( error = > { { console . error ( error ) ; process . exitCode = 1 ; } } ) ;
"""
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 ( " colony limit runtime smoke test failed " )
ok ( " champion breeding, mechanical defaults, and colony limits are wired " )
def physics_tuning_smoke ( ) - > None :
editor = read_text ( ROOT / " js/physics_shape_editor_system.js " )
environment = read_text ( ROOT / " js/world_environment.js " )
helpers = read_text ( ROOT / " js/physics_helpers.js " )
constraints = read_text ( ROOT / " js/constraint_system.js " )
dynamic_tools = read_text ( ROOT / " js/item_dynamic_tool_system.js " )
initializer = read_text ( ROOT / " js/item_type_initializers.js " )
for token in ( " BOUNCE_FENCE_RESTITUTION = 2.40 " , " BOUNCE_FENCE_MIN_SPEED = 520 " , " BOUNCE_FENCE_MAX_SPEED = 1180 " ) :
if token not in environment :
fail ( f " strong bounce-fence tuning missing: { token } " )
if " const iterations = 8 " not in constraints or " const compliance = mode === \" rod \" ? 0.085 / (step * 60) : 0 " not in constraints :
fail ( " rope endpoint constraint is still compliant/stretchable " )
if " STICKY_BOMB_BLAST_SCALE = 0.68 " not in dynamic_tools or " item.blastScale = 0.68 " not in initializer :
fail ( " sticky bomb is not synchronized with small-firecracker blast scale " )
js = f """
const vm = require ( ' vm ' ) ;
const editorSource = { editor ! r } ;
const helperSource = { helpers ! r } ;
const constraintSource = { constraints ! r } ;
const dynamicSource = { dynamic_tools ! r } ;
const context = { { console , globalThis : null } } ;
context . globalThis = context ;
context . window = context ;
context . clamp = ( v , lo , hi ) = > Math . max ( lo , Math . min ( hi , v ) ) ;
context . distXY = ( x1 , y1 , x2 , y2 ) = > Math . hypot ( x2 - x1 , y2 - y1 ) ;
context . CONFIG = { { worldPadding : 0 } } ;
context . Effect = function Effect ( ) { { } } ;
context . TarinaiGeometry = { {
pointSegmentDistance ( px , py , x1 , y1 , x2 , y2 ) { {
const dx = x2 - x1 , dy = y2 - y1 ;
const den = dx * dx + dy * dy | | 1 ;
const t = Math . max ( 0 , Math . min ( 1 , ( ( px - x1 ) * dx + ( py - y1 ) * dy ) / den ) ) ;
return Math . hypot ( px - ( x1 + dx * t ) , py - ( y1 + dy * t ) ) ;
} } ,
rectLocalPoint ( rect , x , y ) { { return { { x : x - ( rect . cx | | 0 ) , y : y - ( rect . cy | | 0 ) } } ; } } ,
rectWorldVector ( _rect , x , y ) { { return { { x , y } } ; } } ,
rectWorldNormal ( _rect , x , y ) { { return { { x , y } } ; } } ,
} } ;
context . TarinaiMechanicalSystem = { {
isMechanicalType ( ) { { return false ; } } ,
isPowered ( ) { { return false ; } } ,
applySurfaceVelocityToCircle ( ) { { } } ,
wakeItem ( ) { { } } ,
} } ;
context . TarinaiPhysicsProjectionSystem = { { markSpatialDirty ( ) { { return false ; } } } } ;
context . TarinaiMovementUpdateStep = { { markSleepExternalMotion ( ) { { } } } } ;
context . itemAngleFor = item = > Number ( item ? . angle | | 0 ) | | 0 ;
context . normalizedItemAngle = angle = > Number ( angle | | 0 ) | | 0 ;
vm . createContext ( context ) ;
vm . runInContext ( editorSource , context , { { filename : ' physics_shape_editor_system.js ' } } ) ;
const circle = context . TarinaiPhysicsShapeEditorSystem . templateSegments ( ' circle ' ) ;
if ( ! Array . isArray ( circle ) | | circle . length != = 24 ) throw new Error ( ' circle template is not a 24-sided contour ' ) ;
for ( const segment of circle ) { {
if ( segment . length != = 4 | | segment . some ( value = > value % 20 != = 0 ) ) throw new Error ( ' circle template left the 20px grid ' ) ;
} }
for ( let i = 0 ; i < circle . length ; i + = 1 ) { {
const next = circle [ ( i + 1 ) % circle . length ] ;
if ( circle [ i ] [ 2 ] != = next [ 0 ] | | circle [ i ] [ 3 ] != = next [ 1 ] ) throw new Error ( ' circle contour is not closed ' ) ;
} }
vm . runInContext ( helperSource , context , { { filename : ' physics_helpers.js ' } } ) ;
const ball = { { x : - 4 , y : 5 , prevX : - 14 , prevY : 5 , vx : 100 , vy : 0 , r : 5 , spinVelocity : 0 } } ;
const fence = { { left : 0 , right : 10 , top : 0 , bottom : 10 , bounce : true , restitution : 2.40 , minBounceSpeed : 520 } } ;
if ( ! context . TarinaiPhysics . bounceCircleOffRect ( ball , fence , 5 , fence . restitution , null , { { minBounceSpeed : fence . minBounceSpeed } } ) ) throw new Error ( ' bounce-fence collision did not resolve ' ) ;
if ( ball . vx > - 519 ) throw new Error ( ' bounce-fence launch is not substantially stronger ' ) ;
context . TarinaiPhysicsBodySystem = { {
scalar ( item , key , fallback ) { { return Number ( item . physicsBody ? . [ key ] ? ? fallback ) ; } } ,
setScalar ( item , key , value ) { { item . physicsBody | | = { { } } ; item . physicsBody [ key ] = value ; return true ; } } ,
linkScalar ( item , key , fallback ) { {
const c = item . physicsConstraint | | { { } } ;
const map = { { len : ' length ' , midX : ' midX ' , midY : ' midY ' , midVx : ' midVx ' , midVy : ' midVy ' , awakeUntil : ' awakeUntil ' } } ;
return Number ( c [ map [ key ] | | key ] ? ? fallback ) ;
} } ,
setLinkScalar ( item , key , value ) { {
const c = item . physicsConstraint | | = { { } } ;
const map = { { len : ' length ' , midX : ' midX ' , midY : ' midY ' , midVx : ' midVx ' , midVy : ' midVy ' , awakeUntil : ' awakeUntil ' } } ;
c [ map [ key ] | | key ] = value ;
return true ;
} } ,
endpoint ( item , index ) { { return item . physicsConstraint ? . endpoints ? . [ index ] | | null ; } } ,
particles ( item ) { { return item . physicsConstraint ? . particles | | null ; } } ,
setParticles ( item , list ) { { item . physicsConstraint . particles = list ; return true ; } } ,
ensureConstraint ( item ) { { return item . physicsConstraint ; } } ,
applyConstraintState ( ) { { return true ; } } ,
normalizeConstraintState ( _item , state ) { { return state ; } } ,
syncPoseFromItem ( ) { { return true ; } } ,
normalizeBodyState ( ) { { return true ; } } ,
markBodyChanged ( ) { { return true ; } } ,
} } ;
vm . runInContext ( constraintSource , context , { { filename : ' constraint_system.js ' } } ) ;
const a = { { id : ' a ' , type : ' ball ' , x : 0 , y : 0 , r : 10 , dead : false } } ;
const b = { { id : ' b ' , type : ' ball ' , x : 200 , y : 0 , r : 10 , dead : false } } ;
const objects = new Map ( [ [ a . id , a ] , [ b . id , b ] ] ) ;
const world = { {
time : 1 ,
items : [ a , b ] ,
tarinai : [ ] ,
itemCounts : { { rope : 1 } } ,
itemById ( id ) { { return objects . get ( id ) | | null ; } } ,
nearbyTarinai ( ) { { return [ ] ; } } ,
nearbySolidObstacleRects ( ) { { return [ ] ; } } ,
markSpatialDirty ( ) { { } } ,
drawListDirty : false ,
} } ;
const rope = { {
id : ' rope-1 ' , type : ' rope ' , dead : false , amount : 999 , world ,
physicsConstraint : { { endpoints : [ { { kind : ' item ' , id : ' a ' , x : 0 , y : 0 } } , { { kind : ' item ' , id : ' b ' , x : 200 , y : 0 } } ] , length : 100 , particles : null } } ,
} } ;
context . TarinaiConstraintSystem . updateFlexibleLink ( rope , 1 / 60 , world ) ;
const tautDistance = Math . hypot ( b . x - a . x , b . y - a . y ) ;
if ( tautDistance > 100.26 ) throw new Error ( ` rope stretched to $ { { tautDistance } } px ` ) ;
const beforeA = { { x : a . x , y : a . y } } , beforeB = { { x : b . x , y : b . y } } ;
b . x = a . x + 60 ;
context . TarinaiConstraintSystem . updateFlexibleLink ( rope , 1 / 60 , world ) ;
if ( Math . hypot ( a . x - beforeA . x , a . y - beforeA . y ) > 0.01 ) throw new Error ( ' slack rope pushed endpoint A ' ) ;
if ( Math . abs ( ( b . x - a . x ) - 60 ) > 0.01 ) throw new Error ( ' slack rope pushed endpoints apart ' ) ;
context . deterministicChance = ( ) = > false ;
context . Effect = function Effect ( ) { { } } ;
context . audio = { { explode ( ) { { } } } } ;
vm . runInContext ( dynamicSource , context , { { filename : ' item_dynamic_tool_system.js ' } } ) ;
let capturedScale = null ;
const sticky = { { type : ' sticky_bomb ' , dead : false , fuseTimer : 0 , stickyBombCarrierId : ' ' , stickyBombTransferCooldown : 0 , spin : 0 , spinVelocity : 0 } } ;
const stickyWorld = { { tarinai : [ ] , explodeFirecracker ( item ) { { capturedScale = item . blastScale ; return true ; } } } } ;
context . TarinaiItemDynamicToolSystem . update ( sticky , 1 / 60 , stickyWorld ) ;
if ( Math . abs ( capturedScale - 0.68 ) > 1e-9 ) throw new Error ( ' sticky bomb does not match small firecracker blast scale ' ) ;
"""
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 ( " physics tuning smoke test failed " )
ok ( " round circle template, strong bounce fence, inextensible rope, and small sticky-bomb blast verified " )
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-07-09 16:08:11 +09:00
need_action_runtime_bridge_smoke ( )
mechanical_collision_and_editor_smoke ( )
physics_tuning_smoke ( )
colony_limits_and_defaults_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 ( )
2026-06-26 12:46:32 +09:00
check_system_order_boundary ( )
simulation_boundary_smoke ( )
check_entity_runtime_boundary ( )
check_item_lifecycle_pipeline_boundary ( )
check_tarinai_update_pipeline_boundary ( )
check_update_policy_boundaries ( )
2026-06-25 14:51:58 +09:00
check_processing_split ( )
2026-06-26 12:46:32 +09:00
command_dispatcher_smoke ( )
2026-06-24 13:48:22 +09:00
check_legacy_behavior_refs ( )
2026-06-27 19:22:38 +09:00
check_legacy_physics_props_removed ( )
2026-06-24 13:48:22 +09:00
if __name__ == " __main__ " :
main ( )