Compare commits

...

16 Commits

Author SHA1 Message Date
46ee0d7f78 chore(agents): restore evidence sharing after the asset purge
Some checks failed
CI / Native — linux (pull_request) Failing after 5m14s
CI / WASM — Web (pull_request) Successful in 36m12s
CI / Formatting (pull_request) Successful in 1m40s
Issue #49 removed game assets from git. It also, silently, removed the only
transport agents had for showing each other evidence: each agent works in its
OWN clone, so "commit the screenshot, the other one pulls it" was the mechanism,
and PROTOCOL.md's file table said so in as many words -- "evidence cited by a
finding -> git". That row sat directly above "🔴 Never commit game content",
which is how 76 MB accumulated: the two contradicted each other and the table
won, because it was the one that told you what to do.

WHAT REPLACES IT

  * one host directory, `Sylpheed/docs/re/captures/`, bind-mounted read-write
    into BOTH agents at /work/docs/re/captures. All three -- host, decoder,
    port -- see the same files live, every citation resolves, and nothing can
    reach git history. Read-write on purpose: showing each other a screenshot
    is the point.
  * PROTOCOL.md's table rewritten. Cited evidence -> present but never
    committed; derived measurements (csv/tsv/txt/log/json) -> still git, they
    are our numbers not game content; evidence that must cross MACHINES ->
    attached to the issue or PR, because a bare clone has no captures.

Verified, not assumed: container A wrote a .png there, a SEPARATE container B
read it back, the host saw it, `git status` reported 0 changes, and
`git check-ignore` named the rule.

THE CHECKER WAS RED ON EVERY CLEAN CHECKOUT

A fresh clone/worktree/CI has no captures, so it called all 134 citations
dangling and exited 1. A gate that is red before anyone changes anything is one
people learn to ignore -- the exact failure this file already carries a comment
about. It now distinguishes "no captures here" (expected, explains itself,
exit 0) from "these are missing" (real, exit 1, unchanged when assets ARE
present). Both paths tested.

ALSO
  * `sylph-decoder` no longer mounts `xenia-rs` -- retired repo, gone from disk,
    the mount pointed at nothing.
  * CONSOLIDATION.md closed: it still described captures as committed and the
    history fork as undecided. Both are settled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 13:12:26 +02:00
c0a5840ce8 Merge pull request 'fix: re-lock the self-referencing git dep after the history rewrite' (#60) from fix/relock-after-history-rewrite into main
Some checks failed
CI / Native — linux (push) Failing after 3h1m17s
CI / WASM — Web (push) Successful in 35m42s
CI / Formatting (push) Successful in 2m7s
Reviewed-on: #60
2026-09-20 11:01:57 +00:00
4156cb6bd1 fix: re-lock the self-referencing git dep after the history rewrite
Some checks failed
CI / Native — linux (pull_request) Failing after 2h42m36s
CI / WASM — Web (pull_request) Successful in 33m28s
CI / Formatting (pull_request) Successful in 2m29s
`crates/sylpheed-export` depends on THIS repository by tag:

    sylpheed-formats = { git = "…/Sylpheed.git", tag = "formats-pin-2026-09-01" }

and `Cargo.lock` pinned it to commit `#1cd5b8b1`. The issue-#49 history rewrite
replaced every commit, so `1cd5b8b1` no longer exists on the remote and the tag
now dereferences to `e2630413`.

⚠️ The failure is invisible here: `~/.cargo/git` still has the old object, so
builds on the machine that did the rewrite keep working. A clean checkout --
CI, or the other desktop -- cannot resolve the locked rev at all. That is the
worst shape for a breakage, which is why this was measured from an empty
CARGO_HOME rather than trusted to a local build.

`CONSOLIDATION.md` called this dependency out as the hard blocker for the
rewrite. It was not re-checked before the push; this is the fix.

The dependency's SOURCE is unchanged -- `crates/sylpheed-formats` has tree
`55461e46` at both the old and the new commit, so the rewrite never touched it
and the build result is identical.

Verified:
  * remote tag peels to e2630413, matching the new lock
  * `cargo fetch --locked` from an EMPTY CARGO_HOME -> exit 0
  * `cargo check -p sylpheed-export --locked` -> exit 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 23:11:06 +02:00
e16556dc29 Merge pull request 'chore(re): stop committing game assets; captures stay local' (#59) from chore/captures-local-only into main
Some checks failed
CI / Native — linux (push) Failing after 6m55s
CI / WASM — Web (push) Failing after 6m24s
CI / Formatting (push) Successful in 1m8s
Reviewed-on: #59
2026-09-19 19:16:47 +00:00
d96b225e47 chore(re): stop committing game assets; captures stay local
Issue #49: the repos carry code, tooling and docs only.

Untracks 143 screenshots, 3 savegame blobs and `tools/re-capture/ob_digits.png`
(a digit-template sheet cut from game frames) -- 146 files, 76.4 MB. They stay
in the working tree and are gitignored, so the pages' relative links still
resolve where the captures exist and nothing ships.

Derived measurements (csv/tsv/txt/log/json/jsonl/npy) are our own numbers, not
game content, and stay tracked -- they are what most claims rest on.

`check-capture-citations` had its contract inverted, and it is the half worth
reading:

  * presence now comes from the WORKING TREE, not `git ls-files`. The assets are
    deliberately untracked, so asking the index would report every screenshot as
    missing and fail all 203 citations.
  * a NEW failure: a game asset that IS tracked. A screenshot that sneaks back
    in is invisible in review -- a binary shows as "Bin 0 -> 1234567 bytes" --
    and is permanent once merged, since removing it later needs a history
    rewrite. So that half has to be loud.

Verified:
  * selftest 8/8, including the new rule
  * scan: 212 present, 212 cited, 0 dangling, 0 tracked  -> exit 0
  * force-add one PNG -> "game assets TRACKED: 1", exit 1, selftest red

⚠️ This does NOT remove the blobs from history; a clone still fetches them.
That needs a filter-repo rewrite and a force-push, which is a separate,
human-run step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 21:03:46 +02:00
4c1e15b818 Merge pull request 'fix(test): open sound.pak once, not once per call' (#58) from fix/slb-suite-one-archive into main
Reviewed-on: #58
2026-09-19 18:48:28 +00:00
9a8746d380 Merge pull request 'fix(ci): run the container as the invoking user, not root' (#57) from fix/ci-run-file-ownership into main
Reviewed-on: #57
2026-09-19 18:48:21 +00:00
1cbbfd90cb Merge pull request 'docs(re): adopt the homeless disc-contents page, re-measured' (#56) from docs/adopt-disc-contents into main
Reviewed-on: #56
2026-09-19 18:48:14 +00:00
e82d7bff6f Merge pull request 'chore: hand the workspace over — the gated launchers, and the state of play' (#55) from chore/handoff-2026-09-18 into main
Reviewed-on: #55
2026-09-19 18:48:08 +00:00
00585952a9 Merge pull request 'ci: pin the toolchain to 1.98.1 in all three jobs' (#54) from fix/ci-pin-toolchain into main
Reviewed-on: #54
2026-09-19 18:48:00 +00:00
cd3c1a2f73 Merge pull request 'test: one disc resolver, no machine-specific defaults, and all three corpora in the CI container' (#53) from fix/corpus-mounts-and-paths into main
Reviewed-on: #53
2026-09-19 18:47:53 +00:00
2f39c8826e fix(test): open sound.pak once, not once per call
`slb_leading_segment_disc` was SIGKILLed by the OOM killer in `docker/ci/run`
at its 7 GB cap, so the documented 45/377 baseline did not reproduce on a 15 GB
box.

`PakArchive` holds the whole concatenated payload in memory and `sound.pak` is
1.01 GB (sound.p00-.p04). `bank()` and `bank_named()` opened it on every call --
inside loops -- and five tests opened their own besides, ~26 opens in all. With
cargo's default thread count that is ~6.1 GB of archive in flight against a
7 GB cap with `--memory-swap` equal to `--memory`, so there is no swap to
absorb it.

The archive is immutable once open and every accessor takes `&self`, so one
`OnceLock` instance is equivalent to N private ones at 1/N the memory.

⚠️ The failure mode is worth knowing: a SIGKILLed suite prints no
`test result:` line at all, so it disappears from a scraped tally instead of
failing visibly. The run still reported "0 failed" -- true, and useless. Check
cargo's exit code (101), not the tally.

Measured in the capped container, 7 GB, default threads:
  * before: SIGKILL (signal 9), 0 of 10 tests reported
  * after : 10 passed in 3.17s
  * (single-threaded before the fix: 10 passed in 22.64s -- the fix is also
     ~7x faster, because it no longer re-reads 1 GB from disc 26 times)
  * cargo fmt --check clean; cargo clippy --tests -D warnings clean

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 16:59:10 +02:00
4a8f1221b2 fix(ci): run the container as the invoking user, not root
Docker on the dev boxes is rootful, so without `--user` every byte the build
writes into the bind-mounted repo is owned by root and the user needs `sudo` to
delete their own artifacts. This is not hypothetical: `export/` in a working
tree held 227 root-owned paths (149 MB) from earlier runs, and the `sylpheed.db`
regen in the workspace CLAUDE.md writes straight into /work, so it lands
root-owned every time.

The catch is that the daemon creates a named volume root-owned, so a `--user`
container cannot write /cargo or /target at all. So take ownership of both
volumes first -- once, and only when it is actually wrong, since a recursive
chown across a ~36 GB target volume is not something to repeat per invocation.
Both are sampled, not just one, because an older run can leave them drifted.

Volume names become overridable (SYLPH_CI_CARGO_VOL / SYLPH_CI_TARGET_VOL),
which is what let the chown path be tested without touching the real caches.

Placed above the corpus-mount block so it does not collide with #53.

Measured, not assumed:
  * fresh root-owned volumes  -> chowns once, then writes as uid 1000
  * second run                -> no chown, correctly cached
  * `cargo check -p sylpheed-ppc` through the runner -> passes, exit 0
  * a file touched in /work   -> owned fabi:fabi, removable without sudo

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 21:30:29 +02:00
06601c2a48 docs(re): adopt the homeless disc-contents page, re-measured
`GAME_CONTENTS.md` sat in the workspace root through the whole consolidation,
adopted by neither repo -- the corpus documents formats in depth but never said
what files the disc holds or where they sit.

Re-measured rather than transcribed, and that caught a real error: the source
placed `resource3d/`, `DefTables` and `MiscBin` under `dat/`. All three are in
`hidden/` -- which is why `SYLPHEED_RES3D` points at `hidden/resource3d`.
Counts, the language table and `media_id` (0x2D2E2EEB, from the XEX header)
re-verified against the retail extract.

Half the source was pre-RE speculation phrased as status -- `dat/*.pak` marked
"Unknown, magic bytes TBD" when the container is decoded disc-wide, plus a table
guessing each archive's contents from its name. Carrying that forward would put
claims into the corpus the corpus has already refuted, so it is dropped and the
page says what was dropped and why. INDEX.md stays the single authority on
format status.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 21:07:03 +02:00
sim
c88391b68c ci: pin the toolchain to 1.98.1 in all three jobs
All three jobs used `dtolnay/rust-toolchain@stable`, so the gate resolved to
whatever stable was on the day it ran. A floating lint gate is not a gate: the
same tree goes green or red by date, and that already produced a disagreement
between two people reading the same commit — `collapsible_else_if` is `warn` on
1.92.0 and `allow`-by-default pedantic on 1.98.1, so both readings were correct.

1.98.1 is what run 206 resolved and what `docker/ci/Dockerfile` already pins, so
`docker/ci/run cargo clippy …` on a desktop becomes a true stand-in for this
workflow instead of an approximation. The header says how to bump: the three
refs here and the Dockerfile's `FROM` in one commit, in a PR of its own, where
the lints the new version turns on are the diff.

Also: the corpus-report step's comment quoted `207/0/14`, a tally two baselines
old. It now describes the shape of the problem without pinning a number that
decays.

Closes #15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 22:21:27 +02:00
sim
cc9392bde4 test: one disc resolver, no machine-specific defaults, all three corpora in the container
Finishes #16 in the three places its earlier remedies missed.

`tests/`: the last four local `disc_root()` copies now use `tests/common`, and
with them goes the one real hardcoded fallback — `ui_keyframe_record_disc.rs`
fell back to an absolute path on one machine, which made `unset SYLPHEED_DISC`
a no-op there. Control: with the corpus absent that suite now finishes in 0.00s
instead of 57.55s, so it skips rather than finding a disc of its own.

`examples/`: seventeen examples defaulted to `/disc`, the mount point inside the
CI container. Redundant there — `docker/ci/run` sets `SYLPHEED_DISC=/disc` — and
wrong everywhere else, where a missing corpus turned into a file-not-found
against a path that has never existed on the host. They now name the variable to
set, like the other hundred examples already did.

`docker/ci/run`: mount `$SYLPHEED_RES3D` and `$SYLPHEED_ISO` alongside the disc.
Only the disc was mounted, so an in-container run sat out the res3d and iso
suites while looking like a full one — the defect this issue is about, in the
runner itself.

Measured in the container on this desktop with all three corpora present:
45 suites / 377 passed / 0 failed / 14 ignored, and `sylpheed-corpus-report.txt`
now reports PRESENT for all three rather than for the disc alone.

Refs #16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 22:18:59 +02:00
37 changed files with 460 additions and 90 deletions

View File

@@ -31,6 +31,24 @@ env:
# So: one job, on the machine that exists, building for the machine that exists.
# If a second architecture is ever wanted here it needs a second RUNNER, not a
# second matrix row.
#
# ── The toolchain is PINNED, in three places, deliberately ───────────────────
#
# All three jobs used `dtolnay/rust-toolchain@stable`, which resolves to whatever
# stable is on the day the job runs. A lint gate that floats is not a gate: the
# same tree goes green or red depending on the date, and this repo has already
# produced a disagreement between two people reading the same commit (#15).
# `collapsible_else_if` is the example — `warn` on 1.92.0, `allow`-by-default
# pedantic on 1.98.1, so a clean local run and a red CI run were both correct.
#
# `1.98.1` is the version run 206 resolved, and `docker/ci/Dockerfile` pins the
# same one, so `docker/ci/run cargo clippy …` on a desktop is a true stand-in for
# this workflow rather than an approximation of it.
#
# To bump: change all three `dtolnay/rust-toolchain@` refs here AND the `FROM
# rust:<version>-bookworm` in `docker/ci/Dockerfile` in one commit, so the two
# can never drift apart silently. A bump is a change to the gate and belongs in
# its own PR, where the new lints it turns on are the diff.
jobs:
# ── Native build, on the one runner there is ────────────────────────────────
@@ -42,13 +60,15 @@ jobs:
- uses: actions/checkout@v4
- name: Install Rust toolchain
# `stable` installs a MINIMAL profile: rustc, cargo, rust-std and no
# Pinned — see the toolchain note at the top of this file.
#
# This action installs a MINIMAL profile: rustc, cargo, rust-std and no
# more. Components have to be named. Without this line the Clippy step
# below dies on "'cargo-clippy' is not installed for the toolchain
# 'stable-aarch64-unknown-linux-gnu'" — which is not a lint result, it
# is the step never having run. The `fmt` job below always got this
# right; this one never did.
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@1.98.1
with:
components: clippy
@@ -78,7 +98,7 @@ jobs:
run: cargo test --workspace
# The tally above cannot tell you what it verified. `cargo test` reports
# the same 207/0/14 whether the disc corpus was exercised or entirely
# the same count whether the disc corpus was exercised or entirely
# absent -- a gated suite that skips still counts as passed, and the
# `ignored` column is a static count of `#[ignore]` attributes that cannot
# move at runtime. Issue #16. This prints what the run ACTUALLY had, from
@@ -114,7 +134,7 @@ jobs:
- uses: actions/checkout@v4
- name: Install Rust toolchain + WASM target
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@1.98.1
with:
targets: wasm32-unknown-unknown
@@ -161,7 +181,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@1.98.1
with:
components: rustfmt
- run: cargo fmt --all -- --check

20
Cargo.lock generated
View File

@@ -221,7 +221,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -232,7 +232,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -2507,7 +2507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -3824,7 +3824,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -4818,7 +4818,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -5116,7 +5116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -5237,7 +5237,7 @@ dependencies = [
[[package]]
name = "sylpheed-formats"
version = "0.1.0"
source = "git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-09-01#1cd5b8b1cb1f02eefc0865e1a1fe280e44831c9d"
source = "git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-09-01#e26304133732792cb1df5563c21df59471f5bf7d"
dependencies = [
"anyhow",
"binrw",
@@ -5417,7 +5417,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@@ -5776,7 +5776,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
]
[[package]]
@@ -6294,7 +6294,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.61.2",
"windows-sys 0.48.0",
]
[[package]]

View File

@@ -7,7 +7,8 @@ use std::process::Command;
use sylpheed_formats::media;
fn main() {
let disc = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let disc =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let src = media::DirectorySource::new(&disc);
for bank in ["BGM_103.slb", "BGM_102.slb", "BGM_001.slb"] {
match media::sound_bank_riffs(&src, bank) {

View File

@@ -18,7 +18,8 @@
use sylpheed_formats::media;
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let src = media::DirectorySource::new(&root);
const WANT: [usize; 2] = [3_876_864, 3_930_112];
let (mut found, mut matches) = (0usize, Vec::new());

View File

@@ -20,7 +20,8 @@ use std::collections::BTreeSet;
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let ar = pak::PakArchive::open(format!("{root}/dat/GP_DIALOG.pak")).expect("GP_DIALOG.pak");
let sets: Vec<Option<BTreeSet<String>>> = ar
.entries()

View File

@@ -17,7 +17,8 @@
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
// 🔴 WIDENED 2026-08-31 to every pak, to check the Decoder's rival search
// independently. They report zero four-button builds within 6 px of
// 259/329/399/469 anywhere on the disc, which turns "another dialog with

View File

@@ -8,7 +8,8 @@
use sylpheed_formats::{pak::PakArchive, ui_layout};
fn main() {
let disc = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let disc =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let ar = PakArchive::open(format!("{disc}/dat/GP_TITLE.pak")).expect("open");
let e = &ar.entries()[4]; // entry 4 = the English title
let bundle = ar.read(e).expect("read");

View File

@@ -48,7 +48,8 @@ fn main() {
.unwrap_or(8);
unsafe { OFFSET = off };
println!(" reading the loop length at header +0x{off:02x}");
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()

View File

@@ -11,7 +11,8 @@
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()

View File

@@ -11,7 +11,8 @@
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let ar = pak::PakArchive::open(format!("{root}/dat/GP_TITLE.pak")).expect("GP_TITLE.pak");
let (mut total, mut hits, mut multipose) = (0usize, 0usize, 0usize);
for (i, e) in ar.entries().iter().enumerate() {

View File

@@ -7,7 +7,8 @@ use std::process::Command;
use sylpheed_formats::{media, slb::VoiceLang};
fn main() {
let disc = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let disc =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let src = media::DirectorySource::new(&disc);
for movie in ["ADV", "S00A", "RT01A"] {
let Some((s, e)) = media::resolve_movie_voice_region(&src, movie, VoiceLang::English)

View File

@@ -1,6 +1,7 @@
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let ar = pak::PakArchive::open(format!("{root}/dat/GP_READY_ROOM.pak")).unwrap();
for (i, e) in ar.entries().iter().enumerate() {
let Ok(by) = ar.read(e) else { continue };

View File

@@ -16,8 +16,7 @@
//! Usage:
//! SYLPHEED_ISO=... cargo run --release --example correlate_capture -- \
//! <capture.log> <Stage_SNN> <ship_id> [ref_part_substr] [--emit]
//! e.g. SYLPHEED_ISO="/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of
//! Deception (USA, Europe) (En,Ja).iso" \
//! e.g. SYLPHEED_ISO="/path/to/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso" \
//! cargo run --release --example correlate_capture -- \
//! xenia_ship_capture.log Stage_S01 e106 bdy_04 --emit

View File

@@ -11,7 +11,8 @@
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()

View File

@@ -15,7 +15,8 @@ use std::collections::BTreeMap;
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()

View File

@@ -6,7 +6,8 @@
use std::collections::BTreeMap;
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()

View File

@@ -14,7 +14,8 @@
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()

View File

@@ -33,7 +33,8 @@ fn opaque_span(el: &ui_layout::Element, thr: u32, tmax: u32) -> Vec<(f64, f64)>
}
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()

View File

@@ -58,7 +58,8 @@ fn forced(b: &ui_layout::UiBuild, el: &ui_layout::Element, tmax: u32, hold: bool
}
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()

View File

@@ -20,7 +20,8 @@ use std::collections::BTreeMap;
use sylpheed_formats::{pak, ratc, ui_layout};
fn main() {
let root = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let root =
std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC to the extracted disc root");
let mut paks: Vec<_> = std::fs::read_dir(format!("{root}/dat"))
.expect("dat/")
.flatten()

View File

@@ -6,14 +6,34 @@
//! silence. One rule, two outcomes.
use std::path::Path;
use std::sync::OnceLock;
use sylpheed_formats::{slb, PakArchive};
mod common;
use common::skip_without_disc;
/// One archive for the whole binary.
///
/// `PakArchive` holds the entire concatenated payload in memory, and
/// `sound.pak` is **1.01 GB** (`sound.p00`-`.p04`). Opening it per call — which
/// the helpers below did, inside loops — put one copy per test thread in flight,
/// so at the default thread count the suite needed ~6 GB and was SIGKILLed by
/// the CI container's 7 GB cap (`--memory-swap` equals `--memory`, so there is
/// no swap to absorb it). A killed suite prints no `test result:` line at all,
/// so it vanishes from the tally rather than failing visibly.
///
/// The archive is immutable once open and every accessor takes `&self`, so one
/// shared instance is equivalent to N private ones — at 1/N the memory.
fn sound(root: &Path) -> &'static PakArchive {
static SOUND: OnceLock<PakArchive> = OnceLock::new();
// Every caller passes the same `disc_root()`, so first-writer-wins is the
// same archive whichever test initialises it.
SOUND.get_or_init(|| PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak"))
}
fn bank(root: &Path, n: u32) -> Vec<u8> {
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
let snd = sound(root);
let path = format!("eng\\etc\\VOICE_D_{n}.slb");
let entry = snd.find_by_name(&path).expect("bank present");
snd.read(entry).expect("read")
@@ -71,7 +91,7 @@ fn all_zero_leading_region_is_skipped() {
}
fn bank_named(root: &Path, path: &str) -> Vec<u8> {
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
let snd = sound(root);
let entry = snd
.find_by_name(path)
.unwrap_or_else(|| panic!("{path} present"));
@@ -154,7 +174,7 @@ fn derived_offset_recovers_voice_banks_without_regressing_etc() {
#[test]
fn scan_data_offset_agrees_with_the_riff_derived_answer() {
skip_without_disc!(root);
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
let snd = sound(&root);
let mut checked = 0usize;
let mut agreed = 0usize;
for lang in ["eng", "jpn"] {
@@ -198,7 +218,7 @@ fn scan_data_offset_agrees_with_the_riff_derived_answer() {
#[test]
fn scan_only_returns_known_offsets() {
skip_without_disc!(root);
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
let snd = sound(&root);
let mut seen = 0usize;
for n in 1u32..200 {
for path in [
@@ -232,7 +252,7 @@ fn scan_only_returns_known_offsets() {
#[test]
fn a_waves_declared_size_is_confirmed_by_the_next_seek() {
skip_without_disc!(root);
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
let snd = sound(&root);
let mut checked = 0usize;
for n in 1u32..400 {
for path in [
@@ -289,7 +309,7 @@ fn a_waves_declared_size_is_confirmed_by_the_next_seek() {
#[test]
fn a_bank_that_states_its_own_header_has_no_leading_segment() {
skip_without_disc!(root);
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
let snd = sound(&root);
let mut with_header = 0usize;
let mut mid_bank = 0usize;
// Peek at the 56-byte header through the archive's flat data rather than
@@ -327,7 +347,7 @@ fn a_bank_that_states_its_own_header_has_no_leading_segment() {
#[test]
fn the_menu_music_bank_is_exactly_two_sub_waves() {
skip_without_disc!(root);
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
let snd = sound(&root);
for (name, sizes) in [
("BGM_103.slb", [3_876_864usize, 3_930_112]),
("BGM_001.slb", [4_466_688, 4_673_536]),

View File

@@ -16,10 +16,8 @@ use std::path::PathBuf;
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
fn disc_root() -> Option<PathBuf> {
let p = PathBuf::from(std::env::var("SYLPHEED_DISC").ok()?);
p.join("dat").is_dir().then_some(p)
}
mod common;
use common::disc_root;
fn build(ar: &PakArchive, i: usize) -> (Vec<u8>, ui_layout::UiBuild) {
let by = ar.read(&ar.entries()[i]).expect("entry");

View File

@@ -26,21 +26,8 @@ use std::path::{Path, PathBuf};
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
fn disc_root() -> Option<PathBuf> {
if let Ok(p) = std::env::var("SYLPHEED_DISC") {
let p = PathBuf::from(p);
if p.join("dat").is_dir() {
return Some(p);
}
}
let default = Path::new(
"/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)",
);
if default.join("dat").is_dir() {
return Some(default.to_path_buf());
}
None
}
mod common;
use common::disc_root;
fn for_each_build(root: &Path, mut f: impl FnMut(&str, &[u8])) {
let mut paks: Vec<PathBuf> = std::fs::read_dir(root.join("dat"))

View File

@@ -22,10 +22,8 @@ use std::path::PathBuf;
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
fn disc_root() -> Option<PathBuf> {
let p = PathBuf::from(std::env::var("SYLPHEED_DISC").ok()?);
p.join("dat").is_dir().then_some(p)
}
mod common;
use common::disc_root;
/// Read a nested record's declared length and its largest keyframe time.
fn record_len_and_maxt(bundle: &[u8], off: usize, size: usize) -> Option<(i64, i64)> {

View File

@@ -25,15 +25,8 @@ use std::path::PathBuf;
use sylpheed_formats::{pak::PakArchive, ratc, ui_layout};
fn disc_root() -> Option<PathBuf> {
if let Ok(p) = std::env::var("SYLPHEED_DISC") {
let p = PathBuf::from(p);
if p.join("dat").is_dir() {
return Some(p);
}
}
None
}
mod common;
use common::disc_root;
/// The case that found the bug, asserted end to end.
#[test]

View File

@@ -21,23 +21,68 @@ IMAGE="${SYLPH_CI_IMAGE:-sylph-ci:local}"
CPUS="${SYLPH_CI_CPUS:-6}"
MEM_GB="${SYLPH_CI_MEM_GB:-7}"
CARGO_VOL="${SYLPH_CI_CARGO_VOL:-sylph-ci-cargo}"
TARGET_VOL="${SYLPH_CI_TARGET_VOL:-sylph-ci-target}"
# 🔴 Docker on the dev boxes is ROOTFUL, so without `--user` every byte the build
# writes into the bind-mounted repo is owned by root — and the user then needs
# `sudo` to delete their own artifacts. The regen command in the workspace
# `CLAUDE.md` writes `sylpheed.db` straight into /work, so it lands root-owned,
# and a stray root-owned file is exactly what survived the last cleanup and had
# to be sudo'd away.
#
# The catch is that the daemon creates a named volume root-owned, so a `--user`
# container cannot write /cargo or /target at all. Take ownership once — and
# only when it is actually wrong, because a recursive chown across a ~36 GB
# target volume is not something to repeat on every invocation.
RUN_UID="$(id -u)"
RUN_GID="$(id -g)"
docker volume create "$CARGO_VOL" >/dev/null
docker volume create "$TARGET_VOL" >/dev/null
# Both volumes, not just one: they are chowned together but can drift apart if an
# older root-owned run created only one of them.
vol_owner="$(docker run --rm -v "$CARGO_VOL:/cargo" -v "$TARGET_VOL:/target" "$IMAGE" \
stat -c %u /cargo /target 2>/dev/null | sort -u | tr '\n' ' ' || echo unknown)"
if [ "$vol_owner" != "$RUN_UID " ]; then
echo "docker/ci/run: chowning the cargo/target volumes to $RUN_UID:$RUN_GID (one-off)" >&2
docker run --rm \
-v "$CARGO_VOL:/cargo" -v "$TARGET_VOL:/target" \
"$IMAGE" chown -R "$RUN_UID:$RUN_GID" /cargo /target
fi
args=(
--rm
--cpus "$CPUS"
--memory "${MEM_GB}g"
--memory-swap "${MEM_GB}g"
--pids-limit 2048
# Run as the invoking user so build output in /work is owned by them, not root.
--user "$RUN_UID:$RUN_GID"
-v "$REPO:/work"
# Named volumes, not bind mounts: the host tree keeps a 32 GB `target/` from
# earlier host-side builds, and mixing the two produces rebuilds that look
# like cache misses and are actually two toolchains fighting over one directory.
-v sylph-ci-cargo:/cargo -e CARGO_HOME=/cargo
-v sylph-ci-target:/target -e CARGO_TARGET_DIR=/target
-v "$CARGO_VOL:/cargo" -e CARGO_HOME=/cargo
-v "$TARGET_VOL:/target" -e CARGO_TARGET_DIR=/target
-w /work
)
# The disc, read-only, when a disc-backed test or the exporter needs it.
# The corpora, read-only, when a disc-backed test or the exporter needs them.
#
# All three, not just the disc: a suite whose corpus is absent self-skips and
# still counts as passed, so mounting one of three made an in-container run look
# like a full one while `res3d` and `iso` suites silently sat out (#16). Each is
# mounted only when it exists, and `target/sylpheed-corpus-report.txt` says which
# ones the run actually had.
DISC="${SYLPHEED_DISC:-$REPO/../sylph_extract}"
[ -d "$DISC" ] && args+=(-v "$DISC:/disc:ro" -e SYLPHEED_DISC=/disc)
RES3D="${SYLPHEED_RES3D:-}"
[ -n "$RES3D" ] && [ -d "$RES3D" ] && args+=(-v "$RES3D:/res3d:ro" -e SYLPHEED_RES3D=/res3d)
ISO="${SYLPHEED_ISO:-}"
[ -n "$ISO" ] && [ -f "$ISO" ] && args+=(-v "$ISO:/disc.iso:ro" -e SYLPHEED_ISO=/disc.iso)
exec docker run "${args[@]}" "$IMAGE" "$@"

View File

@@ -122,11 +122,19 @@ docker_args() {
# (file offset = VA - 0x82000000), which removes the need to boot the
# emulator and scrape /dev/shm to get at it. An earlier belief that this
# file was STALE was tested and refuted -- it is current.
-v "${SYLPH_XENIA_RS:-$PROJECT/xenia-rs}:/xenia-rs:ro"
-v "${SYLPH_PE:-$PROJECT/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).pe}:/image/sylpheed.pe:ro"
-e "SYLPHEED_DB=/xenia-rs/sylpheed.db"
-e "SYLPHEED_PE=/image/sylpheed.pe"
-e "SYLPHEED_IMAGE_BASE=0x82000000"
# 🔴 EVIDENCE IS SHARED, NOT COMMITTED (issue #49). Each agent works in its
# OWN clone, so a gitignored capture written in one container is invisible to
# the other and to the human -- git used to be the transport and no longer is.
# One host directory, bind-mounted into both agents, is: all three see the
# same files live, every `docs/re/captures/...` citation resolves everywhere,
# and nothing can reach git history. Read-write on purpose -- showing each
# other a screenshot is the point.
-v "${SYLPH_CAPTURES:-$PROJECT/Sylpheed/docs/re/captures}:/work/docs/re/captures"
# The shared exchange: transient files with provenance, outside git history.
-v "sylpheed-exchange:/exchange"
-e "PROJECT_DIR=/work"

View File

@@ -67,6 +67,14 @@ docker_args() {
-v "sylpheed-port-claude:/sylph-home/port/.claude"
-v "${SYLPH_CLAUDE_HOME:-$HOME/.claude}:/sylph-home/port/.claude.seed:ro"
-v "${SYLPH_CLAUDE_JSON:-$HOME/.claude.json}:/sylph-home/port/.claude.host.json:ro"
# 🔴 EVIDENCE IS SHARED, NOT COMMITTED (issue #49). Each agent works in its
# OWN clone, so a gitignored capture written in one container is invisible to
# the other and to the human -- git used to be the transport and no longer is.
# One host directory, bind-mounted into both agents, is: all three see the
# same files live, every `docs/re/captures/...` citation resolves everywhere,
# and nothing can reach git history. Read-write on purpose -- showing each
# other a screenshot is the point.
-v "${SYLPH_CAPTURES:-$WORKSPACE/Sylpheed/docs/re/captures}:/work/docs/re/captures"
-v "sylpheed-exchange:/exchange"
-e "PROJECT_DIR=/work"
# Same guardrail as the decoder, added the same day and for its reason: the

View File

@@ -1,3 +1,27 @@
# ✅ CLOSED 2026-09-20 — every phase done, and the two human decisions taken
Nothing on this page is outstanding. It is kept as the record of how the two-repo
layout was reached; **where it describes the repository's present state it is
now out of date on purpose**, and the notes below say how.
| the page says | what is true now |
|---|---|
| "**Still open, for the human**: the history fork" | **Taken: full purge.** `git filter-repo` stripped 182 assets from every commit; force-pushed 2026-09-19. `main` `2a93be99``e16556dc`, `.git` **217 MB → 114 MB**, **0** assets anywhere in history, verified from an independent fresh clone. Backups + runbook: `~/sylph-repo-backup-2026-09-19/` |
| "`docs/re/captures/` is 118 MB of game screenshots inside a public repository" | **Decided (#49): no game assets in the repos.** Captures live on disk, gitignored, shared between the host and both agents by bind mount; derived measurements (csv/tsv/txt/log/json/npy) stay tracked. PR #59 |
| "all six repos are `private=False`" | Still true, and now deliberate — there is nothing game-derived in them |
| "`ai-agent-*.md`, `scratch/`, `_voice_span.rs` — keep or drop" | `scratch/` and `_voice_span.rs` dropped in the 2026-09-18 cleanup; the `ai-agent-*.md` files are the human's and stay |
| the four archived repos | unchanged: `Sylpheed-Godot`, `xenia-rs`, `xex2tractor` archived read-only, `Syplheed-Reborn` deleted. ⚠️ Their local clones are **gone from this machine** too, and `sylph-decoder` no longer mounts `xenia-rs` |
🔴 **The one thing this page got right and nobody acted on.** It named
`crates/sylpheed-export`'s dependency on **this repository by tag** as the hard
blocker for a rewrite. It was not re-checked before the purge, the locked rev
vanished, and the build broke for every clean checkout while still working on the
machine that did the rewrite — its `~/.cargo/git` still held the old object.
Fixed in PR #60 (re-lock) and PR #61 (path dependency, which removes the class).
**Before any future history rewrite, grep for a dependency on the repo's own URL.**
---
# Consolidating six repositories into two
**Set by the human, 2026-09-13.** The end state is **two** repositories:

View File

@@ -88,6 +88,42 @@ sylph-agent-canary-build the warm 235 MB Canary build tr
sylpheed-exchange agent -> agent files, read-only in
```
**Plus one bind mount, which is not a volume on purpose** — the host's
`Sylpheed/docs/re/captures/`, mounted read-write into both agents at
`/work/docs/re/captures`:
```
-v "${SYLPH_CAPTURES:-<workspace>/Sylpheed/docs/re/captures}:/work/docs/re/captures"
```
🔴 **Why it has to exist.** Each agent works in its OWN clone, so before issue
#49 the transport for evidence was *git*: commit the screenshot, the other agent
pulls it. #49 removed that — captures are gitignored now — and without a
replacement a capture written in one container is invisible to the other agent,
to the human, and to `check-capture-citations`, which would call every citation
dangling. One host directory shared by all three restores it: the same file is
live everywhere, every `docs/re/captures/...` citation resolves, and nothing can
reach git history. Read-write on both, because showing each other a screenshot
is the point.
Verified rather than assumed: container A wrote a `.png` there, a **separate**
container B read it back, the host saw it, and `git status` reported **0**
changes.
⚠️ **Evidence that must cross MACHINES still cannot go this way** — a bare clone
on the other desktop has no captures at all. Attach it to the issue or PR; that
is the only channel that travels.
⚠️ **The agent volumes were all deleted in the 2026-09-18 cleanup** and Docker
recreates them empty on next launch. Nothing was lost — both agent clones were
verified clean with nothing unpushed, and the exchange held only spent artefacts.
One side effect is welcome: the empty `*-claude` volumes mean the next launch
starts a **fresh** session, which is the documented workaround for the
"resumes the old brief" defect below.
⚠️ **`sylph-decoder` used to mount `xenia-rs` read-only. That repo is retired and
the directory is gone**, so the mount pointed at nothing; removed.
**Credentials** — three files on the host, `chmod 600`, mounted read-only:
```

View File

@@ -129,10 +129,18 @@ exchange volume carries the working artefacts.
| kind | where | why |
|---|---|---|
| code, decoded knowledge | **git** | history, review, permanence |
| evidence cited by a finding | **git** | it is the proof |
| evidence cited by a finding — a screenshot, a frame, a savegame | **`docs/re/captures/`, present but NEVER committed** | it is the proof, but it is game content. The directory is gitignored and shared between the host and both agents, so all three see the same file; `tools/re/check-capture-citations` fails if a citation has no file, and fails again if an asset is tracked |
| a measurement you derived — csv, tsv, txt, log, json | **git** | our own numbers, not game content, and most findings rest on them |
| evidence that must cross machines | **attached to the issue or PR** | captures are local-only now, so a bare clone has none of them. Attaching is the only channel that reaches the other desktop or a reviewer |
| **evidence a human must look at** — the screenshot or film behind a `state/needs-human` item | **attached to that issue** | it travels *with* the item, a person sees it in a browser, and it cannot be orphaned from the claim it supports |
| exploratory captures, work in progress, "look at this" | **`share`** → `/exchange` | no history; would bloat the repo forever |
⚠️ **This table's first row used to say "git", and that is how 76 MB of
screenshots accumulated** — directly above the rule forbidding exactly that. The
two contradicted each other for months and the table won, because it was the one
that told you what to *do*. Issue #49 settled it and the history was rewritten;
if you find yourself reaching for `git add -f` on a capture, the answer is no.
🔴 **Never commit game content.** Not sprites, not audio, not transcoded video,
not a capture of the running game — under *any* directory name. On 2026-09-04
this rule was live, and freshly tightened, while **545 MB of extracted disc

View File

@@ -5,7 +5,8 @@ Confidence: ✅ `CONFIRMED` · 🟡 `PROBABLE` · ❔ `HYPOTHESIS`. See [README]
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.
[`BACKLOG.md`](BACKLOG.md) — what is still open ·
[`disc-contents.md`](disc-contents.md) — what files the disc actually holds, and where.
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.
@@ -72,6 +73,7 @@ files, which is how the same ground got covered twice.
| [`autopilot-memory-driven.md`](autopilot-memory-driven.md) | Memory-driven autopilot — build log and current state | 🟢 IT FLIES, KILLS AND SURVIVES — but it loses the mission anyway. |
| [`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) | Getting past the title screen in the container — three traps and one blocker | ✅ CONFIRMED for the three traps (each reproduced, and two of them |
| [`challenge-mission-gate.md`](challenge-mission-gate.md) | Challenge / EX missions — the stage set, the GamePart graph, and the kind field | ✅ for the static structure (stage set, GamePart ids, the config-section |
| [`disc-contents.md`](disc-contents.md) | What is actually on the disc, and where | ✅ CONFIRMED — layout, counts and `media_id` re-measured against the retail extract 2026-09-18; `resource3d/` is in `hidden/`, not `dat/` |
| [`dynamic-re-state-restore.md`](dynamic-re-state-restore.md) | The container's dynamic-RE state is not durable — how to rebuild it | ✅ CONFIRMED by rebuilding it (2026-08-23). Everything the dynamic |
| [`flight-controls-runtime.md`](flight-controls-runtime.md) | In-flight control mapping — measured, not assumed | ✅ for the weapon bindings (ammo counters move), 🟡 for the rest (HUD |
| [`flight-speed-law.md`](flight-speed-law.md) | The throttle is a TARGET-SPEED selector — measured against the definition (2026-08-13) | ✅ for the shape of the law, 🟡 for the unit scale. |

View File

@@ -18,6 +18,22 @@ A wrong-but-confident note is worse than no note: someone builds on it and the b
for weeks. Every entry therefore carries an explicit **confidence** and its **evidence**.
This mirrors the project method — *measure the oracle, never infer; refute before believing.*
### Captures are local-only (issue #49)
The repository carries **code, tooling and docs**. Screenshots and savegame
blobs are game-derived, so since 2026-09-19 they live in `docs/re/captures/`
on disk and are **gitignored** — the pages' relative links still resolve on a
machine that has them, and nothing ships.
Derived measurements (`csv`, `tsv`, `txt`, `log`, `json`, `jsonl`, `npy`) are
our own numbers rather than game content, and stay tracked — they are what most
claims here actually rest on.
`tools/re/check-capture-citations` enforces both halves: a cited capture must be
**present**, and a game asset must **not be tracked**. ⚠️ A fresh clone has no
captures, so its citations will not resolve until the captures are copied in;
that is expected, and the checker is a local gate rather than a CI one.
### Clean-room firewall
- ✅ Allowed: behaviour descriptions, field offsets/types, formulas, state machines,

13
docs/re/captures/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
# Game-derived assets stay on disk and ship nowhere (issue #49).
#
# The pages' relative links still resolve on a machine that has the captures,
# so the evidence stays followable where it exists — it is simply not committed.
# `tools/re/check-capture-citations` enforces both halves: a cited capture must
# be PRESENT here, and an asset must NOT be tracked.
#
# Derived measurements (csv, tsv, txt, log, json, jsonl, npy) are our own
# numbers rather than game content, and remain tracked.
*.png
*.jpg
*.jpeg
*.bin

104
docs/re/disc-contents.md Normal file
View File

@@ -0,0 +1,104 @@
# What is actually on the disc, and where
**Status:**`CONFIRMED` for the layout and the counts — every figure below was
re-measured against the retail extract on 2026-09-18, not copied from the source
document. ✅ `CONFIRMED` for `media_id`, read from the XEX header.
This page exists because the corpus never had one: it documents formats in depth
(see [INDEX](INDEX.md)) but nowhere said **what files the disc holds and where they sit**.
It is adopted from a pre-RE-era `GAME_CONTENTS.md` that lived homeless in the workspace
root through the repository consolidation. **Only the measured parts were carried over**
see [What was dropped](#what-was-dropped-and-why) at the end, which matters more than the
rest of the page.
Extracted from the XISO image with [extract-xiso](https://github.com/XboxDev/extract-xiso).
---
## Identity
| Field | Value | Source |
|---|---|---|
| `media_id` | `0x2D2E2EEB` | XEX `execution_info`, via `.xex.json` |
| `title_id` | `0x53512D14` (`"SQ"` + `0x2D14`) | XEX `execution_info` |
| `disc_number` / `disc_count` | 1 / 1 | XEX `execution_info` |
---
## Top-level layout
```
<extract root>/
├── default.xex ← the game executable (XEX2, PowerPC BE)
├── config.ini ← language table (Shift-JIS comments)
├── $SystemUpdate/ ← su20076000_00000000 (dashboard update, not game data)
├── dat/ ← 71 entries
└── hidden/ ← 5 entries
```
⚠️ **`resource3d/` is in `hidden/`, not in `dat/`.** So are `DefTables` and `MiscBin`.
The source document placed all three under `dat/`; that is wrong, and it is the reason
this page re-measured rather than transcribed. `SYLPHEED_RES3D` points at
`hidden/resource3d` for exactly this reason.
### `dat/` — 71 entries
| Group | Count | Note |
|---|---|---|
| `*.pak` + `*.p00` pairs | 33 + 33 | the IPFB archives — format ✅ decoded, see [INDEX](INDEX.md) |
| `sound.pak` + `sound.p00``.p04` | 6 | one archive whose payload is split across five chunks |
| `movie/` | 109 entries | |
`dat/movie/` holds **97 `.wmv`** files plus **six language packs** as `.pak`/`.p00`
pairs (`deu eng esp fra ita jpn`). ⚠️ Those packs carry **subtitles and fonts, not voice**
all voice and SFX live in `sound.pak`. That trap is recorded separately; do not go looking
for dialogue audio in the movie directory.
### `hidden/` — 5 entries
| Entry | Size | Note |
|---|---|---|
| `resource3d/` | 166 files | `.xpr` texture/model containers (`Base.xpr`, `BG_*.xpr`, stage and ship sets) |
| `DefTables.pak` / `.p00` | 17 596 B / 3 058 037 B | balance and definition tables |
| `MiscBin.pak` / `.p00` | 496 B / 22 553 238 B | |
---
## `config.ini`
Plain-text INI, **Shift-JIS** comments (they render as mojibake in a UTF-8 reader — the
file is not corrupt). `[SYSTEM]` is present but empty; `[LANGUAGE]` maps the Xbox 360
locale constants onto the disc's three-letter directory names, with `eng` as the default:
```ini
[LANGUAGE]
= eng ; default
#0x01 = eng ; XC_LANGUAGE_ENGLISH
#0x02 = jpn ; XC_LANGUAGE_JAPANESE
#0x03 = deu ; XC_LANGUAGE_GERMAN
```
Those keys are why the six-language pack naming above is what it is.
---
## What was dropped, and why
The source document was written **before** the formats were reversed, and roughly half of
it was speculation phrased as status. Carrying that forward would have put claims into the
corpus that the corpus itself has already refuted — the precise failure mode
[README](README.md) warns about ("a wrong-but-confident note is worse than no note").
Dropped:
- **A "Known File Formats" status table** marking `dat/*.pak` as *"⏳ Unknown — magic bytes
TBD"*, and `.XWB`/`.XSB` as *"⏳ TODO"*. The `.pak` container is ✅ decoded disc-wide.
[INDEX](INDEX.md) is the authority on format status; a second table would only drift.
- **A "PAK Archive Structure (TBD)" section** guessing each archive's contents from its
name ("`GP_BUNK.pak` — likely barracks/crew quarters UI"). Those are guesses, and the
real contents are known.
- **An "RE Entry Points" section** recommending loading `default.xex` into Ghidra to find
the loaders. Static analysis now goes through `sylpheed.db` (see the workspace
`CLAUDE.md` and `/sylph-dis`).
Nothing measured was dropped. The layout, the counts, the identity fields and the language
table are all re-verified above.

3
tools/re-capture/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Game-derived digit templates for ob_read.py — pixels from the game (issue #49).
ob_digits.png

View File

@@ -10,10 +10,17 @@
largest thing in the repository, and the one place a file can be added, never
cited, and never noticed.
Two failures, which are opposites and must not be conflated:
⚠️ 2026-09-19, issue #49: **game assets are no longer committed.** Screenshots
and savegame blobs live in the working tree and are gitignored, so the pages'
relative links still resolve on a machine that has them while nothing ships.
That inverts half of this check — "is it committed?" became "is it PRESENT?",
and a NEW failure appeared: an asset that IS tracked. Both are below.
* a page cites a capture that **is not committed** — a reader following it
Three failures, which are opposites and must not be conflated:
* a page cites a capture that **is not present** — a reader following it
gets nothing. That is an error, exactly as in `check-citations`.
* a game asset that **is tracked by git** — issue #49 says it must not be.
* a capture that **no page cites** — not an error. It may be evidence a page
should have cited, and deleting on that basis would silently ratify the
omission. Reported, counted, never failed on.
@@ -65,7 +72,16 @@ def committed() -> tuple[set[str], set[str]]:
proposed. `tools/port/check-citations` uses the working tree for exactly
this reason; so does this.
"""
files = {l for l in git("ls-files", ROOT).splitlines() if l}
# 🔴 NOT `git ls-files`: since #49 the assets are deliberately untracked, so
# the index no longer knows they exist. Presence is a question about the
# working tree, and asking git would report every screenshot as missing and
# fail on all 203 citations.
files = set()
for dirpath, _dirnames, filenames in os.walk(ROOT):
for fn in filenames:
if fn == ".gitignore":
continue
files.add(os.path.join(dirpath, fn).replace(os.sep, "/"))
dirs = set()
for f in files:
parts = f.split("/")
@@ -74,6 +90,24 @@ def committed() -> tuple[set[str], set[str]]:
return files, dirs
ASSET_SUFFIXES = (".png", ".jpg", ".jpeg", ".bin")
def tracked_assets() -> list[str]:
"""Game assets that are committed — forbidden since issue #49.
The repo carries code, tooling and docs. A screenshot that sneaks back in
is invisible in review (a binary shows as "Bin 0 -> 1234567 bytes") and is
permanent once merged, because removing it later needs a history rewrite.
So this is the half of the check that has to be loud.
"""
out = []
for l in git("ls-files").splitlines():
if l.lower().endswith(ASSET_SUFFIXES):
out.append(l)
return out
def cited() -> set[str]:
raw = git("grep", "-rhoE", CITE_ERE, "--", *SEARCH, SELF).splitlines()
out = set()
@@ -141,7 +175,7 @@ def selftest() -> int:
real_dir = next(iter(dirs), None)
real_file = next(iter(files), None)
if not real_dir or not real_file:
print("selftest: 🔴 no captures committed — nothing to test against")
print("selftest: 🔴 no captures present — nothing to test against")
return 2
cases = [
("planted dangling caught", "docs/re/captures/no-such-file-anywhere.png", False),
@@ -168,7 +202,15 @@ def selftest() -> int:
print(" %-34s %s"
% ("own fixtures not counted", "🔴 FAILED" if fixture_counted else "ok"))
ok = gathering_ok and not fixture_counted
# 🔴 THE #49 RULE NEEDS ITS OWN TICK. Presence now comes from the working
# tree, so a checkout with the captures present looks identical whether or
# not they are tracked — only this assertion can tell the difference.
stowaways = tracked_assets()
print(" %-34s %s%s"
% ("no game asset tracked", "ok" if not stowaways else "🔴 FAILED",
"" if not stowaways else " (%d tracked)" % len(stowaways)))
ok = gathering_ok and not fixture_counted and not stowaways
for name, path, want in cases:
got = resolves(path.rstrip(".,;:)`"), files, dirs)
mark = "ok" if got == want else "🔴 FAILED"
@@ -188,19 +230,48 @@ def main() -> int:
print(f)
return 0
print("captures committed : %d" % len(files))
stowaways = tracked_assets()
print("captures present on disk : %d" % len(files))
print(" cited by a page or a tool : %d" % (len(files) - len(orphans)))
print(" cited by nothing : %d (reported, not failed —" % len(orphans))
print(" an orphan may be evidence a page owes)")
# 🔴 A BARE CLONE HAS NO CAPTURES AT ALL, and that is not a defect.
# Since #49 the assets are never committed, so a fresh clone, a worktree or
# a CI checkout legitimately has none — and the naive check calls all 134
# citations dangling and exits 1. A gate that is red on every clean checkout
# is one people learn to ignore, which is how the last wrong-by-default
# check in this file cost a session. Distinguish "none here" from "this one
# is missing": with some assets present, a gap is real and still fails.
assets_here = sum(1 for f in files if f.lower().endswith(ASSET_SUFFIXES))
if assets_here == 0 and dangling:
print(" ⓘ no captures on this checkout: %d citations unresolved" % len(dangling))
print(" Expected — captures are local-only (#49) and a fresh clone has none.")
print(" Copy them in, or read the evidence on the issue it is attached to.")
print(" 🔴 game assets TRACKED : %d" % len(stowaways))
return 1 if stowaways else 0
rc = 0
if dangling:
print(" 🔴 cited but NOT committed: %d" % len(dangling))
print(" 🔴 cited but NOT present : %d" % len(dangling))
for d in dangling:
print(" %s" % d)
print("\n🔴 a reader following those gets nothing. Commit the capture, fix the")
print("\n🔴 a reader following those gets nothing. Restore the capture, fix the")
print(" path, or drop the citation.")
return 1
print(" 🔴 cited but NOT committed: 0")
return 0
rc = 1
else:
print(" 🔴 cited but NOT present : 0")
if stowaways:
print(" 🔴 game assets TRACKED : %d (issue #49 — code, tooling, docs only)" % len(stowaways))
for a in stowaways[:10]:
print(" %s" % a)
if len(stowaways) > 10:
print(" … and %d more" % (len(stowaways) - 10))
print("\n🔴 run `git rm --cached` on those; they stay on disk and stay ignored.")
rc = 1
else:
print(" 🔴 game assets TRACKED : 0")
return rc
if __name__ == "__main__":