#!/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 covers static contracts and focused runtime smoke tests. The current codec intentionally supports only the current binary schema. """ 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.registerTarinaiActionSpecs([{{ 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; }}, }}])[0]; 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 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"(? 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_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") def check_redundant_correction_paths_removed() -> None: terms = [ "markBodyChanged", "purgeLegacyPhysicsStorage", "nextTarinaiCollisionCheckAt", "_physicsWorldFrame", "bodyReads", "bodyWrites", "constraintReads", "constraintWrites", "compactEffects", ] 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("redundant correction path returned: " + ", ".join(offenders[:20])) ball_source = read_text(ROOT / "js/item_dynamic_ball_system.js") if ball_source.count("tryBallPickup(") != 1: fail("ball update must perform exactly one pickup check") ok("redundant correction paths remain removed") def direct_feeding_smoke() -> None: source = read_text(ROOT / "js/tarinai_direct_feeding_system.js") placement = read_text(ROOT / "js/world_placement_log.js") ui = read_text(ROOT / "js/ui_input_shared.js") render = read_text(ROOT / "js/render.js") world_source = read_text(ROOT / "js/world.js") js = f""" const vm = require('vm'); class TestEffect {{ constructor(type, x, y, options = {{}}) {{ Object.assign(this, {{ type, x, y }}, options); }} }} const context = {{ console, globalThis: null, window: null, rand(a, b) {{ return (Number(a) + Number(b)) * 0.5; }}, distXY(ax, ay, bx, by) {{ return Math.hypot(ax - bx, ay - by); }}, isServingFoodType(type) {{ return !['grass', 'water'].includes(type); }}, toolLabel(type) {{ return ({{ sweet: '\u305a\u3093\u3060\u9905', protein: '\u30d7\u30ed\u30c6\u30a4\u30f3' }})[type] || type; }}, TarinaiEffect: TestEffect, TarinaiItemRegistry: {{ food: {{ nutrition(type) {{ return type === 'sweet' ? 16.5 : 15; }} }} }}, uiCache: {{ selectedSnapshot: 'stale' }}, }}; context.globalThis = context; context.window = context; context.consumeBehaviorTarget = (t, world, item, role) => {{ item.consumedRole = role; item.foodServingsRemaining = Math.max(0, Number(item.foodServingsRemaining || 1) - 1); return true; }}; context.TarinaiStructureLifecycle = {{ deleteItem(world, item) {{ item.dead = true; world.items = world.items.filter(entry => entry !== item); return true; }} }}; vm.createContext(context); vm.runInContext({source!r}, context, {{ filename: 'tarinai_direct_feeding_system.js' }}); const api = context.TarinaiDirectFeedingSystem; if (!api || !api.isDirectFeedType('sweet') || !api.isDirectFeedType('protein') || api.isDirectFeedType('ball')) throw new Error('direct-feed eligibility mismatch'); const world = {{ tarinai: [], items: [], effects: [], logs: [], time: 12, hoverGiveTarget: null, isTarinaiHiddenInNestBox() {{ return false; }}, log(text, kind, meta) {{ this.logs.push({{ text, kind, meta }}); }}, recordFamily(t) {{ this.recorded = t; }}, }}; function makeTarinai(name, x, y) {{ return {{ name, x, y, radius: 22, world, affection: 0, currentPersonality: {{ openness: 0, sociability: 0 }}, contains(px, py) {{ return Math.hypot(px - this.x, py - this.y) <= 24; }}, adjustPersonality(key, delta, reason) {{ this.currentPersonality[key] += delta; this.personalityReasons = this.personalityReasons || []; this.personalityReasons.push(reason); return delta; }}, bubble(text) {{ this.lastBubble = text; }}, }}; }} const first = makeTarinai('first', 20, 20); const top = makeTarinai('top', 20, 20); world.tarinai = [first, top]; if (api.findTargetAt(world, 20, 20) !== top) throw new Error('top-most direct-feed target was not selected'); const sweet = {{ type: 'sweet', amount: 5, foodServingsRemaining: 5, x: 20, y: 20 }}; world.items.push(sweet); const result = api.give(world, top, sweet, {{ source: 'pinch' }}); if (!result?.foodGift || !result.removed || !sweet.dead || world.items.includes(sweet)) throw new Error('pinched food was not consumed and removed'); const hearts = world.effects.filter(effect => effect.type === 'heart'); if (hearts.length < 10 || hearts.length > 14) throw new Error(`food gift heart count out of range: ${{hearts.length}}`); if (!(top.currentPersonality.openness > 0.018) || !(top.currentPersonality.sociability >= 0.010)) throw new Error('food gift personality effect is too weak'); if (top.currentPersonality.openness <= 0.006) throw new Error('food gift does not exceed one second of cursor openness effect'); const expectedPersonalityReason = '\u30ab\u30fc\u30bd\u30eb\u304b\u3089\u76f4\u63a5\u98df\u3079\u7269\u3092\u98df\u3079\u305f'; if (!top.personalityReasons?.length || top.personalityReasons.some(reason => reason !== expectedPersonalityReason)) throw new Error('direct-feed personality reason mismatch'); if (top.lastBubble !== '\u2661' || context.uiCache.selectedSnapshot !== '') throw new Error('food gift feedback/cache refresh missing'); if (!world.logs.some(entry => entry.meta?.eventType === 'direct_feed')) throw new Error('direct-feed observation log missing'); const priorEffects = world.effects.length; const medicine = {{ type: 'protein', amount: 5, foodServingsRemaining: 5, x: 20, y: 20 }}; world.items.push(medicine); const medicineResult = api.give(world, top, medicine, {{ source: 'pinch' }}); if (!medicineResult || medicineResult.foodGift || medicine.consumedRole !== 'medicine') throw new Error('medicine direct-feed path mismatch'); if (world.effects.length !== priorEffects) throw new Error('medicine incorrectly emitted food hearts'); if (!(top.focusPulseTimer >= 0.72)) throw new Error('medicine feedback pulse missing'); const placementItem = {{ type: 'sweet', amount: 5, foodServingsRemaining: 5, x: 20, y: 20 }}; const placementResult = api.give(world, top, placementItem, {{ source: 'placement' }}); if (!placementResult || placementResult.removed || placementItem.dead) throw new Error('ephemeral placement item should be consumed without lifecycle deletion'); """ 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("direct feeding runtime smoke test failed") placement_start = placement.find("placeItem(item, dropped = true)") direct_call = placement.find("directFeeding.give?.", placement_start) object_limit = placement.find("this.canAddObjects?.(1)", placement_start) blocked = placement.find("this.placementBlocked(item)", placement_start) if min(placement_start, direct_call, object_limit, blocked) < 0 or not (direct_call < object_limit < blocked): fail("direct placement feeding must run before object-limit and overlap rejection") click_section = placement.find("const itemType = toolItemType(tool)") click_direct = placement.find("const directTarget =", click_section) duplicator_set = placement.find("this.directSetDuplicatorAt?.", click_section) if min(click_section, click_direct, duplicator_set) < 0 or duplicator_set > click_direct: fail("Duplicator loading must take precedence over Tarinai direct feeding at the same point") required_ui = ["directFeedTargetAt", 'source: "pinch"', "setGiveHover", "hoverGiveTarget"] if any(token not in ui for token in required_ui): fail("pinch direct-feeding input bridge is incomplete") if 'label: "\\u4e0e\\u3048\\u308b"' not in render or "world.hoverGiveTarget" not in render: fail("direct-feeding hover overlay is missing") if world_source.count("this.hoverGiveTarget = null;") < 2: fail("direct-feeding hover state is not initialized and reset") ok("direct food/medicine feeding, placement interception, personality, hearts, and overlay verified") def check_collision_reason_and_projection_dirty_removed() -> None: collision_paths = [ ROOT / "js/world_environment.js", ROOT / "js/tarinai_update_step_movement.js", ROOT / "js/tarinai_social_move_life.js", ] collision_source = "\n".join(read_text(path) for path in collision_paths) if re.search(r"resolveSolidObstacleCollision\s*\([^)]*\{[^}]*\breason\s*:", collision_source, re.S): fail("unused resolveSolidObstacleCollision reason option returned") environment = read_text(ROOT / "js/world_environment.js") if "opts.reason" in environment: fail("resolveSolidObstacleCollision still reads unused reason option") constraint = read_text(ROOT / "js/constraint_system.js") match = re.search(r"function finishLinkSpatialUpdate\([^)]*\)\s*\{(.*?)\n \}", constraint, re.S) if not match: fail("finishLinkSpatialUpdate helper not found") body = match.group(1) if "Math.hypot" in body or "before" in body or body.count("markConstraintSpatialDirty") != 1: fail("constraint Projection dirty determination is duplicated") ok("unused collision reason and duplicate constraint Projection dirty determination 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", "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("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") simulation_systems = read_text(ROOT / "js/simulation_systems.js") if "runtimeDiagnostics()" not in spatial_budget: fail("World.runtimeDiagnostics hook missing") if "lastRuntimeDiagnostics" not in (world_update + simulation_systems): fail("world update systems 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: 72", "intimidate_enemy: 0", "approach_mate: 72", "approach_friend: 66", "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", ] 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 "social-weighted-fight-mate-start" not in social_move or "pickSocialFightMate" not in social_move: fail("ordinary proximity weighted fight/mate 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)", "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/simulation_ambient_system.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 = SaveSchema.SNAPSHOT_VERSION", "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 = "\\u305f"', 'SAVE_TEXT_BITS = 15', "writeStringTable", "saveTextCharFromValue", "saveTextValueFromCode", "base32768Encode", "base32768Decode", "makeSaveFrame", "openSaveFrame", "writeTarinaiBitPlane", "readTarinaiBitPlane", "TARINAI_COLUMN_LAYOUT", "chooseCommonExtraPlan", "standardItemAmount", "chooseAmountPlan", "worldSeedWords", "saveTextCharFromValue", "saveTextValueFromCode", "itemFlagPlaneBytes", "writeNormalItemExtra", "readNormalItemExtra", "writeBirthDescriptor", "readBirthDescriptor", "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}") if "SAVE_TEXT_ALPHABET" in save_codec or "SAVE_TEXT_REVERSE" in save_codec: fail("15-bit save text still allocates the legacy alphabet or reverse map") if "writer.u(Snapshot.version)" in save_codec: fail("duplicate snapshot version remains in the binary header") if "writeSaveString(writer, w[8]" in save_codec: fail("worldSeed is still stored as a string") for token in ("formatHeader & 0x7f", "formatHeader & 0x80", "payload.length * 2 + (useCompressed ? 1 : 0)"): if token not in save_codec: fail(f"compact save framing token missing: {token}") if "tailMask" in save_codec: fail("redundant Tarinai tailMask remains in current schema") if "costBits < best.costBits" not in save_codec or "itemFlagPlaneBytes" not in save_codec: fail("cost-based common extra or continuous item flag plane is missing") required_storage = [ 'STORAGE_PREFIX = "tarinai_japanese_hash_slot_v1_"', 'hash.startsWith("\\u305f")', "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") effects = read_text(ROOT / "js/world_event_effects.js") 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}") placement = read_text(ROOT / "js/world_placement_log.js") if "playLogSoundForEntry" in placement: fail("log sound mapping still lives in world_placement_log.js") if "player_item_placement" in placement: fail("player item placement should not create placement log entries") ok("binary hash save snapshot, codec, storage, and restore coordinator present") def check_unified_item_registry() -> None: data = manifest() js_files = data["js"] 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] if stale_files: 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"), (facade_source, "food: global.TarinaiFoodRegistry"), (facade_source, "effect: global.TarinaiEffectRegistry"), ] for source, token in required_pairs: if token not in source: fail(f"split item registry token missing: {token}") 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 registry responsibilities split behind facade") 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/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/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") systems = read_text(ROOT / "js/simulation_systems.js") if "TarinaiSimulation" not in world_update: fail("World.update is not delegated to TarinaiSimulation") for token in ["updateClock", "updateEnvironment", "updateItemsAndAnts", "updateCreatures", "runMaintenance", "spawnRainWater"]: if token not in systems: 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 = False 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}") 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") 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") temperature_system = read_text(ROOT / "js/world_temperature_system.js") init = read_text(ROOT / "js/item_type_initializers.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 item_render: fail("item construction/init/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"), ] 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 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'); 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'); 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}") if "TarinaiSystemOrder" not in simulation or "Object.freeze({ update })" not in simulation: fail("simulation.js does not expose the update boundary") 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'); if (!context.TarinaiSystemOrder?.update) throw new Error('system order missing'); if (!context.TarinaiSimulation?.update) throw new Error('simulation facade 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("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}") 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")): 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"), (tarinai_policy, "updateCadence"), (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; }} }}; context.TarinaiPerf = {{ begin() {{ return () => undefined; }}, renderQualityTier() {{ return "normal"; }} }}; context.TarinaiPhysicsWorldSystem = {{ updateWorld() {{ return {{ mechanical: 0, constraintRan: 0, postConstraintPairs: 0 }}; }} }}; context.TarinaiInputMode = {{ snapshot() {{ return {{ touchFirst: false }}; }} }}; 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}") if not (js_files.index("js/item_lifecycle_pipeline.js") < js_files.index("js/item_runtime.js") < js_files.index("js/structure_lifecycle.js")): 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"), (item_scheduler, "TarinaiItemRuntime.updateOne"), (sim_helpers, "TarinaiItemRuntime.updateScheduled"), (creature_system, "TarinaiCreatureRuntime.updateOne"), ] 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", "js/update_step_pipeline_runner.js", "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", "js/update_step_pipeline_runner.js": "global.TarinaiUpdateStepPipelineRunner", "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") if "TarinaiUpdatePipeline?.updateOne" not in runtime and "TarinaiUpdatePipeline.updateOne" not in runtime: fail("Tarinai runtime does not route through TarinaiUpdatePipeline") 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") 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: fail("Tarinai.prototype.interactWithItems is not a thin facade over the interaction system") # These subsystems are now routed by the split update steps rather than the prototype needs mixin. 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}") if "TarinaiItemInteractionContext.create" not in interaction_system or "TarinaiFoodInteractionSystem.updateCandidate" not in interaction_system or "TarinaiContactItemSystem.updateCandidate" not in interaction_system: 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") if "TarinaiNeedPlannerSystem.updateOne" not in needs_items or "function resolveNeedsCore" not in needs_items: fail("Tarinai resolveNeeds is not routed through planner system") if "TarinaiNeedPlannerCoreSystem" not in needs_items: fail("Tarinai need planner core system export missing") 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") for token in ["TarinaiFrameUpdateStep", "TarinaiAiUpdateStep", "TarinaiEnvironmentUpdateStep", "TarinaiMovementUpdateStep", "TarinaiHealthUpdateStep"]: 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; context.deterministicRange = (world, salt, min, max) => (min + max) / 2; context.deterministicSigned = () => 0; context.deterministicChance = () => false; 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, age: 0, reproductionTimer: 10, eatTimer: 0, eatCooldown: 0, foodReactTimer: 0, 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", "js/physics_helpers.js", "js/pin_attachment_system.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", "js/update_step_pipeline_runner.js", "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 = [ "js/physics_helpers.js", "js/pin_attachment_system.js", "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", "js/update_step_pipeline_runner.js", "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", "js/update_step_pipeline_runner.js": "global.TarinaiUpdateStepPipelineRunner", "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") if "TarinaiItemLifecyclePipeline?.updateOne" not in runtime and "TarinaiItemLifecyclePipeline.updateOne" not in runtime: fail("item runtime does not route through TarinaiItemLifecyclePipeline") pipeline = read_text(ROOT / "js/item_lifecycle_pipeline.js") for token in ["TarinaiItemFrameLifecycleStep", "TarinaiItemDecayLifecycleStep", "TarinaiItemDynamicLifecycleStep", "TarinaiItemGrowthLifecycleStep"]: 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") if "TarinaiItemDynamicSystem.update" not in dynamic_step: 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) for token in ["item.updateFan", "item.updateRotator", "item.updateBall", "item.updateDuplicator", "item.updatePushpin", "item.updateZunchiMotion", "item.updateZunchi"]: 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") if "TarinaiItemLifecycleRuntimeSupport" not in lifecycle_support: fail("item lifecycle support export missing") 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: 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; context.updateServingFoodVisualSize = (item) => {{ item.r = (item.r || 12) - 0.1; return true; }}; context.TarinaiItemRegistry = {{ food: {{ }} }}; 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]); }}, markItemBucketsDirty(reason) {{ events.push(['buckets', reason]); }}, markSpatialDirty(reason) {{ events.push(['spatial', 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'); if (food.age !== 0.6 || !food.dropImpactDone || !(food.foodServingsRemaining < 1) || !events.some(([kind, value]) => kind === 'buckets' && value === 'food-passive-resize') || !events.some(([kind, value]) => kind === 'spatial' && value === 'food-passive-resize')) throw new Error('frame/decay step did not run'); 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'); }} }}); if (grass.normalized) throw new Error('grass should not be normalized by per-item growth step'); """ 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") 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; context.isRotatableItemType = type => ['rotator', 'poison_block', 'reciprocator'].includes(String(type || '')); context.defaultItemAngle = type => String(type || '') === 'reciprocator' ? Math.PI / 2 : 0; context.itemRadiusFor = (_type, fallback = 12) => fallback; context.toolItemType = tool => tool; 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/placement_preview_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'); }} }} const reverseRotator = makeItem('rotator', 80, 80); reverseRotator.world = {{ time: 0 }}; context.TarinaiPhysicsBodySystem.ensureBody(reverseRotator, reverseRotator.world); context.TarinaiPhysicsBodySystem.setScalar(reverseRotator, 'motorSpeed', -1.25, 'reverse-test'); if (Math.abs(context.TarinaiPhysicsBodySystem.scalar(reverseRotator, 'motorSpeed', 0) + 1.25) > 1e-9) throw new Error('negative rotator motor speed was clamped'); reverseRotator.angle = 1.0; reverseRotator.world = {{ time: 0, drawListDirty: false, markSpatialDirty() {{}}, nearbyItems() {{ return []; }}, nearbyObstacles() {{ return []; }}, items: [], tarinai: [] }}; context.TarinaiMechanicalSystem.updateRotator(reverseRotator, 0.1, reverseRotator.world, {{ skipInteractions: true }}); if (!(reverseRotator.angle < 0.99)) throw new Error('negative rotator speed did not rotate backward'); 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`); }} const orientationWorld = {{ toolAngleFor() {{ return 0; }} }}; const previewReciprocator = makeItem('reciprocator', 40, 40); context.TarinaiPlacementPreviewSystem.applyToolOrientation(orientationWorld, previewReciprocator, 0); if (Math.abs(previewReciprocator.physicsBody.pose.angle - Math.PI / 2) > 1e-9) throw new Error('reciprocator placement body angle mismatch'); if (Math.abs(previewReciprocator.physicsBody.rail.axisAngle) > 1e-9) throw new Error('reciprocator placement rail angle mismatch'); const rail = makeItem('reciprocator', 0, 0); context.TarinaiPlacementPreviewSystem.applyToolOrientation(orientationWorld, rail, 0); activate(rail); const fence = {{ id: 'fence', type: 'fence_v', x: 8, y: 0, amount: 999, dead: false }}; const collisionWorld = {{ items: [rail, fence], tarinai: [], time: 1, frameCount: 1, w: 900, h: 700, drawListDirty: false, itemBucketRebuildsTotal: 0, ensureItemBuckets() {{}}, markSpatialDirty() {{}}, nearbyObstacles() {{ return this.items; }}, nearbyItems() {{ return this.items; }}, itemsOfType(type) {{ return this.items.filter(item => item.type === type && !item.dead); }}, isFenceType(type) {{ return type === 'fence_v'; }}, solidObstacleRects(item) {{ if (item !== fence) return []; return [{{ left: 4, right: 12, top: -100, bottom: 100, cx: 8, cy: 0, halfW: 4, halfH: 100, angle: 0, cos: 1, sin: 0, oriented: true, type: 'fence_v', item }}]; }}, }}; rail.world = collisionWorld; fence.world = collisionWorld; context.TarinaiMechanicalSystem.updateWorld(collisionWorld, 0.016, {{ maxSubsteps: 1 }}); if (context.TarinaiPhysicsBodySystem.scalar(rail, 'railDir', 1) !== -1) throw new Error('reciprocator did not reverse at a solid obstacle'); const oneWayRail = makeItem('reciprocator', 0, 0); context.TarinaiPlacementPreviewSystem.applyToolOrientation(orientationWorld, oneWayRail, 0); activate(oneWayRail); const oneWayFence = {{ id: 'one-way-fence', type: 'one_way_fence', x: 8, y: 0, amount: 999, dead: false }}; const oneWayWorld = {{ items: [oneWayRail, oneWayFence], tarinai: [], time: 1, frameCount: 2, w: 900, h: 700, drawListDirty: false, itemBucketRebuildsTotal: 0, ensureItemBuckets() {{}}, markSpatialDirty() {{}}, nearbyObstacles() {{ return this.items; }}, nearbyItems() {{ return this.items; }}, itemsOfType(type) {{ return this.items.filter(item => item.type === type && !item.dead); }}, isFenceType(type) {{ return type === 'one_way_fence'; }}, solidObstacleRects(item) {{ if (item !== oneWayFence) return []; return [{{ left: 4, right: 12, top: -100, bottom: 100, cx: 8, cy: 0, halfW: 4, halfH: 100, angle: 0, cos: 1, sin: 0, oriented: true, type: 'one_way_fence', item, oneWay: true, oneWayNx: 1, oneWayNy: 0 }}]; }}, }}; oneWayRail.world = oneWayWorld; oneWayFence.world = oneWayWorld; context.TarinaiMechanicalSystem.updateWorld(oneWayWorld, 0.016, {{ maxSubsteps: 1 }}); if (!Number.isFinite(context.TarinaiPhysicsBodySystem.scalar(oneWayRail, 'railDir', 0))) throw new Error('one-way mechanical fence update produced an invalid rail direction'); const cargoRail = makeItem('reciprocator', 0, 0); context.TarinaiPlacementPreviewSystem.applyToolOrientation(orientationWorld, cargoRail, 0); activate(cargoRail); const ball = {{ id: 'ball', type: 'ball', x: 8, y: 0, r: 18, radius: 18, vx: 0, vy: 0, dead: false }}; const cargoWorld = {{ items: [cargoRail, ball], tarinai: [], time: 2, frameCount: 2, w: 900, h: 700, drawListDirty: false, itemBucketRebuildsTotal: 0, ensureItemBuckets() {{}}, markSpatialDirty() {{}}, nearbyObstacles() {{ return this.items; }}, nearbyItems() {{ return this.items; }}, itemsOfType(type) {{ return this.items.filter(item => item.type === type && !item.dead); }}, isFenceType() {{ return false; }}, solidObstacleRects() {{ return []; }}, }}; cargoRail.world = cargoWorld; ball.world = cargoWorld; context.TarinaiMechanicalSystem.updateWorld(cargoWorld, 0.016, {{ maxSubsteps: 1 }}); if (context.TarinaiPhysicsBodySystem.scalar(cargoRail, 'railDir', 1) !== 1) throw new Error('reciprocator reversed on movable cargo'); if (!(ball.x > 8 || Math.abs(ball.vx) > 0.01 || Math.abs(ball.vy) > 0.01)) throw new Error('reciprocator did not push movable cargo'); """ 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("mechanical pairs, obstacle-only reciprocator reversal, cargo pushing, placement orientation, and 20px editor snapping verified") 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 *= 4.0" not in social: fail("champion-pair breeding preference is incomplete") charts = read_text(ROOT / "js/ui_charts.js") family_social = read_text(ROOT / "js/world_family_social.js") if 'key: "births"' not in charts or 'key: "deaths"' not in charts or "birthEventCount" not in charts: fail("colony population chart birth/death series are missing") if "inheritedChampionPersonality" not in family_social or "parent.isTarinaiChampion" not in family_social or "* 0.70" not in family_social: fail("champion personality inheritance is missing") inheritance_block = family_social.split("function inheritedChampionPersonality", 1)[1].split("(function (global)", 1)[0] if "isZunchiSlave" in inheritance_block or "zunchiSlaveLocked" in inheritance_block: fail("champion inheritance must not alter zunchi-slave handling") 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: 27 }}; context.TarinaiSaveSchema = {{ BINARY_SCHEMA_VERSION: 33, FIELD_IDS: ['garden'], itemTypeValue(i, fallback = '') {{ return fallback; }} }}; 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: 27, 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 = 4.20", "BOUNCE_FENCE_MIN_SPEED = 820", "BOUNCE_FENCE_MAX_SPEED = 1680"): 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; }}, syncPoseFromItem() {{ 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") def champion_inheritance_smoke() -> None: source = read_text(ROOT / "js/world_family_social.js") start = source.find("function inheritedChampionPersonality") end = source.find("(function (global)", start) if start < 0 or end < 0: fail("champion inheritance helper not found") helper = source[start:end] js = f""" const vm = require('vm'); const context = {{ console, globalThis: null }}; context.globalThis = context; context.normalizePersonality = source => ({{ aggression: Math.max(-1, Math.min(1, Number(source?.aggression || 0))), openness: Math.max(-1, Math.min(1, Number(source?.openness || 0))), sociability: Math.max(-1, Math.min(1, Number(source?.sociability || 0))), neuroticism: Math.max(-1, Math.min(1, Number(source?.neuroticism || 0))), }}); context.clampPersonalityValue = value => Math.max(-1, Math.min(1, Number(value) || 0)); context.TarinaiSeedFactory = {{ birthProfile() {{ return {{ birthPersonality: {{ aggression: 0, openness: 0, sociability: 0, neuroticism: 0 }} }}; }} }}; vm.createContext(context); vm.runInContext({helper!r}, context, {{ filename: 'champion_inheritance.js' }}); const normal = {{ birthSeed: 'normal', birthPersonality: {{ aggression: 0, openness: 0, sociability: 0, neuroticism: 0 }}, isTarinaiChampion: false }}; const champion = {{ birthSeed: 'champion', birthPersonality: {{ aggression: 0, openness: 0, sociability: 0, neuroticism: 0 }}, isTarinaiChampion: true }}; const child = context.inheritedChampionPersonality('child', [champion, normal], {{}}); if (!child || child.aggression < 0.5 || child.openness < 0.5 || child.sociability < 0.5) throw new Error('champion boost was not inherited'); if (Math.abs(child.neuroticism) > 1e-9) throw new Error('unboosted personality axis was inherited'); const descendant = {{ birthSeed: 'descendant', birthPersonality: child, isTarinaiChampion: false }}; const grandchild = context.inheritedChampionPersonality('grandchild', [descendant, normal], {{}}); if (!grandchild || !(grandchild.aggression > 0 && grandchild.aggression < child.aggression)) throw new Error('champion lineage did not damp across generations'); """ 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("champion inheritance smoke test failed") ok("champion personality inheritance and generational damping verified") def one_way_routing_smoke() -> None: geometry_source = read_text(ROOT / "js/geometry_helpers.js") source = read_text(ROOT / "js/world_environment.js") render = read_text(ROOT / "js/render.js") js = f""" const vm = require('vm'); class World {{}} const context = {{ console, World, globalThis: null, window: null, CONFIG: {{}}, TarinaiMechanicalShapeSystem: {{ isMechanicalType() {{ return false; }}, reach() {{ return 0; }} }}, TarinaiMechanicalSystem: {{}}, TarinaiPhysicsBodySystem: {{}}, }}; context.globalThis = context; context.window = context; vm.createContext(context); vm.runInContext({geometry_source!r}, context, {{ filename: 'geometry_helpers.js' }}); vm.runInContext({source!r}, context, {{ filename: 'world_environment.js' }}); const world = new context.World(); world.isFenceType = type => ['fence_h', 'one_way_fence'].includes(type); const oneWay = {{ type: 'one_way_fence', item: {{ type: 'one_way_fence' }}, oneWay: true, oneWayNx: 1, oneWayNy: 0, oriented: false, left: -4, right: 4, top: -20, bottom: 20 }}; world.nearbySolidObstacleRects = () => [oneWay]; if (world.pathBlockedByFence(-30, 0, 30, 0, 0)) throw new Error('one-way fence blocked arrow direction'); if (!world.pathBlockedByFence(30, 0, -30, 0, 0)) throw new Error('one-way fence allowed reverse direction'); if (world.pointBlockedByObstacle(0, 0, 0, {{ directionalOneWay: true }})) throw new Error('one-way fence permanently blocked grid point'); if (!world.pointBlockedByObstacle(0, 0, 0)) throw new Error('one-way fence lost physical point occupancy'); const normal = {{ type: 'fence_h', item: {{ type: 'fence_h' }}, oriented: false, left: -4, right: 4, top: -20, bottom: 20 }}; world.nearbySolidObstacleRects = () => [normal]; if (!world.pathBlockedByFence(-30, 0, 30, 0, 0)) throw new Error('ordinary fence stopped blocking routes'); """ 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("one-way fence routing smoke test failed") if 'type === "one_way_fence"' not in render or "arrowLength" not in render: fail("one-way fence placement direction overlay is missing") ok("one-way fence preview and directional routing verified") def check_save_stream_deadlock_guard() -> None: codec = read_text(ROOT / "js/save_codec.js") ui = read_text(ROOT / "js/save_system.js") read_task = codec.find("const readTask = (async () =>") write_call = codec.find("await sink.write(") if read_task < 0 or write_call < 0 or read_task > write_call: fail("save compression output must be drained before awaiting writes") if 'exportBtn.dataset.busy = "1"' not in ui or 'save text generation failed' not in ui: fail("save export UI must recover from pending/error states") ok("save compression stream backpressure guard present") def check_electrocution_gate_chart_and_ascii() -> None: health = read_text(ROOT / "js/health.js") combat_effects = read_text(ROOT / "js/world_combat_effects.js") action_state = read_text(ROOT / "js/tarinai_action_state.js") signal = read_text(ROOT / "js/signal_system.js") charts = read_text(ROOT / "js/ui_charts.js") bind = read_text(ROOT / "js/ui_bind.js") panel = read_text(ROOT / "css/panel.css") if '\\u611f\\u96fb' not in health or '\\u901a\\u96fb\\u4e2d\\u306e\\u96fb\\u7dda' not in health: fail("electrocution cause normalization is missing") if '"gate_fence"' not in signal or 'node.gateOpen = nextOpen' not in signal or 'signal-gate-toggle' not in signal: fail("gate fence signal output handling is missing") if '