Check source versions in selected overlays

This commit is contained in:
2026-10-15 15:44:04 +00:00
parent 184f29c7af
commit 15f5b591b9
5 changed files with 240 additions and 8 deletions

View File

@@ -1,6 +1,6 @@
# Implement shared source version checks
Status: Stages 1–6 complete; selected overlay integration has not started.
Status: Stages 1–7 complete; public load-wide mode integration 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
@@ -416,6 +416,27 @@ Validate: overlay/override/inheritance/version tests and everyday check. Done:
all document roles are integrated privately; synthetic operations are exempt
from header checks, not from existing protection.
Completion evidence: threaded the shared version state through ordinary and
layered selected-overlay loading. After parse and overlay-directive validation,
the selected overlay is checked as a non-main source; accepted headers are
removed from its owned payload before effective-path protection and application.
The original parsed node still supplies raw byte/node charges, and source
identity, filesystem namespace, closure, overlay-control checks, and override
protection remain on their existing paths. Overlay warnings are accumulated with
the other per-load diagnostics. Added coverage for accepted, required-missing,
and rejected overlay declarations; structural validation before callbacks;
warning retention; CLI overrides without document-header checks; mounted-main
protection after header consumption; and sequence deletion retaining original
evidence coordinates while later payload indices shift. Existing default overlay
version/directive/accounting coverage remains passing. These checks passed with
Go `go1.26.8 linux/amd64`:
- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'SourceVersion|Version|Import|Underlay|Overlay|Override|Inheritance|Destination' -count=1`
- `GOTOOLCHAIN=local ./scripts/check.sh`
- `git diff --check`
No environmental limitation affected validation; checks ran on Linux/amd64.
## Stage 8 — Expose the public load-wide mode
1. Add root mode/constants/option fields and both conversions from decision 1.

View File

@@ -142,9 +142,9 @@ func applyInputLayers[T any](source Source, options Options[T], input assembledI
}
assembly := newAssemblyAccumulator(composition)
if activeOverlay(options.Overlay) {
input.sources, diagnostics = loadSelectedOverlayInto(
input.sources, diagnostics = loadSelectedOverlayIntoWithVersion(
source, options.Filesystem, input.parsed, assembly, input.sources,
options.Overlay, options.Definition.Version, defaultImportLimits(),
options.Overlay, options.Definition.Version, defaultImportLimits(), input.versionState,
)
if len(diagnostics) != 0 {
return input, diagnostics

View File

@@ -126,7 +126,7 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a
selection := Source{kind: sourceBytes}
selected := parsedSource{document: &document.Node{Kind: document.Mapping}, source: &SourceInfo{}}
var diagnostics []Diagnostic
input.sources, diagnostics = loadSelectedOverlayInto(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits())
input.sources, diagnostics = loadSelectedOverlayIntoWithVersion(selection, request.overlayFilesystem, selected, assembly, input.sources, request.overlay, request.protectionVersion(), defaultImportLimits(), input.versionState)
if len(diagnostics) != 0 {
return input, diagnostics
}

View File

@@ -38,6 +38,10 @@ func loadSelectedOverlayDocument(source Source, context *FilesystemContext, main
}
func loadSelectedOverlayInto(source Source, context *FilesystemContext, main parsedSource, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) ([]SourceInfo, []Diagnostic) {
return loadSelectedOverlayIntoWithVersion(source, context, main, base, sources, name, rule, limits, nil)
}
func loadSelectedOverlayIntoWithVersion(source Source, context *FilesystemContext, main parsedSource, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits, versionState *sourceVersionState) ([]SourceInfo, []Diagnostic) {
if nativeContext(source, context) {
access, basePath, mainPath, diagnostic := nativeFilesystemAccess(source, context)
if diagnostic != nil {
@@ -46,7 +50,7 @@ func loadSelectedOverlayInto(source Source, context *FilesystemContext, main par
if diagnostic := validateNativeDirectories(access, basePath, mainPath); diagnostic != nil {
return cloneSourceInfos(sources), []Diagnostic{*diagnostic}
}
return loadSelectedOverlayWithAccess(access, base, sources, name, rule, limits)
return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState)
}
rootPath, _, _, diagnostic := importFilesystem(source, context, main)
if diagnostic != nil {
@@ -58,10 +62,14 @@ func loadSelectedOverlayInto(source Source, context *FilesystemContext, main par
value.Primary = nil
return cloneSourceInfos(sources), []Diagnostic{value}
}
return loadSelectedOverlayWithAccess(access, base, sources, name, rule, limits)
return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState)
}
func loadSelectedOverlayWithAccess(access *filesystemAccess, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) ([]SourceInfo, []Diagnostic) {
return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, nil)
}
func loadSelectedOverlayWithAccessAndVersion(access *filesystemAccess, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits, versionState *sourceVersionState) ([]SourceInfo, []Diagnostic) {
closed := false
defer func() {
if !closed {
@@ -126,7 +134,15 @@ func loadSelectedOverlayWithAccess(access *filesystemAccess, base *assemblyAccum
if diagnostics := overlayDirectiveDiagnostics(overlayRoot, Path{}); len(diagnostics) != 0 {
return sources, diagnostics
}
if versionNode := fragmentVersionClaim(overlayRoot, rule); versionNode != nil {
overlayPayload := overlayRoot
if versionState != nil && versionState.mode != sourceVersionModeDefault {
processed, failures := versionState.process(overlayRoot, sourceVersionContribution, Path{}, sourceID)
if len(failures) != 0 {
return sources, failures
}
overlayPayload = processed.payload
}
if versionNode := fragmentVersionClaim(overlayPayload, rule); versionNode != nil {
location := publicLocation(versionNode.Location)
return sources, []Diagnostic{{
Code: CodeOverlayVersion, Severity: SeverityError, Path: rule.Path.clone(),
@@ -139,7 +155,7 @@ func loadSelectedOverlayWithAccess(access *filesystemAccess, base *assemblyAccum
return sources, []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed after reading the selected overlay", nil)}
}
if diagnostics := base.applyOverlay(overlayRoot, nil, nil, LayerOverlay); len(diagnostics) != 0 {
if diagnostics := base.applyOverlay(overlayPayload, nil, nil, LayerOverlay); len(diagnostics) != 0 {
return sources, diagnostics
}
base.current.inputBytes += int64(len(data))

View File

@@ -974,6 +974,201 @@ func TestSourceVersionUnderlaysProtectRemainingMountedPayload(t *testing.T) {
}
}
func TestSourceVersionSelectedOverlayConsumesHeaderAndRetainsWarning(t *testing.T) {
t.Parallel()
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\nsettings: {kept: base}\n")
overlayPath := writeImportFile(t, directory, "overlays/testing.yml", "schema: {version: overlay-v}\nsettings: {changed: overlay}\n")
var checked []string
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
Overlay: "testing", SourceVersionMode: sourceVersionModeRequired,
Definition: Definition[map[string]any]{Version: &VersionRule{
Path: Keys("schema", "version"),
Check: func(version Version, validation *Validation) error {
checked = append(checked, version.Text)
if version.Text == "overlay-v" {
validation.Warning(Path{}, "overlay accepted with advisory")
}
return nil
},
}},
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(checked) != 2 || checked[0] != "main-v" || checked[1] != "overlay-v" {
t.Fatalf("checked source declarations = %#v", checked)
}
if loaded.Config["schema"].(map[string]any)["version"] != "main-v" || loaded.Config["settings"].(map[string]any)["changed"] != "overlay" {
t.Fatalf("overlay payload = %#v", loaded.Config)
}
diagnostics := loaded.Inspection.Diagnostics()
if len(diagnostics) != 1 || diagnostics[0].Code != CodeVersionWarning {
t.Fatalf("overlay warning was not retained: %#v", diagnostics)
}
sources := loaded.Inspection.Sources()
if len(sources) != 2 || sources[1].Path != filepath.Clean(overlayPath) || sources[1].Layer != LayerOverlay {
t.Fatalf("overlay source records = %#v", sources)
}
}
func TestSourceVersionSelectedOverlayRequiresAndChecksItsDeclaration(t *testing.T) {
t.Parallel()
t.Run("required absence", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\n")
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: overlay}\n")
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
Overlay: "testing", SourceVersionMode: sourceVersionModeRequired,
Definition: Definition[map[string]any]{Version: &VersionRule{
Path: Keys("schema", "version"), Check: func(Version, *Validation) error { return nil },
}},
})
if err == nil || loaded != nil {
t.Fatalf("Load = %#v, %v, want required overlay declaration error", loaded, err)
}
loadErr, ok := err.(*LoadError)
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeVersionError {
t.Fatalf("missing overlay diagnostics = %#v", err)
}
})
t.Run("checker rejection", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\n")
writeImportFile(t, directory, "overlays/testing.yml", "schema: {version: rejected-overlay}\nsettings: {changed: hidden}\n")
var checked []string
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
Overlay: "testing", SourceVersionMode: sourceVersionModeIfPresent,
Definition: Definition[map[string]any]{Version: &VersionRule{
Path: Keys("schema", "version"),
Check: func(version Version, _ *Validation) error {
checked = append(checked, version.Text)
if version.Text == "rejected-overlay" {
return errors.New("overlay rejected")
}
return nil
},
}},
})
if err == nil || loaded != nil {
t.Fatalf("Load = %#v, %v, want overlay rejection", loaded, err)
}
loadErr, ok := err.(*LoadError)
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeVersionHookFailed {
t.Fatalf("overlay rejection diagnostics = %#v", err)
}
if len(checked) != 2 || checked[0] != "main-v" || checked[1] != "rejected-overlay" {
t.Fatalf("checked source declarations = %#v", checked)
}
})
t.Run("structural validation precedes declaration processing", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: main-v}\n")
writeImportFile(t, directory, "overlays/testing.yml", "configkit: {imports: true}\nschema: {version: overlay-v}\n")
checks := 0
loaded, err := Load(File(mainPath), testSecretBinding(), Options[map[string]any]{
Overlay: "testing", SourceVersionMode: sourceVersionModeRequired,
Definition: Definition[map[string]any]{Version: &VersionRule{
Path: Keys("schema", "version"), Check: func(Version, *Validation) error { checks++; return nil },
}},
})
if err == nil || loaded != nil {
t.Fatalf("Load = %#v, %v, want structural overlay rejection", loaded, err)
}
loadErr, ok := err.(*LoadError)
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeOverlayDirective {
t.Fatalf("structural overlay diagnostics = %#v", err)
}
if checks != 1 {
t.Fatalf("version checker calls = %d, want main only", checks)
}
})
}
func TestSourceVersionOverlayLeavesCLIOverridesHeaderFree(t *testing.T) {
t.Parallel()
checks := 0
loaded, err := Load(Bytes("main", []byte("schema: {version: main-v}\nsettings: {base: value}\n")), testSecretBinding(), Options[map[string]any]{
SourceVersionMode: sourceVersionModeRequired,
Definition: Definition[map[string]any]{Version: &VersionRule{
Path: Keys("schema", "version"),
Check: func(Version, *Validation) error {
checks++
return nil
},
}},
Overrides: []Override{{Path: Keys("settings", "cli"), Value: "supplied", Flag: "--settings.cli"}},
})
if err != nil {
t.Fatalf("Load with CLI override: %v", err)
}
if checks != 1 || loaded.Config["settings"].(map[string]any)["cli"] != "supplied" {
t.Fatalf("checker count/configuration = %d, %#v", checks, loaded.Config)
}
}
func TestSourceVersionOverlayProtectsMountedMainAndPreservesSequenceCoordinates(t *testing.T) {
t.Parallel()
t.Run("mounted main ownership", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "meta: {version: main-v}\n")
writeImportFile(t, directory, "overlays/testing.yml", "meta: {version: overlay-v}\nservice: {meta: {version: payload}}\n")
_, err := LoadLayered(Input{Source: File(mainPath), Destination: Keys("service")}, testSecretBinding(), LayeredOptions[map[string]any]{
Overlay: "testing", OverlayFilesystem: &FilesystemContext{BaseDir: directory, Root: directory},
SourceVersionMode: sourceVersionModeRequired,
Definition: Definition[map[string]any]{Version: &VersionRule{
Path: Keys("meta", "version"), Check: func(Version, *Validation) error { return nil },
}},
})
loadErr, ok := err.(*LoadError)
if !ok || len(loadErr.Diagnostics()) != 1 || loadErr.Diagnostics()[0].Code != CodeOverlayVersion {
t.Fatalf("mounted overlay ownership error = %#v", err)
}
})
t.Run("sequence location remains source-local after deletion", func(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", "base: true\n")
writeImportFile(t, directory, "overlays/testing.yml", "versions:\n - before\n - overlay-version\n - after\n")
main, diagnostics := parseSource(File(mainPath), "source-main", defaultParseLimits())
if len(diagnostics) != 0 {
t.Fatalf("parse main: %#v", diagnostics)
}
localRule := &VersionRule{Path: Keys("versions").Index(1), Check: func(version Version, _ *Validation) error {
if version.Text != "overlay-version" {
t.Errorf("checked version = %#v", version)
}
return nil
}}
effectiveRule := effectiveSourceVersionRule(localRule, Keys("service"), sourceVersionModeRequired)
state := &sourceVersionState{localRule: localRule, effectiveRule: effectiveRule, mode: sourceVersionModeRequired}
base, compositionDiagnostics := composeDocuments(&document.Node{Kind: document.Mapping})
if len(compositionDiagnostics) != 0 {
t.Fatalf("compose base: %#v", compositionDiagnostics)
}
assembly := newAssemblyAccumulator(base)
_, diagnostics = loadSelectedOverlayIntoWithVersion(
File(mainPath), nil, main, assembly, sourceInfos(main.source), "testing", effectiveRule, defaultImportLimits(), state,
)
if len(diagnostics) != 0 {
t.Fatalf("load selected overlay: %#v", diagnostics)
}
if len(state.evidence) != 1 || state.evidence[0].resolvedPath.String() != "/versions/1" || state.evidence[0].location == nil || state.evidence[0].location.Line != 3 {
t.Fatalf("sequence evidence lost original coordinates: %#v", state.evidence)
}
versions, found := documentAtPath(assembly.current.root, Keys("versions"))
if !found || versions.Kind != document.Sequence || len(versions.Sequence) != 2 || versions.Sequence[0].Text != "before" || versions.Sequence[1].Text != "after" {
t.Fatalf("remaining overlay sequence = %#v", versions)
}
})
}
func TestSourceVersionImportAccountingIncludesConsumedHeaderNodes(t *testing.T) {
t.Parallel()