#!/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 = 3", "function compactWorld", "function compactTarinai", "function compactItem", "function compactAnt", "logs = []", "worldRef.family = {}", "recordFamily", ] for token in required_snapshot: if token not in snapshot: fail(f"current save snapshot token missing: {token}") forbidden_snapshot = [ "logs: clonePlain", "effects: (worldRef.effects", "summary:", "app: \"tarinai_colony_game\"", ] offenders = [token for token in forbidden_snapshot if token in snapshot] if offenders: fail("old broad snapshot fields still present: " + ", ".join(offenders)) required_codec = [ 'EXPORT_PREFIX = "TN3!"', "SAVE_TEXT_ALPHABET", "base91Encode", "base91Decode", 'CompressionStream("deflate-raw")', "DecompressionStream", "async function encodeSnapshot", "async function decodeSnapshot", ] for token in required_codec: if token not in save_codec: fail(f"current save codec token missing: {token}") required_storage = [ 'STORAGE_PREFIX = "tarinai_save_slot_v3_"', "function writeSlot", "function readSlot", "function requireSlot", "function deleteSlot", ] for token in required_storage: if token not in save_storage: fail(f"current 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!", "tarinai_save_slot_v2_", "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("current 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()