From d314a269d53ee884debbdbd229bcf0503a10c5e8 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 25 Oct 2026 13:25:37 +0000 Subject: [PATCH] Separate directory entry admission from traversal --- docs/roadmap/implementation.md | 9 +- internal/engine/import_selection.go | 154 +++++++++++++++++----------- 2 files changed, 100 insertions(+), 63 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 6d47c0c..10dd1b5 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Consolidation Implementation Plan -Status: in progress. Stages 1 through 11 are complete; follow Stage 12 through Stage 20 in order. +Status: in progress. Stages 1 through 12 are complete; follow Stage 13 through Stage 20 in order. The target executor is a Luna coding agent working through one end-to-end sprint. ## Authority, Baseline, And Execution @@ -508,7 +508,7 @@ The declaration parser retains its validation ordering, authored member sites, and nil-versus-empty presence behavior. Import-focused tests and `./scripts/check.sh` passed. -## Stage 12 — Extract Directory Entry Admission And Descent +## Stage 12 — Extract Directory Entry Admission And Descent (Complete) **Owner:** engine `import_selection.go`, using existing directory/link helpers. @@ -529,6 +529,11 @@ and nil-versus-empty presence behavior. Import-focused tests and **Finish:** substantial entry/descent policy is separated from stack orchestration without changing the selection algorithm or adding repeated root walks. +Progress notes (2026-10-25): extracted entry inspection and file/directory +admission into a result-returning helper. The selector retains root handling, +stack and active-path mutation, candidate union, sorting, exclusions, and the +required-empty decision. Directory-focused tests and `./scripts/check.sh` passed. + ## Stage 13 — Extract Imported-Source Admission From Recursion **Owner:** engine `import_acquisition.go` after Stage 10. diff --git a/internal/engine/import_selection.go b/internal/engine/import_selection.go index 56a940d..45ed589 100644 --- a/internal/engine/import_selection.go +++ b/internal/engine/import_selection.go @@ -139,72 +139,23 @@ func selectDirectoryImportCandidates(access *filesystemAccess, basePath string, } entry := frame.entries[frame.next] frame.next++ - accessPath := access.join(frame.directory.accessPath, entry.Name()) - lexicalPath := access.join(frame.directory.path, entry.Name()) - candidate := selectedImportCandidate{ - path: normalizedImportCandidatePath(access, lexicalPath), accessPath: accessPath, - label: relativeImportLabel(access, basePath, lexicalPath), site: frame.directory.site, + result, diagnostic := prepareImportDirectoryEntry(access, basePath, *frame, entry, policy, limits, accounting, activeDirectories) + if diagnostic != nil { + return importSelection{}, []Diagnostic{*diagnostic} } - if !policy.hidden && strings.HasPrefix(entry.Name(), ".") { + switch result.kind { + case importDirectoryEntrySkip: continue - } - target, entryErr := access.importEntryTarget(candidate.accessPath, entry, policy.follow) - if entryErr != nil { - if errors.Is(entryErr, fs.ErrNotExist) { + case importDirectoryEntryFile: + if _, exists := seenCandidates[result.candidate.path]; exists { continue } - return importSelection{}, []Diagnostic{*importDirectoryTargetDiagnostic(frame.directory.site, entryErr)} + seenCandidates[result.candidate.path] = struct{}{} + discovered = append(discovered, result.candidate) + case importDirectoryEntryDirectory: + activeDirectories[result.frame.identity] = struct{}{} + stack = append(stack, result.frame) } - if target.symlink && !policy.follow { - continue - } - if target.dir { - if !directoryDepthAllows(policy, frame.depth) { - continue - } - if _, exists := activeDirectories[target.identity]; exists { - return importSelection{}, []Diagnostic{*importSelectionDiagnostic(frame.directory.site, CodeImportCycle, "directory traversal cycle detected")} - } - childMissing, childIsDirectory, childDiagnostic := validateNamedImportTarget(access, candidate, false, true) - if childDiagnostic != nil { - return importSelection{}, []Diagnostic{*childDiagnostic} - } - if childMissing || !childIsDirectory { - continue - } - childEntries, vanished, childDiagnostic := importDirectoryEntries(access, candidate.accessPath, candidate.site, limits, accounting) - if childDiagnostic != nil { - return importSelection{}, []Diagnostic{*childDiagnostic} - } - if !vanished { - if access.native == nil { - candidate.accessPath = target.identity - } - activeDirectories[target.identity] = struct{}{} - stack = append(stack, importDirectoryFrame{directory: candidate, depth: frame.depth + 1, entries: childEntries, identity: target.identity}) - } - continue - } - if target.identity != "" && access.native == nil { - candidate.accessPath = target.identity - } else { - canonical, canonicalErr := access.canonical(accessPath) - if canonicalErr != nil { - if errors.Is(canonicalErr, fs.ErrNotExist) { - continue - } - return importSelection{}, []Diagnostic{globFilesystemDiagnostic(canonicalErr, "directory search file could not be canonicalized", frame.directory.site.location)} - } - candidate.accessPath = canonical - } - if !matchesImportFilename(entry.Name(), policy.pattern) { - continue - } - if _, exists := seenCandidates[candidate.path]; exists { - continue - } - seenCandidates[candidate.path] = struct{}{} - discovered = append(discovered, candidate) } } sort.Slice(discovered, func(left, right int) bool { return discovered[left].path < discovered[right].path }) @@ -229,6 +180,73 @@ func selectDirectoryImportCandidates(access *filesystemAccess, basePath string, return selection, nil } +func prepareImportDirectoryEntry(access *filesystemAccess, basePath string, frame importDirectoryFrame, entry fs.DirEntry, policy resolvedImportPolicy, limits importLimits, accounting *importAccounting, activeDirectories map[string]struct{}) (importDirectoryEntryResult, *Diagnostic) { + accessPath := access.join(frame.directory.accessPath, entry.Name()) + lexicalPath := access.join(frame.directory.path, entry.Name()) + candidate := selectedImportCandidate{ + path: normalizedImportCandidatePath(access, lexicalPath), accessPath: accessPath, + label: relativeImportLabel(access, basePath, lexicalPath), site: frame.directory.site, + } + if !policy.hidden && strings.HasPrefix(entry.Name(), ".") { + return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil + } + target, err := access.importEntryTarget(candidate.accessPath, entry, policy.follow) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil + } + return importDirectoryEntryResult{}, importDirectoryTargetDiagnostic(frame.directory.site, err) + } + if target.symlink && !policy.follow { + return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil + } + if target.dir { + if !directoryDepthAllows(policy, frame.depth) { + return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil + } + if _, exists := activeDirectories[target.identity]; exists { + return importDirectoryEntryResult{}, importSelectionDiagnostic(frame.directory.site, CodeImportCycle, "directory traversal cycle detected") + } + missing, isDirectory, diagnostic := validateNamedImportTarget(access, candidate, false, true) + if diagnostic != nil { + return importDirectoryEntryResult{}, diagnostic + } + if missing || !isDirectory { + return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil + } + entries, vanished, diagnostic := importDirectoryEntries(access, candidate.accessPath, candidate.site, limits, accounting) + if diagnostic != nil { + return importDirectoryEntryResult{}, diagnostic + } + if vanished { + return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil + } + if access.native == nil { + candidate.accessPath = target.identity + } + return importDirectoryEntryResult{kind: importDirectoryEntryDirectory, frame: importDirectoryFrame{ + directory: candidate, depth: frame.depth + 1, entries: entries, identity: target.identity, + }}, nil + } + if target.identity != "" && access.native == nil { + candidate.accessPath = target.identity + } else { + canonical, err := access.canonical(accessPath) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil + } + diagnostic := globFilesystemDiagnostic(err, "directory search file could not be canonicalized", frame.directory.site.location) + return importDirectoryEntryResult{}, &diagnostic + } + candidate.accessPath = canonical + } + if !matchesImportFilename(entry.Name(), policy.pattern) { + return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil + } + return importDirectoryEntryResult{kind: importDirectoryEntryFile, candidate: candidate}, nil +} + type importDirectoryFrame struct { directory selectedImportCandidate depth int @@ -237,6 +255,20 @@ type importDirectoryFrame struct { identity string } +type importDirectoryEntryKind uint8 + +const ( + importDirectoryEntrySkip importDirectoryEntryKind = iota + importDirectoryEntryFile + importDirectoryEntryDirectory +) + +type importDirectoryEntryResult struct { + kind importDirectoryEntryKind + candidate selectedImportCandidate + frame importDirectoryFrame +} + func importDirectoryTargetDiagnostic(site importFieldSite, err error) *Diagnostic { if isImportLinkCycle(err) { return importSelectionDiagnostic(site, CodeImportCycle, "directory traversal cycle detected")