If you maintain an image-heavy project — a game, a documentation site, a design system — you’ve probably hit the moment where someone adds a file with the wrong extension, a duplicate asset sneaks in under a different name, or a build breaks because a PNG silently became corrupted. A deterministic manifest validator solves this class of problem by walking a directory once, checking every file against fixed rules, and producing the same output every time given the same input.
The core constraint: determinism. Any tool that scans a filesystem risks non-reproducible output, because directory listings aren’t guaranteed to come back in the same order twice. That means sorting is not optional — it’s the first design decision, not an afterthought.
Directory traversal and allowlisting. Walk the tree recursively, but reject anything not on an explicit allowlist of extensions (say .png, .jpg, .webp). Don’t try to sniff file types by content unless you also verify the extension matches, since mismatches are themselves a validation failure worth reporting rather than silently correcting.
Metadata extraction. For each accepted file, read enough of the header to get width, height, and computed aspect ratio, without decoding the full image. Store these alongside the path. Aspect-ratio checks are useful for catching accidentally stretched or misnamed exports — for example, an asset expected to be square that comes in at 4:3.
Content hashing and duplicate detection. Compute a content hash (SHA-256 is a reasonable default) for every accepted file. Two files with identical hashes but different paths are duplicates, even if names differ. Group by hash, and treat any group with more than one path as a warning or hard failure depending on your project’s policy — some teams tolerate intentional duplicates for override layers, others don’t.
Stable sort before output. Sort the final record list by path, then by hash, so that manifest diffs stay meaningful in version control. Unordered output turns every manifest regeneration into a noisy diff, which defeats the purpose of having a manifest at all.
JSON manifest output and exit codes. Emit a single JSON document with an array of records (path, extension, width, height, aspect ratio, hash, duplicate group id). Reserve distinct exit codes: 0 for a clean pass, 1 for validation failures (bad extension, unreadable header), 2 for duplicate detection triggering a hard failure, and a separate code for internal errors like unreadable directories. Clear exit codes matter more than console output for CI integration.
Dry-run mode. A --dry-run flag should perform the full walk and validation but skip writing the manifest file, printing a summary instead. This is essential for testing rule changes against a large asset tree without touching committed output.
Here is an untested, Odin-oriented sketch of the core record type and validation pass, meant to illustrate structure rather than compile:
// untested sketch — illustrative only
Asset_Record :: struct {
path: string,
ext: string,
width: int,
height: int,
aspect: f64,
hash: [32]u8,
dup_group: int,
}
validate_tree :: proc(root: string, allowlist: []string) -> ([]Asset_Record, int) {
records: [dynamic]Asset_Record
exit_code := 0
// walk root recursively
// for each file: check extension against allowlist
// read header for width/height, compute aspect
// compute content hash
// append record
// sort records by path then hash
// group by hash to find duplicates, set dup_group
return records[:], exit_code
}
Testing should deliberately construct failure cases: a directory with a disallowed extension, a truncated image header, two byte-identical files under different names, and an empty directory. Each case should map to a predictable exit code and manifest state, and those fixtures are worth keeping under version control alongside the validator itself.
Once the validator and manifest format are stable, some teams look for an upstream source of new or replacement assets to feed into the same pipeline. Tools like Muse Image describe workflows for generating and editing images from prompts and references, which can sit upstream of a validator like this one — the validator’s job stays the same regardless of where the images originate, since it only inspects the files it’s given.
The main limitation of this design is that it validates structure and uniqueness, not visual correctness — it won’t tell you if an asset looks wrong, only if it’s missing, duplicated, or malformed. Treat it as a gate before a build, not a substitute for a human review pass.
