diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 09b04be..995929c 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Consolidation Implementation Plan -Status: in progress. Stages 1 through 7 are complete; follow Stage 8 through Stage 20 in order. +Status: in progress. Stages 1 through 8 are complete; follow Stage 9 through Stage 20 in order. The target executor is a Luna coding agent working through one end-to-end sprint. ## Authority, Baseline, And Execution @@ -381,7 +381,7 @@ derivation adapters remain for the validator and overlay migration. Focused acquisition tests, `GOTOOLCHAIN=local GOPROXY=off go test ./...`, and `git diff --check` passed. -## Stage 8 — Carry Context Facts Through Input And Passive Views +## Stage 8 — Carry Context Facts Through Input And Passive Views (Complete) **Owners:** engine `load_input.go`, `source_context.go`, `load_request.go`, and the ordinary context backfill in `load.go`. @@ -407,6 +407,12 @@ the ordinary context backfill in `load.go`. **Finish:** ordinary/underlay acquisition and passive views use the common facts; selected-overlay callers are the remaining migration in Stage 9. +Progress notes (2026-10-25): input acquisition now retains resolved filesystem +facts and builds passive contexts from those facts for ordinary and underlay +sources. Migrated the ordinary backfill and reordered-source context test. The +selected-overlay adapters remain for Stage 9. Focused context/acquisition tests, +`GOTOOLCHAIN=local GOPROXY=off go test ./...`, and `git diff --check` passed. + ## Stage 9 — Migrate Selected Overlays Without Merging Their Semantics **Owners:** engine `selected_overlay.go`, ordinary `applyInputLayers`, and diff --git a/internal/engine/acquisition.go b/internal/engine/acquisition.go index 75e0ef5..db44890 100644 --- a/internal/engine/acquisition.go +++ b/internal/engine/acquisition.go @@ -164,7 +164,7 @@ func acquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, sta request.input.source = selectedSource if request.input.imports && selectedSource.kind != sourceNone { importRequest := importAcquisitionRequest{input: request, main: parsed, processing: versionProcessing} - input.composition, input.sources, diagnostics = acquireImportedInput(importRequest, &policy, state, tier) + input.composition, input.sources, input.resolvedFilesystem, diagnostics = acquireImportedInput(importRequest, &policy, state, tier) if len(diagnostics) != 0 { return input, diagnostics } @@ -218,7 +218,14 @@ func acquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, sta accounting.nodes += rawNodes + placementNodes } if !activeOverlay(request.ordinaryOverlay) || request.input.imports { - contexts, diagnostic := contextsForAcquiredSources(selectedSource, request.input.filesystem, parsed.source, input.sources) + if input.resolvedFilesystem == nil && len(input.sources) != 0 && (selectedSource.kind == sourceFile || request.input.filesystem != nil || nativeContext(selectedSource, request.input.filesystem)) { + resolved, diagnostic := resolveFilesystemContext(selectedSource, request.input.filesystem, parsed, tier.hostToken) + if diagnostic != nil { + return input, []Diagnostic{*diagnostic} + } + input.resolvedFilesystem = &resolved + } + contexts, diagnostic := contextsForResolvedSources(input.resolvedFilesystem, input.sources) if diagnostic != nil { return input, []Diagnostic{*diagnostic} } diff --git a/internal/engine/filesystem.go b/internal/engine/filesystem.go index 3430ae7..c273f95 100644 --- a/internal/engine/filesystem.go +++ b/internal/engine/filesystem.go @@ -165,6 +165,10 @@ func validateNativeDirectories(access *filesystemAccess, basePath, mainPath stri } func importFilesystem(source Source, context *FilesystemContext, main parsedSource) (rootPath, basePath, mainPath string, diagnostic *Diagnostic) { + if main.source == nil || main.document == nil { + value := importDiagnostic(CodeImportInvalid, "imports require a selected configuration source", nil) + return "", "", "", &value + } resolved, diagnostic := resolveHostFilesystemContext(source, context, main, nil) if diagnostic != nil { return "", "", "", diagnostic diff --git a/internal/engine/filesystem_context.go b/internal/engine/filesystem_context.go index 5220107..d9f8d1f 100644 --- a/internal/engine/filesystem_context.go +++ b/internal/engine/filesystem_context.go @@ -17,12 +17,35 @@ type resolvedFilesystemContext struct { } func resolveFilesystemContext(source Source, context *FilesystemContext, main parsedSource, hostToken *namespaceToken) (resolvedFilesystemContext, *Diagnostic) { + if source.kind == sourceNone && context != nil { + return resolveAbsentFilesystemContext(context) + } if nativeContext(source, context) { return resolveNativeFilesystemContext(source, context) } return resolveHostFilesystemContext(source, context, main, hostToken) } +func resolveAbsentFilesystemContext(context *FilesystemContext) (resolvedFilesystemContext, *Diagnostic) { + if context.FS != nil { + return resolveNativeFilesystemContext(Source{kind: sourceNone}, context) + } + rootPath, err := canonicalExistingPath(context.Root) + if err != nil { + value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil) + return resolvedFilesystemContext{}, &value + } + basePath := rootPath + if context.BaseDir != "" { + basePath, err = canonicalExistingPath(context.BaseDir) + if err != nil { + value := importDiagnostic(CodeImportInvalid, "import base directory could not be canonicalized", nil) + return resolvedFilesystemContext{}, &value + } + } + return resolvedFilesystemContext{rootPath: rootPath, basePath: basePath}, nil +} + func resolveNativeFilesystemContext(source Source, context *FilesystemContext) (resolvedFilesystemContext, *Diagnostic) { var namespace *nativeNamespace resolved := resolvedFilesystemContext{rootPath: ".", basePath: "."} @@ -50,7 +73,7 @@ func resolveNativeFilesystemContext(source Source, context *FilesystemContext) ( } func resolveHostFilesystemContext(source Source, context *FilesystemContext, main parsedSource, hostToken *namespaceToken) (resolvedFilesystemContext, *Diagnostic) { - if main.source == nil || main.document == nil { + if main.source == nil { value := importDiagnostic(CodeImportInvalid, "imports require a selected configuration source", nil) return resolvedFilesystemContext{}, &value } diff --git a/internal/engine/imports.go b/internal/engine/imports.go index 2364799..14899e8 100644 --- a/internal/engine/imports.go +++ b/internal/engine/imports.go @@ -72,26 +72,30 @@ func compositionTraversal(policy sequencePolicy, rule *VersionRule, versionState } } -func acquireImportedInput(request importAcquisitionRequest, policy *acquisitionPolicy, state *acquisitionState, tier *tierAcquisitionState) (*composedDocument, []SourceInfo, []Diagnostic) { +func acquireImportedInput(request importAcquisitionRequest, policy *acquisitionPolicy, state *acquisitionState, tier *tierAcquisitionState) (*composedDocument, []SourceInfo, *resolvedFilesystemContext, []Diagnostic) { source := request.input.input.source context := request.input.input.filesystem main := request.main resolved, diagnostic := resolveFilesystemContext(source, context, main, tier.hostToken) if diagnostic != nil { - return nil, sourceInfos(main.source), []Diagnostic{*diagnostic} + return nil, sourceInfos(main.source), nil, []Diagnostic{*diagnostic} } if resolved.native == nil { if diagnostic := validateResolvedFilesystemDirectories(resolved); diagnostic != nil { - return nil, sourceInfos(main.source), []Diagnostic{*diagnostic} + return nil, sourceInfos(main.source), nil, []Diagnostic{*diagnostic} } } access, err := openResolvedFilesystemAccess(resolved) if err != nil { diagnostic := globFilesystemDiagnostic(err, "configuration root could not be opened", Location{}) diagnostic.Primary = nil - return nil, sourceInfos(main.source), []Diagnostic{diagnostic} + return nil, sourceInfos(main.source), nil, []Diagnostic{diagnostic} } - return acquireImportedWithAccess(request, policy, state, tier, access, resolved) + composition, sources, diagnostics := acquireImportedWithAccess(request, policy, state, tier, access, resolved) + if len(diagnostics) != 0 { + return composition, sources, nil, diagnostics + } + return composition, sources, &resolved, nil } func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisitionPolicy, loadState *acquisitionState, tier *tierAcquisitionState, access *filesystemAccess, resolved resolvedFilesystemContext) (*composedDocument, []SourceInfo, []Diagnostic) { diff --git a/internal/engine/imports_test.go b/internal/engine/imports_test.go index 6783d57..9e08fe5 100644 --- a/internal/engine/imports_test.go +++ b/internal/engine/imports_test.go @@ -156,7 +156,12 @@ func TestAcquiredContextsUseSelectedFileWhenSourceRecordsAreReordered(t *testing {ID: "source-2", Path: canonicalTestPath(t, importedPath), Kind: SourceKindFile}, {ID: "source-1", Path: canonicalTestPath(t, mainPath), Kind: SourceKindFile}, } - contexts, diagnostic := contextsForAcquiredSources(File(mainPath), nil, nil, sources) + mainInfo := &SourceInfo{Path: canonicalTestPath(t, mainPath)} + resolved, diagnostic := resolveFilesystemContext(File(mainPath), nil, parsedSource{source: mainInfo}, nil) + if diagnostic != nil { + t.Fatalf("resolve main context: %#v", diagnostic) + } + contexts, diagnostic := contextsForResolvedSources(&resolved, sources) if diagnostic != nil { t.Fatalf("source contexts: %#v", diagnostic) } @@ -1443,7 +1448,8 @@ func testAcquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, } func testAcquireImport(request importAcquisitionRequest, policy acquisitionPolicy, state *acquisitionState, tier *tierAcquisitionState) (*composedDocument, []SourceInfo, []Diagnostic) { - return acquireImportedInput(request, &policy, state, tier) + composition, sources, _, diagnostics := acquireImportedInput(request, &policy, state, tier) + return composition, sources, diagnostics } func testAcquireImportWithLimits(request importAcquisitionRequest, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) { diff --git a/internal/engine/load.go b/internal/engine/load.go index 0089056..b3474bb 100644 --- a/internal/engine/load.go +++ b/internal/engine/load.go @@ -264,7 +264,15 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo } } if len(baseSources) != 0 && input.contexts[baseSources[0].ID].FS == nil { - contexts, diagnostic := contextsForAcquiredSources(request.main.source, request.main.filesystem, input.parsed.source, baseSources) + if input.resolvedFilesystem == nil && (request.main.source.kind == sourceFile || request.main.filesystem != nil || nativeContext(request.main.source, request.main.filesystem)) { + resolved, diagnostic := resolveFilesystemContext(request.main.source, request.main.filesystem, input.parsed, nil) + if diagnostic != nil { + accumulator.add(*diagnostic) + return nil, accumulator.loadError() + } + input.resolvedFilesystem = &resolved + } + contexts, diagnostic := contextsForResolvedSources(input.resolvedFilesystem, baseSources) if diagnostic != nil { accumulator.add(*diagnostic) return nil, accumulator.loadError() diff --git a/internal/engine/source_context.go b/internal/engine/source_context.go index 451164c..c13f35b 100644 --- a/internal/engine/source_context.go +++ b/internal/engine/source_context.go @@ -7,62 +7,18 @@ import ( "strings" ) -func contextsForAcquiredSources(source Source, context *FilesystemContext, baseSource *SourceInfo, sources []SourceInfo) (map[string]ResourceContext, *Diagnostic) { +func contextsForResolvedSources(resolved *resolvedFilesystemContext, sources []SourceInfo) (map[string]ResourceContext, *Diagnostic) { if len(sources) == 0 { return nil, nil } - var rootPath, basePath string - var filesystem fs.FS - if source.kind == sourceFilesystem { - filesystem = source.namespace.filesystem - rootPath, basePath = ".", path.Dir(source.path) - if context != nil && context.Root != "" { - rootPath = context.Root - } - } else if context != nil && context.FS != nil { - filesystem = context.FS.namespace.filesystem - rootPath, basePath = context.Root, context.BaseDir - } else if source.kind == sourceFile { - filePath := source.path - if baseSource != nil { - filePath = baseSource.Path - } else { - canonical, err := canonicalExistingPath(filePath) - if err != nil { - value := importDiagnostic(CodeImportInvalid, "selected source could not be canonicalized", nil) - return nil, &value - } - filePath = canonical - } - basePath = filepath.Dir(filePath) - rootPath = basePath - if context != nil && context.Root != "" { - canonical, err := canonicalExistingPath(context.Root) - if err != nil { - value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil) - return nil, &value - } - rootPath = canonical - } - } else if context != nil { - var err error - rootPath, err = canonicalExistingPath(context.Root) - if err != nil { - value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil) - return nil, &value - } - if source.kind == sourceNone && context.BaseDir == "" { - basePath = rootPath - } else { - basePath, err = canonicalExistingPath(context.BaseDir) - if err != nil { - value := importDiagnostic(CodeImportInvalid, "import base directory could not be canonicalized", nil) - return nil, &value - } - } - } else { + if resolved == nil { return nil, nil } + rootPath, basePath := resolved.rootPath, resolved.basePath + var filesystem fs.FS + if resolved.native != nil { + filesystem = resolved.native.filesystem + } var rooted fs.FS if filesystem != nil { rooted = rootedNativeFS{filesystem: filesystem, root: rootPath} @@ -81,7 +37,7 @@ func contextsForAcquiredSources(source Source, context *FilesystemContext, baseS } var relative string if filesystem != nil { - if !fs.ValidPath(rootPath) || !fs.ValidPath(base) || rootPath != "." && base != rootPath && !strings.HasPrefix(base, rootPath+"/") { + if !containsNativePath(rootPath, base) { value := importDiagnostic(CodeImportOutsideRoot, "source base is outside the configuration root", nil) return nil, &value } @@ -110,6 +66,30 @@ func contextsForAcquiredSources(source Source, context *FilesystemContext, baseS return result, nil } +// contextsForAcquiredSources is retained for selected-overlay callers until +// their request migration. Input acquisition resolves the facts at its own +// lifecycle boundary and uses contextsForResolvedSources directly. +func contextsForAcquiredSources(source Source, context *FilesystemContext, baseSource *SourceInfo, sources []SourceInfo) (map[string]ResourceContext, *Diagnostic) { + if len(sources) == 0 { + return nil, nil + } + if source.kind == sourceNone { + if context == nil { + return nil, nil + } + resolved, diagnostic := resolveAbsentFilesystemContext(context) + if diagnostic != nil { + return nil, diagnostic + } + return contextsForResolvedSources(&resolved, sources) + } + resolved, diagnostic := resolveFilesystemContext(source, context, parsedSource{source: baseSource}, nil) + if diagnostic != nil { + return nil, diagnostic + } + return contextsForResolvedSources(&resolved, sources) +} + func mergeResourceContexts(target *map[string]ResourceContext, incoming map[string]ResourceContext) { if len(incoming) == 0 { return