Expose source version modes in load options
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
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.
|
||||
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
|
||||
|
||||
1. Include declaration paths/ancestors in final classification candidates,
|
||||
|
||||
175
facade_source_version_mode_test.go
Normal file
175
facade_source_version_mode_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,13 @@ const (
|
||||
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
|
||||
|
||||
const (
|
||||
|
||||
@@ -23,8 +23,12 @@ type Underlay struct{ Inputs []Input }
|
||||
// LayeredOptions selects lower-priority tiers and final application rules.
|
||||
// An active Overlay requires an explicit OverlayFilesystem; an inactive overlay
|
||||
// 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 {
|
||||
Definition Definition[T]
|
||||
SourceVersionMode SourceVersionMode
|
||||
Underlays []Underlay
|
||||
Overlay string
|
||||
OverlayFilesystem *FilesystemContext
|
||||
@@ -57,7 +61,9 @@ func toEngineInput(input Input) engine.Input {
|
||||
|
||||
func toEngineLayeredOptions[T any](options LayeredOptions[T]) 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),
|
||||
}
|
||||
if options.Underlays != nil {
|
||||
|
||||
41
lifecycle.go
41
lifecycle.go
@@ -17,15 +17,39 @@ type Definition[T any] struct {
|
||||
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.
|
||||
// Imports enables explicit path and glob imports; Overlay selects one fixed overlay;
|
||||
// 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 {
|
||||
Definition Definition[T]
|
||||
Imports bool
|
||||
Overlay string
|
||||
Filesystem *FilesystemContext
|
||||
Overrides []Override
|
||||
Definition Definition[T]
|
||||
Imports bool
|
||||
Overlay string
|
||||
Filesystem *FilesystemContext
|
||||
Overrides []Override
|
||||
SourceVersionMode SourceVersionMode
|
||||
}
|
||||
|
||||
// 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] {
|
||||
result := engine.Options[T]{
|
||||
Definition: toEngineDefinition(options.Definition),
|
||||
Imports: options.Imports,
|
||||
Overlay: options.Overlay,
|
||||
Definition: toEngineDefinition(options.Definition),
|
||||
Imports: options.Imports,
|
||||
Overlay: options.Overlay,
|
||||
SourceVersionMode: engine.SourceVersionModeFromFacade(uint8(options.SourceVersionMode)),
|
||||
}
|
||||
if options.Filesystem != nil {
|
||||
result.Filesystem = toEngineFilesystemContext(options.Filesystem)
|
||||
|
||||
Reference in New Issue
Block a user