Expose source version modes in load options

This commit is contained in:
2026-10-15 15:48:59 +00:00
parent 15f5b591b9
commit 1f95cf905a
5 changed files with 241 additions and 10 deletions

View File

@@ -1,6 +1,6 @@
# Implement shared source version checks # Implement shared source version checks
Status: Stages 1–7 complete; public load-wide mode integration has not started. Status: Stages 1–8 complete; evidence sensitivity classification has not started.
Audience: a Luna coding agent implementing one numbered stage per prompt, in Audience: a Luna coding agent implementing one numbered stage per prompt, in
order. The [feature roadmap](source-version-checks.md) owns target behavior and order. The [feature roadmap](source-version-checks.md) owns target behavior and
@@ -451,6 +451,24 @@ No environmental limitation affected validation; checks ran on Linux/amd64.
Validate: facade version/layered/source and boundary tests; everyday check. Validate: facade version/layered/source and boundary tests; everyday check.
Done: both supported public load APIs expose the implemented modes. Done: both supported public load APIs expose the implemented modes.
Completion evidence: added the root-owned `SourceVersionMode` enum and constants
to ordinary and layered options, with Go documentation for zero-value behavior,
source-local declaration paths, destination-prefixed main protection, and the
two enabled policies. The facade converts explicitly through the internal
engine boundary, which keeps its mode type private and preserves unknown values
for option validation before acquisition. Added external-package coverage for
enabled ordinary and layered loads using byte, file, and mapping sources; zero
mode compatibility; invalid modes and missing rules before reader access; and
reusing one Definition with different modes. Source evidence remains private.
These checks passed with Go `go1.26.8 linux/amd64`:
- `GOTOOLCHAIN=local GOPROXY=off go test . -run 'SourceVersionMode|Version|Layered|SourceInfo|Destination|PublicFacadeBoundary|DependencyImportBoundary' -count=1`
- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'SourceVersion|Version|Layered|Source|Destination|Import' -count=1`
- `GOTOOLCHAIN=local ./scripts/check.sh`
- `git diff --check`
No environmental limitation affected validation; checks ran on Linux/amd64.
## Stage 9 — Classify evidence at original paths and ancestors ## Stage 9 — Classify evidence at original paths and ancestors
1. Include declaration paths/ancestors in final classification candidates, 1. Include declaration paths/ancestors in final classification candidates,

View File

@@ -0,0 +1,175 @@
package configkit_test
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
type sourceVersionModeReader struct {
*strings.Reader
reads int
}
func (reader *sourceVersionModeReader) Read(data []byte) (int, error) {
reader.reads++
return reader.Reader.Read(data)
}
func TestFacadeSourceVersionModesAcrossOrdinaryAndLayeredLoads(t *testing.T) {
t.Parallel()
var checked []configkit.Version
definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
Path: configkit.Keys("meta", "version"),
Check: func(version configkit.Version, _ *configkit.Validation) error {
checked = append(checked, version)
return nil
},
}}
ordinary, err := configkit.Load(configkit.Bytes("ordinary", []byte("meta: {version: ordinary-v}\n")), configkit.Options[map[string]any]{
Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
})
if err != nil || ordinary == nil {
t.Fatalf("ordinary enabled load = %#v, %v", ordinary, err)
}
if got := ordinary.Config["meta"].(map[string]any)["version"]; got != "ordinary-v" {
t.Fatalf("main declaration was not retained: %#v", ordinary.Config)
}
directory := t.TempDir()
mainPath := filepath.Join(directory, "main.yml")
if err := os.WriteFile(mainPath, []byte("meta: {version: main-v}\n"), 0o600); err != nil {
t.Fatal(err)
}
layered, err := configkit.LoadLayered(configkit.Input{Source: configkit.File(mainPath)}, configkit.LayeredOptions[map[string]any]{
Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired,
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
Source: configkit.Mapping("underlay", map[string]any{
"meta": map[string]any{"version": "underlay-v"}, "fallback": "present",
}),
Destination: configkit.Keys("defaults"),
}}}},
})
if err != nil || layered == nil {
t.Fatalf("layered enabled load = %#v, %v", layered, err)
}
defaults, ok := layered.Config["defaults"].(map[string]any)
if !ok || defaults["fallback"] != "present" {
t.Fatalf("layered underlay payload = %#v", layered.Config["defaults"])
}
if meta, ok := defaults["meta"].(map[string]any); !ok || len(meta) != 0 {
t.Fatalf("accepted underlay declaration was not consumed: %#v", defaults["meta"])
}
if len(checked) != 3 {
t.Fatalf("checker calls across separate mode uses = %#v", checked)
}
for _, want := range []string{"ordinary-v", "main-v", "underlay-v"} {
found := false
for _, version := range checked {
found = found || version.Present && version.Text == want
}
if !found {
t.Errorf("checked versions %#v do not include %q", checked, want)
}
}
}
func TestFacadeSourceVersionModeZeroPreservesDefaultBehavior(t *testing.T) {
t.Parallel()
checks := 0
result, err := configkit.Load(configkit.Bytes("default", []byte("version: v1\n")), configkit.Options[map[string]any]{
Definition: configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
Path: configkit.Keys("version"),
Check: func(version configkit.Version, _ *configkit.Validation) error {
checks++
if version != (configkit.Version{Present: true, Text: "v1"}) {
t.Errorf("default version = %#v", version)
}
return nil
},
}},
})
if err != nil || result == nil || result.Config["version"] != "v1" || checks != 1 {
t.Fatalf("zero-mode load = %#v, %v; checks = %d", result, err, checks)
}
}
func TestFacadeSourceVersionModeValidationPrecedesSourceReads(t *testing.T) {
t.Parallel()
validDefinition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil },
}}
for _, test := range []struct {
name string
mode configkit.SourceVersionMode
def configkit.Definition[map[string]any]
layered bool
}{
{name: "unknown ordinary mode", mode: configkit.SourceVersionMode(255), def: validDefinition},
{name: "ordinary mode requires checker", mode: configkit.SourceVersionModeIfPresent},
{name: "unknown layered mode", mode: configkit.SourceVersionMode(255), def: validDefinition, layered: true},
{name: "layered mode requires checker", mode: configkit.SourceVersionModeRequired, layered: true},
} {
t.Run(test.name, func(t *testing.T) {
reader := &sourceVersionModeReader{Reader: strings.NewReader("version: v1\n")}
source := configkit.Reader("version.yml", reader)
var err error
if test.layered {
_, err = configkit.LoadLayered(configkit.Input{Source: source}, configkit.LayeredOptions[map[string]any]{
Definition: test.def, SourceVersionMode: test.mode,
})
} else {
_, err = configkit.Load(source, configkit.Options[map[string]any]{
Definition: test.def, SourceVersionMode: test.mode,
})
}
var loadError *configkit.LoadError
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeOptionsInvalid {
t.Fatalf("invalid mode error = %#v", err)
}
if reader.reads != 0 {
t.Fatalf("invalid options read source %d times", reader.reads)
}
})
}
}
func TestFacadeSourceVersionDefinitionCanBeReusedWithDifferentModes(t *testing.T) {
t.Parallel()
checks := 0
definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{
Path: configkit.Keys("version"),
Check: func(version configkit.Version, _ *configkit.Validation) error {
checks++
if version.Present {
t.Errorf("missing declaration reached checker as %#v", version)
}
return nil
},
}}
source := configkit.Bytes("reused", []byte("value: retained\n"))
ifPresent, err := configkit.Load(source, configkit.Options[map[string]any]{
Definition: definition, SourceVersionMode: configkit.SourceVersionModeIfPresent,
})
if err != nil || ifPresent == nil || checks != 1 {
t.Fatalf("if-present load = %#v, %v; checks = %d", ifPresent, err, checks)
}
required, err := configkit.Load(source, configkit.Options[map[string]any]{
Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired,
})
var loadError *configkit.LoadError
if required != nil || !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 || loadError.Diagnostics()[0].Code != configkit.CodeVersionError {
t.Fatalf("required load = %#v, %v", required, err)
}
if checks != 1 {
t.Fatalf("required absence reached checker: calls = %d", checks)
}
}

View File

@@ -10,6 +10,13 @@ const (
sourceVersionModeRequired sourceVersionModeRequired
) )
// SourceVersionModeFromFacade converts the root-owned public mode while
// keeping the engine's policy type private. Unknown values remain intact for
// pre-acquisition validation.
func SourceVersionModeFromFacade(value uint8) sourceVersionMode {
return sourceVersionMode(value)
}
type sourceVersionRole uint8 type sourceVersionRole uint8
const ( const (

View File

@@ -23,8 +23,12 @@ type Underlay struct{ Inputs []Input }
// LayeredOptions selects lower-priority tiers and final application rules. // LayeredOptions selects lower-priority tiers and final application rules.
// An active Overlay requires an explicit OverlayFilesystem; an inactive overlay // An active Overlay requires an explicit OverlayFilesystem; an inactive overlay
// ignores it. Overrides remain explicit CLI records, not general mappings. // ignores it. Overrides remain explicit CLI records, not general mappings.
// SourceVersionMode applies one declaration policy to main, underlay, import,
// and selected overlay documents. Enabled modes interpret the version path
// source-locally while protecting main's destination-prefixed path.
type LayeredOptions[T any] struct { type LayeredOptions[T any] struct {
Definition Definition[T] Definition Definition[T]
SourceVersionMode SourceVersionMode
Underlays []Underlay Underlays []Underlay
Overlay string Overlay string
OverlayFilesystem *FilesystemContext OverlayFilesystem *FilesystemContext
@@ -57,7 +61,9 @@ func toEngineInput(input Input) engine.Input {
func toEngineLayeredOptions[T any](options LayeredOptions[T]) engine.LayeredOptions[T] { func toEngineLayeredOptions[T any](options LayeredOptions[T]) engine.LayeredOptions[T] {
result := engine.LayeredOptions[T]{ result := engine.LayeredOptions[T]{
Definition: toEngineDefinition(options.Definition), Overlay: options.Overlay, Definition: toEngineDefinition(options.Definition),
SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)),
Overlay: options.Overlay,
OverlayFilesystem: toEngineFilesystemContext(options.OverlayFilesystem), OverlayFilesystem: toEngineFilesystemContext(options.OverlayFilesystem),
} }
if options.Underlays != nil { if options.Underlays != nil {

View File

@@ -17,15 +17,39 @@ type Definition[T any] struct {
Secret func(Path) bool Secret func(Path) bool
} }
// SourceVersionMode controls schema-version declarations across one complete
// load. The zero value preserves the existing effective-path version check.
type SourceVersionMode uint8
const (
// SourceVersionModeDefault preserves the existing main-version check and
// rejects version-path contributions from imported, underlay, and overlay
// payloads.
SourceVersionModeDefault SourceVersionMode = iota
// SourceVersionModeIfPresent checks each participating document that has a
// declaration. A missing main declaration still reaches the checker; an
// absent non-main declaration skips it. Accepted non-main declarations are
// consumed before composition.
SourceVersionModeIfPresent
// SourceVersionModeRequired requires a declaration in each participating
// document. NoSource retains the existing absent-main checker behavior.
// Accepted non-main declarations are consumed before composition.
SourceVersionModeRequired
)
// Options contains the application definition and per-load input selections. // Options contains the application definition and per-load input selections.
// Imports enables explicit path and glob imports; Overlay selects one fixed overlay; // Imports enables explicit path and glob imports; Overlay selects one fixed overlay;
// Overrides contains only values explicitly supplied by an application CLI. // Overrides contains only values explicitly supplied by an application CLI.
// Enabled source-version modes interpret Definition.Version.Path in each
// document's local coordinates. Main protection remains at that path prefixed
// by the main input's Destination.
type Options[T any] struct { type Options[T any] struct {
Definition Definition[T] Definition Definition[T]
Imports bool Imports bool
Overlay string Overlay string
Filesystem *FilesystemContext Filesystem *FilesystemContext
Overrides []Override Overrides []Override
SourceVersionMode SourceVersionMode
} }
// FilesystemContext supplies roots for imports and overlays. With FS nil, // FilesystemContext supplies roots for imports and overlays. With FS nil,
@@ -226,9 +250,10 @@ func (v *Validation) engineContext() *engine.Validation {
func toEngineOptions[T any](options Options[T]) engine.Options[T] { func toEngineOptions[T any](options Options[T]) engine.Options[T] {
result := engine.Options[T]{ result := engine.Options[T]{
Definition: toEngineDefinition(options.Definition), Definition: toEngineDefinition(options.Definition),
Imports: options.Imports, Imports: options.Imports,
Overlay: options.Overlay, Overlay: options.Overlay,
SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)),
} }
if options.Filesystem != nil { if options.Filesystem != nil {
result.Filesystem = toEngineFilesystemContext(options.Filesystem) result.Filesystem = toEngineFilesystemContext(options.Filesystem)