1249 lines
61 KiB
Python
1249 lines
61 KiB
Python
#!/usr/bin/env python3
|
|
"""Lightweight regression guard for the buildless Tarinai app.
|
|
|
|
Checks static-file consistency, generated-load order, syntax, and the ActionSpec
|
|
registry contract. This intentionally avoids save-data migration checks; save
|
|
compatibility is intentionally not retained for save format changes.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MANIFEST = ROOT / "app_manifest.json"
|
|
|
|
|
|
def fail(message: str) -> None:
|
|
print(f"[FAIL] {message}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
|
|
def ok(message: str) -> None:
|
|
print(f"[OK] {message}")
|
|
|
|
|
|
def read_text(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def manifest() -> dict:
|
|
try:
|
|
data = json.loads(read_text(MANIFEST))
|
|
except Exception as exc: # pragma: no cover - diagnostic path
|
|
fail(f"cannot read app_manifest.json: {exc}")
|
|
for key in ("version", "css", "js"):
|
|
if key not in data:
|
|
fail(f"manifest missing {key}")
|
|
return data
|
|
|
|
|
|
def check_files_exist(data: dict) -> None:
|
|
missing = [p for p in [*data["css"], *data["js"]] if not (ROOT / p).exists()]
|
|
if missing:
|
|
fail("manifest references missing files: " + ", ".join(missing))
|
|
ok("manifest file references exist")
|
|
|
|
|
|
def check_versions(data: dict) -> None:
|
|
version = str(data["version"])
|
|
version_js = read_text(ROOT / "js/version.js")
|
|
service_worker = read_text(ROOT / "service-worker.js")
|
|
if f'APP_VERSION = "{version}"' not in version_js:
|
|
fail("js/version.js APP_VERSION does not match manifest")
|
|
if f'APP_VERSION = "{version}"' not in service_worker:
|
|
fail("service-worker.js APP_VERSION does not match manifest")
|
|
ok(f"version synchronized: {version}")
|
|
|
|
|
|
def check_index_order(data: dict) -> None:
|
|
index = read_text(ROOT / "index.html")
|
|
css = re.findall(r'<link rel="stylesheet" href="([^"]+)"\s*/>', index)
|
|
scripts = re.findall(r'<script src="([^"]+)" defer></script>', index)
|
|
css_clean = [entry.split("?", 1)[0] for entry in css if entry.startswith("css/")]
|
|
script_clean = [entry.split("?", 1)[0] for entry in scripts if entry.startswith("js/")]
|
|
if css_clean[: len(data["css"])] != data["css"]:
|
|
fail("index.html CSS order differs from manifest")
|
|
if script_clean[: len(data["js"])] != data["js"]:
|
|
fail("index.html JS order differs from manifest")
|
|
ok("index.html load order matches manifest")
|
|
|
|
|
|
def check_service_worker_order(data: dict) -> None:
|
|
sw = read_text(ROOT / "service-worker.js")
|
|
match = re.search(r"const coreScriptNames = \[\s*(.*?)\s*\];", sw, re.S)
|
|
if not match:
|
|
fail("service-worker.js coreScriptNames not found")
|
|
names = re.findall(r'"([^"]+)"', match.group(1))
|
|
expected = [Path(p).stem for p in data["js"]]
|
|
if names != expected:
|
|
fail("service-worker.js coreScriptNames differs from manifest JS order")
|
|
ok("service-worker.js core script order matches manifest")
|
|
|
|
|
|
def node_check(paths: list[Path]) -> None:
|
|
node = "node"
|
|
for path in paths:
|
|
result = subprocess.run([node, "--check", str(path)], cwd=ROOT, text=True, capture_output=True)
|
|
if result.returncode != 0:
|
|
sys.stderr.write(result.stdout)
|
|
sys.stderr.write(result.stderr)
|
|
fail(f"node --check failed: {path.relative_to(ROOT)}")
|
|
ok(f"node --check passed for {len(paths)} files")
|
|
|
|
|
|
def action_spec_smoke() -> None:
|
|
source = read_text(ROOT / "js/tarinai_action_spec.js")
|
|
js = f"""
|
|
const vm = require('vm');
|
|
const context = {{ console, globalThis: null }};
|
|
context.globalThis = context;
|
|
context.window = context;
|
|
vm.createContext(context);
|
|
vm.runInContext({source!r}, context, {{ filename: 'tarinai_action_spec.js' }});
|
|
const action = context.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 check_legacy_behavior_refs() -> None:
|
|
tokens = re.compile(r"\b(activeBehavior|currentAction|activeForcedBehavior|intent)\b")
|
|
offenders = []
|
|
for path in (ROOT / "js").glob("*.js"):
|
|
for i, line in enumerate(read_text(path).splitlines(), start=1):
|
|
stripped = line.strip()
|
|
if stripped.startswith("//"):
|
|
continue
|
|
if tokens.search(line):
|
|
offenders.append(f"{path.relative_to(ROOT).as_posix()}:{i}")
|
|
if offenders:
|
|
fail("legacy behavior fields still present: " + ", ".join(offenders[:20]))
|
|
ok("legacy behavior runtime fields removed")
|
|
|
|
|
|
|
|
|
|
def check_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_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)",
|
|
"liveItemsOfType(type",
|
|
"this.itemIdMap = new Map()",
|
|
"bucketRebuildsTotal",
|
|
]
|
|
for token in required_tokens:
|
|
if token not in spatial_budget:
|
|
fail(f"item bucket polish helper missing: {token}")
|
|
if "return item && !item.dead ? item : null" not in spatial_budget:
|
|
fail("itemById must not return dead items")
|
|
debug_tools = read_text(ROOT / "js/debug_tools.js")
|
|
if "bucket" not in debug_tools.lower() and "id-map" not in debug_tools.lower():
|
|
fail("debug overlay is missing item bucket/id-map diagnostics")
|
|
routed_files = {
|
|
"js/world_placement_log.js": "addItem?.(item",
|
|
"js/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\\u308a"',
|
|
'RAW_CODEC = "\\u751f"',
|
|
'DEFLATE_CODEC = "\\u7e2e"',
|
|
"SAVE_TEXT_ALPHABET",
|
|
"jp2048Encode",
|
|
"jp2048Decode",
|
|
'CompressionStream("deflate-raw")',
|
|
"DecompressionStream",
|
|
"async function encodeSnapshot",
|
|
"async function decodeSnapshot",
|
|
]
|
|
for token in required_codec:
|
|
if token not in save_codec:
|
|
fail(f"binary save codec token missing: {token}")
|
|
required_storage = [
|
|
'STORAGE_PREFIX = "tarinai_japanese_hash_slot_v1_"',
|
|
"function writeSlot",
|
|
"function readSlot",
|
|
"function requireSlot",
|
|
"function deleteSlot",
|
|
]
|
|
for token in required_storage:
|
|
if token not in save_storage:
|
|
fail(f"binary save storage token missing: {token}")
|
|
required_facade = [
|
|
"TarinaiSaveSystem",
|
|
"TarinaiSaveCodec",
|
|
"TarinaiSaveStorage",
|
|
"TarinaiRestoreCoordinator",
|
|
]
|
|
combined = save + save_codec + save_storage + restore_coordinator
|
|
for token in required_facade:
|
|
if token not in combined:
|
|
fail(f"save/restore split facade token missing: {token}")
|
|
for token in ("TN2!", "TN3!", "tarinai_save_slot_v2_", "tarinai_save_slot_v3_", "legacyForcedColumn"):
|
|
if token in combined or token in snapshot:
|
|
fail(f"save backward compatibility residue still present: {token}")
|
|
if "renderStats" in snapshot or "TarinaiFreezeSystem" in snapshot or "renderArchive" in snapshot:
|
|
fail("snapshot restore must not call UI or freeze subsystems directly")
|
|
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")
|
|
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/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/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")
|
|
for facade_token in ["TarinaiSunbathSystem.updateOne", "TarinaiCursorCareSystem.updateOne", "TarinaiLocalEnvironmentSystem.updateOne"]:
|
|
if facade_token not in needs_items:
|
|
fail(f"Tarinai prototype subsystem facade missing: {facade_token}")
|
|
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; }},
|
|
applyCursorContactCare() {{ calls.push('cursorCare'); }},
|
|
personalityProfile() {{ return {{ fear: 1 }}; }},
|
|
updateFacing() {{ calls.push('facing'); }},
|
|
updateSunbath() {{ calls.push('sunbath'); }},
|
|
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/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/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/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.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]); }},
|
|
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 === 'terrain' && value === 'food-passive-decay')) 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 main() -> None:
|
|
data = manifest()
|
|
check_files_exist(data)
|
|
check_unified_item_registry()
|
|
check_docs_removed()
|
|
check_versions(data)
|
|
check_index_order(data)
|
|
check_service_worker_order(data)
|
|
node_check([*(ROOT / "js").glob("*.js"), ROOT / "service-worker.js"])
|
|
action_spec_smoke()
|
|
behavior_state_smoke()
|
|
check_basic_action_spec_runtime()
|
|
check_social_action_spec_runtime()
|
|
check_action_spec_no_legacy_hooks()
|
|
check_runtime_diagnostics_hooks()
|
|
check_social_balance_guards()
|
|
check_item_bucket_polish()
|
|
check_current_save_format()
|
|
check_phase_and_input_split()
|
|
check_system_order_boundary()
|
|
simulation_boundary_smoke()
|
|
check_entity_runtime_boundary()
|
|
check_item_lifecycle_pipeline_boundary()
|
|
check_tarinai_update_pipeline_boundary()
|
|
check_update_policy_boundaries()
|
|
check_processing_split()
|
|
command_dispatcher_smoke()
|
|
check_legacy_behavior_refs()
|
|
check_legacy_physics_props_removed()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|