stable
This commit is contained in:
parent
8bd4405fe8
commit
d5c661bc22
53 changed files with 4264 additions and 950 deletions
BIN
scripts/__pycache__/regression_check.cpython-313.pyc
Normal file
BIN
scripts/__pycache__/regression_check.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -4,21 +4,34 @@ This project intentionally stays buildless; this script is a lightweight manifes
|
|||
"""
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import json, re
|
||||
import json
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
manifest = json.loads((ROOT / "app_manifest.json").read_text(encoding="utf-8"))
|
||||
version = manifest["version"]
|
||||
css_files = manifest["css"]
|
||||
js_files = manifest["js"]
|
||||
|
||||
index = ROOT / "index.html"
|
||||
s = index.read_text(encoding="utf-8")
|
||||
css_block = "".join([f'\n <link rel="stylesheet" href="{f}?v={version}" />' for f in css_files])
|
||||
s = re.sub(r'\s*<link rel="stylesheet" href="css/[^\"]+" />(?:\s*<link rel="stylesheet" href="css/[^\"]+" />)*', css_block, s, count=1)
|
||||
s = re.sub(
|
||||
r'\s*<link rel="stylesheet" href="css/[^\"]+" />(?:\s*<link rel="stylesheet" href="css/[^\"]+" />)*',
|
||||
css_block,
|
||||
s,
|
||||
count=1,
|
||||
)
|
||||
script_block = "\n".join([f' <script src="{f}?v={version}" defer></script>' for f in js_files])
|
||||
s = re.sub(r'\n\s*<script src="js/version\.js\?v=[^\"]+" defer></script>[\s\S]*?\n\s*<script src="js/debug_tools\.js\?v=[^\"]+" defer></script>', "\n" + script_block, s, count=1)
|
||||
s = re.sub(
|
||||
r'\n\s*<script src="js/version\.js\?v=[^\"]+" defer></script>[\s\S]*?\n\s*<script src="js/debug_tools\.js\?v=[^\"]+" defer></script>',
|
||||
"\n" + script_block,
|
||||
s,
|
||||
count=1,
|
||||
)
|
||||
index.write_text(s, encoding="utf-8")
|
||||
|
||||
core_names = [Path(f).stem for f in js_files]
|
||||
css_entries = ",\n ".join([f'`./{f}?${{v}}`' for f in css_files])
|
||||
js_names = ", ".join([json.dumps(n, ensure_ascii=False) for n in core_names])
|
||||
(ROOT / "service-worker.js").write_text(f'''"use strict";\n\nconst APP_VERSION = "{version}";\nconst CACHE_NAME = `tarinai-colony-${{APP_VERSION}}`;\nconst v = `v=${{APP_VERSION}}`;\n// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.\nconst coreScriptNames = [\n {js_names}\n];\nconst CORE_ASSETS = [\n "./",\n "./index.html",\n {css_entries},\n ...coreScriptNames.map(name => `./js/${{name}}.js?${{v}}`),\n];\n\nself.addEventListener("install", (event) => {{\n event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(CORE_ASSETS)).catch(() => undefined));\n self.skipWaiting();\n}});\n\nself.addEventListener("activate", (event) => {{\n event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)))));\n self.clients.claim();\n}});\n\nfunction isStaticAsset(request) {{\n const url = new URL(request.url);\n return /\\.(?:js|css|webp|png|jpg|jpeg|gif|svg|mp3|wav|woff2?)$/i.test(url.pathname);\n}}\n\nself.addEventListener("fetch", (event) => {{\n const request = event.request;\n if (request.method !== "GET") return;\n const url = new URL(request.url);\n if (url.origin !== self.location.origin) return;\n if (/service-worker\\.js$/i.test(url.pathname)) return;\n if (request.mode === "navigate" || /index\\.html$/i.test(url.pathname)) {{\n event.respondWith(fetch(request).then(response => {{\n const copy = response.clone();\n caches.open(CACHE_NAME).then(cache => cache.put(request, copy));\n return response;\n }}).catch(() => caches.match(request).then(cached => cached || caches.match("./index.html"))));\n return;\n }}\n if (isStaticAsset(request)) {{\n event.respondWith(caches.match(request).then(cached => cached || fetch(request).then(response => {{\n const copy = response.clone();\n caches.open(CACHE_NAME).then(cache => cache.put(request, copy));\n return response;\n }})));\n }}\n}});\n''', encoding="utf-8")
|
||||
(ROOT / "service-worker.js").write_text(f'''"use strict";\n\nconst APP_VERSION = "{version}";\nconst CACHE_NAME = `tarinai-colony-${{APP_VERSION}}`;\nconst v = `v=${{APP_VERSION}}`;\n// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.\nconst coreScriptNames = [\n {js_names}\n];\nconst CORE_ASSETS = [\n "./",\n "./index.html",\n {css_entries},\n ...coreScriptNames.map(name => `./js/${{name}}.js?${{v}}`),\n];\n\nself.addEventListener("install", (event) => {{\n event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(CORE_ASSETS)).catch(() => undefined));\n self.skipWaiting();\n}});\n\nself.addEventListener("activate", (event) => {{\n event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key)))));\n self.clients.claim();\n}});\n\nfunction isStaticAsset(request) {{\n const url = new URL(request.url);\n return /\\.(?:js|css|webp|png|jpg|jpeg|gif|svg|mp3|wav|woff2?)$/i.test(url.pathname);\n}}\n\nfunction cacheableResponse(response) {{\n return response && response.status === 200 && response.type !== "opaque";\n}}\n\nfunction putCacheSafely(request, response) {{\n if (!cacheableResponse(response)) return Promise.resolve(false);\n return caches.open(CACHE_NAME).then(cache => cache.put(request, response.clone())).then(() => true).catch(() => false);\n}}\n\nself.addEventListener("fetch", (event) => {{\n const request = event.request;\n if (request.method !== "GET") return;\n const url = new URL(request.url);\n if (url.origin !== self.location.origin) return;\n if (/service-worker\\.js$/i.test(url.pathname)) return;\n if (request.mode === "navigate" || /index\\.html$/i.test(url.pathname)) {{\n event.respondWith(fetch(request).then(response => {{\n putCacheSafely(request, response);\n return response;\n }}).catch(() => caches.match(request).then(cached => cached || caches.match("./index.html"))));\n return;\n }}\n if (isStaticAsset(request)) {{\n event.respondWith(caches.match(request).then(cached => cached || fetch(request).then(response => {{\n putCacheSafely(request, response);\n return response;\n }})));\n }}\n}});\n''', encoding="utf-8")
|
||||
|
|
|
|||
408
scripts/regression_check.py
Normal file
408
scripts/regression_check.py
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
#!/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 was intentionally dropped in compact save v2.
|
||||
"""
|
||||
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.registerTarinaiActionSpec({{
|
||||
id: 'smoke_action',
|
||||
need: 'fulfill',
|
||||
label: 'smoke',
|
||||
selectTarget() {{ return {{ id: 'target-1' }}; }},
|
||||
canStart(t, world, ctx) {{ const target = ctx.target || this.selectTarget(t, world, ctx); return target && target.id === 'target-1'; }},
|
||||
start(t, world, ctx) {{ t.startedWith = ctx.target.id; return true; }},
|
||||
tick() {{ return 'finished'; }},
|
||||
finish(t) {{ t.finished = true; return true; }},
|
||||
}});
|
||||
const subject = {{}};
|
||||
if (!context.canStartTarinaiAction('smoke_action', subject, {{}})) throw new Error('canStart failed');
|
||||
if (!context.startTarinaiAction(action, subject, {{}})) throw new Error('start failed');
|
||||
if (subject.startedWith !== 'target-1') throw new Error('selectTarget/start context failed');
|
||||
if (context.tickTarinaiAction('smoke_action', subject, {{}}, 0.1, {{}}) !== 'finished') throw new Error('tick failed');
|
||||
if (!context.finishTarinaiAction('smoke_action', subject, {{}}, {{ phase: 'finished' }})) throw new Error('finish failed');
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||
f.write(js)
|
||||
tmp = Path(f.name)
|
||||
try:
|
||||
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
if result.returncode != 0:
|
||||
sys.stderr.write(result.stdout)
|
||||
sys.stderr.write(result.stderr)
|
||||
fail("ActionSpec smoke test failed")
|
||||
ok("ActionSpec lifecycle smoke test passed")
|
||||
|
||||
|
||||
def behavior_state_smoke() -> None:
|
||||
source = read_text(ROOT / "js/tarinai_behavior_state.js")
|
||||
js = f"""
|
||||
const vm = require('vm');
|
||||
const context = {{ console, globalThis: null }};
|
||||
context.globalThis = context;
|
||||
context.window = context;
|
||||
context.Tarinai = class Tarinai {{}};
|
||||
vm.createContext(context);
|
||||
vm.runInContext({source!r}, context, {{ filename: 'tarinai_behavior_state.js' }});
|
||||
const t = new context.Tarinai();
|
||||
t.world = {{ time: 12 }};
|
||||
t.state = 'seek_food';
|
||||
context.setTarinaiBehavior(t, {{ id: 'eat_food', need: 'food', label: 'eat', reasonText: 'hungry', tiedNeeds: ['food', 'sleep'], forced: true, forcedId: 'f1', source: 'test', 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 flag not visible');
|
||||
const forced = context.getTarinaiForcedBehavior(t);
|
||||
if (!forced || forced.uid !== 'f1' || forced.priority !== 99) throw new Error('forced request not normalized');
|
||||
if (t.intent.actionId !== 'eat_food' || t.currentAction.need !== 'food' || t.activeForcedBehavior.uid !== 'f1') throw new Error('legacy views not synced');
|
||||
context.setTarinaiForcedBehavior(t, null);
|
||||
if (context.currentTarinaiBehavior(t).forced || context.getTarinaiForcedBehavior(t)) throw new Error('forced flag 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:
|
||||
allowed = {"js/tarinai.js", "js/tarinai_behavior_state.js"}
|
||||
tokens = re.compile(r"\b(activeBehavior|currentAction|activeForcedBehavior|intent)\b")
|
||||
offenders = []
|
||||
for path in (ROOT / "js").glob("*.js"):
|
||||
rel = path.relative_to(ROOT).as_posix()
|
||||
if rel in allowed:
|
||||
continue
|
||||
for i, line in enumerate(read_text(path).splitlines(), start=1):
|
||||
if tokens.search(line) and "intentLockTimer" not in line and "//" not in line:
|
||||
offenders.append(f"{rel}:{i}")
|
||||
if offenders:
|
||||
fail("legacy behavior fields outside compatibility layer: " + ", ".join(offenders[:20]))
|
||||
ok("legacy behavior fields isolated to compatibility layer")
|
||||
|
||||
|
||||
|
||||
|
||||
def check_basic_action_spec_migration() -> None:
|
||||
source = read_text(ROOT / "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 migrated into ActionSpec factories")
|
||||
|
||||
|
||||
|
||||
def check_social_action_spec_migration() -> None:
|
||||
source = read_text(ROOT / "js/tarinai_needs_items.js")
|
||||
required_factories = [
|
||||
"createPanicEscapeActionSpec",
|
||||
"createFleeActionSpec",
|
||||
"createApproachFriendActionSpec",
|
||||
"createApproachFamilyActionSpec",
|
||||
"createApproachMateActionSpec",
|
||||
"createFightRivalActionSpec",
|
||||
"createIntimidateEnemyActionSpec",
|
||||
"createBirthRitualActionSpec",
|
||||
]
|
||||
for factory in required_factories:
|
||||
if factory not in source:
|
||||
fail(f"social/combat ActionSpec factory missing: {factory}")
|
||||
definitions_match = re.search(r"const TARINAI_ACTION_DEFINITIONS = \[(.*?)\];", source, re.S)
|
||||
if not definitions_match:
|
||||
fail("TARINAI_ACTION_DEFINITIONS block missing")
|
||||
definitions = definitions_match.group(1)
|
||||
for factory in required_factories:
|
||||
if f"{factory}()" not in definitions:
|
||||
fail(f"{factory} is not registered in TARINAI_ACTION_DEFINITIONS")
|
||||
if "function configureTarinaiNeedActions" in source:
|
||||
fail("configureTarinaiNeedActions should not be needed after ActionSpec runtime migration")
|
||||
forbidden_late_ticks = [
|
||||
'set("panic_escape",',
|
||||
'set("flee",',
|
||||
'set("approach_friend",',
|
||||
'set("approach_parent_or_child",',
|
||||
'set("approach_mate",',
|
||||
'set("fight_rival",',
|
||||
'set("intimidate_enemy",',
|
||||
'set("sunbath",',
|
||||
"birthRitualAction",
|
||||
"TARINAI_ACTIONS.push",
|
||||
]
|
||||
offenders = [token for token in forbidden_late_ticks if token in source]
|
||||
if offenders:
|
||||
fail("actions still patched late: " + ", ".join(offenders))
|
||||
ok("social/fear/combat/mate/birth/sunbath actions migrated into ActionSpec factories")
|
||||
|
||||
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")
|
||||
if "runtimeDiagnostics()" not in spatial_budget:
|
||||
fail("World.runtimeDiagnostics hook missing")
|
||||
if "lastRuntimeDiagnostics" not in world_update:
|
||||
fail("world update does 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")
|
||||
ok("runtime diagnostics hooks present")
|
||||
|
||||
def check_social_balance_guards() -> None:
|
||||
needs = read_text(ROOT / "js/tarinai_needs_items.js")
|
||||
world_social = read_text(ROOT / "js/world_family_social.js")
|
||||
social_move = read_text(ROOT / "js/tarinai_social_move_life.js")
|
||||
required_need_tokens = [
|
||||
"fight_rival: 56",
|
||||
"intimidate_enemy: 42",
|
||||
"approach_mate: 68",
|
||||
"approach_friend: 66",
|
||||
"return ({ conflict: 34, mate: 24, family: 12, bond: 14",
|
||||
"if (subNeed !== \"conflict\" && subValue >= threshold) return true",
|
||||
"subValue >= threshold && subValue >= socialPull * 0.82",
|
||||
]
|
||||
for token in required_need_tokens:
|
||||
if token not in needs:
|
||||
fail(f"social balance guard token missing: {token}")
|
||||
if "canStart: basicCanStartWithTarget,\n start(t, world, ctx = {}) {\n const rival" in needs:
|
||||
fail("intimidate_enemy reverted to ungated basicCanStart")
|
||||
required_world_tokens = [
|
||||
"fightPairCooldowns",
|
||||
"fightPairKey(a, b)",
|
||||
"fightPairCooldownRemaining(a, b)",
|
||||
"markFightPairCooldown(a, b",
|
||||
"isAlreadyFightingPair(a, b)",
|
||||
"if (this.isAlreadyFightingPair?.(a, b)) return true",
|
||||
]
|
||||
for token in required_world_tokens:
|
||||
if token not in world_social:
|
||||
fail(f"fight re-entry guard missing: {token}")
|
||||
if "dt * (battleDrug ? 0.23 : 0.085)" not in social_move:
|
||||
fail("ordinary proximity fight probability guard missing")
|
||||
ok("social balance and fight re-entry guards present")
|
||||
|
||||
|
||||
def check_item_bucket_polish() -> None:
|
||||
spatial_budget = read_text(ROOT / "js/world_spatial_budget.js")
|
||||
required_tokens = [
|
||||
"markItemBucketsDirty(reason",
|
||||
"ensureItemBuckets(reason",
|
||||
"addItem(item",
|
||||
"itemById(id)",
|
||||
"itemsOfType(type)",
|
||||
"liveItemsOfType(type",
|
||||
"this.itemIdMap = new Map()",
|
||||
"bucketRebuildsTotal",
|
||||
]
|
||||
for token in required_tokens:
|
||||
if token not in spatial_budget:
|
||||
fail(f"item bucket polish helper missing: {token}")
|
||||
if "return item && !item.dead ? item : null" not in spatial_budget:
|
||||
fail("itemById must not return dead items")
|
||||
debug_tools = read_text(ROOT / "js/debug_tools.js")
|
||||
if "bucket" not in debug_tools.lower() and "id-map" not in debug_tools.lower():
|
||||
fail("debug overlay is missing item bucket/id-map diagnostics")
|
||||
routed_files = {
|
||||
"js/world_placement_log.js": "addItem?.(item",
|
||||
"js/world_update.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_needs_items.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_compact_save_v2() -> None:
|
||||
snapshot = read_text(ROOT / "js/snapshot_system.js")
|
||||
save = read_text(ROOT / "js/save_system.js")
|
||||
required_snapshot = [
|
||||
"const SNAPSHOT_VERSION = 2",
|
||||
"function compactWorld",
|
||||
"function compactTarinai",
|
||||
"function compactItem",
|
||||
"function compactAnt",
|
||||
"logs = []",
|
||||
"worldRef.family = {}",
|
||||
"recordFamily",
|
||||
]
|
||||
for token in required_snapshot:
|
||||
if token not in snapshot:
|
||||
fail(f"compact save v2 snapshot token missing: {token}")
|
||||
forbidden_snapshot = [
|
||||
"logs: clonePlain",
|
||||
"effects: (worldRef.effects",
|
||||
"summary:",
|
||||
"app: \"tarinai_colony_game\"",
|
||||
]
|
||||
offenders = [token for token in forbidden_snapshot if token in snapshot]
|
||||
if offenders:
|
||||
fail("old broad snapshot fields still present: " + ", ".join(offenders))
|
||||
required_save = [
|
||||
'STORAGE_PREFIX = "tarinai_save_slot_v2_"',
|
||||
'EXPORT_PREFIX = "TN2!"',
|
||||
"SAVE_TEXT_ALPHABET",
|
||||
"base91Encode",
|
||||
"base91Decode",
|
||||
'CompressionStream("deflate-raw")',
|
||||
"DecompressionStream",
|
||||
"async function encodeSnapshot",
|
||||
"async function decodeSnapshot",
|
||||
]
|
||||
for token in required_save:
|
||||
if token not in save:
|
||||
fail(f"compact save v2 encoder token missing: {token}")
|
||||
if "TARINAI_SAVE_V1" in save or "tarinai_save_slot_v1" in save:
|
||||
fail("old save v1 prefix still present")
|
||||
ok("compact save v2 snapshot and encoder present")
|
||||
|
||||
def main() -> None:
|
||||
data = manifest()
|
||||
check_files_exist(data)
|
||||
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_migration()
|
||||
check_social_action_spec_migration()
|
||||
check_runtime_diagnostics_hooks()
|
||||
check_social_balance_guards()
|
||||
check_item_bucket_polish()
|
||||
check_compact_save_v2()
|
||||
check_legacy_behavior_refs()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue