diff --git a/src/shim/shim.cfg.cbpin b/src/shim/shim.cfg.cbpin index 042542a..b02ffb0 100644 --- a/src/shim/shim.cfg.cbpin +++ b/src/shim/shim.cfg.cbpin @@ -1,7 +1,17 @@ -# Lane CB -- the pinned creation-turn capture. `shim.cfg.cbcapture` with `aiseed=pin -# The seeds run C3 (unpinned, same save, same guest, same build) observed for itself. Pinning -# them reproduces a turn that actually happened rather than inventing one. -aiseed.values=32=e70a4703,496=0c63ca36,512=372be4df`. +# Lane CB -- the pinned creation-turn capture. `shim.cfg.cbcapture` with `aiseed=pin`. +# +# 2026-09-09: this header used to wrap onto a LIVE `aiseed.values=` line carrying a trailing +# backtick, so the file both contradicted its own "filled in per run" note and shipped a stray +# setting ahead of the real one at the bottom. Lane BP found it while copying the file, and its +# own first draft reproduced the same wrap -- which is what a trap looks like. The seeds now +# appear exactly once, at the end, next to `aiseed=pin`. +# +# exhaustive +# +# ^ that marker is checked by tools/check_shim_configs.py: this is a determinism probe, +# so it must name EVERY registered hook, because under `hooks=trace` one it forgets is +# a detour it did not ask for -- and on a determinism probe an extra detour is exactly +# what rule 19 says can move the bytes. Six were missing until lane BP found them. # # WHY PINNING IS NOT CHEATING. Lane L1 showed every AI client's generator is seeded with a # fresh per-process word, so `turn1-state -> turn2` has an outcome set of size k > 1 and NO @@ -33,6 +43,16 @@ hook.Game::StrategyServer::NodeLineDecay=off hook.Game::StrategyServer::ProcessNodeSpaceTravel=off hook.Game::EncounterDetect::AssignContacts=off hook.Game::EncounterDetect::ProcessTeamRecord=off +# These six were absent, and under `hooks=trace` an unnamed hook defaults back ON -- so this +# config was quietly running six more detours than it claimed. Lane BP, 2026-09-09. Name every +# registered hook; a template that lists most of them is worse than one that lists none, because +# it reads as exhaustive. +hook.Game::StrategyServer::BeginProcessTurn=off +hook.Game::SVSOSwarmQueen::OnTurnBegin=off +hook.Game::SVSOSwarmQueen::RegisterHives=off +hook.Game::SVSOSwarmQueen::TickHives=off +hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off +hook.Mars::RNG::Seed=off trace.path=C:\SOTS\shim.trace.jsonl trace.flush=always probes=off diff --git a/tools/check_shim_configs.py b/tools/check_shim_configs.py new file mode 100644 index 0000000..fe5e470 --- /dev/null +++ b/tools/check_shim_configs.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Structural checks on the shim.cfg.* templates. + +Lane BP found two defects in `shim.cfg.cbpin` while copying it for a determinism +probe, and its own first draft reproduced one of them -- which is what a trap looks +like. Both are checked here: + + * A **wrapped header comment landing on a live setting.** `cbpin`'s first line ran + on past the comment and left `aiseed.values=...\\u0060.` as a real setting ahead of the + real one at the bottom, so the file contradicted its own "filled in per run" note. + * A **key set twice.** The last one silently wins, which is exactly how a stale + value hides in a config nobody re-reads. + +There is deliberately **no global check that a template names every registered +hook**. `hooks=trace` means "trace everything except what is turned off", and most +templates rely on that on purpose -- an exhaustiveness rule applied to all of them +would be a rule about a style nobody agreed to. It matters only where a lane wants +a *minimal* hook set, and no file states that intent, so a template opts in by +carrying a line reading exactly `# exhaustive` in its header. For those, an unnamed +hook is a detour the config did not ask for, and on a determinism probe an extra +detour is precisely what rule 19 says can move the bytes. + +Exits non-zero with the offending file and what is wrong with it. +""" +import pathlib +import re +import sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent +SRC = ROOT / "src" / "shim" + +# A hook declares itself with `static constexpr const char* name = "...";` +NAME_RE = re.compile(r'static\s+constexpr\s+const\s+char\*\s+name\s*=\s*"([^"]+)"') +HOOK_RE = re.compile(r"^hook\.([^=]+)=") + +# Not real hooks: the doc-comment's example in trace/hook.h, and the self-test hooks, +# which exist to make the harness fail on purpose and are never part of a measurement. +EXEMPT = {"Game::Foo"} +EXEMPT_PREFIXES = ("Shim::SelfTest::",) + +# Settings whose values legitimately end in a period or a path separator. +PROSE_SAFE = ("path", "out", "dir", "file") + + +def registered_hooks(): + names = set() + for path in SRC.rglob("*"): + if path.suffix in (".h", ".cpp"): + for m in NAME_RE.finditer(path.read_text(encoding="utf-8", errors="replace")): + names.add(m.group(1)) + return {n for n in names if n not in EXEMPT and not n.startswith(EXEMPT_PREFIXES)} + + +def check(cfg, hooks): + problems = [] + lines = cfg.read_text(encoding="utf-8", errors="replace").splitlines() + settings = [l for l in lines if l.strip() and not l.lstrip().startswith("#")] + + for l in settings: + key = l.split("=", 1)[0].strip() + if "=" not in l: + problems.append(f"line is neither a comment nor a setting: {l!r}") + elif l.rstrip().endswith(("`", "`.")) or ( + l.rstrip().endswith(".") and not any(p in key.lower() for p in PROSE_SAFE) + ): + problems.append(f"setting looks like a wrapped comment: {l!r}") + + seen = {} + for l in settings: + key = l.split("=", 1)[0].strip() + if key in seen: + problems.append(f"{key!r} set twice ({seen[key]!r} then {l!r})") + seen[key] = l + + if any(l.strip() == "# exhaustive" for l in lines): + if not any(l.strip() == "hooks=trace" for l in settings): + problems.append("declares `# exhaustive` but does not set `hooks=trace`, so it does nothing") + named = {m.group(1) for m in (HOOK_RE.match(l) for l in settings) if m} + missing = sorted(hooks - named) + if missing: + problems.append( + f"declares `# exhaustive` but does not name {len(missing)} registered hook(s), " + "which therefore default ON: " + ", ".join(missing) + ) + return problems + + +def main(): + hooks = registered_hooks() + if not hooks: + print("check_shim_configs: found no registered hooks -- the parser is wrong, not the configs") + return 1 + + failures = [] + exhaustive = 0 + for cfg in sorted(SRC.glob("shim.cfg.*")): + if any(l.strip() == "# exhaustive" for l in cfg.read_text(errors="replace").splitlines()): + exhaustive += 1 + for p in check(cfg, hooks): + failures.append(f"{cfg.name}: {p}") + + if failures: + print("check_shim_configs: FAILED") + for f in failures: + print(" " + f) + return 1 + print(f"check_shim_configs: OK ({len(hooks)} registered hooks, {exhaustive} template(s) declared exhaustive)") + return 0 + + +if __name__ == "__main__": + sys.exit(main())