From 729c6fb5d5c01df8196c5c89ed5def5d2096ce68 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 25 Oct 2026 13:28:43 +0000 Subject: [PATCH] Extract imported source admission from traversal --- docs/roadmap/implementation.md | 10 +- internal/engine/import_acquisition.go | 212 +++++++++++++++----------- internal/engine/import_scope.go | 18 +-- 3 files changed, 139 insertions(+), 101 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 10dd1b5..efa15d3 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Consolidation Implementation Plan -Status: in progress. Stages 1 through 12 are complete; follow Stage 13 through Stage 20 in order. +Status: in progress. Stages 1 through 13 are complete; follow Stage 14 through Stage 20 in order. The target executor is a Luna coding agent working through one end-to-end sprint. ## Authority, Baseline, And Execution @@ -534,7 +534,7 @@ 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 +## Stage 13 — Extract Imported-Source Admission From Recursion (Complete) **Owner:** engine `import_acquisition.go` after Stage 10. @@ -560,6 +560,12 @@ required-empty decision. Directory-focused tests and `./scripts/check.sh` passed read/process path and the original scheduling/evidence algorithms. Run A again because Stages 10–13 changed the import implementation after the first checkpoint. +Progress notes (2026-10-25): introduced an explicit admitted-import record and +optional-missing outcome for source read/parse/version processing. `visitPath` +retains containment, identity/cycle checks, graph updates, and recursive +placement coordination; item placement receives the admitted record directly. +Validation group A and `./scripts/check.sh` passed. + ## Stage 14 — Extract Inheritance Child And Parent Preparation **Owners:** engine `inheritance_schedule.go`, new `inheritance_prepare.go`, and diff --git a/internal/engine/import_acquisition.go b/internal/engine/import_acquisition.go index b16361d..7b09fef 100644 --- a/internal/engine/import_acquisition.go +++ b/internal/engine/import_acquisition.go @@ -10,6 +10,29 @@ import ( yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml" ) +type importAdmissionOutcome uint8 + +const ( + importAdmissionReady importAdmissionOutcome = iota + importAdmissionOptionalMissing +) + +type admittedImport struct { + canonical string + identity resourceIdentity + nodeKey importNodeKey + source SourceInfo + payload *document.Node + declarations []importDeclarationSpec + sequenceDeclarations []sequenceDeclarationSpec + processing *sourceVersionProcessingResult +} + +type importAdmissionResult struct { + outcome importAdmissionOutcome + source admittedImport +} + func acquireImportedInput(request importAcquisitionRequest, policy *acquisitionPolicy, state *acquisitionState, tier *tierAcquisitionState) (*composedDocument, []SourceInfo, *resolvedFilesystemContext, []Diagnostic) { source := request.input.input.source context := request.input.input.filesystem @@ -276,6 +299,83 @@ func (s *importTraversal) recordEmptyImportSelection(spec importDeclarationSpec) } } +func (s *importTraversal) admitImportSource(canonical string, identity resourceIdentity, nodeKey importNodeKey, candidate selectedImportCandidate, policy resolvedImportPolicy, destination Path, location Location) (importAdmissionResult, []Diagnostic) { + sourceID := "source-" + strconv.Itoa(s.state.accounting.sources+1) + kind := SourceKindFile + label := candidate.label + if label == "" { + label = candidate.path + } + if s.access.native != nil { + kind = SourceKindFilesystem + label = s.access.label + "/" + canonical + } + info := SourceInfo{ID: sourceID, Label: label, Path: canonical, Destination: s.sourceDestination(destination), Layer: LayerBase, Kind: kind, ImportedFrom: &location} + if policy.placement == placementModeSequenceItem { + info.placementMode = placementModeSequenceItem + } + // A source's own item placement still has an exact sequence-container + // destination. Only an unresolved enclosing item index makes the path a + // fallback container for this snapshot. + info.destinationUnresolved = s.itemScope + if kind == SourceKindFilesystem { + info.namespace = s.access.token + } + s.sources = append(s.sources, info) + data, skipped, diagnostic := s.readImport(canonical, sourceID, policy.required) + if diagnostic != nil { + return importAdmissionResult{}, []Diagnostic{*diagnostic} + } + if skipped { + s.sources = s.sources[:len(s.sources)-1] + return importAdmissionResult{outcome: importAdmissionOptionalMissing}, nil + } + s.state.accounting.sources++ + + root, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{ + Depth: maxDocumentDepth, Nodes: maxDocumentNodes, + }) + if len(issues) != 0 { + return importAdmissionResult{}, parserDiagnostics(sourceID, issues) + } + nodeCount := countInputNodes(root) + if s.state.accounting.nodes+nodeCount > s.policy.importLimits.nodes { + value := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location) + return importAdmissionResult{}, []Diagnostic{value} + } + s.state.accounting.nodes += nodeCount + + directiveContext := s.request.input.directiveContext + if directiveContext == 0 { + directiveContext = rootDirectiveContextImportsOnly + } + versionDestination := destination + if s.itemScope { + versionDestination = appendDestination(s.itemPrefix.Index(0), destination) + } + var processing sourceVersionProcessingResult + if s.state.versionState != nil && s.state.versionState.mode != sourceVersionModeDefault { + processed, failures := s.state.versionState.processWithDirectiveContext(root, sourceVersionContribution, versionDestination, sourceID, policy.placement, directiveContext) + if len(failures) != 0 { + return importAdmissionResult{}, failures + } + processing = processed + } else { + processed := processSourceVersionWithDirectiveContext(root, s.request.input.protectionRule, sourceVersionContribution, versionDestination, sourceVersionModeDefault, directiveContext) + if len(processed.diagnostics) != 0 { + return importAdmissionResult{}, processed.diagnostics + } + processing = processed + } + return importAdmissionResult{source: admittedImport{ + canonical: canonical, identity: identity, nodeKey: nodeKey, + source: cloneSourceInfo(info), payload: processing.payload, + declarations: processing.imports, + sequenceDeclarations: cloneSequenceDeclarations(processing.sequences), + processing: &processing, + }}, nil +} + func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec importDeclarationSpec, policy resolvedImportPolicy, destination, relativeDestination Path, depth int) []Diagnostic { location := candidate.site.location if location.SourceID == "" && location.Line == 0 && location.Column == 0 { @@ -328,83 +428,15 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo 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.state.accounting.sources+1) - kind := SourceKindFile - label := candidate.label - if label == "" { - label = candidate.path + admission, diagnostics := s.admitImportSource(canonical, identity, nodeKey, candidate, policy, destination, location) + if len(diagnostics) != 0 { + return diagnostics } - if s.access.native != nil { - kind = SourceKindFilesystem - label = s.access.label + "/" + canonical - } - info := SourceInfo{ID: sourceID, Label: label, Path: canonical, Destination: s.sourceDestination(destination), Layer: LayerBase, Kind: kind, ImportedFrom: &location} - if policy.placement == placementModeSequenceItem { - info.placementMode = placementModeSequenceItem - } - // A source's own item placement still has an exact sequence-container - // destination. Only an unresolved enclosing item index makes the path a - // fallback container for this snapshot. - info.destinationUnresolved = s.itemScope - if kind == SourceKindFilesystem { - info.namespace = s.access.token - } - s.sources = append(s.sources, info) - data, skipped, diagnostic := s.readImport(canonical, sourceID, policy.required) - if diagnostic != nil { - return []Diagnostic{*diagnostic} - } - if skipped { - s.sources = s.sources[:len(s.sources)-1] + if admission.outcome == importAdmissionOptionalMissing { return nil } - s.state.accounting.sources++ + admitted := admission.source - root, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{ - Depth: maxDocumentDepth, Nodes: maxDocumentNodes, - }) - if len(issues) != 0 { - return parserDiagnostics(sourceID, issues) - } - nodeCount := countInputNodes(root) - if s.state.accounting.nodes+nodeCount > s.policy.importLimits.nodes { - return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)} - } - s.state.accounting.nodes += nodeCount - - var body *document.Node - var declarations []importDeclarationSpec - var sequenceDeclarations []sequenceDeclarationSpec - var diagnostics []Diagnostic - var processing *sourceVersionProcessingResult - directiveContext := s.request.input.directiveContext - if directiveContext == 0 { - directiveContext = rootDirectiveContextImportsOnly - } - 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.state.versionState.processWithDirectiveContext(root, sourceVersionContribution, versionDestination, sourceID, policy.placement, directiveContext) - if len(failures) != 0 { - return failures - } - processing = &processed - body, declarations = processed.payload, processed.imports - } else { - versionDestination := destination - if s.itemScope { - versionDestination = appendDestination(s.itemPrefix.Index(0), destination) - } - processed := processSourceVersionWithDirectiveContext(root, s.request.input.protectionRule, sourceVersionContribution, versionDestination, sourceVersionModeDefault, directiveContext) - if len(processed.diagnostics) != 0 { - return processed.diagnostics - } - processing = &processed - body, declarations = processed.payload, processed.imports - } - sequenceDeclarations = cloneSequenceDeclarations(processing.sequences) placementDestination := destination mode := placementModeMapping if policy.placement == placementModeSequenceItem { @@ -414,19 +446,19 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo if mode == placementModeSequenceItem { role = importRoleItem } - s.importGraph.addNode(importGraphNode{key: nodeKey, canonical: canonical, sourceID: sourceID, role: role}) - s.importGraph.addEdge(importerKey, nodeKey, location) + s.importGraph.addNode(importGraphNode{key: admitted.nodeKey, canonical: admitted.canonical, sourceID: admitted.source.ID, role: role}) + s.importGraph.addEdge(importerKey, admitted.nodeKey, location) if mode == placementModeSequenceItem { - route := importRoute{canonical: canonical, locations: currentRoute.locations, key: nodeKey} - s.active[identity] = len(s.stack) - s.tier.visited[identity] = route + route := importRoute{canonical: admitted.canonical, locations: currentRoute.locations, key: admitted.nodeKey} + s.active[admitted.identity] = len(s.stack) + s.tier.visited[admitted.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) + diagnostics := s.visitItemSource(admitted, destination, relativeDestination, depth) + delete(s.active, admitted.identity) s.stack = s.stack[:len(s.stack)-1] return diagnostics } - _, placed, err := placeDocument(body, placementDestination, body.Location, maxDocumentDepth, maxDocumentNodes) + _, placed, err := placeDocument(admitted.payload, placementDestination, admitted.payload.Location, maxDocumentDepth, maxDocumentNodes) if err != nil { return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", &location)} } @@ -435,18 +467,18 @@ func (s *importTraversal) visitPath(candidate selectedImportCandidate, spec impo return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)} } s.state.accounting.nodes += placementNodes - route := importRoute{canonical: canonical, locations: currentRoute.locations, key: nodeKey} + route := importRoute{canonical: admitted.canonical, locations: currentRoute.locations, key: admitted.nodeKey} s.scope.contributions = append(s.scope.contributions, acquiredContribution{ - source: cloneSourceInfo(info), payload: body, document: placed, - identity: nodeKey, role: role, scopeOwner: s.scope.owner, - destination: placementDestination.clone(), relativeDestination: relativeDestination.clone(), processing: processing, - sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations), + source: cloneSourceInfo(admitted.source), payload: admitted.payload, document: placed, + identity: admitted.nodeKey, role: role, scopeOwner: s.scope.owner, + destination: placementDestination.clone(), relativeDestination: relativeDestination.clone(), processing: admitted.processing, + sequenceDeclarations: cloneSequenceDeclarations(admitted.sequenceDeclarations), }) - s.active[identity] = len(s.stack) - s.tier.visited[identity] = route + s.active[admitted.identity] = len(s.stack) + s.tier.visited[admitted.identity] = route s.stack = append(s.stack, route) - diagnostics = s.walkDeclarations(s.access.dir(canonical), declarations, depth, destination) - delete(s.active, identity) + diagnostics = s.walkDeclarations(s.access.dir(admitted.canonical), admitted.declarations, depth, destination) + delete(s.active, admitted.identity) s.stack = s.stack[:len(s.stack)-1] return diagnostics } diff --git a/internal/engine/import_scope.go b/internal/engine/import_scope.go index 9d3e4ba..db27a5b 100644 --- a/internal/engine/import_scope.go +++ b/internal/engine/import_scope.go @@ -168,10 +168,10 @@ func reservePendingItemPlacements(scope *acquisitionScope, limits importLimits, return nil } -func (s *importTraversal) visitItemSource(canonical string, identity importNodeKey, destination, relativeDestination Path, depth int, info SourceInfo, body *document.Node, declarations []importDeclarationSpec, sequenceDeclarations []sequenceDeclarationSpec, processing *sourceVersionProcessingResult) []Diagnostic { +func (s *importTraversal) visitItemSource(admitted admittedImport, destination, relativeDestination Path, depth int) []Diagnostic { parentScope := s.scope parentItemScope, parentPrefix, parentFallback := s.itemScope, s.itemPrefix, s.itemFallback - s.scope = acquisitionScope{owner: identity} + s.scope = acquisitionScope{owner: admitted.nodeKey} s.itemScope = true if parentItemScope { s.itemPrefix = appendDestination(parentPrefix, destination) @@ -180,11 +180,11 @@ func (s *importTraversal) visitItemSource(canonical string, identity importNodeK s.itemFallback = destination.clone() } s.scope.contributions = append(s.scope.contributions, acquiredContribution{ - source: cloneSourceInfo(info), payload: body, document: body, - identity: identity, role: importRoleItem, scopeOwner: identity, - processing: processing, sequenceDeclarations: cloneSequenceDeclarations(sequenceDeclarations), + source: cloneSourceInfo(admitted.source), payload: admitted.payload, document: admitted.payload, + identity: admitted.nodeKey, role: importRoleItem, scopeOwner: admitted.nodeKey, + processing: admitted.processing, sequenceDeclarations: cloneSequenceDeclarations(admitted.sequenceDeclarations), }) - diagnostics := s.walkDeclarations(s.access.dir(canonical), declarations, depth, Path{}) + diagnostics := s.walkDeclarations(s.access.dir(admitted.canonical), admitted.declarations, depth, Path{}) itemScope := s.scope s.scope, s.itemScope, s.itemPrefix, s.itemFallback = parentScope, parentItemScope, parentPrefix, parentFallback if len(diagnostics) != 0 { @@ -192,9 +192,9 @@ func (s *importTraversal) visitItemSource(canonical string, identity importNodeK } itemScope.destination = Path{} s.scope.contributions = append(s.scope.contributions, acquiredContribution{ - source: cloneSourceInfo(info), payload: body, - identity: identity, role: importRoleItem, scopeOwner: parentScope.owner, - destination: destination.clone(), relativeDestination: relativeDestination.clone(), processing: processing, + source: cloneSourceInfo(admitted.source), payload: admitted.payload, + identity: admitted.nodeKey, role: importRoleItem, scopeOwner: parentScope.owner, + destination: destination.clone(), relativeDestination: relativeDestination.clone(), processing: admitted.processing, pendingItem: &itemScope, }) return nil