The counts were generated from a working tree holding profiles that are
still being written and are not committed, so the freshness check
regenerated different numbers from the same revision. They are read from
the committed set now, which is what the check compares against.
generate_emulator_page rendered the header, the metadata rows, the
platform block, every structured field and the file table in one body.
Four pieces come out - the scalar rows, the platform block, the
collapsible fields, and the predicate deciding whether the collection
holds a file - and the page reads as the sequence it always was.
Complexity 68 to 51, each new piece A or B rank.
Verified against the previous revision on frozen inputs: every generated
page identical.
The modules are run directly, run with -m, and imported by the tests and
the type checker. Only the first form puts this directory on the path, so
the package marker carries the bootstrap the other two need; without it
the first sibling import fails. Three tests hold the three forms open.
Three groups leave generate_site.py: the write-and-sweep bookkeeping the
whole build shares, the pinned permalinks into cited sources, and the
small renderings every page uses. Each sits below what calls it, so the
page generators stay the only composite layer.
Verified against the previous revision on frozen inputs: 571 artefacts
identical, and the rendered site still resolves all local links across
511 pages.
generate_pack.py held six responsibilities in 4744 lines. Five move out
in dependency order, so nothing above reaches back down: destinations,
core extras, resolution with its storage tiers, the notes shipped inside
a pack, and the pack verifier. generate_pack.py keeps the build and the
command line, and re-exports the rest.
Two things the move surfaced. The offline switch was a module global the
command line assigned, which a re-export would have copied and frozen at
False; it is set through a call now and not re-exported. And a facade
placed after the entry point binds too late: importing the module worked,
running it did not, so the manifest run died on a name the tests never
exercised because tests import.
Verified against the previous revision on frozen inputs: every generated
artefact is identical, save the catalogue that embeds hashes of files
carrying a build timestamp.
common.py had grown to 1833 lines by accumulation. Six coherent pieces
move out - untrusted parsing, digests, archives, generated artefacts,
release assets, dump catalogues - and common.py re-exports them, so the
sixty existing import sites keep working and migrating them stays
optional.
The site build is now reproducible, which is what made the move
checkable. It deleted its generated directories first, so every page was
new and write_if_changed had no earlier version to compare against: a
deploy republished six hundred pages for the clock alone. Directories
are swept instead, a page is removed only once nothing produces it, and
the body pass compares against the body of the file on disk rather than
against the decorated page. Two consecutive builds on the same inputs
now produce identical bytes; before, 1034 files differed.
Four sites still compared the verification mode to a literal after the
module owning that policy existed. One of them mattered: an unrecognised
mode fell through to MD5 verification while compute_severity was scoring
it as existence, so a typo in a platform YAML produced a report whose
checks and severities described different platforms.
The mode is normalized once per run and the consumers ask for what they
need. A test reads the sources and fails on a literal comparison, so the
next consumer cannot quietly grow a fifth copy.
A filename-agnostic core accepts any name for its BIOS, so the builder
scans the directory holding the candidates. It picked that directory
from a first-hit lookup by name, the one piece of evidence that lands
in another emulator's tree: five files answer to GameIndex.yaml and one
belongs to an Android package, rom1.bin is a PS2 ROM and a Roland
SC-55 ROM. One wrong match became every file beside it, flattened into
the BIOS root of platforms that do not run that emulator: 45 files in
the Recalbox pack, 170 in the RetroArch one.
Four things decide it now. A destination and the repo layout meet on a
tail, so the path index is tried from the longest tail down and never
to the bare filename; that alone corrects seven files, among them the
Japanese GameCube slot, which held the US dump. A seed has to declare
the shape it is looking for, since no shape means the whole directory.
An ambiguous name needs the profile's other files to agree before the
scan walks anywhere. What the scan emits carries the SHA-1 it selected,
so packing never resolves it by name again.
The builder and the coverage report each grouped their own candidates
before asking which regional alternatives to withdraw. The builder
grouped the platform files and the core extras; the report grouped the
platform files alone, and keyed them on an unsanitized destination.
So a region run withdrew 73 files from a recalbox pack while the report
withdrew 14, and described the other 59 as covered by a pack that would
not carry them.
platform_region_groups builds the grouping once and both sides read it.
The extras it returns are keyed by emulator, name and path: Dolphin
declares three IPL.bin that differ by path alone, and a name-keyed map
withdraws the wrong one. Manifests are byte-identical before and after.
The pack builder accepts the aliases declared in the target overrides,
so --target switch works there, but the installer's target manifests
carried only canonical names. The documented word was the one that
failed, and the installer then carried on with every file: 1911 files
and 4.1 GB where the user had asked for the 863 that target needs.
Aliases are emitted beside their canonical target, and an unknown
target now stops the run and lists what is available. A filter is
applied or refused, never ignored.
The gap analysis answered from the name index, so a core extra whose
local copy contradicts its declared hash counted as held. Under a
digest mode the builder drops exactly that file, so the coverage report
described a pack that would not contain it: seven files across
Batocera, Recalbox and RetroBat.
An entry that states what its content should be is now resolved by
content; the name still answers for entries that declare nothing to
check against, and existence mode is unchanged because there the
frontend never opens the file and the pack does carry it.
The per-emulator report counted an entry nobody can supply -- a
per-user key, a slot the user fills, a dump that was never made -- as
plainly missing. fpse-ng read 14 missing when twelve were documented as
unobtainable, which invites the wrong repair: dropping the flag,
deleting the entry, or chasing a vendor's whole install tree.
They are listed with the reason the profile records rather than hidden,
and the summary counts them apart. Platform reports are untouched.
The per-emulator report captured the status resolve_local_file returns
and then ignored it: any non-empty path became OK. An entry whose only
candidate contradicts its declared hash therefore read as covered, so a
same-named file from another system stood in for one the collection
does not hold. 36 entries across 14 profiles were affected, among them
config.ini, ROM and rom2.bin, names that collide across systems.
Emulator validation still runs first, since it names the field that
disagrees; the resolution status is consulted only when validation had
nothing to say.
export_native turns the profile data back into each platform's own
file, for a maintainer there to pick up and use, and none of the ten
exporters had a test. Coverage goes from 0 to 83%.
Writing them found the EmuDeck exporter rejecting its own output: its
export skips placeholder entries, its validate looked for them anyway
and reported the omission as a missing hash. Both sides apply the same
filter now.
The placeholder assertion initially passed for the wrong reason twice
over -- the fixture entry had no hash, so it was dropped as incomplete
rather than as a placeholder, and the assertion then read the name
while the exporters write the path. It is pinned on an unmistakable
destination now, and removing the guard fails two tests.