refactor: push Unreal/pak knowledge behind the MergeCompiler seam (#256) - #263
Conversation
Adds the five pieces of Unreal/Icarus format knowledge core currently holds - base-pak location, pak fingerprinting, the .pak convert test, merge-source kind classification, and the merged artifact's name/label - as methods on *Icarus, with the kind constants moved into this package. Additive step: internal/source's MergeCompiler interface widens in the next commit, which swings internal/core over to these methods. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…am (#256) Closes all five format leaks the issue enumerates: 1. resolveBasePak (Icarus/Content/Data/data.pak path) -> mc.ResolveBaseArtifact 2. basePakIndexHash (go-unrealpak import in core) -> mc.FingerprintBase 3. isConvertEligiblePakFile's .pak suffix test -> mc.IsConvertibleArtifact (core keeps only the DeployCompile/ConvertPaks policy half) 4. MergeSourceExmodz/MergeSourcePak constants -> moved into the icarus package; core classifies via mc.ClassifyMergeSource and round-trips kinds as opaque strings 5. mergedPakFileName (zzz UE sort-order convention) and the 'Icarus Merged Pak' display name -> mc.MergedArtifactName/MergedArtifactLabel source.MergeCompiler widens from 2 to 8 methods and now states the full contract a second DeployCompile game must implement. Core resolves the compile source lazily on merge paths (only once something retained needs classifying), so no-compiler/no-retained flows keep succeeding exactly as before. ModHasPakMergeSource gains the *domain.Game parameter its classification now requires; callers in cmd/lmm, internal/tui, and moddetail updated. Test accommodations (no behavioral assertions changed): compile-source fakes gain an embedded fakeMergeFormat mirroring the icarus conventions; fixtures for DeployCompile games now map/register a MergeCompiler source (required by the seam where core previously classified statically); moved-symbol references (source.MergeSource* constants, mergedFingerprintsEqual's new classifier arg) updated in place. TestMergeSourceKind's table moved to icarus TestClassifyMergeSource. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors the DeployCompile / merged-artifact pipeline so that Unreal .pak / Icarus-specific format knowledge no longer lives in internal/core, and is instead provided by the game’s source.MergeCompiler implementation (Icarus today) via an expanded interface contract.
Changes:
- Expand
source.MergeCompilerto include base-artifact resolution, fingerprinting, convertibility checks, merge-source kind classification, and merged output naming/labeling; update core to depend on these methods instead of hardcoded.paklogic/strings. - Add Icarus implementations for the new “format vocabulary” methods (
internal/source/icarus/format.go) with dedicated unit tests. - Update CLI/TUI/core tests and fixtures to register/implement the widened
MergeCompilerseam (via embeddedfakeMergeFormathelpers) and to pass*domain.Gamewhere classification now requires it.
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| internal/tui/service_core.go | Updates TUI overview mapping to call ModHasPakMergeSource(game, mod) with the game context now needed for classification. |
| internal/tui/service_core_test.go | Adjusts TUI details fixture to map/register a merge-compiler source for ConvertPaks classification. |
| internal/tui/service_core_recompile_test.go | Embeds fakeMergeFormat in the TUI MergeCompiler fake to satisfy the widened interface. |
| internal/tui/service_core_convert_test.go | Registers a MergeCompiler source in the convert fixture so HasPakSource classification can resolve the compiler. |
| internal/tui/merge_format_helpers_test.go | Adds a TUI test helper implementing the MergeCompiler “format vocabulary” methods for fakes. |
| internal/source/source.go | Widens MergeCompiler interface and updates documentation to describe the full DeployCompile contract. |
| internal/source/icarus/merge.go | Switches pak-kind checks to Icarus-local merge-source kind constants. |
| internal/source/icarus/merge_test.go | Updates tests to use Icarus-local kind constants. |
| internal/source/icarus/format.go | Introduces Icarus’s MergeCompiler “format vocabulary” implementation (base artifact path, fingerprinting, convertibility, kind classification, merged name/label). |
| internal/source/icarus/format_test.go | Adds unit tests covering the new Icarus format-vocabulary methods. |
| internal/core/service.go | Removes unrealpak from core, adds mergeCompilerForGame(*Game) resolver, and routes convert-eligibility through mc.IsConvertibleArtifact. |
| internal/core/service_test.go | Updates compiler-related mocks to satisfy the widened MergeCompiler interface. |
| internal/core/service_icarus_compile_test.go | Updates core compile fakes to satisfy the widened MergeCompiler interface. |
| internal/core/pak_convert_e2e_test.go | Converts kind assertions from source.MergeSource* constants to literal kind strings round-tripped through the seam. |
| internal/core/moddetail.go | Updates ModDetail to call ModHasPakMergeSource(game, installed) with game context. |
| internal/core/moddetail_test.go | Registers/maps a MergeCompiler source for DeployCompile subtests now relying on compiler classification. |
| internal/core/merged_pak.go | Replaces core-owned .pak/kind/name logic with MergeCompiler classification and naming; adjusts fingerprint helpers to take a classifier function. |
| internal/core/merged_pak_test.go | Updates merged-pak fixtures to register/map a MergeCompiler source for classification. |
| internal/core/merged_pak_internal_test.go | Refactors internal tests to pass a classifier function; adds a minimal MergeCompiler fake for ModHasPakMergeSource tests. |
| internal/core/merged_pak_import_flow_test.go | Embeds fakeMergeFormat in the import-flow compiler fake to satisfy the widened interface. |
| internal/core/merge_format_helpers_test.go | Adds a core test helper implementing the MergeCompiler “format vocabulary” methods for fakes. |
| internal/core/importer.go | Routes convertible-artifact detection through the compiler seam (mc.IsConvertibleArtifact) and adjusts when compiler resolution is attempted. |
| internal/core/flows.go | Uses isConvertEligibleArtifact(game, mc, ...) rather than core’s former .pak-suffix predicate. |
| internal/core/flows_variant_exclusivity_test.go | Embeds fakeMergeFormat in the test source to satisfy the widened MergeCompiler interface. |
| cmd/lmm/mod.go | Updates mod convert to pass game context to ModHasPakMergeSource. |
| cmd/lmm/mod_convert_test.go | Registers a MergeCompiler-capable wrapper source so convert-flag surfaces can classify pak-kind fileIDs via the seam. |
| cmd/lmm/merge_format_helpers_test.go | Adds a CLI test helper implementing the MergeCompiler “format vocabulary” methods for fakes. |
| cmd/lmm/list.go | Updates list output to use ModHasPakMergeSource(game, mod) with game context. |
| cmd/lmm/install_compile_test.go | Embeds fakeMergeFormat in the CLI compile-source fake to satisfy the widened MergeCompiler interface. |
| CHANGELOG.md | Adds an Unreleased entry documenting the internal refactor and “no user-visible change” intent. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if game.DeployMode != domain.DeployCompile || !game.ConvertPaks || !mod.ConvertPaks { | ||
| return false, nil | ||
| } | ||
| if mergeSourceKind(fileID) != source.MergeSourcePak { | ||
| mc, err := s.mergeCompilerForGame(game) | ||
| if err != nil { | ||
| return false, err | ||
| } |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.
Suppressed comments (1)
internal/source/icarus/format.go:54
- Typo in comment: "week-correct" reads like a time period; likely meant "correct" (or similar).
// This pak is also the direct source of base table *content* (#175):
// MergeCompile reads each patched table straight out of it, so a compile is
// always week-correct by construction (there's no separate dump to go stale
// relative to the install) and works entirely offline.
…-256-mergecompiler-seam # Conflicts: # CHANGELOG.md
…er seam (#256) Closes the sixth leak: core's static isExmodzFile hardcoded Icarus's NATIVE merge-source format, so a second DeployCompile game's native files would silently never reach the validate+retain branch - the exact core-edit-per-game outcome #256 exists to prevent. A new IsNativeMergeSource(fileName) method (the exact mirror of IsConvertibleArtifact) joins source.MergeCompiler rather than widening ClassifyMergeSource: ClassifyMergeSource must default UNKNOWN ids to the native kind (legacy fingerprint compat), so its result cannot distinguish 'genuinely native archive' from 'unknown junk' - using it for ingest routing would send every unrecognized file into validate+retain. All four call sites now go through the seam, via a new Service.isNativeMergeFile helper that prefers the file's own source's view and falls back to the game's sole compile source - keeping the 'native archive from a non-compile-capable source' download hard error reachable on mixed-source games. Two deliberate behavior deltas, both confined to games with NO resolvable compile source (broken config), where nothing is left to define 'native': - Importing a native archive into such a game still fails loud and still caches nothing, but with the legacy path's 'unsupported archive format: .exmodz' (the Extractor is extension-keyed) instead of the old compiler-specific message - that message required core itself to know the extension. TestImportMod_DeployCompile_NoCompilerSourceFailsLoud updated accordingly; pak/zip no-compiler imports are byte-identical. - A standalone core.NewImporter (nil resolver) now fails loud for EVERY DeployCompile import up front, preserving its original error verbatim; production only ever constructs the service-backed importer. The mixed-selection message loses its exmodz vocabulary: 'raw pak and native merge archive are alternate forms of the same mod - select one' (the 'alternate forms of the same mod' substring callers match on is unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 32 changed files in this pull request and generated no new comments.
Suppressed comments (3)
internal/source/source.go:160
- The MergeCompiler doc comment enumerating the “format vocabulary” methods omits IsNativeMergeSource, even though it’s now part of the interface and required for DeployCompile imports/selection validation. This makes the interface contract description incomplete/inaccurate for implementers.
// plus the format vocabulary core needs to orchestrate them without knowing
// the game's artifact format itself - where the base artifact lives
// (ResolveBaseArtifact), how to fingerprint it (FingerprintBase), which
// files are convertible raw artifacts (IsConvertibleArtifact,
// ClassifyMergeSource), and what the merged output is called
internal/core/service.go:141
- This error message still hard-codes “pak”, but the MergeCompiler seam is now intended to support compile-mode games whose raw artifact format may not be a pak. Consider making the message format-agnostic so it doesn’t leak Unreal vocabulary and remains correct for future DeployCompile sources.
if native && other {
return fmt.Errorf("raw pak and native merge archive are alternate forms of the same mod - select one")
}
internal/core/service.go:1086
- isNativeMergeFile silently returns false when mergeCompilerForGame(game) errors. That can hide actionable configuration problems (e.g. ambiguous/multiple MergeCompiler sources) and cause native archives to fall back to the legacy extract/copy path, producing a generic “unsupported archive format” error instead of surfacing the real misconfiguration.
if mc == nil {
gmc, err := s.mergeCompilerForGame(game)
if err != nil {
return false
}
…lize the last format word (#256) - The MergeCompiler preamble now enumerates IsNativeMergeSource with the rest of the format vocabulary (its omission made the contract description incomplete for implementers). - The mixed-selection rejection drops 'pak': 'raw artifact and native merge archive are alternate forms of the same mod - select one' - the message was this PR's one remaining core string a second DeployCompile game would have had to edit. Declined (with reasoning on the PR): isNativeMergeFile deliberately treats an unresolvable compile source as 'nothing defines native' rather than propagating - an ambiguous-compilers config cannot stay hidden, as every merge-execution path (sync, import, staleness) still fails loud on the same resolution. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Copilot round-2 triage (3 suppressed notes, none posted inline):
|
| if game.DeployMode == domain.DeployCompile { | ||
| if i.resolveMergeCompiler == nil { | ||
| mcErr = fmt.Errorf("game %q requires DeployCompile to import %q, but this Importer was constructed without service context (via core.NewImporter, not Service.NewImporter) and has no compiler resolver to consult - import via the service-backed importer instead", game.ID, filename) | ||
| } else { | ||
| mc, mcErr = i.resolveMergeCompiler(game.ID) | ||
| return nil, fmt.Errorf("game %q requires DeployCompile to import %q, but this Importer was constructed without service context (via core.NewImporter, not Service.NewImporter) and has no compiler resolver to consult - import via the service-backed importer instead", game.ID, filename) | ||
| } | ||
| mc, mcErr = i.resolveMergeCompiler(game.ID) |
There was a problem hiding this comment.
Confirmed real — good catch, and it invalidated the round-2 design's premise: detectFormatFromPath does content-sniff zip magic after the extension miss, so the fall-through would have silently extracted a real zip-backed .exmodz. Fixed in 48012c2: an unresolvable MergeCompiler is now a hard error for every import into a DeployCompile game (with no compiler, core cannot tell a native archive from anything else, and the resolver message names the fix). This restored TestImportMod_DeployCompile_NoCompilerSourceFailsLoud to its original assertions and supersedes the import-side half of #221 I1 — the pak fall-through test is rewritten as …PakNoCompilerSourceFailsLoud with the history documented.
| mc, isMergeCompiler := src.(source.MergeCompiler) | ||
| convertEligiblePak := isMergeCompiler && isConvertEligiblePakFile(game, safeFileName) | ||
| if game.DeployMode == domain.DeployCompile && (isExmodzFile(safeFileName) || convertEligiblePak) { | ||
| convertEligiblePak := isMergeCompiler && isConvertEligibleArtifact(game, mc, safeFileName) | ||
| if game.DeployMode == domain.DeployCompile && (s.isNativeMergeFile(game, mc, safeFileName) || convertEligiblePak) { |
There was a problem hiding this comment.
Real but bounded, and deliberately accepted on this path (documented on isNativeMergeFile in 0088253): hard-erroring here would break #221 I1's protected download contract — a non-MergeCompiler source's paks/zips must fall through to legacy (pinned by TestDownloadPak_NonMergeCompilerSource_FallsThroughToLegacyPath, whose game maps no compiler at all). The residual requires a hand-broken SourceIDs map (icarus is a built-in and always registered) and a foreign source serving real native archives; whenever the game maps its compiler, the pre-existing hard error still fires via the game-level fallback. The import path — which has no per-archive source contract — now hard-errors instead (48012c2).
…ts (#256) Copilot round-3 catch, confirmed real: the legacy extract path content-sniffs zip magic (detectFormatFromPath's header fallback - NOT extension-only, as the round-2 design assumed), so letting a DeployCompile import fall through when the game's MergeCompiler cannot be resolved would silently extract and cache a real, zip-backed native archive without ValidateSource. Import path: an unresolvable compiler is now a hard error for EVERY import into a DeployCompile game - with no compiler, core cannot tell a native archive from anything else, and the resolver's own message names the fix. This restores TestImportMod_DeployCompile_NoCompilerSourceFailsLoud to its ORIGINAL assertions (the round-2 edit is reverted), and supersedes the import-side half of #221 I1: the pak fall-through test becomes TestImportMod_DeployCompile_PakNoCompilerSourceFailsLoud, doc'd with the history. The download-side I1 fall-through (per-source pin) is unchanged and its test passes untouched. Download path: unchanged - the non-compiler-source fall-through is a protected #221 contract. The residual (native archive from a non-compiler source on a game with NO resolvable compiler anywhere gets sniff-ingested) requires a hand-broken SourceIDs map plus a foreign source serving native archives; documented on isNativeMergeFile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…isNativeMergeFile (#256) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 32 changed files in this pull request and generated no new comments.
Suppressed comments (2)
internal/source/icarus/format_test.go:29
- Use the repo’s preferred Go octal literal style (
0o644) instead of legacy0644for file mode constants.
if err := os.WriteFile(wantPath, []byte("stub"), 0644); err != nil {
internal/source/icarus/format_test.go:25
- Use the repo’s preferred Go octal literal style (
0o755) instead of legacy0755for file mode constants (this package already uses0o644, etc.).
This issue also appears on line 29 of the same file.
if err := os.MkdirAll(basePakDir, 0755); err != nil {
…colliding files named in the rejection (#256) Item 1: #261's rawPakRestoreName held the last live format knowledge in core - a '.pak' extension test and the synthesized '<mod-id>_P.pak' Icarus override name. The extension test was already expressed by the seam (IsConvertibleArtifact: EqualFold-on-Ext and suffix-on-lowered are equivalent for every filename), so it reuses that; the synthesized name is genuinely new vocabulary and becomes the tenth method, RestoredArtifactName(modID) - the per-mod analogue of MergedArtifactName, byte-identical output ('<modID>_P.pak', core Base's the input) so healed installs keep their on-disk names. The import-vs-download provenance split STAYS in core: it follows from how ingest keys fileIDs, which is uniform across games - only the two format questions inside it moved. #261's heal/prune tests pass unmodified. Item 2: the mixed-selection rejection now names the actual colliding files ('Mod_P.pak and Mod.exmodz are alternate forms of the same mod - select one') - format-agnostic because the vocabulary comes from the selection itself, and strictly more useful than both prior wordings. The trigger is unchanged (booleans, not name-emptiness, so a file with an empty FileName still trips it). Variant-exclusivity expectations updated deliberately; cmd/lmm/install_test.go's local fake updated to mirror the production message shape it stands in for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.
Suppressed comments (3)
CHANGELOG.md:20
- The changelog bullet wraps a backticked example across a line break and the continuation line isn’t indented, which breaks Markdown rendering for the bullet and the inline-code span. Keep the example in a single code span and ensure all continuation lines are indented under the list item.
implement (#256). The one user-visible change: rejecting a mixed
pak+exmodz install selection now names the two colliding files
(`Mod_P.pak and Mod.exmodz are alternate forms of the same mod - select
one`) instead of the generic wording.
internal/source/source.go:163
- The MergeCompiler contract docblock lists the required format-vocabulary methods but omits RestoredArtifactName, which is also part of the required interface and matters for #250 healing behavior. Please include it so the “complete contract” claim stays accurate.
// files are the source's native merge format (IsNativeMergeSource), which
// are convertible raw artifacts (IsConvertibleArtifact,
// ClassifyMergeSource), and what the merged output is called
// (MergedArtifactName, MergedArtifactLabel). A second compile-mode game is
// a new source package implementing these methods plus one registration
internal/core/service.go:212
- The doc comment block mentions "compilerSourceForGame", but the function is named mergeCompilerSourceForGame (and it resolves MergeCompiler sources). This mismatch can make code search/navigation confusing.
// check to the specific source a file was downloaded from
// (DownloadModToCache's src.(source.MergeCompiler) check); Importer.Import
// has no such per-archive source to key off of, so it resolves against
// every source the game maps in its registry instead — at most one of a
…eration, stale doc name (#256) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 33 out of 33 changed files in this pull request and generated no new comments.
Suppressed comments (1)
internal/core/merged_pak.go:84
- ModHasPakMergeSource now always tries to resolve the game’s MergeCompiler, even for non-DeployCompile games. This can add repeated registry lookups (and construction of the “requires DeployCompile…” error) in callers that don’t guard on DeployCompile (e.g. the TUI Overview builds rows for all games). Since pak-conversion is only meaningful for DeployCompile games, this should short-circuit to false when game is nil or not DeployCompile (and also avoids a nil-game panic).
func (s *Service) ModHasPakMergeSource(game *domain.Game, mod *domain.InstalledMod) bool {
if mod == nil {
return false
}
mc, err := s.mergeCompilerForGame(game)
…e-agnostic (#256) Copilot round-6 triage: the nil guard is taken; the suggested DeployCompile short-circuit is NOT - it breaks a pinned CLI behavior: 'lmm mod convert' persists the per-mod flag on non-compile games with an advisory note (TestModConvertCommand_NonCompileGame), which requires classification to answer whenever the game's compile source resolves, exactly as the pre-#256 static test did. Documented on the method; the internal test now covers the non-compile-with-compiler true case and the nil-game case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes #256 — well, fixes; per repo convention the issue is closed manually at develop-merge.
What
internal/coreknew what an Unreal.pakis. This PR pushes each of the issue's five enumerated leaks behind thesource.MergeCompilerseam so core asks the source rather than knowing:resolveBasePakhardcoded<InstallPath>/Icarus/Content/Data/data.pakmc.ResolveBaseArtifact(game)(icarus impl keeps the path + thelocating base pak for %qwrap)basePakIndexHashimportedgo-unrealpakin coremc.FingerprintBase(path)— core holds an opaque string; unrealpak import gone from core non-test codeisConvertEligiblePakFiletested.paksuffix in coreDeployCompile && ConvertPakspolicy half (isConvertEligibleArtifact); the format half ismc.IsConvertibleArtifactMergeSourcePak/MergeSourceExmodzconstants in sharedsourcepackagemc.ClassifyMergeSource(id) (kind, convertible)and round-trips kinds as opaque strings (incl. stored fingerprint kinds and the legacy empty Kind)mergedPakFileName = "zzz_LMM_Merged_P.pak"(UE sort-order convention) in coremc.MergedArtifactName(); the"Icarus Merged Pak"display name likewise viamc.MergedArtifactLabel()source.MergeCompilerwidens from 2 to 8 methods and its doc block now states the complete contract a secondDeployCompilegame must implement — a new source package plus one registration line, zero core changes.Behavior preservation
No user-visible change. Kind wire values (
"pak"/"exmodz") and the merged filename are unchanged, so stored fingerprints, cache entries, and deployed artifacts all remain valid. Core resolves the compile source lazily on merge paths (only once a retained file actually needs classifying), so every no-compiler/no-retained flow that succeeded before still succeeds — verified by the existing no-compiler fall-through tests passing untouched.Two deliberate structural choices worth reviewer eyes:
mergeCompilerForGame(game)(struct-based) joins the existingmergeCompilerSourceForGame(gameID)— merged-pak paths always receive their game as a parameter and previously never consulteds.games, so resolving off the struct preserves that.ModHasPakMergeSourcegains a*domain.Gameparameter — classification now needs the game's compile source. Callers incmd/lmm(×3),internal/tui(×1), andmoddetail.goupdated.Test changes (all called out, none assert different behavior)
internal/source/icarus/format_test.gocovers the six seam methods (written first, TDD);TestMergeSourceKind's table lives on there asTestClassifyMergeSource.fakeMergeFormat(per test package: core_test, cmd/lmm, tui_test) mirroring the icarus conventions the fixtures already encode — wrappers (recordingMergeCompilerSource,pakOutcomeCompilerSource,healthE2ESource,failingValidateCompilerSource) inherit it for free.moddetail_test,merged_pak_test'sTestEnabledMergeSources…,mod_convert_test, both tui fixtures).source.MergeSource*constant references became literals/icarus constants;mergedFingerprintsEqualcalls inmerged_pak_internal_test.gopass the classifier;TestModHasPakMergeSourcerewired to a registry-backed service and gains a no-compiler⇒false case.The protected suites —
service_icarus_compile_test.go,pak_convert_e2e_test.go,merged_pak_*_test.go— pass with only the accommodations above (embed lines, fixture source registration, moved-symbol references); every behavioral assertion is untouched.Acceptance (from the issue)
rg -l unrealpak internal/core/→ only_test.gofiles ✅.pakstring literal, noIcaruspath segment (remaining matches are prose comments) ✅.pakextension, merge-source kinds, merged filename ✅go build ./... && go vet ./... && gofmt -l . && go test -count=1 ./...→ clean, 18/18 packages ok ✅Round 2 (post-review follow-up)
origin/develop(fix(tui): auto-open a warnings overlay when an outcome collapses to (N warnings) #258 TUI fix, fix(cache): keep a converted pak's raw-fallback copy through prune; heal pruned entries (#250) #261 prune/heal fix). fix(cache): keep a converted pak's raw-fallback copy through prune; heal pruned entries (#250) #261's raw-fallback restore and prune exemption survive intact on top of the refactor; note itsrawPakRestoreNamereintroduces.pak/_P.paknaming knowledge into core — flagged to the coordinator as a candidate for a follow-up seam method, deliberately not restructured here.isExmodzFileis gone. A newIsNativeMergeSource(fileName)method (exact mirror ofIsConvertibleArtifact) joinsMergeCompiler— chosen over wideningClassifyMergeSource, whose unknown-id→native-kind default (legacy fingerprint compat) cannot distinguish a genuinely native archive from unknown junk. All four call sites route throughService.isNativeMergeFile, which falls back to the game's sole compile source so the mixed-source download hard error stays reachable. Two behavior deltas, both confined to games with no resolvable compile source: a native-archive import there still fails loud and caches nothing but asunsupported archive format: .exmodz(one test updated — the old compiler-specific message required core to know the extension); a standalonecore.NewImporternow fails every DeployCompile import up front (original error verbatim; production never constructs one). The mixed-selection message is reworded to drop exmodz vocabulary (asserted substring unchanged).Round 3
rawPakRestoreNamebehind the seam (was flagged in round 2; acceptance criterion 2 required it): the.pakextension test reusesIsConvertibleArtifact(behaviorally identical to theEqualFold-on-Extform for every filename), and the synthesized<mod-id>_P.pakheal name becomes the tenth method,RestoredArtifactName(modID)— the per-mod analogue ofMergedArtifactName, byte-identical output so healed installs keep their on-disk names. The import-vs-download provenance split stays in core (it follows from ingest's fileID keying, uniform across games). fix(cache): keep a converted pak's raw-fallback copy through prune; heal pruned entries (#250) #261's heal/prune tests pass unmodified.Mod_P.pak and Mod.exmodz are alternate forms of the same mod - select one. Format-agnostic (vocabulary comes from the selection itself) and strictly more useful; trigger unchanged (booleans, not name-emptiness). Variant-exclusivity expectations updated deliberately;cmd/lmm/install_test.go's local fake updated to mirror the production shape it stands in for. CHANGELOG amended (this is the PR's one user-visible change).ModHasPakMergeSourcenote split — nil-game guard taken, the suggested DeployCompile short-circuit rejected with evidence (it breakslmm mod convert's pinned advisory behavior on non-compile games, caught by the suite). Final round ona05274a: zero findings, zero suppressed.Not done (deliberately)
merged_pak.go,MergedFingerprint,SyncMergedPak, …) — skipped per its own guidance; public-API churn for cosmetics.installer.gountouched (sibling work on syncMergedPak's uninstall-to-zero branch errors when the merged-pak cache entry is absent (steady state after disabling the last merge source) #260 in flight).🤖 Generated with Claude Code