From bf9a3e1e33a305ad3aa51e663aa4095ab42ceb12 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Tue, 4 Aug 2026 10:46:42 +0200 Subject: [PATCH] fix(extract): keep committed palette skeletons out of stale cleanup Extract skips writing *palcus.itp back to source (build regenerates them from blueprints), but the skip branch never marked the skeleton's target path as desired, so cleanupStaleFiles deleted the committed source/palettes/*.itp.json on every extract. Mark the skipped target as desired so the skeletons survive. Co-Authored-By: Claude Fable 5 --- internal/pipeline/extract.go | 8 ++- internal/pipeline/pipeline_test.go | 82 ++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/internal/pipeline/extract.go b/internal/pipeline/extract.go index a5d00a8..cde1743 100644 --- a/internal/pipeline/extract.go +++ b/internal/pipeline/extract.go @@ -126,8 +126,14 @@ func extractArchiveResources(p *project.Project, archive erf.Archive, desired ma } // *palcus.itp are Toolset-generated palette projections; the module // build regenerates them from source blueprints, so extraction never - // writes them back into source. + // writes them back into source. The committed skeleton still has to + // survive stale cleanup, so mark its target as desired. if ext == "itp" && isPaletteProjectionResref(resource.Name) { + effective := p.EffectiveConfig() + target, err := extractionTarget(p, "paths.source", effective.Paths.Source, p.SourceDir(), sourceSubdir(ext), strings.ToLower(resource.Name)+"."+ext+".json") + if err == nil { + desired[target] = struct{}{} + } skippedCount++ continue } diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go index 23c20b7..6b3ace3 100644 --- a/internal/pipeline/pipeline_test.go +++ b/internal/pipeline/pipeline_test.go @@ -3312,6 +3312,88 @@ func TestExtractOverwritesAndRemovesStaleFiles(t *testing.T) { } } +func TestExtractKeepsPaletteSkeletonsThroughStaleCleanup(t *testing.T) { + root := t.TempDir() + mustMkdir(t, filepath.Join(root, "src", "module")) + mustMkdir(t, filepath.Join(root, "src", "palettes")) + mustMkdir(t, filepath.Join(root, "assets")) + mustMkdir(t, filepath.Join(root, "build")) + + mustWriteFile(t, filepath.Join(root, "nwn-tool.json"), `{ + "module": { + "name": "Test Module", + "resref": "testmod" + }, + "paths": { + "source": "src", + "assets": "assets", + "build": "build" + } +} +`) + + mustWriteFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{ + "file_type": "IFO ", + "file_version": "V3.2", + "root": { + "struct_type": 0, + "fields": [ + { + "label": "Mod_Name", + "type": "CExoString", + "value": "Original Module" + } + ] + } +} +`) + skeletonPath := filepath.Join(root, "src", "palettes", "creaturepalcus.itp.json") + mustWriteFile(t, skeletonPath, `{ + "file_type": "ITP ", + "file_version": "V3.2", + "root": { + "struct_type": 0, + "fields": [ + { + "label": "MAIN", + "type": "List", + "value": [] + } + ] + } +} +`) + + p, err := project.Load(root) + if err != nil { + t.Fatalf("load project: %v", err) + } + if err := p.ValidateLayout(); err != nil { + t.Fatalf("validate layout: %v", err) + } + if err := p.Scan(); err != nil { + t.Fatalf("scan: %v", err) + } + + if _, err := BuildModule(p); err != nil { + t.Fatalf("build module: %v", err) + } + if err := p.Scan(); err != nil { + t.Fatalf("rescan before extract: %v", err) + } + + result, err := Extract(p) + if err != nil { + t.Fatalf("extract: %v", err) + } + if result.Removed != 0 { + t.Fatalf("expected no removed files, got %d", result.Removed) + } + if _, err := os.Stat(skeletonPath); err != nil { + t.Fatalf("expected palette skeleton to survive extract, stat err=%v", err) + } +} + func TestExtractMergesConfiguredGFFJSONFieldsAndLists(t *testing.T) { root := t.TempDir() mustMkdir(t, filepath.Join(root, "src", "module")) -- 2.54.0