Commit Graph
628 Commits
Author SHA1 Message Date
Abdessamad Derraz 6582c3237e fix: name the stale pin when the file is only shorter
The diagnostic caught a ref whose file was missing at the pin. nestopia
showed the other shape: the file is there and the line is not yet. Its
palette and database loads were cited at 2041 and 2063, which is where
HEAD carries them, against a pin four hundred lines shorter, and that
reported as a plain GONE with nothing to act on.

A cited line past the end of the pinned file that fits HEAD is the same
finding as before and now says so. nestopia's pin moved to the revision
its refs describe: 8 refs, all anchored.
2026-09-05 01:31:10 +02:00
Abdessamad Derraz c738073f66 fix: move the refs and the pin together or not at all
Recaling refs while the pin stays put produces exactly the state the
all-or-nothing rule exists to prevent: a profile whose refs describe one
revision and whose source_commit names another. The tool manufactured it
on mariani, where three prose runs it could not rewrite kept
bump_commit refusing while eleven refs had already moved.

Asking for both writes is now atomic. The work happens on a copy, which
is promoted only when the pin follows, and a rebase is refused outright
when something visible beforehand will hold the pin: an annotated ref,
one under a mode key, or a prose run whose tokens cannot be located well
enough to rewrite. Where the block only appears after the write, the
copy is discarded and the profile is named on stderr rather than left
half moved.

mariani is back on its pin and stays at four refs to read, which is
honest: three of them have to be rewritten by hand before anything can
advance.
2026-09-04 18:55:15 +02:00
Abdessamad Derraz 6f27a296f7 feat: refuse a citation no repository can hold
kenji-nx cited tmp/es-de/ANDROID.md:470-474, a path from the machine of
whoever profiled it. No revision of any declared repository holds it, so
profile_sync could only report it missing, every pass, forever, and no
amount of reading would ever settle it.

validate_schemas now refuses a scratch directory, an absolute path, a
Windows drive path and one climbing out of the tree, and names the
offending citation rather than the scalar that carries it. Offline, so it
runs on every push and every pull request rather than waiting for a
network pass.

The ES-DE citation reads as external now, which is what it always was.
kenji-nx is at 37 refs, all anchored: the three changed blocks were var
giving way to explicit types.
2026-09-04 18:26:27 +02:00
Abdessamad Derraz d124f6c516 feat: watch the profiles that declare no files
An empty file list is the one assertion here that ages unwatched. Nothing
can go missing and no ref can drift, so nothing notices when a core that
embedded everything grows a path. virtualjaguar said "No external BIOS
files are required or loaded by this core" while its source had grown
eleven filenames read from the system directory, and only a reading
found it.

fileless_audit looks for the request itself, the system directory ask, in
the sources each profile already cites. Over the 151 fileless profiles it
named eleven, of which two were covered by data_directories, six carried
an exclusion_note, and three had nothing written down at all: craft
writes its world database in that directory, dice stores the answer in a
variable no other file in the tree names, lutro hands it to the Lua game.
Each now says so.

The check settles: declared files, a declared directory, or a written
answer all end it, so what it reports is the set nobody has read yet. A
test holds the corpus at zero.
2026-09-04 18:13:53 +02:00
Abdessamad Derraz 1a1f19be2d feat: check a MAME ref against the set it names
profile_sync follows content, so a ref that drifted still anchors where
the cited text went. That is drift detection working, and it cannot
answer the only question a MAME romset ref asks: does this line declare
this set. It flagged five refs in one driver file where nineteen were
stale, the fourteen others having been relocatable somewhere plausible.

mame_ref_audit asks the stronger question and found ninety-two across
the four profiles whose upstream still moves: mame 66, mamearcade 18,
mamemess 6, groovymame 2. Each had exactly one declaration to point at.
The frozen generations, mame2009 through mame2016, come out clean, which
is the check saying it finds drift only where drift can happen.

The set name is argument 1 of the machine macro. Matching it anywhere on
the line matches every clone naming it as parent, which is most of a
driver, and comments are stripped first because a declaration can sit
behind one. A set no machine declares is reported as not judgeable, not
wrong: device archives take their DEFINE_DEVICE_TYPE shortname.
2026-09-04 17:48:24 +02:00
Abdessamad Derraz ece637d52d feat: let a profile state that its upstream is gone
Saying those profiles would stay open no matter what was wrong. A dead
forge is a verifiable fact, and a fact can be recorded: upstream_gone
carries why, and the profile stops being an open problem. The refs
describe the last revision anyone could reach, which is all any reader
can ask of them.

