Skip to content

Add function-level merge fallback for whole-file merge failures - #18

Merged
TheValiantOne merged 1 commit into
mainfrom
feature/function-level-merge-engine
Aug 10, 2026
Merged

Add function-level merge fallback for whole-file merge failures#18
TheValiantOne merged 1 commit into
mainfrom
feature/function-level-merge-engine

Conversation

@TheValiantOne

Copy link
Copy Markdown
Owner

Summary

DiffPlexMergeEngine.MergeHeadless merges whole .ws files line-by-line, which means real, incidental whitespace/comment differences (or a confirmed upstream DiffPlex bug that gets worse as edit density rises — see WitcherScriptMerger.Core/CLAUDE.md's "Compatibility constraint" section) can block an entire file from auto-solving even when the actual overlapping logic changes are confined to one or two functions.

This adds a fallback, activated only at the two points where the whole-file merge has already failed for a given pairwise chain step — never a parallel code path, so every conflict that already auto-solves today is unaffected:

  • ScriptUnitExtractor — splits a .ws file into function/event/@addField-field units via a string/comment-aware brace-matching tokenizer. Not a full parser: confirmed via direct research into WitcherScript's grammar that class/state/struct/enum declarations are top-level only and the language has no nested function-like constructs at all, so plain brace/paren counting is sufficient.
  • UnitAligner — aligns each side's units against vanilla's by name (LCS-based), handling insertions and deletions symmetrically.
  • FunctionLevelMergeEngine — resolves each vanilla function independently: cheap shortcuts for untouched/single-sided edits, a real per-function 3-way merge for non-overlapping changes, and a most-distinct-from-vanilla tiebreak (via DiffPlex.Differ's 2-way line diff, not the buggy ThreeWayDiffer) for genuine collisions. An edit always wins over a competing deletion. Every non-mechanical resolution is recorded in an audit trail — surfaced via DiffPlexMergeEngine.LastFunctionLevelDecisionsFileMerger.HeadlessMergeSummary.FunctionLevelDecisions → both hosts' CLI output and the MCP merge_conflicts tool's new functionLevelDecisions field — never applied silently.

Full design rationale is in WitcherScriptMerger.Core/CLAUDE.md's new "Function-level merge engine" section.

AI-assisted development disclosure (per CONTRIBUTING.md): this PR was developed with Claude Code, including the design research, empirical validation, and implementation.

Why this design

Before writing any code, I measured a real, live Witcher 3 install's actor.ws conflict (vanilla + 6 real overhaul mods): only 6 of 395 functions were genuine two-mod collisions, confirming per-function decomposition was worth building rather than just relocating the same conflict into a smaller, statistically more DiffAlgorithmException-prone box.

Mid-implementation, a chain-step replay against the real install's 5 currently-unresolved conflicts found that an insertion-only alignment (the originally-scoped design) would rescue only 2 of the 5 — one real mod in that install deletes several vanilla functions outright, and that deletion was found to persist through the merge chain even at steps where the deleting mod isn't a direct input (an earlier clean whole-file merge step faithfully propagates a one-sided deletion into the accumulated text). UnitAligner's symmetric insert/delete handling exists specifically to cover this — confirmed as the right call before building it further.

Test plan

  • dotnet build WitcherScriptMerger.sln --configuration Release — clean, 0 errors (pre-existing unrelated CA1823 warnings only)
  • dotnet format whitespace WitcherScriptMerger.sln --verify-no-changes — clean
  • dotnet test WitcherScriptMerger.sln — 125/125 passing, including 3 new test files (ScriptUnitExtractorTests, UnitAlignerTests, FunctionLevelMergeEngineTests) covering round-trip extraction fidelity, alignment (matches/insertions/deletions/both), every one-sided shortcut, the tiebreak (including its deterministic tie-break and a scaled-down DiffAlgorithmException case), edit-survives-competing-deletion, insertion reconciliation, and gap-comment detection
  • Real-data validation, not just synthetic fixtures: built an isolated scratch copy of a real Witcher 3 install's vanilla files + real mod files for the 5 conflicts that don't auto-solve today, ran the actual MCP merge_conflicts tool against it end-to-end. All 5 files merged successfully, including actor.ws itself (the flagship case that motivated this feature) — well-formed UTF-16LE+BOM output, no conflict markers, itemized functionLevelDecisions explaining every resolution. Never run against the live install directly (dry-run predicted the "output already exists" guard correctly with no writes).

🤖 Generated with Claude Code

https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah

DiffPlexMergeEngine.MergeHeadless merges whole files line-by-line, which
means a handful of real, incidental whitespace/comment differences (or a
confirmed upstream DiffPlex bug that gets worse as edit density rises) can
block an entire file from auto-solving even when the actual overlapping
logic changes are confined to one or two functions.

Adds a fallback, activated only at the two points where the whole-file
merge has already failed for a given pairwise step: split vanilla and both
sides into function/field units (ScriptUnitExtractor, a brace/paren-matching
tokenizer - WitcherScript has no nested functions, so this doesn't need a
full parser), align each side against vanilla by name (UnitAligner, LCS-based,
handling both insertions and deletions), and resolve each function
independently (FunctionLevelMergeEngine) - cheap shortcuts for untouched or
single-sided edits, a real 3-way merge for non-overlapping changes, and a
most-distinct-from-vanilla tiebreak for genuine collisions. An edit always
wins over a competing deletion. Every non-mechanical resolution is recorded
in an audit trail (DiffPlexMergeEngine.LastFunctionLevelDecisions ->
FileMerger.HeadlessMergeSummary.FunctionLevelDecisions -> both hosts' CLI
output and the MCP merge_conflicts tool's functionLevelDecisions field),
never applied silently.

Validated empirically before and after building: a real live install's
actor.ws conflict (6 real overhaul mods) showed only 6 of 395 functions
were genuine two-mod collisions, confirming per-function decomposition was
worth building. A chain-step replay against that same install's 5
real-world unresolved conflicts found an insertion-only alignment would
rescue only 2 of 5 (one mod deletes several vanilla functions outright,
and that deletion persists through the merge chain even at steps where
the deleting mod isn't a direct input) - UnitAligner's symmetric
insert/delete handling was added specifically to cover this. Final
end-to-end validation against an isolated copy of the same 5 real files
got all 5 to merge successfully, including actor.ws itself, with
well-formed output and no conflict markers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
@TheValiantOne
TheValiantOne merged commit 9b009db into main Aug 10, 2026
1 check passed
@TheValiantOne
TheValiantOne deleted the feature/function-level-merge-engine branch August 11, 2026 01:36
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