Extract imported source admission from traversal

This commit is contained in:
2026-10-25 13:28:43 +00:00
parent d314a269d5
commit 729c6fb5d5
3 changed files with 139 additions and 101 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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