Expose source version modes in load options
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
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
|
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 (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
41
lifecycle.go
41
lifecycle.go
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user