Files
sortof/docs/specs/2026-04-30-patch-tier.md

6.9 KiB

Spec G-patch - Patch tier (Final Loads)

Date: 2026-04-30 Status: Draft (awaiting review) Sibling specs: A (multi-branch picker), B (collection expansion + live progress), C (build context), D (dep "Add" button), E (precacher), F (folded into B), G (cleanups bundle - this spec carves a piece out)

1. Summary

Add a "patch" tier to the load-order calculation: mods explicitly authored or detected as patches sort after every non-patch mod, including those flagged loadLast=on. Implementation is a single new axis at the top of mlos_sort._initial_sort_key plus a heuristic in derive_category. No schema migration. No new endpoint. No backwards-incompat changes for existing mods.

2. Problem

The PZ load-order convention (and the user-supplied 37-bucket taxonomy, bucket 37 "Final Loads") treats compatibility patches and retextures-of-other-mods as a strictly-last tier - they have to load after loadLast=on map mods, because they intercept or override the things those mods install. Today our sort key has no such tier:

PREORDER → loadFirst → loadLast → category → in-category loadFirst → in-category loadLast → alpha

A loadLast=on map mod ends up in the same bucket as a patch, ordered alphabetically. Patches that need to override the map mod can land before it. Silent corruption - output looks valid, the wrong mod wins at runtime.

3. Detection rules

A mod is a patch iff any of these is true:

  1. Explicit: mod.info contains category=patch (new value added to RAW_CATEGORY_ORDER).
  2. Author-tagged via sorting_rules.txt: user-supplied [modId]\ncategory=patch overrides anything else (existing mechanism, no change).
  3. Name heuristic (conservative): mod.name matches the case-insensitive regex \b(patch|compat|compatibility)\b. Examples that match: BetterFlashlight Patch, BB Compatibility, RavenCreek - MoreSimpleClothing Compat. Examples that do not match: BugFixes, LittleTweaks, BalanceFix - "fix" / "tweak" / "fixes" are too broad and would over-flag.

The first matching rule wins. The heuristic is intentionally narrow; mod authors who want to opt in should use rule 1.

4. Sort behavior

Insert a new axis at position 0 (above PREORDER) of the sort tuple:

(is_patch, PREORDER, loadFirst, loadLast, category, in-cat loadFirst, in-cat loadLast, alpha)

is_patch = 1 for patches, 0 otherwise. Tuple comparison guarantees patches sort after every non-patch mod regardless of every downstream axis. Within the patch group, the existing sub-keys still apply (a patch with PREORDER=2, e.g. ModManagerServer-Patch, still sorts second-among-patches).

5. Backend changes

  • mlos_sort.py:
    • Append "patch" to RAW_CATEGORY_ORDER (so it's a valid mod.category value and topo sort treats it like any other category).
    • Extend derive_category(mod) with the §3 name heuristic, returning "patch" when matched and category is otherwise undefined.
    • Modify _initial_sort_key: prepend 1 if mod.category == "patch" else 0 as the new tuple element.
  • adapters.py: extend CAT_MAP with "patch": "patch" so the frontend pill key is preserved (see §6).
  • worker.py: no change. mod.info parsing already accepts arbitrary category=… values; once "patch" is in CATEGORY_ORDER, existing parser code passes it through unchanged.
  • No schema migration. mod_parsed.category is already TEXT NOT NULL DEFAULT 'undefined' - "patch" fits without alteration.

6. Frontend changes

  • New pill patch in the mod-table category column. Recommended palette: muted mauve / pale grey to distinguish from gameplay (the current default for tweaks-shaped mods) without competing for attention.
  • Pill is descriptive only - sort position already telegraphs "this is a patch" since patches cluster at the bottom of the table. The pill is a quick visual confirmation, not a signal the user has to learn.
  • CSS addition only (one rule): .cat.patch { background: …; color: …; }. No layout or component changes.

If the user prefers to skip the pill (5 buckets stays cleaner), the spec is satisfied without it; sort behavior is the load-bearing change.

7. Out of scope

  • Detecting patches from Steam workshop tags (Steam's vocabulary has no canonical "Patch" tag - Misc and Framework are the closest, both too noisy to map).
  • Multiple patch sub-tiers (e.g., "patches-of-patches"). YAGNI; the existing loadAfter mechanism handles ordering between two patches when needed.
  • A mod_parsed.is_patch boolean column. Derived from category is sufficient and avoids a migration.
  • Auto-detecting patches via mod content inspection (Lua module overrides, file collisions). Heuristics only.

8. Acceptance criteria

  • mlos_sort._initial_sort_key returns an 8-element tuple with is_patch (0 or 1) at index 0.
  • RAW_CATEGORY_ORDER includes "patch".
  • derive_category returns "patch" when the name regex \b(patch|compat|compatibility)\b (case-insensitive) matches and mod.info's category= is unset or undefined.
  • Explicit category=patch in mod.info is honored by the existing parser (no parser change required).
  • sorting_rules.txt category=patch override forces a mod into the patch tier.
  • In a sort with one loadLast=on map mod and one patch, the patch sorts after the map mod in SORTED_ORDER.
  • In a sort with two patches, alphabetical ordering applies between them (existing alpha tiebreaker preserved).
  • In a sort with no patches, SORTED_ORDER is bit-identical to pre-spec output (is_patch=0 for all rows preserves existing total ordering).
  • MOD_DB rows for patches carry cat: "patch" once adapters.CAT_MAP is extended.

9. Test cases

  1. Explicit patch via mod.info - wsid X has category=patch. Expect: sorts last regardless of loadLast. MOD_DB.cat = "patch".
  2. Heuristic match - mod named BB Compatibility Patch, no explicit category. Expect: detected as patch, sorted last.
  3. Heuristic miss (intentional) - mods named BugFixes, LittleTweaks, BalanceFix. Expect: NOT in patch tier.
  4. Patch + loadLast map mod - input: a loadLast=on map mod (Eerie_County) and a patch (Eerie_County - Brita Compat). Expect: Eerie_County precedes the patch in SORTED_ORDER.
  5. Two patches - AAA-Compatibility and ZZZ-Patch. Expect: alphabetical order preserved within the tier.
  6. No patches in input - sort identical to current behavior; regression test against a saved canonical fixture (e.g. 2169435993;2392709985;2487022075).
  7. sorting_rules.txt override - user supplies [Some_Mod]\ncategory=patch; expected to force into tier even if name doesn't match heuristic and mod.info doesn't declare it.
  8. Patch with PREORDER mod_id - hypothetical ModManagerServer-Patch (mod_id matches PREORDER table). Expect: still sorts within the patch tier (last), but among patches uses PREORDER=2 sub-ordering.