diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 1152557..73d627a 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Implement shared source version checks -Status: Stages 1–12 complete; adoption examples and documentation reconciliation remain. +Status: Stages 1–13 complete; documentation reconciliation remains. Audience: a Luna coding agent implementing one numbered stage per prompt, in order. The [feature roadmap](source-version-checks.md) owns target behavior and @@ -615,7 +615,7 @@ with Go `go1.26.8 linux/amd64`: No environmental limitation affected validation; checks ran on Linux/amd64. -## Stage 13 — Add maintained adoption examples +## Stage 13 — Add maintained adoption examples — Done 1. Add a small executable example showing if-present and required adoption with one Definition.Version policy, accepted distinct declarations, main/import/ @@ -630,6 +630,22 @@ No environmental limitation affected validation; checks ran on Linux/amd64. Validate: example/consumer tests, links, and everyday check. Done: tested examples demonstrate the coherent shared policy with reduced consumer plumbing. +Completion evidence: added an executable layered consumer example and embedded +main/underlay fixtures. One source-local `Definition.Version` policy accepts the +distinct declarations in if-present and required modes; the mounted main maps +its local `version` to `/service/version`, while ordinary application release +metadata remains at `service.metadata.release`. The example inspects only +sanitized presence/path/redaction fields, and its test verifies final payloads +and effective-path override protection without printing declaration contents or +promising callback order. These checks passed with Go `go1.26.8 linux/amd64`: + +- `GOTOOLCHAIN=local GOPROXY=off go test ./examples/consumers` +- `GOTOOLCHAIN=local GOPROXY=off go test . -run '^TestLocalDocumentationLinks$' -count=1` +- `GOTOOLCHAIN=local ./scripts/check.sh` +- `git diff --check` + +No environmental limitation affected validation; checks ran on Linux/amd64. + ## Stage 14 — Reconcile documentation and finish the audit 1. Update Go docs and docs/config.md as canonical owners for modes, participation, diff --git a/examples/consumers/layered.go b/examples/consumers/layered.go index 28e35dd..e26e7c0 100644 --- a/examples/consumers/layered.go +++ b/examples/consumers/layered.go @@ -9,7 +9,7 @@ import ( configkit "gitea.maximumdirect.net/eric/ConfigKit" ) -//go:embed testdata/native-source testdata/layered-bundle testdata/collection-bundle testdata/ordinary-imports +//go:embed testdata/native-source testdata/layered-bundle testdata/collection-bundle testdata/ordinary-imports testdata/source-version var bundledFiles embed.FS // BundledConfig is a small configuration loaded directly from an embedded file. diff --git a/examples/consumers/layered_test.go b/examples/consumers/layered_test.go index de6deba..9f3acfe 100644 --- a/examples/consumers/layered_test.go +++ b/examples/consumers/layered_test.go @@ -60,6 +60,58 @@ func Example_layeredCatalog() { // Output: production false 0 bundle example Welcome from the bundle. } +func Example_sourceVersionAdoption() { + result, err := consumers.LoadVersionedService(configkit.SourceVersionModeIfPresent) + if err != nil { + fmt.Println(err) + return + } + checks, redacted := 0, true + for _, source := range result.Sources() { + if check := source.VersionCheck; check != nil { + checks++ + redacted = redacted && check.Redacted && check.Version.Text == "" + } + } + required, requiredErr := consumers.LoadVersionedService(configkit.SourceVersionModeRequired) + fmt.Println(result.Config.Service.Version, result.Config.Service.Metadata.Release, + checks, redacted, required != nil && requiredErr == nil) + // Output: service-v2 application-2026.10 2 true true +} + +func TestVersionedServiceModesAndMountedProtection(t *testing.T) { + for _, mode := range []configkit.SourceVersionMode{ + configkit.SourceVersionModeIfPresent, + configkit.SourceVersionModeRequired, + } { + result, err := consumers.LoadVersionedService(mode) + if err != nil || result == nil || result.Config.Service.Version != "service-v2" || + result.Config.Service.Name != "example service" || result.Config.Service.Endpoint != "https://service.example.invalid" || + result.Config.Service.Metadata.Release != "application-2026.10" { + t.Fatalf("versioned service mode %d = %#v, %v", mode, result, err) + } + sources := result.Sources() + if len(sources) != 2 { + t.Fatalf("versioned service sources = %#v", sources) + } + for _, source := range sources { + check := source.VersionCheck + if check == nil || check.Path.String() != "/version" || !check.Version.Present || + !check.Redacted || check.Version.Text != "" { + t.Fatalf("safe source version evidence = %#v", sources) + } + } + } + _, err := consumers.LoadVersionedService(configkit.SourceVersionModeIfPresent, + configkit.Override{Path: configkit.Keys("service", "version"), Value: "cli-v3", Flag: "--service-version"}) + var loadError *configkit.LoadError + if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || + loadError.Diagnostics()[0].Code != configkit.CodeOverrideVersion || + loadError.Diagnostics()[0].Path.String() != "/service/version" { + t.Fatalf("mounted main version override error = %v", err) + } +} + func TestCatalogRetainsPresenceAndSourceContext(t *testing.T) { result, err := consumers.LoadCatalog(fixture("layered-main", "config.yml")) if err != nil { diff --git a/examples/consumers/source_version.go b/examples/consumers/source_version.go new file mode 100644 index 0000000..8058143 --- /dev/null +++ b/examples/consumers/source_version.go @@ -0,0 +1,64 @@ +package consumers + +import ( + "errors" + "io/fs" + + configkit "gitea.maximumdirect.net/eric/ConfigKit" +) + +// VersionedServiceConfig demonstrates a mounted main input whose source-local +// schema version is retained at the effective service path. +type VersionedServiceConfig struct { + Service struct { + Version string `yaml:"version"` + Name string `yaml:"name"` + Endpoint string `yaml:"endpoint"` + Metadata struct { + Release string `yaml:"release"` + } `yaml:"metadata"` + } `yaml:"service"` +} + +// LoadVersionedService checks the main and underlay declarations with one +// application policy. The local rule path is "version" for each source; the +// main input's destination places its protected value at /service/version. +// Optional CLI overrides are passed through to ConfigKit for normal protection. +func LoadVersionedService(mode configkit.SourceVersionMode, overrides ...configkit.Override) (*configkit.Result[VersionedServiceConfig], error) { + files, err := fs.Sub(bundledFiles, "testdata/source-version") + if err != nil { + return nil, err + } + main, err := fs.ReadFile(files, "main.yml") + if err != nil { + return nil, err + } + underlay, err := fs.ReadFile(files, "underlay.yml") + if err != nil { + return nil, err + } + definition := configkit.Definition[VersionedServiceConfig]{ + Version: &configkit.VersionRule{ + Path: configkit.Keys("version"), + Check: func(version configkit.Version, _ *configkit.Validation) error { + if !version.Present || version.Null || version.Text == "" { + return errors.New("a nonempty source version is required") + } + return nil + }, + }, + Secret: func(path configkit.Path) bool { + return path.String() == "/service/version" + }, + } + return configkit.LoadLayered(configkit.Input{ + Source: configkit.Bytes("service main", main), Destination: configkit.Keys("service"), + }, configkit.LayeredOptions[VersionedServiceConfig]{ + Definition: definition, + SourceVersionMode: mode, + Overrides: overrides, + Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{ + Source: configkit.Bytes("service defaults", underlay), Destination: configkit.Keys("service"), + }}}}, + }) +} diff --git a/examples/consumers/testdata/source-version/main.yml b/examples/consumers/testdata/source-version/main.yml new file mode 100644 index 0000000..cd36dc0 --- /dev/null +++ b/examples/consumers/testdata/source-version/main.yml @@ -0,0 +1,4 @@ +version: service-v2 +name: example service +metadata: + release: application-2026.10 diff --git a/examples/consumers/testdata/source-version/underlay.yml b/examples/consumers/testdata/source-version/underlay.yml new file mode 100644 index 0000000..bafc73b --- /dev/null +++ b/examples/consumers/testdata/source-version/underlay.yml @@ -0,0 +1,2 @@ +version: catalog-v1 +endpoint: https://service.example.invalid