The declaration is guarded rather than trusted, because a forge can come
back and a profile that keeps asserting a death nobody rechecks is worse
than one that fails loudly. Declared and unreachable reports as a
recorded fact; declared and answering reports as the contradiction it
is, and the profile has to be read again.

yuzu and suyu carry GitHub's 451. citron carries the loss of
git.citron-emu.org and the squatter now sitting on the .com. Each names
what was probed and when, so the next reader retraces it rather than
repeating it.
2026-09-04 17:25:23 +02:00
Abdessamad Derraz 1fb717324b fix: name the stale pin instead of a missing file
A ref whose file is absent at the pinned revision reported "pin revision
missing", which reads as code that vanished. When the same file is at
HEAD and the cited range fits it, nothing vanished: the ref was written
against HEAD while source_commit still names an older revision, and the
profile describes two trees at once. The reason now says so, because the
fix is the pin and not a hunt for a move that never happened.

This is the state three profiles were left in during their own
reprofiling, against a warning the repository already carries. A range
that overruns HEAD stays a plain miss, and a file absent from both
revisions is unchanged.
2026-09-04 16:45:51 +02:00
Abdessamad Derraz ca307a4ef1 feat: keep a profile checkable when its forge goes
Four Switch profiles were unverifiable and said so on stderr every pass.
yuzu and suyu answer 451, citron's host stopped resolving, and
git.eden-emu.dev returns 403 to anything that is not a browser. Each
aborted its own report, so nothing could be said about any of them, and
the noise repeated on every run over the whole corpus.

A withdrawn forge is now a fact rather than a failure. GoneError covers
451, 410 and a host that does not resolve; none is retried, since three
attempts with backoff end in the same place. Those profiles land in
their own summary bucket, out of the review backlog where nobody could
act on them anyway. A 403 stays what it was, a refusal, because small
Forgejo instances behind anti-bot filters issue it routinely.

A profile can now name a source_mirror, consulted after source and
upstream so a live primary always decides attribution. Reaching it took
two more changes: a repository that refuses is muted for the rest of the
pass instead of ending it, keyed by host as well as slug because a
mirror carries the same slug on another forge; and a refused miss is not
cached, or the mute would answer for the mirror that was about to be
asked.

eden now reads from its Codeberg copy, which holds the same head and the
pinned commit: 5 refs, all anchored, where the profile could not be
checked at all. yuzu, suyu and citron have no mirror that serves
content, and now say so once instead of failing loudly.
2026-09-04 15:35:00 +02:00
Abdessamad Derraz 6a0fb1e776 fix: resolve a bare cited name at the pin first
A ref citing a bare filename, the way prose does, was matched against the
HEAD tree alone. A file that moved since the pin then resolved to its
HEAD path, which does not exist at the pin, and the ref reported GONE
with "pin revision missing": it failed for the one reason it never
should, its own success at HEAD.

The pin tree is searched first, HEAD stays the fallback, and the rename
search carries the pin path forward as it does for any written path.
This is what the resolver already documents for prefixed and suffixed
paths; the bare-name branch was the one that did not follow it.

linapple cites Memory.cpp, src/Memory.cpp at its pin and
src/apple2/Memory.cpp today. Its refs now name where the code went
instead of reporting it missing.
2026-09-04 14:46:46 +02:00
Abdessamad Derraz 3891d897a6 fix: report the writes a dry run would make
--backfill-commits and --realign-prose have always printed what they
would write. --rebase-refs and --bump-commit took the flag and printed
nothing, so the only way to read a plan was to let it happen, and the
recale and the pin had to be done in two full network passes with
--force in between.

Both now plan. The plan runs the production write path over a throwaway
copy rather than a parallel branch, so it cannot drift from the write,
and the planned bump reads the text the recale would have left: a pin
held back by prose the same pass would move is no longer reported as
blocked. One pass does both, recale before bump on each profile.

