agent: move the loop prompt's memory into the corpus, and let it push

The autonomous agent's loop prompt had grown to ~9 000 words of accumulated
findings, refutations and traps. That is a symptom, not a style: it was doing
the job the corpus should do, in the one place that dies with the container.
Three concrete failures followed from it, and each gets a structural fix rather
than a louder instruction.

1. IT REPEATED WORK IT HAD ALREADY DONE.
   The "do not revive" list and the method traps existed ONLY in the prompt, so
   nothing pointed a fresh iteration at them. Extracted verbatim into two
   tracked files:

     docs/re/REFUTED.md  105 claims tested and dead, grouped by subject so a
                         grep for your noun finds the neighbourhood
     docs/re/METHOD.md   the traps already paid for -- controls, inference,
                         searching, reading data, runtime

   Both are linked from INDEX.md, and the loop prompt now opens by requiring
   them to be read. This is the fix for "re-derived something already known":
   the knowledge is now where the next iteration looks, not in a context window.

2. IT FORGOT TO ARM THE NEXT WAKEUP.
   The prompt tried to solve this by shouting at itself in the first line. The
   real fix is to stop asking: `loose` now defaults to a FIXED interval (45m),
   so the harness owns the cadence and a forgotten ScheduleWakeup cannot end the
   run. SYLPH_LOOP_INTERVAL= (empty) restores self-pacing.

3. IT COULD NOT PUBLISH, SO THE WORK ONLY EXISTED IN THE CONTAINER.
   New `push-work`, plus a read-only credentials mount
   (SYLPH_GIT_CREDENTIALS, default ~/.sylph-git-credentials). It pushes the
   CURRENT branch only, refuses anything that is not auto/*, and never
   force-pushes -- so the consolidated line stays a human's decision and a
   confused iteration cannot rewrite history. The loop prompt now requires a
   push on every iteration that commits, rather than at the end of some longer
   arc, which is exactly when a container dies.

The prompt itself drops from ~9 000 words to 85 lines and, more importantly,
stops accumulating: findings go in the corpus, and the prompt points at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylpheed RE agent
2026-08-28 15:46:59 +02:00
parent 03ece95c06
commit e1dcc689bc
6 changed files with 453 additions and 31 deletions

67
docker/agent/bin/push-work Executable file
View File

@@ -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/<topic>`; 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/<topic>" >&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://<user>:<token>@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"

View File

@@ -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/<topic>`, 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/<topic>` 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.

View File

@@ -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://<user>:<token>@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://<user>:<token>@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"

View File

@@ -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.

105
docs/re/METHOD.md Normal file
View File

@@ -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 -- <path>` 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.**

185
docs/re/REFUTED.md Normal file
View File

@@ -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" → S01S16 story, S17 **cut**, S18S23
tutorials, S24S29 challenge.
* "S24S29 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_<l>###_<FACTION>_<name>`" → the grammar is
`UN_<letter>###_[<subkind>_]<FACTION>_<name>`.
* "`_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_<id>.xpr`" → refuted.
* "`<X>ID` + `<X>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**.