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