From cc98be8f0a11788849bd294cd43ac64590fda03d Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 12 Sep 2026 13:17:22 +0000 Subject: [PATCH] Verify layered resolution provenance --- docs/roadmap/implementation.md | 16 +- provenance_test.go | 290 +++++++++++++++++++++++++++++++++ 2 files changed, 305 insertions(+), 1 deletion(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 35ed148..932464c 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -167,7 +167,7 @@ Stage 2 risks were identified. ## Stage 3 — Verify layered provenance and snapshot independence -Status: not started. Dependencies: Stage 2. +Status: complete (2026-09-12). Dependencies: Stage 2. Scope: public lifecycle/provenance, import/profile/override, and ownership tests; production fixes only for gaps exposed at those boundaries. @@ -202,6 +202,20 @@ is introduced. Any discovered defect is fixed before the stage is marked complet Validation: `go test . -run 'Resolution|Provenance|Import|Profile|Override|Metadata|Path'`, `go test -race ./...`, then the everyday checker. +Completion record (2026-09-12): public lifecycle tests now build real temporary +main/import/profile graphs and prove scalar, sequence, composite-container, +profile, and equal-text CLI provenance through `InputInfo`/`Source`, deriving source +IDs from returned locations. Coverage also exercises byte and reader roots with +filesystem imports, a standalone non-file root, retained defaults, unknown IDs, +typed/inlined/map/array/dynamic shape matching, signed mapping keys, and invalid +sequence shapes. Recorded normalization, deletion, addition, and move operations +leave entry queries fixed while final metadata reflects resolution. Returned records, +completed results, another load, and concurrent independent loads remain detached. +No production gap was exposed. `go test . -run +'Resolution|Provenance|Import|Profile|Override|Metadata|Path'`, `go test -race +./...`, and `./scripts/check.sh` passed with Go 1.26.7 on Linux. No deviations or +remaining Stage 3 risks were identified. + ## Stage 4 — Demonstrate source-aware normalization and safe inspection Status: not started. Dependencies: Stage 3. diff --git a/provenance_test.go b/provenance_test.go index 76f5127..41fd53e 100644 --- a/provenance_test.go +++ b/provenance_test.go @@ -3,6 +3,7 @@ package configkit import ( "errors" "reflect" + "strings" "sync" "testing" ) @@ -205,6 +206,295 @@ func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) { } } +func TestResolutionInputKeepsLayeredOriginsAndSources(t *testing.T) { + directory := t.TempDir() + mainPath := writeImportFile(t, directory, "main.yml", `configkit: + imports: [{path: first.yml}, {path: second.yml}] +main: main +equal: same +cli_equal: same +composite: {main: main} +`) + firstPath := writeImportFile(t, directory, "first.yml", `imported: imported +sequence: [first, second] +composite: {first: first} +`) + secondPath := writeImportFile(t, directory, "second.yml", "composite: {second: second}\n") + profilePath := writeImportFile(t, directory, "profiles/testing.yml", "equal: same\nprofile: profile\n") + compositeSources := map[string]bool{ + canonicalTestPath(t, mainPath): true, + canonicalTestPath(t, firstPath): true, + canonicalTestPath(t, secondPath): true, + } + type config struct { + Main string `yaml:"main"` + Imported string `yaml:"imported"` + Sequence []string `yaml:"sequence"` + Composite map[string]string `yaml:"composite"` + Equal string `yaml:"equal"` + Profile string `yaml:"profile"` + CLIEqual string `yaml:"cli_equal"` + } + var callbackErr error + result, err := Load[config](File(mainPath), Options[config]{ + Imports: true, Profile: "testing", Overrides: []Override{{Path: Keys("cli_equal"), Value: "same", Flag: "--cli-equal"}}, + Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error { + check := func(ok bool, message string) { + if !ok && callbackErr == nil { + callbackErr = errors.New(message) + } + } + fileOwner := func(path Path, want string) FieldInfo { + info, found := resolution.InputInfo(path) + check(found && len(info.Origin.Locations) == 1, "scalar has no exact origin") + if !found || len(info.Origin.Locations) != 1 { + return FieldInfo{} + } + source, exists := resolution.Source(info.Origin.Locations[0].SourceID) + check(exists && source.Path == want, "scalar source does not match its declaration") + return info + } + fileOwner(Keys("main"), canonicalTestPath(t, mainPath)) + fileOwner(Keys("imported"), canonicalTestPath(t, firstPath)) + fileOwner(Keys("sequence").Index(1), canonicalTestPath(t, firstPath)) + profile := fileOwner(Keys("equal"), canonicalTestPath(t, profilePath)) + fileOwner(Keys("profile"), canonicalTestPath(t, profilePath)) + check(profile.Origin.Layer == LayerProfile, "profile replacement retained base ownership") + cli, found := resolution.InputInfo(Keys("cli_equal")) + check(found && cli.Origin.Layer == LayerCLI && len(cli.Origin.Locations) == 1, "CLI replacement lost ownership") + if found && len(cli.Origin.Locations) == 1 { + source, exists := resolution.Source(cli.Origin.Locations[0].SourceID) + check(exists && source.Layer == LayerCLI && source.Path == "", "CLI source has a file path or is missing") + } + + composite, found := resolution.InputInfo(Keys("composite")) + check(found && len(composite.Origin.Locations) == 3, "composite mapping did not retain every contributor") + for _, location := range composite.Origin.Locations { + source, exists := resolution.Source(location.SourceID) + check(exists && compositeSources[source.Path], "composite location has no expected source record") + delete(compositeSources, source.Path) + } + check(len(compositeSources) == 0, "composite sources are incomplete") + + imported, _ := resolution.InputInfo(Keys("imported")) + importedSourceID := imported.Origin.Locations[0].SourceID + imported.Origin.Locations[0].SourceID = "mutated" + importSource, _ := resolution.Source(importedSourceID) + check(importSource.ImportedFrom != nil, "import source is missing its import location") + if importSource.ImportedFrom != nil { + importSource.ImportedFrom.SourceID = "mutated" + } + again, _ := resolution.InputInfo(Keys("imported")) + againSource, _ := resolution.Source(importedSourceID) + check(again.Origin.Locations[0].SourceID == importedSourceID, "input record is aliased") + check(againSource.ImportedFrom != nil && againSource.ImportedFrom.SourceID == "source-1", "source record is aliased") + return callbackErr + }}, + }) + if callbackErr != nil { + t.Fatal(callbackErr) + } + if err != nil { + t.Fatal(err) + } + if result.Config.Main != "main" || result.Config.Imported != "imported" || !reflect.DeepEqual(result.Config.Sequence, []string{"first", "second"}) || + result.Config.Equal != "same" || result.Config.Profile != "profile" || result.Config.CLIEqual != "same" { + t.Fatalf("configuration = %#v", result.Config) + } + + resultField, _ := result.Metadata().Lookup(Keys("imported")) + resultSource := result.Sources()[1] + if resultField.Origin.Locations[0].SourceID != "source-2" || resultSource.ImportedFrom == nil || resultSource.ImportedFrom.SourceID != "source-1" { + t.Fatal("callback record mutation changed the completed result") + } +} + +func TestResolutionInputUsesEntryShapesAcrossTypedAndDynamicValues(t *testing.T) { + t.Parallel() + + type Embedded struct { + Name string `yaml:"name"` + } + type config struct { + Embedded `yaml:",inline"` + Mappings map[string]string `yaml:"mappings"` + Array [2]string `yaml:"array"` + Dynamic any `yaml:"dynamic"` + } + var callbackErr error + _, err := Load[config](Bytes("shapes.yml", []byte("name: inlined\nmappings: {'-0': key}\narray: [zero, one]\ndynamic: {nested: [dynamic]}\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error { + check := func(path Path, want bool) { + _, found := resolution.InputInfo(path) + if found != want && callbackErr == nil { + callbackErr = errors.New("entry shape lookup disagrees with its document container") + } + } + parsedMapping, _ := ParsePath("/mappings/-0") + check(Keys("name"), true) + check(parsedMapping, true) + check(Keys("mappings").Index(0), false) + check(Keys("array").Index(1), true) + check(Keys("array", "1"), false) + check(Keys("array").Index(2), false) + check(Keys("dynamic", "nested").Index(0), true) + check(Keys("dynamic", "nested", "0"), false) + negative, _ := ParsePath("/array/-1") + check(negative, false) + return callbackErr + }}}) + if callbackErr != nil { + t.Fatal(callbackErr) + } + if err != nil { + t.Fatal(err) + } +} + +func TestResolutionInputDistinguishesNonFileRootsAndImportedFiles(t *testing.T) { + directory := t.TempDir() + importPath := writeImportFile(t, directory, "fragment.yml", "imported: file\n") + type config struct { + Root string `yaml:"root"` + Imported string `yaml:"imported"` + Default string `yaml:"default"` + } + input := "configkit: {imports: [{path: fragment.yml}]}\nroot: memory\n" + context := &FilesystemContext{BaseDir: directory, Root: directory} + for _, test := range []struct { + name string + source func() Source + }{ + {name: "bytes", source: func() Source { return Bytes("memory://config.yml", []byte(input)) }}, + {name: "reader", source: func() Source { return Reader("memory://config.yml", strings.NewReader(input)) }}, + } { + t.Run(test.name, func(t *testing.T) { + var callbackErr error + _, err := Load[config](test.source(), Options[config]{Imports: true, Filesystem: context, Definition: Definition[config]{ + Defaults: func() config { return config{Default: "retained"} }, + Resolve: func(_ *config, resolution *Resolution) error { + root, _ := resolution.InputInfo(Keys("root")) + imported, _ := resolution.InputInfo(Keys("imported")) + defaulted, _ := resolution.InputInfo(Keys("default")) + rootSource, rootFound := resolution.Source(root.Origin.Locations[0].SourceID) + importSource, importFound := resolution.Source(imported.Origin.Locations[0].SourceID) + if !rootFound || rootSource.Path != "" || !importFound || importSource.Path != canonicalTestPath(t, importPath) || defaulted.Explicit { + callbackErr = errors.New("non-file root or imported file provenance is wrong") + } + if _, found := resolution.Source("unknown"); found { + callbackErr = errors.New("unknown source was found") + } + return callbackErr + }, + }}) + if callbackErr != nil { + t.Fatal(callbackErr) + } + if err != nil { + t.Fatal(err) + } + }) + } + + var callbackErr error + _, err := Load[config](Bytes("memory://standalone.yml", []byte("root: memory\n")), Options[config]{Definition: Definition[config]{ + Defaults: func() config { return config{Default: "retained"} }, + Resolve: func(_ *config, resolution *Resolution) error { + root, rootFound := resolution.InputInfo(Keys("root")) + defaulted, defaultFound := resolution.InputInfo(Keys("default")) + source, sourceFound := resolution.Source(root.Origin.Locations[0].SourceID) + if !rootFound || !defaultFound || !sourceFound || source.Path != "" || defaulted.Explicit { + callbackErr = errors.New("standalone non-file provenance is wrong") + } + return callbackErr + }, + }}) + if callbackErr != nil { + t.Fatal(callbackErr) + } + if err != nil { + t.Fatal(err) + } +} + +func TestResolutionInputRemainsFixedAcrossRecordedMutations(t *testing.T) { + t.Parallel() + + type config struct { + Existing string `yaml:"existing"` + Values map[string]string `yaml:"values"` + } + var callbackErr error + result, err := Load[config](Bytes("mutations.yml", []byte("existing: original\nvalues: {original: move}\n")), Options[config]{Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error { + original, originalFound := resolution.InputInfo(Keys("values", "original")) + _, destinationFound := resolution.InputInfo(Keys("values", "destination")) + existing, existingFound := resolution.InputInfo(Keys("existing")) + Normalize(resolution, Keys("existing"), &value.Existing, "normalized", "normalized existing value") + value.Values["destination"] = value.Values["original"] + delete(value.Values, "original") + resolution.Record(Keys("values"), ChangeNormalization, "moved configured value") + againOriginal, againOriginalFound := resolution.InputInfo(Keys("values", "original")) + againDestination, againDestinationFound := resolution.InputInfo(Keys("values", "destination")) + againExisting, againExistingFound := resolution.InputInfo(Keys("existing")) + if !originalFound || destinationFound || !existingFound || !againOriginalFound || againDestinationFound || !againExistingFound || + !reflect.DeepEqual(original, againOriginal) || !reflect.DeepEqual(existing, againExisting) || !reflect.DeepEqual(againDestination, FieldInfo{}) { + callbackErr = errors.New("entry view changed during resolution") + } + return callbackErr + }}}) + if callbackErr != nil { + t.Fatal(callbackErr) + } + if err != nil { + t.Fatal(err) + } + if result.Config.Existing != "normalized" || result.Config.Values["destination"] != "move" { + t.Fatalf("configuration = %#v", result.Config) + } + if _, found := result.Metadata().Lookup(Keys("values", "original")); found { + t.Fatal("final metadata retained a removed path") + } + destination, found := result.Metadata().Lookup(Keys("values", "destination")) + existing, existingFound := result.Metadata().Lookup(Keys("existing")) + if !found || destination.Origin.Layer != LayerResolution || !existingFound || existing.Origin.Layer != LayerResolution { + t.Fatalf("final metadata = destination %#v existing %#v", destination, existing) + } +} + +func TestResolutionInputQueriesRemainIndependentAcrossLoads(t *testing.T) { + t.Parallel() + + type config struct { + Value string `yaml:"value"` + } + const loads = 16 + failures := make(chan error, loads) + var group sync.WaitGroup + for index := 0; index < loads; index++ { + group.Add(1) + go func() { + defer group.Done() + _, err := Load[config](Bytes("parallel.yml", []byte("value: stable\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error { + info, found := resolution.InputInfo(Keys("value")) + source, sourceFound := resolution.Source(info.Origin.Locations[0].SourceID) + if !found || !sourceFound || source.ID != "source-1" { + return errors.New("independent entry lookup failed") + } + info.Origin.Locations[0].SourceID = "mutated" + source.Label = "mutated" + return nil + }}}) + if err != nil { + failures <- err + } + }() + } + group.Wait() + close(failures) + for err := range failures { + t.Fatal(err) + } +} + func assertHistoryLayersAndPaths(t *testing.T, history []Change, layers []Layer, paths []string) { t.Helper() if len(history) != len(layers) || len(history) != len(paths) {