Normalize ordinary and grouped load preflight
This commit is contained in:
@@ -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`.
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user