bump_commit also stopped announcing a rewrite of the pin to the value it
already held. On the corpus that was 126 of 232 announcements, which
buried the 106 profiles that did move.
2026-09-04 14:41:37 +02:00
Abdessamad Derraz 1a96853aee feat: sign the release checksum list
SHA256SUMS.txt sat beside the artifacts it vouches for, so whoever could
rewrite a release rewrote the list with it. The packs were already
reproducible, which answers corruption and lets a third party rebuild an
archive byte for byte; nothing answered a rewritten release.

The list is now signed with an ed25519 key kept for this alone, and the
public half is allowed_signers at the repository root, so verification
does not go through the release page: ssh-keygen -Y verify against the
committed file, then sha256sum --check. Rehearsed on all three outcomes:
a good signature, a tampered pack caught by the sums, a rewritten list
caught by the signature.

The release steps sign and upload the signature, the README points a
downloader at the procedure, and the reproducibility section says what
each half proves. Rotation keeps retired lines so past releases stay
verifiable. Three tests hold the trust root, the signing step and the
documented principal in agreement.
2026-09-04 14:01:05 +02:00
Abdessamad Derraz fe77535c3b fix: state one catalog ratio, correct the faq
The home page and the stats export counted every file carrying a
provenance record, the provenance page and the README only the system
files. The site published 553 and 566 for the same quantity, one click
apart, and the export paired the wider count with composition.systems as
its denominator. common.count_catalog_matched is now the single source,
scoped to the systems bucket.

The FAQ had drifted from the profiles it describes: MAME pinned at 0.287
against 0.289 in mame.yml, Adler-32 attributed to Dolphin's IPL rather
than the DSP ROMs that carry known_hash_adler32, and the per-emulator
verbose report named as the only content check on an existence platform,
which skips the DISCREPANCY line the platform report raises itself.

Tests read both sides: no generator may count matches inline, and each
FAQ claim is checked against the profile or the script that owns it.
2026-09-04 11:41:17 +02:00
Abdessamad Derraz ee6e7558f9 refactor: assign each module constant once 2026-09-04 10:19:04 +02:00
Abdessamad Derraz 361b958f41 feat: download packs published as split volumes 2026-09-04 10:18:14 +02:00
Abdessamad Derraz d368b37165 docs: surface region filtering in the readme 2026-09-04 08:31:05 +02:00
Abdessamad Derraz b1d1eeab06 docs: give the installer a page of its own 2026-09-04 06:55:23 +02:00
Abdessamad Derraz 7ff6ea20d1 docs: one pack per platform, the complete one 2026-09-04 04:10:15 +02:00
Abdessamad Derraz 26df60db75 chore: build releases locally, retire the ci build 2026-09-04 03:37:01 +02:00
Abdessamad Derraz 3d7852cdb7 chore: refuse a busy output before any work 2026-09-04 03:11:11 +02:00
Abdessamad Derraz 59d3b0683a refactor: let cached branch tips expire 2026-09-03 22:29:48 +02:00
Abdessamad Derraz de1b4c4e6a refactor: resolve subproject paths by suffix 2026-09-03 21:54:47 +02:00
Abdessamad Derraz 131d133025 chore: keep the zoneinfo aliases out of dedup 2026-09-03 21:49:42 +02:00
Abdessamad Derraz c1835d15ae refactor: judge binary and linked citations 2026-09-03 21:48:23 +02:00
Abdessamad Derraz 562e3fb20c feat: serve the pc engine cards to turbografx-cd 2026-09-03 17:51:34 +02:00
Abdessamad Derraz 75f4542ea8 refactor: scope validation to a platform's cores 2026-09-03 17:51:34 +02:00
Abdessamad Derraz 232ef1a6b8 fix: let the constrained declaration win in manifests too
A destination can be declared by more than one system, bare in one and
hash-constrained in another. generate_pack resolves that with
_preferred_entries so the constrained sibling claims the destination;
generate_manifest never did, and named whatever answered to the name.

RetroDECK's bios/d2fdc.zip is the case: declared with an md5 in the
arcade system and bare under apple-ii. The pack carried the right
archive, 262 bytes reached through the MAME clone map, while the
manifest sent install.py to a 256-byte Apple II ROM. Downloading the
ZIP and running the installer gave different files.

