diff --git a/docker/agent/bin/push-work b/docker/agent/bin/push-work new file mode 100755 index 00000000..74ac8a28 --- /dev/null +++ b/docker/agent/bin/push-work @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Push the current topic branch to origin — the ONLY sanctioned way out of the +# container. +# +# Why a wrapper instead of plain `git push`: +# +# * **`main` and shared branches are refused.** The agent commits to +# `auto/`; a human merges. A token that can push anywhere is one +# confused iteration away from rewriting the consolidated line. +# * **Force-push is refused**, always. Nothing here needs it, and history +# rewriting is the one mistake that cannot be undone by merging. +# * It pushes the CURRENT branch only, by name, so a stray `--all` cannot +# publish another agent's worktree branch mid-experiment. +# +# Credentials come from a file mounted read-only at ~/.git-credentials (see +# `sylph-agent`). They are never printed, never logged, and never passed on a +# command line. +# +# push-work push the current branch +# push-work --dry-run say what it would do +set -euo pipefail + +DRY=0 +[ "${1:-}" = "--dry-run" ] && DRY=1 + +repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || { + echo "push-work: not inside a git repository" >&2; exit 1; } +cd "$repo_root" + +branch=$(git rev-parse --abbrev-ref HEAD) +if [ "$branch" = "HEAD" ]; then + echo "push-work: detached HEAD — check out a branch first" >&2; exit 1 +fi + +case "$branch" in + auto/*) ;; + *) + echo "push-work: refusing to push '$branch'." >&2 + echo " Only auto/* topic branches may leave the container; a human merges" >&2 + echo " them into main. Move your work: git switch -c auto/" >&2 + exit 1 ;; +esac + +if [ ! -s "$HOME/.git-credentials" ]; then + echo "push-work: no credentials mounted at ~/.git-credentials." >&2 + echo " The host must start the container with SYLPH_GIT_CREDENTIALS pointing" >&2 + echo " at a file containing one line:" >&2 + echo " https://:@git.mc02.dev" >&2 + exit 1 +fi + +# `store` reads the file we mounted; nothing is written back (it is read-only). +git config --local credential.helper "store --file=$HOME/.git-credentials" + +ahead=$(git rev-list --count "origin/$branch..$branch" 2>/dev/null || git rev-list --count HEAD) +echo "push-work: $branch — $ahead commit(s) to publish" + +if [ "$DRY" = 1 ]; then + echo "push-work: --dry-run, stopping here" + exit 0 +fi + +# --force-with-lease is deliberately NOT offered. If this is rejected as +# non-fast-forward, someone else moved the branch: fetch and merge, do not +# overwrite. +git push --set-upstream origin "$branch" +echo "push-work: pushed $branch" diff --git a/docker/agent/loop-task.md b/docker/agent/loop-task.md index 3e03533f..ee561269 100644 --- a/docker/agent/loop-task.md +++ b/docker/agent/loop-task.md @@ -1,48 +1,85 @@ Work the Project Sylpheed reverse-engineering backlog, one item at a time. -Read `Syplheed-Reborn/docker/agent/AGENT.md` first — it has the container's -tooling and, more importantly, the method rules this corpus is built on. +## Read these first, every iteration, before proposing anything + +They are short on purpose, and they are the reason this prompt is short: + +1. `Syplheed-Reborn/docs/re/REFUTED.md` — **claims already tested and dead.** + If your idea is on that list, it is finished; pick another. Grep it for your + nouns before you design anything. +2. `Syplheed-Reborn/docs/re/METHOD.md` — the traps this corpus has already paid + for. Most wasted iterations are one of these repeated. +3. `Syplheed-Reborn/docs/re/INDEX.md` — what is already **decoded**. Do not + re-derive a row that is already ✅. Re-deriving a known format is not a + finding; asking whether its values *resolve* is. +4. `Syplheed-Reborn/docs/re/BACKLOG.md` — the open items. It is long; skim the + `##` headings and open only the one you pick. +5. `Syplheed-Reborn/docker/agent/AGENT.md` — the container's tooling. + +**These files are the memory.** If a finding, a dead end or a trap lives only in +your context, it is lost at the end of the run. Write it down where the next +iteration will find it — that is what makes this a corpus rather than a +transcript. ## Each iteration -1. **Pick one item.** Take the next open entry from - `Syplheed-Reborn/docs/re/BACKLOG.md`, preferring the one whose "first step" - is cheapest and most decisive. If you are mid-item from a previous - iteration, continue it rather than starting another. -2. **Do the smallest experiment that could settle it**, and try to *refute* - your hypothesis before you believe it. -3. **Write the result down** in `docs/re/` under the ✅/🟡/❔ convention, with - the evidence. A withdrawn or refuted result is a real result — record it, - with the reasoning, rather than deleting it. -4. **Commit** to a topic branch (below), one logical change per commit. -5. **Say plainly what you did not settle**, and stop the iteration. +1. **Pick one item** from `BACKLOG.md`, preferring the one whose "first step" is + cheapest and most decisive. If you are mid-item, continue it rather than + starting another. +2. **Do the smallest experiment that could settle it**, and try to *refute* your + hypothesis before believing it. Run the known-positive through any new filter + first; a filter that fails its own control is dead, not tuneable. +3. **Write the result down** in `docs/re/` under the ✅/🟡/❔ convention, with the + evidence and the *reach* of any negative. A withdrawn result is a real + result — record it, with the reasoning. + * If you **refuted** something, add a line to `REFUTED.md`. + * If you were bitten by a general trap, add a line to `METHOD.md`. + * If you **closed** a format, update its `INDEX.md` row. +4. **Commit** to `auto/`, one logical change per commit. +5. **Publish**: `push-work`. Your branch must leave the container or the work + dies with it. See "Publishing" below. +6. **Say plainly what you did not settle**, and stop the iteration. + +## Publishing + +`push-work` pushes the current branch to origin. It refuses anything that is not +`auto/*` and never force-pushes, so the consolidated line stays a human's +decision. Run it **every iteration that produced a commit** — not at the end of +some longer arc, which is exactly when a container dies. + +If it reports no credentials, say so in your reply and continue working; do not +improvise another route out (no remote rewrite, no credential helper of your +own, no alternate transport). A push that is blocked is a blocked push. ## Hard rules -* **Never commit to `main`.** Work on `auto/` in whichever repo you are - touching, branched from the current `main`. Create it if it does not exist. -* **Never push.** No push credentials are mounted, and that is deliberate — a - human reviews before anything leaves the box. -* **One emulator at a time.** `run-canary` enforces this with a lockfile; do not - work around it. -* **Do not edit `main`'s history**, do not rebase shared branches, and do not - delete branches. +* **Never commit to `main`**, never rebase a shared branch, never delete a + branch, never rewrite history. +* **Do not touch another agent's worktree.** `git worktree list` first; branches + marked `+` are checked out elsewhere. +* **One emulator at a time** — `run-canary` enforces it with a lockfile. * **Measure the oracle; never infer it.** An iteration that reasons about the game without running it is a red flag unless it is a pure static-format task. * **Verify with an artifact**, not with "it compiles": `build-reborn test` (it wires up `SYLPHEED_DISC` — without it the disc tests silently self-skip and a - green run means almost nothing), `sylpheed-cli mesh render`, `screen render`, - `save info`, a screenshot. + green run means almost nothing), `sylpheed-cli mesh render` / `screen render` / + `save info`, a screenshot. A regenerated artefact that comes out + byte-identical is a strong check that a change is additive; when it does + change, check that every diff line pairs exactly. ## When you are blocked -If an item needs something the container cannot do — hardware Vulkan for a -rendering question, a push, a decision only the user can make — **do not -improvise around it**. Write what you found, note the blocker in `BACKLOG.md`, -and move to the next item. +If an item needs something the container cannot do — hardware Vulkan, a decision +only the user can make — **do not improvise around it**. Write what you found, +note the blocker in `BACKLOG.md`, and move to the next item. ## Pacing -Self-pace. A useful iteration is one experiment plus its write-up, not a -marathon; stopping with a clean commit and an honest "here is what is still -open" is the goal every time. +One experiment plus its write-up is a good iteration; a marathon is not. Stop +with a clean commit, a push, and an honest list of what is still open. + +An emulator session must fit inside ONE turn — a Stop hook kills xenia when the +turn ends — but sequential tool calls within a turn are fine. + +The loop runs on a fixed interval set by the harness, so you do **not** need to +arm the next wakeup yourself. Spend that attention on the write-up instead. diff --git a/docker/agent/sylph-agent b/docker/agent/sylph-agent index d689b47f..93fdeb15 100755 --- a/docker/agent/sylph-agent +++ b/docker/agent/sylph-agent @@ -21,6 +21,9 @@ # SYLPH_VULKAN=sw force software Vulkan (lavapipe) even if /dev/dri exists # SYLPH_REMOTE=0 do NOT enable Remote Control (default: enabled for `loose`) # SYLPH_REMOTE_NAME Remote Control session name (default: sylpheed-agent) +# SYLPH_GIT_CREDENTIALS file with `https://:@host` for push-work +# (default: $HOME/.sylph-git-credentials) +# SYLPH_LOOP_INTERVAL fixed loop cadence, e.g. 30m (default: 45m) # SYLPH_CPUS / SYLPH_MEM_GB override the computed half set -euo pipefail @@ -125,6 +128,21 @@ docker_args() { echo " packaged SPIRV-Tools is too old. Running is unaffected." >&2 fi + # ── git push ── + # Read-only, and only ever used by `push-work`, which refuses anything but an + # auto/* branch and never force-pushes. Without this the agent's work only + # exists inside the container and dies with it. + GITCRED="${SYLPH_GIT_CREDENTIALS:-$HOME/.sylph-git-credentials}" + if [ -f "$GITCRED" ]; then + _out+=(-v "$GITCRED:/sylph-home/re/.git-credentials:ro") + else + echo "==> NOTE: no git credentials at $GITCRED — the agent cannot push," >&2 + echo " so its work will be lost if the container is destroyed. Create it" >&2 + echo " with a single line and chmod 600:" >&2 + echo " https://:@git.mc02.dev" >&2 + echo " or point SYLPH_GIT_CREDENTIALS elsewhere." >&2 + fi + [ -n "${ANTHROPIC_API_KEY:-}" ] && _out+=(-e "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY") [ -n "${SYLPH_VULKAN:-}" ] && _out+=(-e "SYLPH_VULKAN=$SYLPH_VULKAN") [ -n "${SYLPH_REMOTE:-}" ] && _out+=(-e "SYLPH_REMOTE=$SYLPH_REMOTE") @@ -191,7 +209,12 @@ case "${1:-}" in TASK="Work the RE backlog in Syplheed-Reborn/docs/re/BACKLOG.md." fi fi - INTERVAL="${SYLPH_LOOP_INTERVAL:-}" # empty = let the model self-pace + # A FIXED interval by default, not self-pacing. Self-pacing requires the + # agent to call ScheduleWakeup itself at the end of every turn, and the one + # thing an agent deep in an experiment reliably forgets is the bookkeeping + # after it. With an interval the harness owns the cadence and a forgotten + # wakeup cannot end the run. Set SYLPH_LOOP_INTERVAL= (empty) to self-pace. + INTERVAL="${SYLPH_LOOP_INTERVAL-45m}" declare -a ARGS; docker_args ARGS ARGS+=(-e SYLPH_AUTONOMOUS=1 -w "$PROJECT") echo "==> loose | cpus=$CPUS mem=${MEM_GB}g shm=${SHM_GB}g" diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index 4d32711b..5d38026c 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -2,6 +2,11 @@ Confidence: ✅ `CONFIRMED` · 🟡 `PROBABLE` · ❔ `HYPOTHESIS`. See [README](README.md). +Also durable, and worth reading before proposing anything: +[`REFUTED.md`](REFUTED.md) — what has already been tested and died · +[`METHOD.md`](METHOD.md) — the traps this corpus has already paid for · +[`BACKLOG.md`](BACKLOG.md) — what is still open. + Formats we've already reversed are, for now, **documented by their parser + disc round-trip tests** (the executable spec) rather than a prose file — the "Spec" column points there. Promote to a prose `structures/…md` file when a format needs behavioural notes beyond layout. diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md new file mode 100644 index 00000000..387e2870 --- /dev/null +++ b/docs/re/METHOD.md @@ -0,0 +1,105 @@ +# Method traps already paid for + +Each line cost an iteration at least once. They are general — they are not about +Sylpheed, they are about how this kind of measurement goes wrong. + +Like [`REFUTED.md`](REFUTED.md), this list had been living in the autonomous +agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the +✅/🟡/❔ confidence convention itself. + +## Controls + +* **Every result needs a control. A control that fails kills the instrument.** +* **Run the known-positive through a new filter FIRST.** Three filters have been + killed by their own control. When one fails, **read the known-good's + disassembly** before assuming a shape. +* **A measured negative is a result** — but a negative is only as strong as the + route you ran, so **state its reach**. +* **A null result needs its cause shown to have happened.** +* **A result with NO unknowns is suspicious.** +* **Census the whole set; always run the other population as the control.** + **Zero partials is stronger than a majority.** +* **A 2×2 partition is the sharpest general tool** — both off-diagonals empty is + a law. +* **Re-derive a doc's own numbers as the control.** + +## Inference + +* **Never conclude from ONE sample.** +* **A law proved on one population is a hypothesis on the next.** +* **Finding one exception does not imply a family.** +* **Consistency is not proof. A suggestive coincidence is a coincidence until + measured.** **An analogy is not a measurement.** +* **Same layout ≠ same instance.** **Same record-name set ≠ same object.** +* **A marker is only proven by what it leaves out.** +* **A high-confidence SCORE is not a high-confidence MECHANISM.** +* **Knowing HOW MANY is not knowing WHICH.** +* **Round numbers matching is weak evidence — unless you read the constant.** +* **My own last-turn result is a hypothesis too.** +* **A global partition can understate a per-owner one.** +* **A residual is measured against a population — name it.** + +## Searching and tooling + +* **A search that returns thousands has no power; state the reach.** +* **A substring match is not a hit.** **A regex miss looks like a null result — + print one raw sample before believing a zero.** +* **A derived table can be a cross product — measure its shape first.** +* **After refuting an instrument, sweep everything that depended on it.** +* **Before measuring how wrong a tool is, read what the tool actually does.** +* **The instrument must pass its own control.** +* **Classify a bulk before mining it. The residual is the prize.** +* **Rank by similarity — the cliff is the finding.** But **read the values + before trusting the rank.** +* **Grep the nouns before designing the experiment — and believe it.** +* **Grep gives you a file list — read *every* file on it.** +* **The answer is often already in the doc that owns the subject — read it end + to end.** A 🟡 often names its own route. +* **Before re-trying a blocked idea, check whether the blocker's own doc already + tried it.** +* **Ship a regenerator with every artefact.** An artefact that moves by a pure + reorder is a tool bug. +* **Never print per-entry lines from a disc-wide sweep — aggregate.** + +## Reading the data + +* **Read what a loader NAMES, not where it stores.** +* **A field the disc never values still gets named by the loader.** +* **An indexed read beats a deduped-pool adjacency read.** +* **Check the whole string set, not the one matching word.** +* **A dict keyed by record name across a multi-entry pak is a lie.** +* **A set-difference over names hides reuse — join per USER.** +* **A self-index names records, not files.** +* **Case-insensitive hashing means two spellings can be one entry.** +* **An "unresolved" name may be the wrong kind, namespace or prefix — or part of + a cut asset.** +* **A garbled value may be a real string in another encoding.** +* **Two of my own counts disagreeing is a grammar clue.** +* **A game's own typo is a join key.** +* **A bias constant in the code is a join key.** +* **A prefix trap: enumerate maximal `[A-Za-z0-9_]` runs, not `startswith`.** +* **Re-deriving a format is not a finding — asking whether its values *resolve* + is.** + +## Mechanics that have bitten + +* **Never hand-convert a decimal VA — print `hex()`.** +* **`grep -c` counts LINES** — use `grep -o | wc -l`. +* **`Counter.most_common()` tie-breaks by insertion order — use `sorted()`.** +* **Raw grep cannot see inside compressed pak entries.** +* **Commit messages go in a file** (`git commit -F`); a literal `|` in a table + cell needs escaping; `git log --all -- ` can hang. + +## Runtime / emulator + +* **Look at the PNG** — and check its dimensions. +* **"Animating" is not "still in a mission".** +* **Dedup entity enumerations by position value.** +* **Do not diagnose timing or liveness under gdb.** `ps %cpu` is cumulative. +* **Classify screens by whole-image statistics, not named pixels** — a named + pixel is only valid while the image sits at a known place, and nothing errors + when it moves. +* **Do not poll faster than the guest updates** — it manufactures a clean curve + out of noise. +* **A probe that never performs the action will "prove" the action does not + exist.** diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md new file mode 100644 index 00000000..943a5174 --- /dev/null +++ b/docs/re/REFUTED.md @@ -0,0 +1,185 @@ +# Refuted — claims that were tested and died + +**Read this before proposing a hypothesis.** Every line below was believed at +some point, measured, and found false. Reviving one costs a whole iteration and +produces nothing. + +This file exists because the list had been living in the autonomous agent's +*loop prompt* — the only copy, lost the moment the container was. A refuted +result is a real result; it belongs in the corpus like any other. + +**Format:** each entry is the claim as it was believed. Where the true answer is +known it follows after `→`. Grouped by subject so a grep for your noun finds the +neighbourhood, not just the line. + +--- + +## Offsets, structs and the progress singleton + +* `position = instance − 0x12c` → refuted. +* `+0x29d0` → refuted. +* "an offset intersection can find a struct's consumer" → **only for LARGE or + unusual offsets.** Small ones have no power (`+184`: 301/351/115 hits). +* "a `+1956` store means a progress write" → writes go through the COPY, not + direct stores. 9 direct stores, none of them a progress write. +* "the singleton-global filter can find progress writers" → it fails its own + control. +* "the progress copy destination is an `r1`-relative stack local" → it is a + **frame register**. The `r1` assumption returned 0 for all 21 candidates + *including the known-good* — the filter was killed by its own control. +* "word B's writer also stores the Time/Points record" → it does not. +* "the debriefing records the metric with the clear bit" → 44 calls, exactly two + strings (`DEBRIEFING`, `BASE_INFO`), no `Time`, no `Points`. +* "`0x820AF030` holds live state" → all 384 words constant; it is a + spawned-entity record, not live state. + +## Screens, classes and RTTI + +* "the RTTI route can name the anonymous classes" → 1 150 vtables: 1 150 + `ANON_`, 0 `rtti_present`, 0 base classes. +* "the sibling vtable methods name the class" → they cannot. +* "`xrefs` can name the callers of a vtable method" → no. +* "the `ind_call` refutation voids existing corpus claims" → damage bounded, + 4/4 caller claims verify. But **`xrefs.ind_call` is a CROSS PRODUCT** — always + filter `kind='call'`. +* "`BASE_INFO` marks the 5-slot screen family" → it discriminates + screen-config from table-read, 9/9 vs 10/10. +* "a high key count means a rich screen" → `sub_82297550` / `sub_822A2F00`'s 27 + "keys" are coordinate pairs, i.e. a layout table. +* "`EX_` = the CHALLENGE-mission debriefing" → `EX_` is **EXTRA**, mission-kind + 3. +* "the `EX_` selection has not been shown" → it has: `[[obj+4]+184] == 3`. + +## Stages, missions and the challenge set + +* "the disc's stages are numbered 1..28" → S01–S16 story, S17 **cut**, S18–S23 + tutorials, S24–S29 challenge. +* "S24–S29 are story missions" → they are the challenge missions. +* "the challenge missions have their own maps" → they reuse + `GP_MAIN_GAME_E.pak`'s stage records. +* "the challenge `REQUIREMENT` values are 16,25,26,27,29" → the chain is + 16→24→25→26→27→28. +* "the `Extra0n` family shares one leaderboard metric" → `RECORD_TYPE` is + per-stage: 3 Time / 3 Points. +* "stage = filled SHAB count + 1" → refuted. +* "the first TRIGGER is always the point of no return" → refuted. +* "`EnumUnit_S14.tbl` might be missing" / "S14's 13 are a manifest omission" / + "asteroids are exempt from the manifest" → S14's 13 are **dangling + deployments**. NEEDS-HUMAN: fly S14. +* "`StageMessageSet_S02.tbl` is not in the pak" → it is. +* "`S28_p1` has an asteroid volume with no definition" → refuted. +* "`test_s8p1_asteroid.tbl` is test-only" → refuted. +* "the settings family has 28 or 29 objects" → 24. + +## ISL / mission scripting + +* "the bytecode is in the `.embsec_` sections" → refuted. +* "only 31 built-ins take a unit" → refuted. +* "`sub_8230C398` is the message pump" → refuted. +* "`bus+8216` is the subscriber registry" → refuted. +* "the ScriptPhase vtable is ≥200 slots" → 113. + +## IDXD, paks and naming + +* "IDXD record keys are `name_hash`" → record keys are **`tag_hash`** + (case-SENSITIVE); `name_hash` is case-INSENSITIVE and used for pak keys. +* "pak TOC order is stage order" / "TOC order is semantic order" → it is not. +* "the executable holds the asset names" → the image names **no data value at + all**; that route is powerless. +* "the image might name a data VALUE" → powerless. +* "the XPR2 manifest names hash to the DefTables tables" → refuted. +* "the `DefTables` model names are unreachable" → reachable via the `Enumerate` + declaration tables (1 413/1 425, 99.2 %). +* "the `GP_MAIN_GAME_*` unnamed block is undiscovered data" → refuted. +* "each `GP_MAIN_GAME_*` `Enumerate` object declares something" → refuted. +* "`GP_HANGAR_ARSENAL` is missing data tables" → refuted. +* "the `Enumeration` self-index can name objects" → a self-index names + **records, not files**. +* "a per-pak prefix might close the 2D blocker" → no. +* "the `+` paths might name the 2D or `GP_READY_ROOM` keys" → the `+`-dictionary + route is exhausted, 0 of 1 817. +* "the `game:\` paths are unresolved" → refuted. +* "a set-difference over file names can see reuse" → it cannot; **join per + USER**. Per-pak copies are ×6. + +## Units, weapons, effects and assets + +* "`Generic` (394) is the unit datasheet" → refuted. +* "a loadout's `Arm1` names an item" → it names a **hardpoint slot** + (`Turret_NNN`), 59/59. +* "`EnumUnit` and the unit datasheet share a vocabulary" → they do not. +* "the roster is the `Generic.Model` set" → roster 40, `Generic.Model` 46, + `GameResourceID` 480 — three vocabularies. +* "every unit ID is `UN_###__`" → the grammar is + `UN_###_[_]_`. +* "`_EXn` is the `Extra0n` index" → three different `EX` vocabularies exist. +* "the only two `_EX5` names on the disc are the AA gun and the DeltaSaber" → + refuted. +* "running the tutorial will instantiate the `_Ttrl` weapons" → refuted. +* "the disc has exactly three `EnumWeapon` tables" → four. +* "the `wep_NN` package gaps are unshipped weapons" / "`wep_85` is the tip of a + family" → `wep_85` is the **only** declared-but-unshipped asset (59/0/1/26). +* "nothing is deployed without being declared" → refuted. +* "effects are one namespace" → refuted. +* "the 58 undeclared effect names are missing assets" → refuted. +* "all five orphan effects are unshipped" → refuted. +* "`eff_f0002` ships in `Base.xpr`" → refuted. +* "`Base.xpr` holds more bound effects than `ptc_pack`" → refuted. +* "the 34 unlocated are a scatter" → refuted. +* "the 9 unlocated might be under another prefix" → refuted. +* "`ptc_pack` has 532 names" → 727. +* "the `_e`/`_f` law is effect-FIELD-specific" → it is the **faction law**, + 564/564. +* "a disc-wide `.xpr` byte search can show an effect is ABSENT" → it cannot. +* "`rot_n001` is on the disc" → refuted. +* "`rou_f004`'s mesh is in `Stage_S28.xpr`" → it is in `DeltaSaber_A.xpr`. +* "`parent` + `_all` + `_child` is the composite-model convention" → refuted. + `_hangar` **is** real (59 of 166, 59/59 with a bare twin); `_all`/`_child` is + not. +* "`Motion_guard_start` has no damage variants" → refuted. +* "`CoverArea` bits 2 and 3 are mutually exclusive" → refuted. +* "the 27 unresolved `NamePlate` values are missing objects" → refuted. + +## LOD, background and misc tables + +* "`EnumLODSet_*` is a per-stage family" → `EnumLODSet_test.tbl` serves 17 + stages; 17+5+1 = 23. +* "there are 8 orphan LOD tables" → 6. +* "the orphans are stale copies of `_test`" → refuted. +* "S25 is absent from the `DefTables` LOD families" → refuted. +* "`BackGroundID` has no referent anywhere" → it is an **identity**. +* "`BackGroundPackage == BG_.xpr`" → refuted. +* "`ID` + `Package` is a convention" → refuted. +* "`Placement_*` / `RouteTest_*` are unattached" → refuted. +* "the `AsteroidDefinition` join does not reproduce by hash" → it does. +* "the 8-value frame is a new finding" → it was already in the corpus. + +## Loaders, config and tuning + +* "the config reader is XML" → INI. +* "`sub_822F9498` is the unit-definition loader" → it is `PlayerParams`'s. +* "`sub_822AE628` reads the main-game `Tweak`" → refuted. +* "`sub_8230D1F8` is a rank table" → it is the stage-settings loader. +* "`sub_82286BC8`'s key list is new" → refuted. +* "`sub_825F2CF0` / `sub_825F2F88` read a post-processing table" → refuted. +* "`Booster` is a new schema" / "`Booster` is the player craft's flight + envelope" → refuted; nothing selects `Booster`. +* "the `AnalogRevice`/`Tweak` block is unreachable" → reachable + (`sub_821A6CF0`, base `0x820A1630`). +* "a 0-xref string block has no reader" → refuted. +* "the AI table was NEEDS-HUMAN" → refuted. +* "the `PG*` HUD names are undocumented" → they are documented. +* "a base-solver row identifies a FUNCTION" → it does not. +* "a 64K-boundary base is low confidence" → **inverted**; it is high + confidence. +* "the 0x820B0000 cluster is a false positive" → refuted. +* "a pointer to a function in the image implies a registry" → refuted. + `.pdata` is not a registry. +* "the 13 player-facing chatter tables are the WINGMAN tables" → refuted. +* "the 8 undeclared chatter tables are tutorial chatter" → refuted. +* "other datasheets ship a schema too" → refuted. + +## Encoding and text + +* "every IDXD string value is ASCII" → 6 non-ASCII values of 99 328. +* "`文字列` is a dev placeholder" → they are Shift-JIS **type words**.