From 3f0efcb4ff3b75238e110f8508ff58bafe676607 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 25 Oct 2026 12:52:50 +0000 Subject: [PATCH] Normalize ordinary and grouped load preflight --- docs/roadmap/implementation.md | 9 ++++- internal/engine/load.go | 48 +++++++++++++--------- internal/engine/load_request.go | 26 ++++-------- internal/engine/options.go | 71 ++++++++++++++++++--------------- 4 files changed, 82 insertions(+), 72 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 487b91c..a6b87dd 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # 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. ## 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 '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 `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 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 **Owners:** new engine `acquisition.go` and existing `load_input.go`. diff --git a/internal/engine/load.go b/internal/engine/load.go index 7fccf49..e1c1ea6 100644 --- a/internal/engine/load.go +++ b/internal/engine/load.go @@ -26,19 +26,18 @@ type Definition[T any] struct { // overlay and requires filesystem context for NoSource. Overrides contain only // values explicitly supplied by an application CLI. type Options[T any] struct { - Definition Definition[T] - Imports bool - Overlay string - Filesystem *FilesystemContext - Overrides []Override - SourceVersionMode sourceVersionMode - importDefaults importDefaults - destination Path - placementMode placementMode - sequencePolicy sequencePolicy - deferAssembly bool - directiveContext rootDirectiveContext - importDefaultsValidated bool + Definition Definition[T] + Imports bool + Overlay string + Filesystem *FilesystemContext + Overrides []Override + SourceVersionMode sourceVersionMode + importDefaults importDefaults + destination Path + placementMode placementMode + sequencePolicy sequencePolicy + deferAssembly bool + directiveContext rootDirectiveContext } // 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) { - options, diagnostics := selectOptions(source, supplied) - policy, policyDiagnostics := compileSequenceIdentityPolicy(options.Definition.sequenceIdentityRegistrations) - diagnostics = append(diagnostics, policyDiagnostics...) + if len(supplied) > 1 { + return nil, (&diagnosticAccumulator{diagnostics: []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "Load accepts at most one options value")}}).loadError() + } + 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 { return nil, (&diagnosticAccumulator{diagnostics: diagnostics}).loadError() } definition := options.Definition definition.sequenceIdentityRegistrations = nil request := loadRequest[T]{ - main: loadInput{source: source, imports: options.Imports, filesystem: options.Filesystem, destination: options.destination.clone(), placementMode: options.placementMode}, - definition: definition, importDefaults: snapshotImportDefaults(options.importDefaults), sequencePolicy: policy, overlay: options.Overlay, + main: input, + definition: definition, importDefaults: defaults, sequencePolicy: policy, overlay: options.Overlay, overrides: options.Overrides, ordinary: true, 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) } diff --git a/internal/engine/load_request.go b/internal/engine/load_request.go index 1746a0c..1b9a955 100644 --- a/internal/engine/load_request.go +++ b/internal/engine/load_request.go @@ -49,35 +49,23 @@ type acquiredUnderlayGroup struct { func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBinding, lookup secretLookup) (*Loaded[T], error) { request.ordinary = false - var diagnostics []Diagnostic - request.importDefaults = snapshotImportDefaults(request.importDefaults) - diagnostics = append(diagnostics, validateImportDefaults(request.importDefaults)...) - policy, policyDiagnostics := compileSequenceIdentityPolicy(request.definition.sequenceIdentityRegistrations) + defaults, policy, diagnostics := loadSettings(request.importDefaults, request.definition, request.sourceVersionMode) + request.importDefaults = defaults request.sequencePolicy = policy - diagnostics = append(diagnostics, policyDiagnostics...) request.definition.sequenceIdentityRegistrations = nil - mainOptions, mainDiagnostics := selectOptions(request.main.source, []Options[T]{{ - Definition: request.definition, Imports: request.main.imports, Filesystem: request.main.filesystem, destination: request.main.destination.clone(), - SourceVersionMode: request.sourceVersionMode, placementMode: request.main.placementMode, - importDefaults: request.importDefaults, importDefaultsValidated: true, - }}) - request.main.destination = mainOptions.destination + main, mainDiagnostics := normalizeInput(request.main, "") + request.main = main diagnostics = append(diagnostics, mainDiagnostics...) request.effectiveVersion = effectiveSourceVersionRuleForPlacement(request.definition.Version, request.main.destination, request.sourceVersionMode, request.main.placementMode) for groupIndex := range request.underlays { for inputIndex := range request.underlays[groupIndex].inputs { input := &request.underlays[groupIndex].inputs[inputIndex] - inputOptions, inputDiagnostics := selectOptions(input.source, []Options[T]{{ - Imports: input.imports, Filesystem: input.filesystem, destination: input.destination.clone(), placementMode: input.placementMode, - importDefaults: request.importDefaults, importDefaultsValidated: true, - }}) - input.destination = inputOptions.destination + normalized, inputDiagnostics := normalizeInput(*input, "") + *input = normalized diagnostics = append(diagnostics, inputDiagnostics...) } } - if !validOverlayName(request.overlay) { - diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "overlay name must contain only ASCII letters, digits, underscore, or hyphen")) - } + diagnostics = append(diagnostics, overlayOptionDiagnostics(request.overlay)...) if activeOverlay(request.overlay) { if request.overlayFilesystem == nil { diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "selected overlay requires explicit filesystem context")) diff --git a/internal/engine/options.go b/internal/engine/options.go index 4b833a9..bce7c05 100644 --- a/internal/engine/options.go +++ b/internal/engine/options.go @@ -1,52 +1,52 @@ package engine -func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []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) +func normalizeInput(input loadInput, ordinaryOverlay string) (loadInput, []Diagnostic) { var diagnostics []Diagnostic - if !options.importDefaultsValidated { - diagnostics = validateImportDefaults(options.importDefaults) - } - if options.placementMode != placementModeMapping && options.placementMode != placementModeSequenceItem { + if input.placementMode != placementModeMapping && input.placementMode != placementModeSequenceItem { diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "input placement mode is invalid")) } - if normalized, err := normalizeDestination(options.destination); err != nil { - diagnostics = append(diagnostics, invalidDestinationDiagnostic(options.destination)) + if normalized, err := normalizeDestination(input.destination); err != nil { + diagnostics = append(diagnostics, invalidDestinationDiagnostic(input.destination)) } 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")) } - definition := options.Definition - if !validOverlayName(options.Overlay) { - diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "overlay name must contain only ASCII letters, digits, underscore, or hyphen")) + if input.filesystem != nil { + diagnostics = append(diagnostics, filesystemOptionDiagnostics(input.source, input.filesystem)...) } - if options.Filesystem != nil { - diagnostics = append(diagnostics, filesystemOptionDiagnostics(source, options.Filesystem)...) - } - if source.kind == sourceSearch { - for _, issue := range source.searchIssues { + if input.source.kind == sourceSearch { + for _, issue := range input.source.searchIssues { diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, issue)) } - if source.search == nil { + if input.source.search == nil { diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "configuration search is invalid")) } 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")) } - 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")) } + 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.Path.valid() { 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")) } } - 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")) - } 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")) } - 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 {