D048 - Per-title overrides: one file per title, three layers, merged per key

decided · 2026-08-19

Title-specific behaviour never touches the core. No if title == … anywhere. Instead the core reads generic, named settings, and a per-title file declares what that title needs.

One file per title, not one large database. Identified by content hash initially, since metadata parsing is deferred (D040) and a hash is exact and immune to renaming.

Three layers, user wins: global defaults → repo per-title → user per-title. Repo-shipped overrides are our compatibility knowledge and live in overrides/, versioned and reviewable - distinct from titles/, which is gitignored corpus content. Knowledge is tracked, content is not. User files live in the data dir, so they follow portable mode automatically (D038).

Merge per key, never wholesale replacement. A user file setting resolution must not silently drop the repo's compatibility entries for that title. Whole-file replacement produces bug reports that cannot be falsified, and it is a known failure of config systems shaped like this.

Two kinds of key share the file:

Prefer typed overrides to boolean flags. Booleans multiply (tolerate_unaligned_alloc, tolerate_tiny_alloc, …) where a typed value generalises (direct_memory_alignment = 4096). Fewer keys, reads as configuration rather than a list of exceptions.

Compatibility entries carry one of three labels, because each resolves differently:

Without that split the file becomes a place where every bug we did not fix acquires a respectable name and turns permanent and invisible. A workaround should be uncomfortable to look at, and answerable by a "what are we papering over" report.

Worked example, since the abstract rule is easy to misapply. A commercial title fails on something we cannot support, and we know it proceeds if that thing is skipped:

# overrides/<title-hash>.toml
[compat]
raytracing_enabled = { value = false, kind = "unsupported",
                       reason = "no RT pipeline yet; title proceeds without it" }

The core reads raytracing_enabled and skips the work. The title is named nowhere in the code - only a clear toggle whose name describes the capability. A second title needing the same thing adds a line to its own file and no code changes at all.

Note the key is raytracing_enabled = false, not disable_raytracing = true. Negative booleans acquire double negatives the moment something must be forced on (disable_raytracing = false reads terribly), and a positive-sense key extends cleanly if the toggle later becomes an enum. Same reasoning as preferring typed overrides above.

Applied overrides are never silent. Each emits a structured WARN and appears in the run report with per-key provenance (D046) - otherwise behaviour is being diagnosed against a configuration that cannot be seen, which is the D008 problem again.

Confirmed 2026-08-19: file-only initially, with GUI per-title properties arriving at phase 2b rather than blocking it; and the corpus survey records which overrides each title needs, so the dependency is answerable in both directions and can become a regression assertion.

Identity hashes the executable, not the directory (amended 2026-08-19 after inspecting real material). A title directory is around 96 GB; hashing that per run is a non-starter. The executable is both small enough (tens of MB) and the right thing semantically - it is what changes when a title is patched.