diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 83f2c40..4f78788 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Consolidation Implementation Plan -Status: in progress. Stages 1 through 4 are complete; follow Stage 5 through Stage 20 in order. +Status: in progress. Stages 1 through 5 are complete; follow Stage 6 through Stage 20 in order. The target executor is a Luna coding agent working through one end-to-end sprint. ## Authority, Baseline, And Execution @@ -270,7 +270,7 @@ test command currently cannot compile because the old assembly callers in `load_input.go` and `load_request.go` are scheduled to migrate in Stage 6; test verification is deferred until that integration. -## Stage 5 — Consolidate Import Requests And Default Ownership +## Stage 5 — Consolidate Import Requests And Default Ownership (Complete) **Owners:** engine import entry points, `importTraversal`, `source_version.go`, and tests that directly invoke the old import variants. @@ -301,6 +301,13 @@ and tests that directly invoke the old import variants. longer travel through version processing; no forwarding ladder remains merely to supply another nil/default positional parameter. +Progress notes (2026-10-25): replaced the import entry ladders with named +requests and one access worker, moved defaults to acquisition policy, and +retargeted direct import tests. The focused engine test command cannot compile +yet because the remaining ordinary and grouped assembly callers are scheduled +for Stage 6; verification is deferred until that integration. `git diff --check` +passed. + ## Stage 6 — Wire Ordinary And Grouped Lifecycle To Acquisition **Owners:** engine `load.go`, `load_request.go`, `load_input.go`, related tests. diff --git a/internal/engine/acquisition.go b/internal/engine/acquisition.go index a70d544..958df9e 100644 --- a/internal/engine/acquisition.go +++ b/internal/engine/acquisition.go @@ -170,11 +170,8 @@ func acquireInput(request inputAcquisitionRequest, policy acquisitionPolicy, sta } } if request.input.imports && selectedSource.kind != sourceNone { - if request.deferAssembly { - input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessingDeferred(selectedSource, request.input.filesystem, parsed, destination, request.protectionRule, limits, accounting, visited, hostToken, versionState, versionProcessing, policy.sequencePolicy, true) - } else { - input.composition, input.sources, diagnostics = loadImportedDocumentsTrackedWithProcessing(selectedSource, request.input.filesystem, parsed, destination, request.protectionRule, limits, accounting, visited, hostToken, versionState, versionProcessing, policy.sequencePolicy) - } + importRequest := importAcquisitionRequest{input: request, main: parsed, processing: versionProcessing} + input.composition, input.sources, diagnostics = acquireImportedInput(importRequest, &policy, state, tier) if len(diagnostics) != 0 { return input, diagnostics } diff --git a/internal/engine/destination_accounting_test.go b/internal/engine/destination_accounting_test.go index 5f7a184..97348f1 100644 --- a/internal/engine/destination_accounting_test.go +++ b/internal/engine/destination_accounting_test.go @@ -216,9 +216,8 @@ func TestImportedEmptyBodiesChargeFullRelativeDestinations(t *testing.T) { currentLimits := limits currentLimits.nodes = wantNodes + test.delta accounting := &importAccounting{} - composed, sources, diagnostics := loadImportedDocumentsTracked( - File(mainPath), nil, main, entryDestination, nil, currentLimits, accounting, nil, nil, - ) + request := testImportRequest(File(mainPath), nil, main, entryDestination, nil, nil) + composed, sources, diagnostics := testAcquireImportWithAccounting(request, currentLimits, accounting) if test.wantError { if composed != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded { t.Fatalf("below-bound relative placement = %#v, %#v", composed, diagnostics) @@ -279,9 +278,8 @@ func TestGlobMatchesChargeTheirSharedRelativeDestination(t *testing.T) { currentLimits := limits currentLimits.nodes = wantNodes + test.delta accounting := &importAccounting{} - composed, sources, diagnostics := loadImportedDocumentsTracked( - File(mainPath), nil, main, entryDestination, nil, currentLimits, accounting, nil, nil, - ) + request := testImportRequest(File(mainPath), nil, main, entryDestination, nil, nil) + composed, sources, diagnostics := testAcquireImportWithAccounting(request, currentLimits, accounting) if test.wantError { if composed != nil || len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded { t.Fatalf("below-bound glob placement = %#v, %#v", composed, diagnostics) @@ -366,9 +364,8 @@ func TestAbsentOptionalImportsDoNotChargeUnusedDeepDestinations(t *testing.T) { main := parseImportMain(t, mainPath) limits := defaultImportLimits() accounting := &importAccounting{} - composed, sources, diagnostics := loadImportedDocumentsTracked( - File(mainPath), nil, main, destination, nil, limits, accounting, nil, nil, - ) + request := testImportRequest(File(mainPath), nil, main, destination, nil, nil) + composed, sources, diagnostics := testAcquireImportWithAccounting(request, limits, accounting) if len(diagnostics) != 0 || composed == nil || len(sources) != 1 { t.Fatalf("absent optional import = %#v, %#v, %#v", composed, sources, diagnostics) } diff --git a/internal/engine/destination_import_test.go b/internal/engine/destination_import_test.go index b94e23c..6f964f2 100644 --- a/internal/engine/destination_import_test.go +++ b/internal/engine/destination_import_test.go @@ -108,8 +108,11 @@ parent: true } defer func() { _ = access.close() }() state := importTraversal{ - access: access, accounting: &importAccounting{}, limits: defaultImportLimits(), - active: make(map[resourceIdentity]int), visited: make(map[resourceIdentity]importRoute), + access: access, + policy: &acquisitionPolicy{importLimits: defaultImportLimits()}, + state: &acquisitionState{accounting: &importAccounting{}}, + tier: &tierAcquisitionState{visited: make(map[resourceIdentity]importRoute)}, + active: make(map[resourceIdentity]int), } if diagnostics := state.walkDeclarations(directory, declarations, 0, Keys("entry")); len(diagnostics) != 0 { t.Fatalf("walk diagnostics = %#v", diagnostics) diff --git a/internal/engine/glob_test.go b/internal/engine/glob_test.go index 7180838..7cb76fd 100644 --- a/internal/engine/glob_test.go +++ b/internal/engine/glob_test.go @@ -216,7 +216,8 @@ func TestGlobTraversalBoundsNonmatchesAndAcceptedCandidates(t *testing.T) { main := parseImportMain(t, mainPath) limits := defaultImportLimits() limits.entries = 2 - _, _, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, limits) + request := testImportRequest(File(mainPath), nil, main, Path{}, nil, nil) + _, _, diagnostics := testAcquireImportWithLimits(request, limits) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded { t.Fatalf("entry-budget diagnostics = %#v", diagnostics) } @@ -228,7 +229,8 @@ func TestGlobTraversalBoundsNonmatchesAndAcceptedCandidates(t *testing.T) { main = parseImportMain(t, mainPath) limits = defaultImportLimits() limits.sources = 2 - _, _, diagnostics = loadImportedDocuments(File(mainPath), nil, main, nil, limits) + request = testImportRequest(File(mainPath), nil, main, Path{}, nil, nil) + _, _, diagnostics = testAcquireImportWithLimits(request, limits) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded { t.Fatalf("candidate-budget diagnostics = %#v", diagnostics) } diff --git a/internal/engine/imports.go b/internal/engine/imports.go index b185ad7..bb5c6a2 100644 --- a/internal/engine/imports.go +++ b/internal/engine/imports.go @@ -46,13 +46,11 @@ type importAccounting struct { } type importTraversal struct { - access *filesystemAccess - accounting *importAccounting - limits importLimits - rule *VersionRule - versionState *sourceVersionState - directiveContext rootDirectiveContext - defaults importDefaults + access *filesystemAccess + request importAcquisitionRequest + policy *acquisitionPolicy + state *acquisitionState + tier *tierAcquisitionState scope acquisitionScope itemScope bool @@ -60,38 +58,37 @@ type importTraversal struct { itemFallback Path sources []SourceInfo active map[resourceIdentity]int - visited map[resourceIdentity]importRoute stack []importRoute importGraph importInclusionGraph } -func loadImportedDocuments(source Source, context *FilesystemContext, main parsedSource, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) { - return loadImportedDocumentsTracked(source, context, main, Path{}, rule, limits, &importAccounting{}, nil, nil) -} - -func loadImportedDocumentsTracked(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken) (*composedDocument, []SourceInfo, []Diagnostic) { - return loadImportedDocumentsTrackedWithVersion(source, context, main, destination, rule, limits, accounting, visited, hostToken, nil) -} - -func loadImportedDocumentsTrackedWithVersion(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken, versionState *sourceVersionState) (*composedDocument, []SourceInfo, []Diagnostic) { - var processing *sourceVersionProcessingResult - if versionState != nil && versionState.mode != sourceVersionModeDefault { - processing = versionState.mainProcessing +func compositionTraversal(policy sequencePolicy, rule *VersionRule, versionState *sourceVersionState, accounting *importAccounting, graph importInclusionGraph) importTraversal { + return importTraversal{ + request: importAcquisitionRequest{input: inputAcquisitionRequest{protectionRule: rule}}, + policy: &acquisitionPolicy{sequencePolicy: policy, importLimits: defaultImportLimits()}, + state: &acquisitionState{accounting: accounting, versionState: versionState}, + tier: &tierAcquisitionState{}, + importGraph: graph.clone(), } - return loadImportedDocumentsTrackedWithProcessing(source, context, main, destination, rule, limits, accounting, visited, hostToken, versionState, processing, sequencePolicy{}) } -func loadImportedDocumentsTrackedWithProcessing(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken, versionState *sourceVersionState, processing *sourceVersionProcessingResult, policy sequencePolicy) (*composedDocument, []SourceInfo, []Diagnostic) { - return loadImportedDocumentsTrackedWithProcessingDeferred(source, context, main, destination, rule, limits, accounting, visited, hostToken, versionState, processing, policy, false) -} - -func loadImportedDocumentsTrackedWithProcessingDeferred(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, hostToken *namespaceToken, versionState *sourceVersionState, processing *sourceVersionProcessingResult, policy sequencePolicy, deferAssembly bool) (*composedDocument, []SourceInfo, []Diagnostic) { +func acquireImportedInput(request importAcquisitionRequest, policy *acquisitionPolicy, state *acquisitionState, tier *tierAcquisitionState) (*composedDocument, []SourceInfo, []Diagnostic) { + 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} } - return loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, policy, deferAssembly) + 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) if diagnostic != nil { @@ -103,37 +100,26 @@ func loadImportedDocumentsTrackedWithProcessingDeferred(source Source, context * diagnostic.Primary = nil return nil, sourceInfos(main.source), []Diagnostic{diagnostic} } - if hostToken != nil { - access.token = hostToken + if tier.hostToken != nil { + access.token = tier.hostToken } - return loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, policy, deferAssembly) + return acquireImportedWithAccess(request, policy, state, tier, access, resolvedFilesystemContext{ + hostToken: tier.hostToken, rootPath: rootPath, basePath: basePath, mainPath: mainPath, + }) } -func loadImportedWithAccess(access *filesystemAccess, basePath, mainPath string, main parsedSource, rule *VersionRule, limits importLimits, accounting *importAccounting) (*composedDocument, []SourceInfo, []Diagnostic) { - return loadImportedWithAccessTracked(access, basePath, mainPath, main, Path{}, rule, limits, accounting, nil) -} - -func loadImportedWithAccessTracked(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute) (*composedDocument, []SourceInfo, []Diagnostic) { - return loadImportedWithAccessTrackedWithVersion(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, nil) -} - -func loadImportedWithAccessTrackedWithVersion(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, versionState *sourceVersionState) (*composedDocument, []SourceInfo, []Diagnostic) { - var processing *sourceVersionProcessingResult - if versionState != nil && versionState.mode != sourceVersionModeDefault { - processing = versionState.mainProcessing +func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisitionPolicy, loadState *acquisitionState, tier *tierAcquisitionState, access *filesystemAccess, resolved resolvedFilesystemContext) (*composedDocument, []SourceInfo, []Diagnostic) { + main := request.main + destination := request.input.input.destination + limits := policy.importLimits + accounting := loadState.accounting + versionState := loadState.versionState + processing := request.processing + basePath, mainPath := resolved.basePath, resolved.mainPath + if tier.visited == nil { + tier.visited = make(map[resourceIdentity]importRoute) } - return loadImportedWithAccessTrackedAndProcessing(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing) -} - -func loadImportedWithAccessTrackedAndProcessing(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, versionState *sourceVersionState, processing *sourceVersionProcessingResult) (*composedDocument, []SourceInfo, []Diagnostic) { - return loadImportedWithAccessTrackedAndProcessingPolicy(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, sequencePolicy{}) -} - -func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, versionState *sourceVersionState, processing *sourceVersionProcessingResult, policy sequencePolicy) (*composedDocument, []SourceInfo, []Diagnostic) { - return loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access, basePath, mainPath, main, destination, rule, limits, accounting, visited, versionState, processing, policy, false) -} - -func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystemAccess, basePath, mainPath string, main parsedSource, destination Path, rule *VersionRule, limits importLimits, accounting *importAccounting, visited map[resourceIdentity]importRoute, versionState *sourceVersionState, processing *sourceVersionProcessingResult, policy sequencePolicy, deferAssembly bool) (*composedDocument, []SourceInfo, []Diagnostic) { + visited := tier.visited if !access.contains(basePath) || (mainPath != "" && !access.contains(mainPath)) { _ = access.close() return nil, sourceInfos(main.source), []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil)} @@ -163,21 +149,19 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem accounting.sources++ accounting.bytes += main.bytes accounting.nodes += countInputNodes(main.document) - defaults := importDefaults{} - if versionState != nil { - defaults = snapshotImportDefaults(versionState.importDefaults) + directiveContext := request.input.directiveContext + if directiveContext == 0 { + directiveContext = rootDirectiveContextImportsOnly } - directiveContext := rootDirectiveContextImportsOnly if processing != nil && processing.directiveContext != 0 { directiveContext = processing.directiveContext } + request.input.directiveContext = directiveContext state := importTraversal{ - access: access, accounting: accounting, limits: limits, rule: rule, versionState: versionState, - directiveContext: directiveContext, - defaults: defaults, - scope: acquisitionScope{owner: mainNodeKey, destination: destination.clone()}, - sources: []SourceInfo{mainInfo}, - active: make(map[resourceIdentity]int), visited: visited, + access: access, request: request, policy: policy, state: loadState, tier: tier, + scope: acquisitionScope{owner: mainNodeKey, destination: destination.clone()}, + sources: []SourceInfo{mainInfo}, + active: make(map[resourceIdentity]int), importGraph: newImportInclusionGraph(), } if mainIsItem { @@ -187,9 +171,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem state.scope.destination = Path{} state.sources[0] = cloneSourceInfo(mainInfo) } - if state.visited == nil { - state.visited = make(map[resourceIdentity]importRoute) - } + if diagnostic := state.checkInitialLimits(); diagnostic != nil { _ = access.close() return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic} @@ -203,7 +185,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem state.importGraph.addNode(importGraphNode{key: mainNodeKey, canonical: mainIdentity, sourceID: mainInfo.ID, role: importRoleMain}) mainRoute.key = mainNodeKey state.active[mainResource] = 0 - state.visited[mainResource] = mainRoute + tier.visited[mainResource] = mainRoute state.stack = append(state.stack, mainRoute) var body *document.Node var declarations []importDeclarationSpec @@ -269,7 +251,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem }}, } } - if deferAssembly { + if request.input.deferAssembly { if diagnostic := reservePendingItemPlacements(&acquisition, limits, accounting); diagnostic != nil { return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic} } @@ -286,7 +268,7 @@ func loadImportedWithAccessTrackedAndProcessingPolicyDeferred(access *filesystem parsed: main, composition: acquired, acquisition: cloneAcquisitionScopes(acquired.acquisition), sources: state.sources, versionState: versionState, accounting: accounting, } - composed, diagnostics := composeAcquiredInputWithPolicies(input, collectionPolicyScope{}, policy) + composed, diagnostics := composeAcquiredInputWithPolicies(input, collectionPolicyScope{}, policy.sequencePolicy) return composed, cloneSourceInfos(state.sources), diagnostics } @@ -311,11 +293,7 @@ func composeAcquiredInputWithPolicies(input assembledInput, lower collectionPoli if accounting == nil { accounting = &importAccounting{sources: len(input.sources), bytes: acquired.inputBytes, nodes: acquired.inputNodes} } - traversal := importTraversal{ - accounting: accounting, - limits: defaultImportLimits(), rule: rule, versionState: input.versionState, - importGraph: acquired.importGraph.clone(), - } + traversal := compositionTraversal(policy, rule, input.versionState, accounting, acquired.importGraph) if len(scope.contributions) == 1 && scope.contributions[0].role == importRoleMain && scope.contributions[0].pendingItem != nil { main := scope.contributions[0] localScope := cloneAcquisitionScope(*main.pendingItem) @@ -361,7 +339,7 @@ func composeAcquiredInputWithPolicies(input assembledInput, lower collectionPoli } if !main.placementCharged { placementNodes := destinationWrapperNodeCountForMode(main.destination, placementModeSequenceItem) - if accounting.nodes+placementNodes > traversal.limits.nodes { + if accounting.nodes+placementNodes > traversal.policy.importLimits.nodes { return nil, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", main.source.ImportedFrom)} } accounting.nodes += placementNodes @@ -401,10 +379,7 @@ func composeAcquiredScopeWithPolicy( versionState *sourceVersionState, accounting *importAccounting, ) (*composedDocument, []Diagnostic) { - traversal := importTraversal{ - accounting: accounting, limits: defaultImportLimits(), rule: rule, - versionState: versionState, importGraph: graph.clone(), - } + traversal := compositionTraversal(policy, rule, versionState, accounting, graph) if diagnostics := traversal.composePendingItems(scope); len(diagnostics) != 0 { return nil, diagnostics } @@ -454,15 +429,15 @@ func reservePendingItemPlacements(scope *acquisitionScope, limits importLimits, } func (s *importTraversal) checkInitialLimits() *Diagnostic { - if s.accounting.sources > s.limits.sources { + if s.state.accounting.sources > s.policy.importLimits.sources { value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source-count limit", nil) return &value } - if s.accounting.bytes > s.limits.bytes { + if s.state.accounting.bytes > s.policy.importLimits.bytes { value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate byte limit", nil) return &value } - if s.accounting.nodes > s.limits.nodes { + if s.state.accounting.nodes > s.policy.importLimits.nodes { value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", nil) return &value } @@ -472,7 +447,7 @@ func (s *importTraversal) checkInitialLimits() *Diagnostic { func (s *importTraversal) walkDeclarations(basePath string, declarations []importDeclarationSpec, depth int, parentDestination Path) []Diagnostic { policies := make([]resolvedImportPolicy, len(declarations)) for index, spec := range declarations { - policy, diagnostics := resolveImportPolicy(spec, s.defaults) + policy, diagnostics := resolveImportPolicy(spec, s.policy.importDefaults) if len(diagnostics) != 0 { return diagnostics } @@ -490,9 +465,9 @@ func (s *importTraversal) walkDeclarations(basePath string, declarations []impor case importSelectorPath: selection, diagnostics = selectNamedImportCandidates(s.access, basePath, spec, policy) case importSelectorDirectory: - selection, diagnostics = selectDirectoryImportCandidates(s.access, basePath, spec, policy, s.limits, s.accounting) + selection, diagnostics = selectDirectoryImportCandidates(s.access, basePath, spec, policy, s.policy.importLimits, s.state.accounting) case importSelectorGlob: - selection, diagnostics = selectGlobImportCandidates(s.access, basePath, spec, policy, s.limits, s.accounting) + selection, diagnostics = selectGlobImportCandidates(s.access, basePath, spec, policy, s.policy.importLimits, s.state.accounting) default: location := spec.location return []Diagnostic{importDiagnostic(CodeImportInvalid, "import selector is invalid", &location)} @@ -554,7 +529,7 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo if !s.access.contains(canonical) { return []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "import target resolves outside the configuration root", &location)} } - if depth > s.limits.depth { + if depth > s.policy.importLimits.depth { return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the active import-depth limit", &location)} } @@ -570,18 +545,18 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo cycle := append([]importRoute(nil), s.stack[activeIndex:]...) return []Diagnostic{importCycleDiagnostic(canonical, location, cycle)} } - if previous, ok := s.visited[identity]; ok { + if previous, ok := s.tier.visited[identity]; ok { s.importGraph.addEdge(importerKey, nodeKey, location) return []Diagnostic{importRepeatedDiagnostic(location, previous, currentRoute)} } - if s.accounting.sources+1 > s.limits.sources { + if s.state.accounting.sources+1 > s.policy.importLimits.sources { return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source-count limit", &location)} } if _, err := s.access.relative(canonical); err != nil { return []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "import target could not be addressed inside the configuration root", &location)} } - sourceID := "source-" + strconv.Itoa(s.accounting.sources+1) + sourceID := "source-" + strconv.Itoa(s.state.accounting.sources+1) kind := SourceKindFile label := candidate.label if label == "" { @@ -611,7 +586,7 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo s.sources = s.sources[:len(s.sources)-1] return nil } - s.accounting.sources++ + s.state.accounting.sources++ root, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{ Depth: maxDocumentDepth, Nodes: maxDocumentNodes, @@ -620,26 +595,26 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo return parserDiagnostics(sourceID, issues) } nodeCount := countInputNodes(root) - if s.accounting.nodes+nodeCount > s.limits.nodes { + if s.state.accounting.nodes+nodeCount > s.policy.importLimits.nodes { return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)} } - s.accounting.nodes += nodeCount + s.state.accounting.nodes += nodeCount var body *document.Node var declarations []importDeclarationSpec var sequenceDeclarations []sequenceDeclarationSpec var diagnostics []Diagnostic var processing *sourceVersionProcessingResult - directiveContext := s.directiveContext + directiveContext := s.request.input.directiveContext if directiveContext == 0 { directiveContext = rootDirectiveContextImportsOnly } - if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault { + if s.state.versionState != nil && s.state.versionState.mode != sourceVersionModeDefault { versionDestination := destination if s.itemScope { versionDestination = appendDestination(s.itemPrefix.Index(0), destination) } - processed, failures := s.versionState.processWithDirectiveContext(root, sourceVersionContribution, versionDestination, sourceID, policy.placement, directiveContext) + processed, failures := s.state.versionState.processWithDirectiveContext(root, sourceVersionContribution, versionDestination, sourceID, policy.placement, directiveContext) if len(failures) != 0 { return failures } @@ -650,7 +625,7 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo if s.itemScope { versionDestination = appendDestination(s.itemPrefix.Index(0), destination) } - processed := processSourceVersionWithDirectiveContext(root, s.rule, sourceVersionContribution, versionDestination, sourceVersionModeDefault, directiveContext) + processed := processSourceVersionWithDirectiveContext(root, s.request.input.protectionRule, sourceVersionContribution, versionDestination, sourceVersionModeDefault, directiveContext) if len(processed.diagnostics) != 0 { return processed.diagnostics } @@ -672,7 +647,7 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo if mode == placementModeSequenceItem { route := importRoute{canonical: canonical, locations: currentRoute.locations, key: nodeKey} s.active[identity] = len(s.stack) - s.visited[identity] = route + s.tier.visited[identity] = route s.stack = append(s.stack, route) diagnostics := s.visitItemSource(canonical, nodeKey, destination, relativeDestination, depth, info, body, declarations, sequenceDeclarations, processing) delete(s.active, identity) @@ -684,10 +659,10 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", &location)} } placementNodes := destinationWrapperNodeCount(placementDestination) - if s.accounting.nodes+placementNodes > s.limits.nodes { + if s.state.accounting.nodes+placementNodes > s.policy.importLimits.nodes { return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)} } - s.accounting.nodes += placementNodes + s.state.accounting.nodes += placementNodes route := importRoute{canonical: canonical, locations: currentRoute.locations, key: nodeKey} s.scope.contributions = append(s.scope.contributions, acquiredContribution{ source: cloneSourceInfo(info), payload: body, document: placed, @@ -696,7 +671,7 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations), }) s.active[identity] = len(s.stack) - s.visited[identity] = route + s.tier.visited[identity] = route s.stack = append(s.stack, route) diagnostics = s.walkDeclarations(s.access.dir(canonical), declarations, depth, destination) delete(s.active, identity) @@ -799,10 +774,10 @@ func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnos } placementNodes := destinationWrapperNodeCountForMode(contribution.destination, placementModeSequenceItem) if !contribution.placementCharged { - if s.accounting.nodes+placementNodes > s.limits.nodes { + if s.state.accounting.nodes+placementNodes > s.policy.importLimits.nodes { return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", contribution.source.ImportedFrom)} } - s.accounting.nodes += placementNodes + s.state.accounting.nodes += placementNodes } contribution.payload = completed.root contribution.document = placed @@ -1126,7 +1101,7 @@ func projectSourceVersionEvidence(processing *sourceVersionProcessingResult, des } func (s *importTraversal) readImport(name, sourceID string, required bool) ([]byte, bool, *Diagnostic) { - remaining := s.limits.bytes - s.accounting.bytes + remaining := s.policy.importLimits.bytes - s.state.accounting.bytes limit := min(int64(maxSourceBytes), remaining) if limit < 0 { limit = 0 @@ -1139,7 +1114,7 @@ func (s *importTraversal) readImport(name, sourceID string, required bool) ([]by value := sourceReadDiagnostic(failure, sourceID, CodeImportLimitExceeded, "configuration exceeds a source or aggregate byte limit") return nil, false, &value } - s.accounting.bytes += int64(len(data)) + s.state.accounting.bytes += int64(len(data)) return data, false, nil } diff --git a/internal/engine/imports_test.go b/internal/engine/imports_test.go index e0fdf69..08ca2d1 100644 --- a/internal/engine/imports_test.go +++ b/internal/engine/imports_test.go @@ -1268,7 +1268,8 @@ func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) { {name: "aggregate nodes", limits: importLimits{depth: 8, sources: 8, bytes: maxTotalBytes, nodes: countInputNodes(main.document) + 1}}, } { t.Run(test.name, func(t *testing.T) { - _, _, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, test.limits) + request := testImportRequest(File(mainPath), nil, main, Path{}, nil, nil) + _, _, diagnostics := testAcquireImportWithLimits(request, test.limits) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded { t.Fatalf("budget diagnostics = %#v", diagnostics) } @@ -1278,7 +1279,8 @@ func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) { writeImportFile(t, directory, "one.yml", "configkit: {imports: [{path: nested.yml}]}\n") writeImportFile(t, directory, "nested.yml", "settings: {nested: nested}\n") main = parseImportMain(t, mainPath) - _, _, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, importLimits{depth: 1, sources: 8, bytes: maxTotalBytes, nodes: maxDocumentNodes}) + request := testImportRequest(File(mainPath), nil, main, Path{}, nil, nil) + _, _, diagnostics := testAcquireImportWithLimits(request, importLimits{depth: 1, sources: 8, bytes: maxTotalBytes, nodes: maxDocumentNodes}) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportLimitExceeded { t.Fatalf("depth diagnostics = %#v", diagnostics) } @@ -1312,7 +1314,9 @@ func TestImportDepthBudgetCountsSelectedEdges(t *testing.T) { writeImportFile(t, directory, "parts/next.yml", "next: true\n") limits := defaultImportLimits() limits.depth = 1 - composed, sources, diagnostics := loadImportedDocuments(File(mainPath), nil, parseImportMain(t, mainPath), nil, limits) + main := parseImportMain(t, mainPath) + request := testImportRequest(File(mainPath), nil, main, Path{}, nil, nil) + composed, sources, diagnostics := testAcquireImportWithLimits(request, limits) if test.code != "" { if len(diagnostics) != 1 || diagnostics[0].Code != test.code { t.Fatalf("diagnostics = %#v, want %s", diagnostics, test.code) @@ -1342,7 +1346,8 @@ func TestImportTraversalAcceptsExactPrivateBudgets(t *testing.T) { bytes: main.bytes + int64(len(data)), nodes: countInputNodes(main.document) + countInputNodes(imported.document), } - composed, sources, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, limits) + request := testImportRequest(File(mainPath), nil, main, Path{}, nil, nil) + composed, sources, diagnostics := testAcquireImportWithLimits(request, limits) if len(diagnostics) != 0 || composed == nil || len(sources) != 2 { t.Fatalf("exact-limit composition = (%#v, %#v, %#v)", composed, sources, diagnostics) } @@ -1401,6 +1406,32 @@ func parseImportMain(t *testing.T, filename string) parsedSource { return parsed } +func testImportRequest(source Source, context *FilesystemContext, main parsedSource, destination Path, rule *VersionRule, processing *sourceVersionProcessingResult) importAcquisitionRequest { + return importAcquisitionRequest{ + input: inputAcquisitionRequest{ + input: loadInput{ + source: source, imports: true, filesystem: context, + destination: destination.clone(), placementMode: main.source.placementMode, + }, + role: sourceVersionMain, directiveContext: rootDirectiveContextImportsOnly, + protectionRule: rule, + }, + main: main, processing: processing, + } +} + +func testAcquireImport(request importAcquisitionRequest, policy acquisitionPolicy, state *acquisitionState, tier *tierAcquisitionState) (*composedDocument, []SourceInfo, []Diagnostic) { + return acquireImportedInput(request, &policy, state, tier) +} + +func testAcquireImportWithLimits(request importAcquisitionRequest, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) { + return testAcquireImport(request, acquisitionPolicy{importLimits: limits}, &acquisitionState{accounting: &importAccounting{}}, &tierAcquisitionState{}) +} + +func testAcquireImportWithAccounting(request importAcquisitionRequest, limits importLimits, accounting *importAccounting) (*composedDocument, []SourceInfo, []Diagnostic) { + return testAcquireImport(request, acquisitionPolicy{importLimits: limits}, &acquisitionState{accounting: accounting}, &tierAcquisitionState{}) +} + func parseImportSpecificationsYAML(t *testing.T, input string) []importDeclarationSpec { t.Helper() declarations, diagnostics := parseImportSpecificationYAML(input) diff --git a/internal/engine/item_policy_test.go b/internal/engine/item_policy_test.go index 7cae94b..4a8c673 100644 --- a/internal/engine/item_policy_test.go +++ b/internal/engine/item_policy_test.go @@ -92,7 +92,7 @@ func TestPendingItemCompositionProjectsNestedIdentityDiagnostics(t *testing.T) { destination: Keys("items"), pendingItem: &itemScope, }}} - traversal := importTraversal{accounting: &importAccounting{}, limits: defaultImportLimits()} + traversal := compositionTraversal(sequencePolicy{}, nil, nil, &importAccounting{}, newImportInclusionGraph()) diagnostics := traversal.composePendingItems(&outer) if len(diagnostics) != 1 { t.Fatalf("diagnostics = %#v, want one conflict", diagnostics) diff --git a/internal/engine/load.go b/internal/engine/load.go index e1c1ea6..2713458 100644 --- a/internal/engine/load.go +++ b/internal/engine/load.go @@ -208,7 +208,6 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo } versionState := &sourceVersionState{ localRule: request.definition.Version, effectiveRule: request.protectionVersion(), mode: request.sourceVersionMode, - importDefaults: snapshotImportDefaults(request.importDefaults), } accumulator.versionState = versionState input, diagnostics := assembleBaseInputTrackedWithVersion(request.main.source, mainOptions, request.main.destination, accounting, nil, nil, versionState) diff --git a/internal/engine/load_request.go b/internal/engine/load_request.go index 1b9a955..f635dce 100644 --- a/internal/engine/load_request.go +++ b/internal/engine/load_request.go @@ -86,7 +86,9 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a projectVersion := sequenceVersionPath(request.protectionVersion()) for index, group := range request.underlays { evidenceStart := len(input.versionState.evidenceSince(0)) - acquired, diagnostics := acquireUnderlayGroupScopes(group, index+1, request.protectionVersion(), accounting, input.versionState, request.sequencePolicy) + acquired, diagnostics := acquireUnderlayGroupScopes(group, index+1, request.protectionVersion(), accounting, input.versionState, acquisitionPolicy{ + importDefaults: request.importDefaults, sequencePolicy: request.sequencePolicy, importLimits: defaultImportLimits(), + }) mergeResourceContexts(&input.contexts, acquired.contexts) if len(diagnostics) != 0 { input.sources = append(input.sources, acquired.sources...) @@ -213,7 +215,7 @@ func assembleLayeredInput[T any](request loadRequest[T], input assembledInput, a return input, versionDiagnostics } -func acquireUnderlayGroupScopes(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState, policy sequencePolicy) (*acquiredUnderlayGroup, []Diagnostic) { +func acquireUnderlayGroupScopes(group loadGroup, index int, rule *VersionRule, accounting *importAccounting, versionState *sourceVersionState, policy acquisitionPolicy) (*acquiredUnderlayGroup, []Diagnostic) { initialBytes, initialNodes := accounting.bytes, accounting.nodes visited := make(map[resourceIdentity]importRoute) hostToken := &namespaceToken{} @@ -231,14 +233,9 @@ func acquireUnderlayGroupScopes(group loadGroup, index int, rule *VersionRule, a options := Options[struct{}]{ Imports: contribution.imports, Filesystem: contribution.filesystem, Definition: Definition[struct{}]{Version: rule}, destination: contribution.destination.clone(), - placementMode: contribution.placementMode, sequencePolicy: policy, deferAssembly: true, + placementMode: contribution.placementMode, sequencePolicy: policy.sequencePolicy, deferAssembly: true, directiveContext: rootDirectiveContextLayered, - importDefaults: func() importDefaults { - if versionState == nil { - return importDefaults{} - } - return snapshotImportDefaults(versionState.importDefaults) - }(), + importDefaults: snapshotImportDefaults(policy.importDefaults), } acquired, diagnostics := assembleBaseInputTrackedForRole(contribution.source, options, contribution.destination, accounting, visited, hostToken, versionState, sourceVersionContribution) for _, source := range acquired.sources { diff --git a/internal/engine/load_request_test.go b/internal/engine/load_request_test.go index b8ad56d..7bb7cc5 100644 --- a/internal/engine/load_request_test.go +++ b/internal/engine/load_request_test.go @@ -80,7 +80,7 @@ func TestUnderlayGroupAcquisitionRetainsScopesUntilComposition(t *testing.T) { }) accounting := &importAccounting{} group := loadGroup{inputs: []loadInput{{source: filesystem.File("main.yml"), imports: true}}} - acquired, diagnostics := acquireUnderlayGroupScopes(group, 2, nil, accounting, nil, sequencePolicy{}) + acquired, diagnostics := acquireUnderlayGroupScopes(group, 2, nil, accounting, nil, acquisitionPolicy{importLimits: defaultImportLimits()}) if len(diagnostics) != 0 { t.Fatalf("acquire underlay group: %#v", diagnostics) } @@ -108,7 +108,7 @@ func TestUnderlayGroupAcquisitionRetainsScopesUntilComposition(t *testing.T) { t.Fatalf("composed group handoff = %#v", composition) } - empty, diagnostics := acquireUnderlayGroupScopes(loadGroup{}, 3, nil, accounting, nil, sequencePolicy{}) + empty, diagnostics := acquireUnderlayGroupScopes(loadGroup{}, 3, nil, accounting, nil, acquisitionPolicy{importLimits: defaultImportLimits()}) if len(diagnostics) != 0 || len(empty.scopes) != 0 { t.Fatalf("empty group acquisition = %#v, %#v", empty, diagnostics) } @@ -469,7 +469,7 @@ func TestGroupedLoadStrictConflictsRepetitionAndBudgets(t *testing.T) { } func testAcquireUnderlayGroup(group loadGroup, index int, rule *VersionRule, accounting *importAccounting) (*composedDocument, []SourceInfo, map[string]ResourceContext, []Diagnostic) { - acquired, diagnostics := acquireUnderlayGroupScopes(group, index, rule, accounting, nil, sequencePolicy{}) + acquired, diagnostics := acquireUnderlayGroupScopes(group, index, rule, accounting, nil, acquisitionPolicy{importLimits: defaultImportLimits()}) if len(diagnostics) != 0 { return nil, acquired.sources, acquired.contexts, diagnostics } diff --git a/internal/engine/native_filesystem_test.go b/internal/engine/native_filesystem_test.go index d6e6cf4..7ff839f 100644 --- a/internal/engine/native_filesystem_test.go +++ b/internal/engine/native_filesystem_test.go @@ -182,7 +182,12 @@ func TestNativeFilesystemRootAndOptionalImports(t *testing.T) { t.Fatal(diagnostics) } access := newNativeAccess(filesystem, "config", namespace.token) - composed, sources, diagnostics := loadImportedWithAccess(access, "config/sub", source.path, parsed, nil, defaultImportLimits(), &importAccounting{}) + request := testImportRequest(source, nil, parsed, Path{}, nil, nil) + policy := &acquisitionPolicy{importLimits: defaultImportLimits()} + state := &acquisitionState{accounting: &importAccounting{}} + tier := &tierAcquisitionState{} + resolved := resolvedFilesystemContext{native: namespace, rootPath: "config", basePath: "config/sub", mainPath: source.path} + composed, sources, diagnostics := acquireImportedWithAccess(request, policy, state, tier, access, resolved) if len(diagnostics) != 0 || composed == nil || len(sources) != 2 || sources[1].Path != "config/shared.yml" { t.Fatalf("contained imports = (%#v, %#v, %#v)", composed, sources, diagnostics) } @@ -196,7 +201,12 @@ func TestNativeFilesystemRootAndOptionalImports(t *testing.T) { if len(diagnostics) != 0 { t.Fatal(diagnostics) } - _, _, diagnostics = loadImportedWithAccess(newNativeAccess(outside, "config", outsideNamespace.token), "config/sub", "config/sub/main.yml", parsed, nil, defaultImportLimits(), &importAccounting{}) + request = testImportRequest(filesystemFile(outsideNamespace, "config/sub/main.yml"), nil, parsed, Path{}, nil, nil) + policy = &acquisitionPolicy{importLimits: defaultImportLimits()} + state = &acquisitionState{accounting: &importAccounting{}} + tier = &tierAcquisitionState{} + resolved = resolvedFilesystemContext{native: outsideNamespace, rootPath: "config", basePath: "config/sub", mainPath: "config/sub/main.yml"} + _, _, diagnostics = acquireImportedWithAccess(request, policy, state, tier, newNativeAccess(outside, "config", outsideNamespace.token), resolved) if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportOutsideRoot { t.Fatalf("escaping import diagnostics = %#v", diagnostics) } diff --git a/internal/engine/source_version.go b/internal/engine/source_version.go index a1c368a..22aeed0 100644 --- a/internal/engine/source_version.go +++ b/internal/engine/source_version.go @@ -47,7 +47,6 @@ type sourceVersionState struct { localRule *VersionRule effectiveRule *VersionRule mode sourceVersionMode - importDefaults importDefaults mainProcessing *sourceVersionProcessingResult evidence []*sourceVersionEvidence diagnostics []Diagnostic diff --git a/internal/engine/source_version_test.go b/internal/engine/source_version_test.go index 72221d5..d00df1f 100644 --- a/internal/engine/source_version_test.go +++ b/internal/engine/source_version_test.go @@ -1523,9 +1523,11 @@ func TestSourceVersionImportAccountingIncludesConsumedHeaderNodes(t *testing.T) limits.nodes = wantNodes accounting := &importAccounting{} - composed, sources, foundDiagnostics := loadImportedDocumentsTrackedWithVersion( - File(mainPath), nil, main, Path{}, rule, limits, accounting, nil, nil, state, - ) + request := testImportRequest(File(mainPath), nil, main, Path{}, rule, nil) + policy := acquisitionPolicy{importLimits: limits} + owners := &acquisitionState{accounting: accounting, versionState: state} + tier := &tierAcquisitionState{} + composed, sources, foundDiagnostics := testAcquireImport(request, policy, owners, tier) if len(foundDiagnostics) != 0 || composed == nil || len(sources) != 2 || accounting.nodes != wantNodes || accounting.bytes != wantBytes { t.Fatalf("exact raw byte/node accounting = %#v, %d/%d, want %d/%d, %#v", composed, accounting.bytes, accounting.nodes, wantBytes, wantNodes, foundDiagnostics) } @@ -1539,10 +1541,11 @@ func TestSourceVersionImportAccountingIncludesConsumedHeaderNodes(t *testing.T) below := defaultImportLimits() below.nodes = wantNodes - 1 accounting = &importAccounting{} - composed, _, foundDiagnostics = loadImportedDocumentsTrackedWithVersion( - File(mainPath), nil, main, Path{}, rule, below, accounting, nil, nil, - &sourceVersionState{localRule: rule, effectiveRule: rule, mode: sourceVersionModeRequired}, - ) + request = testImportRequest(File(mainPath), nil, main, Path{}, rule, nil) + policy.importLimits = below + owners = &acquisitionState{accounting: accounting, versionState: &sourceVersionState{localRule: rule, effectiveRule: rule, mode: sourceVersionModeRequired}} + tier = &tierAcquisitionState{} + composed, _, foundDiagnostics = testAcquireImport(request, policy, owners, tier) if composed != nil || len(foundDiagnostics) != 1 || foundDiagnostics[0].Code != CodeImportLimitExceeded { t.Fatalf("raw header nodes were not charged: %#v, %#v", composed, foundDiagnostics) } @@ -1550,10 +1553,11 @@ func TestSourceVersionImportAccountingIncludesConsumedHeaderNodes(t *testing.T) below = defaultImportLimits() below.bytes = wantBytes - 1 accounting = &importAccounting{} - composed, _, foundDiagnostics = loadImportedDocumentsTrackedWithVersion( - File(mainPath), nil, main, Path{}, rule, below, accounting, nil, nil, - &sourceVersionState{localRule: rule, effectiveRule: rule, mode: sourceVersionModeRequired}, - ) + request = testImportRequest(File(mainPath), nil, main, Path{}, rule, nil) + policy.importLimits = below + owners = &acquisitionState{accounting: accounting, versionState: &sourceVersionState{localRule: rule, effectiveRule: rule, mode: sourceVersionModeRequired}} + tier = &tierAcquisitionState{} + composed, _, foundDiagnostics = testAcquireImport(request, policy, owners, tier) if composed != nil || len(foundDiagnostics) != 1 || foundDiagnostics[0].Code != CodeImportLimitExceeded { t.Fatalf("raw header bytes were not charged: %#v, %#v", composed, foundDiagnostics) }