A test now hashes every manifest entry a platform pins and compares it
against what the platform declares: 3295 entries, on the platforms whose
frontend reads the bytes. It accepts any of several declarations for one
destination, the member-composite MD5 Recalbox pins for arcade archives,
and Batocera's 29-character prefixes. Reinstating the old manifest entry
fails it.
2026-08-23 12:14:56 +02:00
Abdessamad Derraz 3a266be7a5 fix: keep the contributors when the request fails
The contributors block is the only part of the README that comes from
the network, and a refused request returned an empty list, which deleted
the section. That happened during a pipeline run and the result was
committed; the freshness check then regenerated the section and failed
on the difference.

An unavailable list now republishes the one already there and says so.
Losing it is a worse answer than a stale one, and it makes an offline
regeneration additive rather than destructive. Reverting the change and
simulating the same outage empties the section again.
2026-08-23 09:59:26 +02:00
Abdessamad Derraz 8b404e500f fix: keep a packed file's executable bit
Pinning every member's metadata made packs reproducible and took the
executable bit with it. The RetroDECK pack ships the two Voxatron engine
binaries, and extracted at 644 they cannot be run.

Git records the bit, so reading it from the source file keeps a pack the
same from any clone. Nothing else about the source's mode reaches the
archive: 2569 members ship at 644 and 942 at 755, which is what the
builder produced before the pinning.

Nothing caught this. The comparison that proved the pinning inert
checked member names, CRCs and sizes, and mode is none of those. A test
now builds a runnable payload and asserts it survives extraction.

RetroDECK rebuilds to the same bytes twice and passes its integrity
check, 2008/2008 baseline and 1551/1551 cores.
2026-08-23 07:58:23 +02:00
Abdessamad Derraz 593b277bc4 refactor: one place decides which profiles answer
The verifier and the builder each resolved the profiles a run names, in
thirty-five lines that differed only in how they failed: one exits, the
other returns empty-handed. An alias is the same binary under another
name and a launcher only starts an emulator, so neither has requirements
of its own, and both refusals have to say the same thing.

common raises now and each caller chooses its own ending. Six tests hold
the refusals, one of them reading both sources so a copy cannot grow
back. The manifest's core-complement phase comes out of generate_manifest
in the same pass, 60 to 34.

Verified inert: manifests identical entry for entry, and the Handy pack
rebuilds to the same bytes.
2026-08-23 07:18:41 +02:00
Abdessamad Derraz a269f9b677 refactor: lift two preambles out of the verifiers
Resolving the profiles a run names, which refuses an alias or a launcher
because neither has requirements of its own, and the structural checks
on an archive that hold whatever it contains: a duplicate entry, an
absolute path, a traversal, a zero-byte member. Complexity 58 to 50 and
63 to 51.

Extracting the second surfaced a Counter it had been reading from the
enclosing module, which pyflakes caught before the tests did.
2026-08-12 17:04:29 +02:00
Abdessamad Derraz bf3196ce06 refactor: name the gap filter's two kinds of skip
find_undeclared_files decided per entry whether a core requirement can
be a gap, through a chain that mixed two skips whose difference is easy
to lose. Some record the requirement as settled so no other profile
reconsiders it; the rest leave it open, because the same file can be
libretro-only in one profile and standalone-only in another and the key
carries no emulator.

The chain returns a named verdict now, complexity 60 to 45. Twelve tests
cover the verdicts and two more cover the distinction end to end: a
standalone-only entry seen first must not answer for the profile that
needs the file. Collapsing the two skips into one passes every other
test in the suite and fails that pair.
2026-08-12 16:44:30 +02:00
Abdessamad Derraz c313b32347 chore: neutral report path and plain punctuation
The markdown report wrote to a directory named after the tooling that
happened to produce it. It takes --report-dir now, defaulting to
reports/, so nothing in the tree names anything but the project.

Em-dashes replaced throughout the sources and tests, rephrased rather
than swapped for a comma where the dash carried an apposition.
2026-08-12 16:16:52 +02:00
Abdessamad Derraz d2cc806e76 refactor: name the pack builder's decisions
Two decisions taken before a byte is written come out of generate_pack.
Which declaration wins when several claim one destination: a platform
may declare the same file bare in one system and hash-constrained in
another, and first-come dedup would let the bare one pack whatever
answers to the name. And which regional or slot alternatives the pack
leaves out, decided once over the baseline and the core extras together.

