Exclude the merged mod from the conflict scan so re-merges stay idempotent - #37
Open
TheValiantOne wants to merge 1 commit into
Open
Exclude the merged mod from the conflict scan so re-merges stay idempotent#37TheValiantOne wants to merge 1 commit into
TheValiantOne wants to merge 1 commit into
Conversation
…otent The mod-directory filter honored only IgnoreModNames, so mod0000_MergedFiles - this tool's own output - matched BuildAsync's "mod*" glob and got scanned as an ordinary source mod. Every re-merge then fed the previous run's output back in alongside the mods it was built from, re-applying their edits on top of already-merged text: inserted blocks gained a fresh copy per run, and a losing most-distinct-from-vanilla tiebreak could revert an edit a previous run had kept. Found on a live 249-mod install after a routine re-merge: a single modBloodAndSteel insertion present 6x in actor.ws and a modCriSlowMoCR one 6x in damageManagerProcessor.ws (each appears exactly once in the mod's own file), 37 duplicated mod-added lines across 11 of 42 merged files, and one modTTMutagenSwap edit reverted outright. Nothing surfaced this as an error - the output stayed syntactically valid and merged "successfully" every time, which is why it went unnoticed across repeated merges. GetIgnoredModNames now also excludes MergedModName, via a new pure BuildIgnoredModNames(ignoreSetting, mergedNameSetting) that's unit-testable without touching AppState.Settings, and Paths.NormalizeMergedModName - a non-interactive counterpart to RetrieveMergedModName (which can prompt via ConfirmInvalidModName, and must not on a scan path) sharing its new MergedModNameMaxLength truncation so the excluded name always equals the directory a merge actually writes. Also drops the WinForms merge verb's bundle-tool gate: it used the combined ValidateDependencyPaths(), so `merge` refused to start at all without QuickBMS/wcc_lite even when every conflict was flat-file. Neither binary is committed to this repo, so a plain clone-and-run hit that refusal every time. Now matches Headless's text-merge-only gate; bundle conflicts still degrade gracefully per-conflict. 15 new tests (173 total). Verified A/B against origin/main on a scratch tree carrying a stale mod0000_MergedFiles whose marker strings appear in no mod and not in vanilla: pre-fix those markers leaked into the new output and the decision log named mod0000_MergedFiles as a merge participant; post-fix, zero leakage and no such decisions. Re-verified on the live install - a clean rebuild has 0 duplicated mod lines where the previous output had 37. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FP8H6rBLCGPBFRSVsF3Kgw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What's wrong
ModFileIndex.BuildAsyncenumeratesDirectory.GetDirectories(ModsDirectory, "mod*")and filters the result throughGetIgnoredModNames()— which honored only theIgnoreModNamessetting.mod0000_MergedFilesis this tool's own output, but its name starts withmod, so it matched the same glob and was scanned as an ordinary source mod.Every re-merge therefore fed the previous run's output back in alongside the mods it was built from, re-applying their edits on top of already-merged text. A re-merge was cumulative, not idempotent. Inserted blocks gained a fresh copy per run, and a losing most-distinct-from-vanilla tiebreak could additionally revert an edit a previous run had kept.
Nothing surfaced it as an error. The output stayed syntactically valid, braces balanced, and every run reported
Merged N file(s), skipped 0.— which is why it accumulated unnoticed.How it was found
A routine re-merge on a live 249-mod install, after adding mods. Measured against each mod's own file as ground truth:
modBloodAndSteelInCustomDodge()inactor.wsmodCriSlowMoCRdismemberChanceindamageManagerProcessor.wsAcross the merged set: 37 duplicated mod-added lines in 11 of 42 files, plus one
modTTMutagenSwapedit reverted outright (OnSkillMutagenUnequipped(..., false)→true).The fix
GetIgnoredModNames()now excludesMergedModNameon top of whatever the user configured.ModFileIndex.BuildIgnoredModNames(ignoreSetting, mergedNameSetting)— a pure function over the two raw setting values, so it's unit-testable without touchingAppState.Settings(seeWitcherScriptMerger.Tests/CLAUDE.md's safety constraints).Paths.NormalizeMergedModName(string), a non-interactive counterpart toRetrieveMergedModName(). The scan path must not use the latter — it can prompt viaConfirmInvalidModNameand message throughAppState.Notifier, neither of which may fire just because mod directories are being enumerated. Both share the newPaths.MergedModNameMaxLength(64) truncation, so the excluded name always equals the directory a merge actually writes.NormalizeMergedModNamealso trims, deliberately — its result is compared against aDirectoryInfo.Name, which never carries surrounding whitespace.Second, smaller fix
The WinForms
mergeverb gated on the combinedPaths.ValidateDependencyPaths(), so it refused to start at all without QuickBMS/wcc_lite — even when every conflict was flat-file.ws/.xml. Neither binary is committed to this repo, so a plain clone-and-run hit it every time, with an error pointing at the GUI's dependency setup for tooling the run didn't need. It now usesValidateTextMergeDependencies(), matchingWitcherScriptMerger.Headless. Bundle conflicts still degrade gracefully per-conflict.This is what blocked the original merge and is why the run that exposed the main bug went through the Headless host instead.
Verification
Unit tests — 15 new in
FileIndex/ModFileIndexTests.cs, 173 total, all passing. Covers the bug itself (exclusion with noIgnoreModNamesconfigured), a non-defaultMergedModName, user entries surviving alongside, case-insensitive de-duplication when the merged mod is already listed by hand (the pre-fix workaround), blank/unconfiguredMergedModNameadding no phantom entry, and the truncation + trimming behavior.A/B against
origin/main— built both hosts and ran each against a scratch game tree seeded with a stalemod0000_MergedFileswhose two marker strings appear in no mod and not in vanilla:A simpler two-mod fixture does not reproduce it — with a clean prior merge the extra input converges to the same result. The stale-output fixture is what isolates the mechanism.
Live install — a clean rebuild of all 42 merged files, verified against every contributing mod's own source: 0 duplicated mod-added lines, where the previous output had 37. Also re-checked encoding (UTF-16LE + BOM on all 42), brace balance against vanilla, enum integrity, and no duplicate function definitions beyond vanilla's own 31 legitimate same-name cases.
Second fix — the WinForms
mergeverb now completes with no QuickBMS/wcc_lite present, printing[Bundle Checking Unavailable] ... skipping bundle-content checking for this scan.thenMerged 1 file(s), skipped 0.(exit 0). Previously exit 1 with no merge performed.dotnet format whitespace --verify-no-changesclean.Note for users on an existing install
Any
mod0000_MergedFilesproduced before this fix may already carry accumulated duplicates. The fix prevents further accumulation but does not retroactively clean existing output — delete the merged mod folder once and re-merge to get a correct rebuild.AI-assisted development
This PR was produced by Claude Code (Opus 5), including the investigation that found the bug on a live install, the fix, the tests, and the A/B verification above. Per
CONTRIBUTING.md: everything reported here was actually run, not inferred — the marker counts, the A/B output, and the live-install rebuild figures are from real runs against real data.