Normalize ordinary and grouped load preflight

This commit is contained in:
2026-10-25 12:52:50 +00:00
parent 19f5c2a2d5
commit 3f0efcb4ff
4 changed files with 82 additions and 72 deletions

View File

@@ -1,6 +1,6 @@
# Consolidation Implementation Plan # Consolidation Implementation Plan
Status: in progress. Stages 1 and 2 are complete; follow Stage 3 through Stage 20 in order. Status: in progress. Stages 1 through 3 are complete; follow Stage 4 through Stage 20 in order.
The target executor is a Luna coding agent working through one end-to-end sprint. The target executor is a Luna coding agent working through one end-to-end sprint.
## Authority, Baseline, And Execution ## Authority, Baseline, And Execution
@@ -203,7 +203,7 @@ and shared override conversion across ordinary and layered options. Validation
group F passed with `GOTOOLCHAIN=local GOPROXY=off go test . -run group F passed with `GOTOOLCHAIN=local GOPROXY=off go test . -run
'SourceInfo|SourceVersionCheck|ImportSelection|Facade|PublicFacadeBoundary|DependencyImportBoundary' -count=1`. 'SourceInfo|SourceVersionCheck|ImportSelection|Facade|PublicFacadeBoundary|DependencyImportBoundary' -count=1`.
## Stage 3 — Normalize Preflight Without Fabricated Options ## Stage 3 — Normalize Preflight Without Fabricated Options (Complete)
**Owners:** engine `options.go`, ordinary entry in `load.go`, grouped entry in **Owners:** engine `options.go`, ordinary entry in `load.go`, grouped entry in
`load_request.go`, and their option tests. `load_request.go`, and their option tests.
@@ -232,6 +232,11 @@ group F passed with `GOTOOLCHAIN=local GOPROXY=off go test . -run
the ordinary options type to validate grouped inputs. Acquisition may still use the ordinary options type to validate grouped inputs. Acquisition may still use
old options until Stage 6; do not broaden this stage into that migration. old options until Stage 6; do not broaden this stage into that migration.
Progress notes (2026-10-25): added shared normalization over `loadInput`, moved
load-wide defaults, source-version, and sequence-registration validation into
shared settings preparation, and removed `selectOptions` plus its validation
flag from grouped preflight. `GOTOOLCHAIN=local GOPROXY=off go test ./...` passed.
## Stage 4 — Express Input Acquisition With Non-Generic Requests ## Stage 4 — Express Input Acquisition With Non-Generic Requests
**Owners:** new engine `acquisition.go` and existing `load_input.go`. **Owners:** new engine `acquisition.go` and existing `load_input.go`.

View File

@@ -26,19 +26,18 @@ type Definition[T any] struct {
// overlay and requires filesystem context for NoSource. Overrides contain only // overlay and requires filesystem context for NoSource. Overrides contain only
// values explicitly supplied by an application CLI. // values explicitly supplied by an application CLI.
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 SourceVersionMode sourceVersionMode
importDefaults importDefaults importDefaults importDefaults
destination Path destination Path
placementMode placementMode placementMode placementMode
sequencePolicy sequencePolicy sequencePolicy sequencePolicy
deferAssembly bool deferAssembly bool
directiveContext rootDirectiveContext directiveContext rootDirectiveContext
importDefaultsValidated bool
} }
// FilesystemContext supplies filesystem roots for imports and overlays. For an // FilesystemContext supplies filesystem roots for imports and overlays. For an
@@ -153,20 +152,31 @@ func Load[T any](source Source, secrets SecretBinding, supplied ...Options[T]) (
} }
func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup secretLookup, supplied ...Options[T]) (*Loaded[T], error) { func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup secretLookup, supplied ...Options[T]) (*Loaded[T], error) {
options, diagnostics := selectOptions(source, supplied) if len(supplied) > 1 {
policy, policyDiagnostics := compileSequenceIdentityPolicy(options.Definition.sequenceIdentityRegistrations) return nil, (&diagnosticAccumulator{diagnostics: []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "Load accepts at most one options value")}}).loadError()
diagnostics = append(diagnostics, policyDiagnostics...) }
var options Options[T]
if len(supplied) == 1 {
options = supplied[0]
}
input, diagnostics := normalizeInput(loadInput{
source: source, imports: options.Imports, filesystem: options.Filesystem,
destination: options.destination.clone(), placementMode: options.placementMode,
}, options.Overlay)
defaults, policy, settingsDiagnostics := loadSettings(options.importDefaults, options.Definition, options.SourceVersionMode)
diagnostics = append(diagnostics, settingsDiagnostics...)
diagnostics = append(diagnostics, overlayOptionDiagnostics(options.Overlay)...)
if len(diagnostics) != 0 { if len(diagnostics) != 0 {
return nil, (&diagnosticAccumulator{diagnostics: diagnostics}).loadError() return nil, (&diagnosticAccumulator{diagnostics: diagnostics}).loadError()
} }
definition := options.Definition definition := options.Definition
definition.sequenceIdentityRegistrations = nil definition.sequenceIdentityRegistrations = nil
request := loadRequest[T]{ request := loadRequest[T]{
main: loadInput{source: source, imports: options.Imports, filesystem: options.Filesystem, destination: options.destination.clone(), placementMode: options.placementMode}, main: input,
definition: definition, importDefaults: snapshotImportDefaults(options.importDefaults), sequencePolicy: policy, overlay: options.Overlay, definition: definition, importDefaults: defaults, sequencePolicy: policy, overlay: options.Overlay,
overrides: options.Overrides, ordinary: true, overrides: options.Overrides, ordinary: true,
sourceVersionMode: options.SourceVersionMode, sourceVersionMode: options.SourceVersionMode,
effectiveVersion: effectiveSourceVersionRuleForPlacement(options.Definition.Version, options.destination, options.SourceVersionMode, options.placementMode), effectiveVersion: effectiveSourceVersionRuleForPlacement(options.Definition.Version, input.destination, options.SourceVersionMode, input.placementMode),
} }
return executeLoadRequest(request, secrets, lookup) return executeLoadRequest(request, secrets, lookup)
} }

View File

@@ -49,35 +49,23 @@ type acquiredUnderlayGroup struct {
func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBinding, lookup secretLookup) (*Loaded[T], error) { func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBinding, lookup secretLookup) (*Loaded[T], error) {
request.ordinary = false request.ordinary = false
var diagnostics []Diagnostic defaults, policy, diagnostics := loadSettings(request.importDefaults, request.definition, request.sourceVersionMode)
request.importDefaults = snapshotImportDefaults(request.importDefaults) request.importDefaults = defaults
diagnostics = append(diagnostics, validateImportDefaults(request.importDefaults)...)
policy, policyDiagnostics := compileSequenceIdentityPolicy(request.definition.sequenceIdentityRegistrations)
request.sequencePolicy = policy request.sequencePolicy = policy
diagnostics = append(diagnostics, policyDiagnostics...)
request.definition.sequenceIdentityRegistrations = nil request.definition.sequenceIdentityRegistrations = nil
mainOptions, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{ main, mainDiagnostics := normalizeInput(request.main, "")
Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem, destination: request.main.destination.clone(), request.main = main
SourceVersionMode: request.sourceVersionMode, placementMode: request.main.placementMode,
importDefaults: request.importDefaults, importDefaultsValidated: true,
}})
request.main.destination = mainOptions.destination
diagnostics = append(diagnostics, mainDiagnostics...) diagnostics = append(diagnostics, mainDiagnostics...)
request.effectiveVersion = effectiveSourceVersionRuleForPlacement(request.definition.Version, request.main.destination, request.sourceVersionMode, request.main.placementMode) request.effectiveVersion = effectiveSourceVersionRuleForPlacement(request.definition.Version, request.main.destination, request.sourceVersionMode, request.main.placementMode)
for groupIndex := range request.underlays { for groupIndex := range request.underlays {
for inputIndex := range request.underlays[groupIndex].inputs { for inputIndex := range request.underlays[groupIndex].inputs {
input := &request.underlays[groupIndex].inputs[inputIndex] input := &request.underlays[groupIndex].inputs[inputIndex]
inputOptions, inputDiagnostics := selectOptions(input.source, []Options[T]{{ normalized, inputDiagnostics := normalizeInput(*input, "")
Imports: input.imports, Filesystem: input.filesystem, destination: input.destination.clone(), placementMode: input.placementMode, *input = normalized
importDefaults: request.importDefaults, importDefaultsValidated: true,
}})
input.destination = inputOptions.destination
diagnostics = append(diagnostics, inputDiagnostics...) diagnostics = append(diagnostics, inputDiagnostics...)
} }
} }
if !validOverlayName(request.overlay) { diagnostics = append(diagnostics, overlayOptionDiagnostics(request.overlay)...)
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "overlay name must contain only ASCII letters, digits, underscore, or hyphen"))
}
if activeOverlay(request.overlay) { if activeOverlay(request.overlay) {
if request.overlayFilesystem == nil { if request.overlayFilesystem == nil {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "selected overlay requires explicit filesystem context")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "selected overlay requires explicit filesystem context"))

View File

@@ -1,52 +1,52 @@
package engine package engine
func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []Diagnostic) { func normalizeInput(input loadInput, ordinaryOverlay string) (loadInput, []Diagnostic) {
if len(supplied) > 1 {
return Options[T]{}, []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "Load accepts at most one options value")}
}
var options Options[T]
if len(supplied) == 1 {
options = supplied[0]
}
options.importDefaults = snapshotImportDefaults(options.importDefaults)
var diagnostics []Diagnostic var diagnostics []Diagnostic
if !options.importDefaultsValidated { if input.placementMode != placementModeMapping && input.placementMode != placementModeSequenceItem {
diagnostics = validateImportDefaults(options.importDefaults)
}
if options.placementMode != placementModeMapping && options.placementMode != placementModeSequenceItem {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "input placement mode is invalid")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "input placement mode is invalid"))
} }
if normalized, err := normalizeDestination(options.destination); err != nil { if normalized, err := normalizeDestination(input.destination); err != nil {
diagnostics = append(diagnostics, invalidDestinationDiagnostic(options.destination)) diagnostics = append(diagnostics, invalidDestinationDiagnostic(input.destination))
} else { } else {
options.destination = normalized input.destination = normalized
} }
if options.placementMode == placementModeSequenceItem && len(options.destination.segments) == 0 { if input.placementMode == placementModeSequenceItem && len(input.destination.segments) == 0 {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "sequence-item placement requires a nonempty destination")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "sequence-item placement requires a nonempty destination"))
} }
definition := options.Definition if input.filesystem != nil {
if !validOverlayName(options.Overlay) { diagnostics = append(diagnostics, filesystemOptionDiagnostics(input.source, input.filesystem)...)
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "overlay name must contain only ASCII letters, digits, underscore, or hyphen"))
} }
if options.Filesystem != nil { if input.source.kind == sourceSearch {
diagnostics = append(diagnostics, filesystemOptionDiagnostics(source, options.Filesystem)...) for _, issue := range input.source.searchIssues {
}
if source.kind == sourceSearch {
for _, issue := range source.searchIssues {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, issue)) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, issue))
} }
if source.search == nil { if input.source.search == nil {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "configuration search is invalid")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "configuration search is invalid"))
} else { } else {
diagnostics = append(diagnostics, validateSearch(*source.search, source.searchApp)...) diagnostics = append(diagnostics, validateSearch(*input.source.search, input.source.searchApp)...)
} }
} }
if source.kind == sourceNone && activeOverlay(options.Overlay) && options.Filesystem == nil { if input.source.kind == sourceNone && activeOverlay(ordinaryOverlay) && input.filesystem == nil {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context"))
} }
if options.Filesystem == nil && (source.kind == sourceBytes || source.kind == sourceReader || source.kind == sourceMapping) && (options.Imports || activeOverlay(options.Overlay)) { if input.filesystem == nil && (input.source.kind == sourceBytes || input.source.kind == sourceReader || input.source.kind == sourceMapping) && (input.imports || activeOverlay(ordinaryOverlay)) {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "synthetic-source imports or overlays require filesystem context")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "synthetic-source imports or overlays require filesystem context"))
} }
return input, canonicalDiagnostics(diagnostics)
}
func loadSettings[T any](defaults importDefaults, definition Definition[T], mode sourceVersionMode) (importDefaults, sequencePolicy, []Diagnostic) {
defaults = snapshotImportDefaults(defaults)
var diagnostics []Diagnostic
diagnostics = append(diagnostics, validateImportDefaults(defaults)...)
diagnostics = append(diagnostics, sourceVersionOptionDiagnostics(definition, mode)...)
policy, policyDiagnostics := compileSequenceIdentityPolicy(definition.sequenceIdentityRegistrations)
diagnostics = append(diagnostics, policyDiagnostics...)
return defaults, policy, canonicalDiagnostics(diagnostics)
}
func sourceVersionOptionDiagnostics[T any](definition Definition[T], mode sourceVersionMode) []Diagnostic {
var diagnostics []Diagnostic
if definition.Version != nil { if definition.Version != nil {
if !definition.Version.Path.valid() { if !definition.Version.Path.valid() {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "schema-version path is invalid")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "schema-version path is invalid"))
@@ -55,12 +55,19 @@ func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []D
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "schema-version rule requires a checker")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "schema-version rule requires a checker"))
} }
} }
if options.SourceVersionMode != sourceVersionModeDefault && options.SourceVersionMode != sourceVersionModeIfPresent && options.SourceVersionMode != sourceVersionModeRequired { if mode != sourceVersionModeDefault && mode != sourceVersionModeIfPresent && mode != sourceVersionModeRequired {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "source-version mode is invalid")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "source-version mode is invalid"))
} else if options.SourceVersionMode != sourceVersionModeDefault && (definition.Version == nil || !definition.Version.Path.valid() || definition.Version.Check == nil) { } else if mode != sourceVersionModeDefault && (definition.Version == nil || !definition.Version.Path.valid() || definition.Version.Check == nil) {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "source-version checking requires a valid version rule and checker")) diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "source-version checking requires a valid version rule and checker"))
} }
return options, canonicalDiagnostics(diagnostics) return diagnostics
}
func overlayOptionDiagnostics(name string) []Diagnostic {
if validOverlayName(name) {
return nil
}
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "overlay name must contain only ASCII letters, digits, underscore, or hyphen")}
} }
func optionDiagnostic(code Code, message string) Diagnostic { func optionDiagnostic(code Code, message string) Diagnostic {