Complexity 170 to 142. The Recalbox pack rebuilds to the same bytes as
before the change, and the manifests and site are unchanged on frozen
inputs.
2026-08-12 16:14:25 +02:00
Abdessamad Derraz 6aa5685fc7 refactor: split the extras collector into its passes
_collect_emulator_extras ran three passes in one body: the undeclared
files a platform's cores need, a second copy of an archive under the
subdirectory some cores read, and the scan a filename-agnostic core
allows. The last two are named now, complexity 98 to 54.

The archive-prefix pass had no test, and extracting it surfaced why that
mattered: it read an index from the enclosing scope, so on its own it
would have raised. Three tests cover it, including the case it exists to
prevent - claiming a prefixed path for an archive the collection does
not hold.
2026-08-12 15:46:02 +02:00
Abdessamad Derraz 53fc5b7005 refactor: name the resolver's weakest steps
resolve_local_file is an ordered chain where the order is the policy:
content first, then a declared path, then a filename. The last steps -
the walk through the cached data directories and the shape-only match a
filename-agnostic core allows - were inline, and the predicate judging a
candidate found by name was a closure with no test of its own.

All three are named now, and the predicate is the interesting one: it
decides whether a file the walk found by filename actually satisfies
what the entry declares. Sixteen tests cover it, including the truncated
MD5 prefixes Batocera publishes and the case where one hash matches
while another is contradicted. Accepting a name match without checking
content fails nine of them.

Complexity 164 to 143. Verified inert against the previous revision on
frozen inputs.
2026-08-12 15:30:13 +02:00
Abdessamad Derraz a2bd197b9b fix: pin every pack member to a fixed date
A pack was still not a function of its inputs. ZipFile.write copies the
source file's mtime into the member: the wall clock for an archive this
build rebuilt in tmp/, the checkout time for a file from the collection.
Two consecutive builds of the Recalbox pack differed on 348 members
whose content matched byte for byte, and a pack built from a fresh clone
could never match one built from another.

Every member now goes through one writer that stamps the epoch the
archive rebuilder already uses, streaming the content so a firmware
image of several hundred megabytes is not read whole.

The pack was already covered by a two-builds-are-identical test, which
passed: its fixture held no romset, so it never reached the rebuild
path. The fixture has one now, and reverting the writer fails both that
test and the new one.

Verified on the real collection: Recalbox and RetroArch rebuild to the
same bytes twice, contents unchanged from the previous revision (1319
and 4517 members, zero CRC differences), and both still pass their
native integrity check.
2026-08-12 15:14:48 +02:00
Abdessamad Derraz e9b3fefcc4 refactor: give the emulator page its sections
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.
2026-08-12 13:03:37 +02:00
Abdessamad Derraz 306637d90e chore: declare scripts as a package
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.
2026-08-12 12:47:01 +02:00
Abdessamad Derraz b8bdeec5ce refactor: lift the site's shared layers out
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.
2026-08-12 12:37:27 +02:00
Abdessamad Derraz c31ce0b693 refactor: split the pack builder into its layers
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.
2026-08-12 12:20:17 +02:00
Abdessamad Derraz b5fd643ccf refactor: give common.py's parts their own modules
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.
2026-08-12 11:49:54 +02:00
Abdessamad Derraz 5e168b86c8 refactor: ask the mode module instead of retyping it
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.
2026-08-12 07:36:29 +02:00
Abdessamad Derraz b11c8b0638 fix: keep the agnostic scan inside its own tree
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.
2026-08-12 07:23:23 +02:00
Abdessamad Derraz b33d045175 fix: group region candidates once for both sides
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.
2026-08-12 06:43:47 +02:00
Abdessamad Derraz 34619c778f fix: publish target aliases and refuse an unknown one
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.
2026-08-12 06:13:21 +02:00
Abdessamad Derraz f998f4d77a fix: judge core extras by content where the builder does
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.
2026-08-12 05:38:16 +02:00
Abdessamad Derraz b28f8d12a3 feat: report unsourceable entries apart from gaps
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.
2026-08-12 05:20:25 +02:00
Abdessamad Derraz f097184b00 fix: stop counting a contradicted hash as covered
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.
2026-08-12 04:21:54 +02:00
Abdessamad Derraz 7c043475cd feat: follow mame clones in the gap analysis 2026-08-12 03:15:05 +02:00
Abdessamad Derraz b86a34933c feat: parse gamel machines and computed sizes 2026-08-12 03:15:05 +02:00