diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 4b0b416..43f1a34 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -342,7 +342,7 @@ budgets and introduces no unrelated caps. ## Stage 8 — Preserve provenance, history, and source resource contexts -Status: planned. Scope: engine inspection and retained evidence. +Status: complete. Scope: engine inspection and retained evidence. 1. Audit `compositionOrigins`, `completeCompositionOrigins`, application replay, metadata construction/finalization, and source-record cloning against placed @@ -359,9 +359,18 @@ Status: planned. Scope: engine inspection and retained evidence. inspect each source record, and verify independent destination paths. Repeat inspection after comparison and independent loads to detect retained mutation. -Validate: destination/metadata/provenance/history/resource tests, -`go test -race ./internal/engine`, and everyday check. Done: inspection explains -effective placement and physical origin without consumer reconstruction. +Validated with `go test ./internal/engine -run 'Destination|Metadata|Provenance|History|Resource|SourceContext'`, +`go test -race ./internal/engine`, and `./scripts/check.sh` (go1.26.8 +linux/amd64); all passed. `TestLayeredDestinationsRetainOriginsHistoryAndDetachedRecords` +checks synthetic-container fallback locations, authored leaf coordinates, +composite origins, removed-descendant history, and the same host file retained +at distinct destinations in separate tiers before and after comparison and an +independent load. `TestLayeredImportedInheritedValuesRetainResourceContexts` +uses real temporary files to verify resolution-time and completed-result +relative-resource lookup through an imported surviving parent and its inherited +child. Existing metadata and resource-context tests continue to cover detached +records and loading-handle closure. Done: inspection explains effective +placement and physical origin without consumer reconstruction. ## Stage 9 — Verify final inheritance, sensitivity, and lifecycle integration diff --git a/internal/engine/destination_provenance_test.go b/internal/engine/destination_provenance_test.go new file mode 100644 index 0000000..06e38f4 --- /dev/null +++ b/internal/engine/destination_provenance_test.go @@ -0,0 +1,181 @@ +package engine + +import ( + "errors" + "io/fs" + "path" + "reflect" + "testing" +) + +func TestLayeredDestinationsRetainOriginsHistoryAndDetachedRecords(t *testing.T) { + t.Parallel() + + directory := t.TempDir() + sharedPath := writeImportFile(t, directory, "shared.yml", "section:\n shared: lower\n branch:\n child: old\n kept: retained\n") + shared := File(sharedPath) + load := func() (*Loaded[map[string]any], error) { + return LoadLayered( + Input{Source: NoSource()}, testSecretBinding(), + LayeredOptions[map[string]any]{Underlays: []Underlay{ + {Inputs: []Input{ + {Source: shared, Destination: Keys("catalog")}, + {Source: Bytes("peer", []byte("section:\n peer: present\n")), Destination: Keys("catalog")}, + }}, + {Inputs: []Input{{Source: shared, Destination: Keys("backup")}}}, + {Inputs: []Input{{Source: Bytes("replacement", []byte("section:\n branch: replaced\n")), Destination: Keys("catalog")}}}, + }}, + ) + } + loaded, err := load() + if err != nil { + t.Fatalf("LoadLayered: %v, diagnostics %#v", err, err.(*LoadError).Diagnostics()) + } + result := testLoaded(loaded) + metadata := result.Metadata() + + container, ok := metadata.Lookup(Keys("catalog")) + if !ok || !container.Explicit || len(container.Origin.Locations) < 3 { + t.Fatalf("mounted container origin = %#v, %v", container, ok) + } + containerSources := make(map[string]bool) + for _, location := range container.Origin.Locations { + containerSources[location.SourceID] = true + if location.Line < 1 { + t.Fatalf("introduced container lacks its source-root fallback: %#v", location) + } + } + for _, sourceID := range []string{"source-1", "source-2", "source-4"} { + if !containerSources[sourceID] { + t.Fatalf("container contributors = %#v, missing %s", container.Origin.Locations, sourceID) + } + } + + sharedField, ok := metadata.Lookup(Keys("catalog", "section", "shared")) + if !ok || len(sharedField.Origin.Locations) != 1 || sharedField.Origin.Locations[0].SourceID != "source-1" || sharedField.Origin.Locations[0].Line != 2 { + t.Fatalf("authored leaf origin = %#v, %v", sharedField, ok) + } + peerField, ok := metadata.Lookup(Keys("catalog", "section", "peer")) + if !ok || len(peerField.Origin.Locations) != 1 || peerField.Origin.Locations[0].SourceID != "source-2" { + t.Fatalf("peer leaf origin = %#v, %v", peerField, ok) + } + if _, ok := metadata.Lookup(Keys("catalog", "section", "branch", "child")); ok { + t.Fatal("replaced descendant remains effective") + } + removed := metadata.History(Keys("catalog", "section", "branch", "child")) + if len(removed) < 2 || removed[0].Origin.Locations[0].SourceID != "source-1" { + t.Fatalf("removed descendant history = %#v", removed) + } + + backup, ok := metadata.Lookup(Keys("backup", "section", "shared")) + if !ok || len(backup.Origin.Locations) != 1 || backup.Origin.Locations[0].SourceID != "source-3" { + t.Fatalf("second-tier leaf origin = %#v, %v", backup, ok) + } + sources := result.Sources() + if len(sources) != 4 || sources[0].Path != sources[2].Path || sources[0].Destination.String() != "/catalog" || sources[2].Destination.String() != "/backup" { + t.Fatalf("same source in separate tiers = %#v", sources) + } + if sources[0].Layer != LayerUnderlay || sources[0].UnderlayIndex != 1 || sources[2].Layer != LayerUnderlay || sources[2].UnderlayIndex != 2 { + t.Fatalf("tier source records = %#v", sources) + } + + other, err := load() + if err != nil { + t.Fatalf("independent load: %v", err) + } + differences, err := Diff(loaded.Inspection, other.Inspection) + if err != nil || len(differences) != 0 { + t.Fatalf("equivalent inspection diff = %#v, %v", differences, err) + } + + sources[0].Destination.segments[0].key = "mutated" + sharedField.Origin.Locations[0].SourceID = "mutated" + removed[0].Path.segments[0].key = "mutated" + if got := result.Sources()[0].Destination.String(); got != "/catalog" { + t.Fatalf("source record copy changed after inspection: %q", got) + } + fresh, ok := result.Metadata().Lookup(Keys("catalog", "section", "shared")) + if !ok || fresh.Origin.Locations[0].SourceID != "source-1" { + t.Fatalf("effective origin changed after mutating returned records: %#v, %v", fresh, ok) + } + if got := result.Metadata().History(Keys("catalog", "section", "branch", "child")); len(got) < 2 || got[0].Path.String() != "/catalog/section/branch/child" { + t.Fatalf("historical path changed after mutating returned records: %#v", got) + } + if got := other.Inspection.sources[0].Destination.String(); got != "/catalog" { + t.Fatalf("independent load destination changed: %q", got) + } +} + +func TestLayeredImportedInheritedValuesRetainResourceContexts(t *testing.T) { + t.Parallel() + + type entry struct { + Extends string `yaml:"extends"` + Resource string `yaml:"resource"` + Count int `yaml:"count"` + } + type config struct { + Catalog struct { + Entries map[string]entry `yaml:"entries"` + } `yaml:"catalog"` + } + directory := t.TempDir() + mainPath := writeImportFile(t, directory, "config/main.yml", "child:\n extends: parent\n") + inputPath := writeImportFile(t, directory, "shared/input.yml", "configkit: {imports: [{path: parent.yml}]}\n") + writeImportFile(t, directory, "shared/parent.yml", "parent:\n resource: icon.txt\n count: 1\n") + writeImportFile(t, directory, "shared/icon.txt", "parent-resource") + var resolutionContext ResourceContext + loaded, err := LoadLayered( + Input{Source: File(mainPath), Destination: Keys("catalog", "entries")}, + testSecretBinding(), + LayeredOptions[config]{ + Underlays: []Underlay{{Inputs: []Input{{ + Source: File(inputPath), Imports: true, Destination: Keys("catalog", "entries"), + }}}}, + Definition: Definition[config]{Resolve: func(configuration *config, resolution *Resolution) error { + info, ok := resolution.InputInfo(Keys("catalog", "entries", "child", "resource")) + if !ok || len(info.Origin.Locations) != 1 || len(info.Origin.Inheritance) != 1 { + return errors.New("inherited resource lacks its source origin") + } + var found bool + resolutionContext, found = resolution.state.input.sourceContext(info.Origin.Locations[0].SourceID) + if !found || resolutionContext.BaseDir != "." || resolutionContext.FS == nil { + return errors.New("resolver could not find the declaring resource context") + } + data, err := fs.ReadFile(resolutionContext.FS, path.Join(resolutionContext.BaseDir, configuration.Catalog.Entries["child"].Resource)) + if err != nil || string(data) != "parent-resource" { + return errors.New("resolver could not read the inherited relative resource") + } + return nil + }}, + }, + ) + if err != nil { + t.Fatalf("LoadLayered: %v, diagnostics %#v", err, err.(*LoadError).Diagnostics()) + } + result := testLoaded(loaded) + child := result.Config.Catalog.Entries["child"] + if child.Resource != "icon.txt" || child.Count != 1 || child.Extends != "" { + t.Fatalf("inherited child = %#v", child) + } + field, ok := result.Metadata().Lookup(Keys("catalog", "entries", "child", "resource")) + if !ok || len(field.Origin.Locations) != 1 || len(field.Origin.Inheritance) != 1 { + t.Fatalf("inherited resource origin = %#v, %v", field, ok) + } + context, ok := result.Inspection.SourceContext(field.Origin.Locations[0].SourceID) + if !ok || context.BaseDir != "." || context.FS == nil || resolutionContext.BaseDir != context.BaseDir { + t.Fatalf("completed resource contexts = %#v, %#v, %v", resolutionContext, context, ok) + } + data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, child.Resource)) + if err != nil || string(data) != "parent-resource" { + t.Fatalf("completed source-relative resource = %q, %v", data, err) + } + if !reflect.DeepEqual(result.Config.Catalog.Entries["parent"], entry{Resource: "icon.txt", Count: 1}) { + t.Fatalf("surviving lower-tier parent = %#v", result.Config.Catalog.Entries["parent"]) + } + for _, source := range result.Sources() { + if source.Layer == LayerUnderlay && source.Destination.String() != "/catalog/entries" { + t.Errorf("underlay destination = %#v", source) + } + } +}