diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index b2e90aa..09b04be 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Consolidation Implementation Plan -Status: in progress. Stages 1 through 6 are complete; follow Stage 7 through Stage 20 in order. +Status: in progress. Stages 1 through 7 are complete; follow Stage 8 through Stage 20 in order. The target executor is a Luna coding agent working through one end-to-end sprint. ## Authority, Baseline, And Execution @@ -347,7 +347,7 @@ direct tests to construct named requests. A search-source regression was caught and fixed during verification. The focused engine selection, facade selection, consumer example tests, and `GOTOOLCHAIN=local GOPROXY=off go test ./...` passed. -## Stage 7 — Separate Resolved Filesystem Facts From Access +## Stage 7 — Separate Resolved Filesystem Facts From Access (Complete) **Owners:** new engine `filesystem_context.go`, `filesystem.go`, `filesystem_access.go`. @@ -374,6 +374,13 @@ consumer example tests, and `GOTOOLCHAIN=local GOPROXY=off go test ./...` passed **Finish:** common context facts have one private representation while validation timing and live resource ownership remain explicit. +Progress notes (2026-10-25): added shared host/native filesystem descriptions, +separated directory validation and access creation, and migrated import +acquisition to pass resolved facts alongside its owned access. The prior +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 **Owners:** engine `load_input.go`, `source_context.go`, `load_request.go`, and diff --git a/internal/engine/acquisition.go b/internal/engine/acquisition.go index 4240429..75e0ef5 100644 --- a/internal/engine/acquisition.go +++ b/internal/engine/acquisition.go @@ -33,14 +33,6 @@ type importAcquisitionRequest struct { processing *sourceVersionProcessingResult } -type resolvedFilesystemContext struct { - native *nativeNamespace - hostToken *namespaceToken - rootPath string - basePath string - mainPath string -} - func acquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, state *acquisitionState, tier *tierAcquisitionState) (assembledInput, []Diagnostic) { source := request.input.source destination := request.input.destination diff --git a/internal/engine/filesystem.go b/internal/engine/filesystem.go index 2e35483..3430ae7 100644 --- a/internal/engine/filesystem.go +++ b/internal/engine/filesystem.go @@ -3,7 +3,6 @@ package engine import ( "io/fs" "os" - "path" "path/filepath" "strings" ) @@ -137,32 +136,17 @@ func nativeContext(source Source, context *FilesystemContext) bool { // nativeFilesystemAccess resolves namespace context without opening caller // resources. File inputs always derive their base from the selected file. func nativeFilesystemAccess(source Source, context *FilesystemContext) (*filesystemAccess, string, string, *Diagnostic) { - var namespace *nativeNamespace - rootPath := "." - basePath := "." - mainPath := "" - if source.kind == sourceFilesystem { - namespace = source.namespace - mainPath = source.path - basePath = path.Dir(source.path) - if context != nil && context.Root != "" { - rootPath = context.Root - } - } else if context != nil && context.FS != nil { - namespace = context.FS.namespace - rootPath, basePath = context.Root, context.BaseDir + resolved, diagnostic := resolveNativeFilesystemContext(source, context) + if diagnostic != nil { + return nil, "", "", diagnostic } - if namespace == nil || nilFilesystem(namespace.filesystem) || !fs.ValidPath(rootPath) || !fs.ValidPath(basePath) { - value := optionDiagnostic(CodeOptionsInvalid, "filesystem context is invalid") + access, err := openResolvedFilesystemAccess(resolved) + if err != nil { + value := globFilesystemDiagnostic(err, "configuration root could not be opened", Location{}) + value.Primary = nil return nil, "", "", &value } - access := newNativeAccess(namespace.filesystem, rootPath, namespace.token) - access.label = namespace.label - if !access.contains(basePath) || mainPath != "" && !access.contains(mainPath) { - value := importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil) - return nil, "", "", &value - } - return access, basePath, mainPath, nil + return access, resolved.basePath, resolved.mainPath, nil } func validateNativeDirectories(access *filesystemAccess, basePath, mainPath string) *Diagnostic { @@ -181,64 +165,14 @@ 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 } - - if source.kind == sourceFile { - if context != nil && context.BaseDir != "" { - value := optionDiagnostic(CodeOptionsInvalid, "file sources determine their own import base directory") - return "", "", "", &value - } - canonical, err := canonicalExistingPath(source.path) - if err != nil { - value := importDiagnostic(CodeImportInvalid, "selected source path could not be canonicalized", nil) - return "", "", "", &value - } - mainPath = canonical - basePath = filepath.Dir(canonical) - rootPath = basePath - if context != nil && context.Root != "" { - rootPath, err = canonicalExistingPath(context.Root) - if err != nil { - value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil) - return "", "", "", &value - } - } - } else { - if context == nil || context.BaseDir == "" || context.Root == "" { - value := optionDiagnostic(CodeOptionsInvalid, "byte and reader imports require absolute base and root directories") - return "", "", "", &value - } - if !filepath.IsAbs(context.BaseDir) || !filepath.IsAbs(context.Root) { - value := optionDiagnostic(CodeOptionsInvalid, "byte and reader import base and root directories must be absolute") - return "", "", "", &value - } - var err error - basePath, err = canonicalExistingPath(context.BaseDir) - if err != nil { - value := importDiagnostic(CodeImportInvalid, "import base directory could not be canonicalized", nil) - return "", "", "", &value - } - rootPath, err = canonicalExistingPath(context.Root) - if err != nil { - value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil) - return "", "", "", &value - } + if diagnostic := validateResolvedFilesystemDirectories(resolved); diagnostic != nil { + return "", "", "", diagnostic } - if !containedPath(rootPath, basePath) || (mainPath != "" && !containedPath(rootPath, mainPath)) { - value := importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil) - return "", "", "", &value - } - for _, candidate := range []string{rootPath, basePath} { - info, err := os.Stat(candidate) - if err != nil || !info.IsDir() { - value := importDiagnostic(CodeImportInvalid, "configuration root and import base must be directories", nil) - return "", "", "", &value - } - } - return rootPath, basePath, mainPath, nil + return resolved.rootPath, resolved.basePath, resolved.mainPath, nil } func canonicalExistingPath(name string) (string, error) { diff --git a/internal/engine/filesystem_context.go b/internal/engine/filesystem_context.go new file mode 100644 index 0000000..5220107 --- /dev/null +++ b/internal/engine/filesystem_context.go @@ -0,0 +1,149 @@ +package engine + +import ( + "io/fs" + "os" + "path" + "path/filepath" + "strings" +) + +type resolvedFilesystemContext struct { + native *nativeNamespace + hostToken *namespaceToken + rootPath string + basePath string + mainPath string +} + +func resolveFilesystemContext(source Source, context *FilesystemContext, main parsedSource, hostToken *namespaceToken) (resolvedFilesystemContext, *Diagnostic) { + if nativeContext(source, context) { + return resolveNativeFilesystemContext(source, context) + } + return resolveHostFilesystemContext(source, context, main, hostToken) +} + +func resolveNativeFilesystemContext(source Source, context *FilesystemContext) (resolvedFilesystemContext, *Diagnostic) { + var namespace *nativeNamespace + resolved := resolvedFilesystemContext{rootPath: ".", basePath: "."} + if source.kind == sourceFilesystem { + namespace = source.namespace + resolved.mainPath = source.path + resolved.basePath = path.Dir(source.path) + if context != nil && context.Root != "" { + resolved.rootPath = context.Root + } + } else if context != nil && context.FS != nil { + namespace = context.FS.namespace + resolved.rootPath, resolved.basePath = context.Root, context.BaseDir + } + if namespace == nil || nilFilesystem(namespace.filesystem) || !fs.ValidPath(resolved.rootPath) || !fs.ValidPath(resolved.basePath) { + value := optionDiagnostic(CodeOptionsInvalid, "filesystem context is invalid") + return resolvedFilesystemContext{}, &value + } + resolved.native = namespace + if !containsNativePath(resolved.rootPath, resolved.basePath) || resolved.mainPath != "" && !containsNativePath(resolved.rootPath, resolved.mainPath) { + value := importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil) + return resolvedFilesystemContext{}, &value + } + return resolved, nil +} + +func resolveHostFilesystemContext(source Source, context *FilesystemContext, main parsedSource, hostToken *namespaceToken) (resolvedFilesystemContext, *Diagnostic) { + if main.source == nil || main.document == nil { + value := importDiagnostic(CodeImportInvalid, "imports require a selected configuration source", nil) + return resolvedFilesystemContext{}, &value + } + resolved := resolvedFilesystemContext{hostToken: hostToken} + if source.kind == sourceFile { + if context != nil && context.BaseDir != "" { + value := optionDiagnostic(CodeOptionsInvalid, "file sources determine their own import base directory") + return resolvedFilesystemContext{}, &value + } + canonical := main.source.Path + if canonical == "" { + var err error + canonical, err = canonicalExistingPath(source.path) + if err != nil { + value := importDiagnostic(CodeImportInvalid, "selected source path could not be canonicalized", nil) + return resolvedFilesystemContext{}, &value + } + } + resolved.mainPath = canonical + resolved.basePath = filepath.Dir(canonical) + resolved.rootPath = resolved.basePath + if context != nil && context.Root != "" { + rootPath, err := canonicalExistingPath(context.Root) + if err != nil { + value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil) + return resolvedFilesystemContext{}, &value + } + resolved.rootPath = rootPath + } + } else { + if context == nil || context.BaseDir == "" || context.Root == "" { + value := optionDiagnostic(CodeOptionsInvalid, "byte and reader imports require absolute base and root directories") + return resolvedFilesystemContext{}, &value + } + if !filepath.IsAbs(context.BaseDir) || !filepath.IsAbs(context.Root) { + value := optionDiagnostic(CodeOptionsInvalid, "byte and reader import base and root directories must be absolute") + return resolvedFilesystemContext{}, &value + } + var err error + resolved.basePath, err = canonicalExistingPath(context.BaseDir) + if err != nil { + value := importDiagnostic(CodeImportInvalid, "import base directory could not be canonicalized", nil) + return resolvedFilesystemContext{}, &value + } + resolved.rootPath, err = canonicalExistingPath(context.Root) + if err != nil { + value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil) + return resolvedFilesystemContext{}, &value + } + } + if !containedPath(resolved.rootPath, resolved.basePath) || resolved.mainPath != "" && !containedPath(resolved.rootPath, resolved.mainPath) { + value := importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil) + return resolvedFilesystemContext{}, &value + } + return resolved, nil +} + +func containsNativePath(root, name string) bool { + if !fs.ValidPath(name) || !fs.ValidPath(root) { + return false + } + return root == "." || name == root || strings.HasPrefix(name, root+"/") +} + +func validateResolvedFilesystemDirectories(resolved resolvedFilesystemContext) *Diagnostic { + for _, directory := range []string{resolved.rootPath, resolved.basePath} { + var info fs.FileInfo + var err error + if resolved.native != nil { + info, err = fs.Stat(resolved.native.filesystem, directory) + } else { + info, err = os.Stat(directory) + } + if err != nil || !info.IsDir() { + value := importDiagnostic(CodeImportInvalid, "configuration root and import base must be directories", nil) + return &value + } + } + return nil +} + +func openResolvedFilesystemAccess(resolved resolvedFilesystemContext) (*filesystemAccess, error) { + if resolved.native != nil { + access := newNativeAccess(resolved.native.filesystem, resolved.rootPath, resolved.native.token) + access.label = resolved.native.label + return access, nil + } + access, err := openHostAccess(resolved.rootPath) + if err != nil { + return nil, err + } + if resolved.hostToken != nil { + access.token = resolved.hostToken + } + return access, nil +} diff --git a/internal/engine/imports.go b/internal/engine/imports.go index bb5c6a2..2364799 100644 --- a/internal/engine/imports.go +++ b/internal/engine/imports.go @@ -76,36 +76,22 @@ func acquireImportedInput(request importAcquisitionRequest, policy *acquisitionP source := request.input.input.source context := request.input.input.filesystem main := request.main - if nativeContext(source, context) { - access, basePath, mainPath, diagnostic := nativeFilesystemAccess(source, context) - if diagnostic != nil { - return nil, sourceInfos(main.source), []Diagnostic{*diagnostic} - } - native := source.namespace - if native == nil && context != nil && context.FS != nil { - native = context.FS.namespace - } - return acquireImportedWithAccess(request, policy, state, tier, access, resolvedFilesystemContext{ - native: native, hostToken: tier.hostToken, rootPath: access.rootPath, - basePath: basePath, mainPath: mainPath, - }) - } - rootPath, basePath, mainPath, diagnostic := importFilesystem(source, context, main) + resolved, diagnostic := resolveFilesystemContext(source, context, main, tier.hostToken) if diagnostic != nil { return nil, sourceInfos(main.source), []Diagnostic{*diagnostic} } - access, err := openHostAccess(rootPath) + if resolved.native == nil { + if diagnostic := validateResolvedFilesystemDirectories(resolved); diagnostic != nil { + return nil, sourceInfos(main.source), []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} } - if tier.hostToken != nil { - access.token = tier.hostToken - } - return acquireImportedWithAccess(request, policy, state, tier, access, resolvedFilesystemContext{ - hostToken: tier.hostToken, rootPath: rootPath, basePath: basePath, mainPath: mainPath, - }) + return acquireImportedWithAccess(request, policy, state, tier, access, resolved) } func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisitionPolicy, loadState *acquisitionState, tier *tierAcquisitionState, access *filesystemAccess, resolved resolvedFilesystemContext) (*composedDocument, []SourceInfo, []Diagnostic) {