Skip to content

Let a mod ranking decide whose version of a function wins - #43

Open
TheValiantOne wants to merge 2 commits into
mainfrom
feature/mod-priority-tiebreak
Open

Let a mod ranking decide whose version of a function wins#43
TheValiantOne wants to merge 2 commits into
mainfrom
feature/mod-priority-tiebreak

Conversation

@TheValiantOne

Copy link
Copy Markdown
Owner

First increment of ranked overrides: an opt-in mod ranking that resolves the whole-function tiebreak.

Why this lever

Reading the code surfaced a distinction worth stating up front: merge order and tiebreak-winner are different things.

  • The existing per-file orderOverrides sets the chain order, and demands you name every mod for the file it covers.
  • When two mods edit the same function differently, the winner is picked by "most distinct from vanilla wins" — order-independent, and with no way for a user to express a preference.

"Ranked overrides by originating mod" targets that second lever, which didn't exist. This adds it without touching ordering.

Behaviour

  • Consulted only at the point FunctionLevelMergeEngine was already going to pick a whole side. A ranking never overrides a clean, genuinely-merged result.
  • Partial by design. Rank only what you care about; a pair where neither side is ranked returns PreferredSide.None and behaves exactly as before. A ranked mod beats an unranked one.
  • The accumulated side takes its best rank. A chain step's old side is the merge of everything before it, so it carries several mods at once. Without this, a highly-ranked mod would stop winning the moment one more mod merged on top of it — the opposite of what ranking it means.
  • Every ranking-driven choice lands in the same Decisions audit trail as every other non-mechanical decision, naming the ranking as the reason.

Configuration

Through the existing order file's reserved "*" entry:

{
  "*": ["modIWantToWin", "modSecondChoice"],
  "game\actor.ws": ["modA", "modB"]
}

That reuses every existing path at once — both hosts' --order-file, the MCP merge_conflicts orderOverrides argument, and the Vortex extension — with no new config surface. * is safe as a sentinel because it's a reserved character in Windows paths, so it can't collide with a real relative path; MergeConflictsHeadless strips it before ResolveMergeOrder can see it as a path override.

Verification

18 new tests, 193 total, all passing. dotnet format whitespace --verify-no-changes clean.

Rank comparison both directions, ranked-beats-unranked on either side, the accumulated side taking its best rank, case-insensitivity, all three no-opinion cases, and the order-file plumbing (extraction, blank trimming, and that the reserved key never reaches ResolveMergeOrder).

Next

This is the by-mod increment. By-function overrides ({"path.ws": {"CR4Player.OnSpawned": "modX"}}) are the natural follow-on and fit the same seam — ResolveUnit already has the unit name in hand where it consults preferredSide.

AI-assisted development

Produced by Claude Code (Opus 5). Design shape (which lever, where configured, how strict) was chosen by the repo owner from options; implementation and tests are mine. Test and format results are from real runs.

Chris Knight and others added 2 commits August 21, 2026 23:46
Adds an opt-in, user-supplied mod ranking that resolves the whole-function
tiebreak. Until now that tiebreak was "most distinct from vanilla wins" - a guess
at which side did more work, with no way for a user to say "I want this mod's
version of this function."

Deliberately a different lever from the existing per-file order override
(FileMerger.ResolveMergeOrder): that sets the chain ORDER and must name every mod
for the file it covers. This one reorders nothing. It only answers "whose code
survives" at the exact point FunctionLevelMergeEngine was already going to pick a
whole side - never overriding a clean, genuinely-merged result.

Partial by design: rank only the mods you care about. A pair where neither side is
ranked returns PreferredSide.None and behaves exactly as before, so adding a
ranking can never change the outcome of a conflict it doesn't mention. A ranked
mod beats an unranked one.

The accumulated side of a chain step carries every mod merged into it so far, so
it takes the BEST rank among them - without that, a highly-ranked mod would stop
winning the moment one more mod merged on top of it, which is the opposite of what
ranking it means.

Configured through the existing order file's reserved "*" entry, alongside its
ordinary per-path entries. That reuses all the existing plumbing at once - both
hosts' --order-file, the MCP merge_conflicts orderOverrides argument, and the
Vortex extension - with no new config surface. "*" is safe as a sentinel because
it's a reserved character in Windows paths, so it can never collide with a real
relative path; MergeConflictsHeadless strips it before ResolveMergeOrder can ever
see it as a path override.

Every ranking-driven decision is recorded in the same Decisions audit trail as
every other non-mechanical choice, naming the ranking as the reason.

18 new tests (193 total): rank comparison in both directions, ranked-beats-
unranked, the accumulated side taking its best rank, case-insensitivity,
no-opinion cases (no ranking, neither side ranked, same mod both sides), and the
order-file plumbing (extraction, blank trimming, and that the reserved key never
reaches ResolveMergeOrder). dotnet format clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FP8H6rBLCGPBFRSVsF3Kgw
A plain ToDictionary rebuilds with the default ordinal comparer, silently undoing
the StringComparer.OrdinalIgnoreCase that WsmMcpTools.MergeConflicts deliberately
installs (alongside separator normalization) so a differently-cased but otherwise
correct path key isn't ignored. Dropping it turned those keys back into silent
no-ops - but only for callers that supply a ranking, so the bug would have stayed
hidden until the feature was actually used.

Now reads the source dictionary's own comparer and reuses it, falling back to the
default only when it can't be read - so it neither drops a deliberately-installed
comparer nor imposes one on the CLI hosts, which build their order dictionary with
the default and must stay exactly as case-sensitive as before.

Found by an independent review pass over this branch, not by the tests - nothing
exercised the interaction. 2 new tests (195 total) cover both directions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FP8H6rBLCGPBFRSVsF3Kgw
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant