Retain ordered acquisition scopes

This commit is contained in:
2026-10-16 15:48:36 +00:00
parent 66dced0823
commit 1cf3b0d78f
6 changed files with 115 additions and 23 deletions

View File

@@ -7,6 +7,7 @@ import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
// origins retain every source that contributes to a composite mapping.
type composedDocument struct {
root *document.Node
acquisition []acquisitionScope
claims []compositionClaim
sequenceRanges []sequenceRange
origins map[string]Origin
@@ -46,6 +47,7 @@ func newAssemblyAccumulator(owned *composedDocument) *assemblyAccumulator {
func cloneComposedDocument(base *composedDocument) *composedDocument {
return &composedDocument{
root: document.Clone(base.root), claims: cloneCompositionClaims(base.claims),
acquisition: cloneAcquisitionScopes(base.acquisition),
sequenceRanges: cloneSequenceRanges(base.sequenceRanges),
origins: cloneOriginIndex(base.origins), overlays: cloneOverlayRecords(base.overlays),
applications: cloneHistoryApplications(base.applications),

View File

@@ -4,6 +4,8 @@ import (
"path/filepath"
"testing"
"testing/fstest"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
type importDestinationConfig struct {
@@ -133,7 +135,11 @@ parent: true
}
}
composed, diagnostics := composeDocuments(state.documents...)
documents := make([]*document.Node, 0, len(state.scope.contributions))
for _, contribution := range state.scope.contributions {
documents = append(documents, contribution.document)
}
composed, diagnostics := composeDocuments(documents...)
if len(diagnostics) != 0 {
t.Fatalf("composition diagnostics = %#v", diagnostics)
}

View File

@@ -51,11 +51,11 @@ type importTraversal struct {
rule *VersionRule
versionState *sourceVersionState
documents []*document.Node
sources []SourceInfo
active map[resourceIdentity]int
visited map[resourceIdentity]importRoute
stack []importRoute
scope acquisitionScope
sources []SourceInfo
active map[resourceIdentity]int
visited map[resourceIdentity]importRoute
stack []importRoute
}
func loadImportedDocuments(source Source, context *FilesystemContext, main parsedSource, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
@@ -144,9 +144,9 @@ func loadImportedWithAccessTrackedAndProcessing(access *filesystemAccess, basePa
accounting.nodes += countInputNodes(main.document)
state := importTraversal{
access: access, accounting: accounting, limits: limits, rule: rule, versionState: versionState,
documents: make([]*document.Node, 0, 1),
sources: []SourceInfo{mainInfo},
active: make(map[resourceIdentity]int), visited: visited,
scope: acquisitionScope{destination: destination.clone()},
sources: []SourceInfo{mainInfo},
active: make(map[resourceIdentity]int), visited: visited,
}
if state.visited == nil {
state.visited = make(map[resourceIdentity]importRoute)
@@ -182,7 +182,10 @@ func loadImportedWithAccessTrackedAndProcessing(access *filesystemAccess, basePa
diagnostics = []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", nil)}
} else {
accounting.nodes += placementNodes
state.documents = append(state.documents, placed)
state.scope.contributions = append(state.scope.contributions, acquiredContribution{
source: cloneSourceInfo(mainInfo), payload: body, document: placed,
destination: destination.clone(), processing: processing,
})
diagnostics = state.walkDeclarations(basePath, declarations, 0, destination)
}
}
@@ -194,8 +197,13 @@ func loadImportedWithAccessTrackedAndProcessing(access *filesystemAccess, basePa
if closeErr != nil {
return nil, cloneSourceInfos(state.sources), []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed", nil)}
}
composed, diagnostics := composeDocuments(state.documents...)
documents := make([]*document.Node, 0, len(state.scope.contributions))
for _, contribution := range state.scope.contributions {
documents = append(documents, contribution.document)
}
composed, diagnostics := composeDocuments(documents...)
if composed != nil {
composed.acquisition = []acquisitionScope{cloneAcquisitionScope(state.scope)}
composed.inputBytes = accounting.bytes
composed.inputNodes = accounting.nodes
}
@@ -237,20 +245,20 @@ func (s *importTraversal) walkDeclarations(basePath string, declarations []impor
matched := declaration
matched.path = match
matched.glob = ""
if diagnostics := s.visitPath(basePath, matched, childDestination, depth+1); len(diagnostics) != 0 {
if diagnostics := s.visitPath(basePath, matched, childDestination, declaration.destination, depth+1); len(diagnostics) != 0 {
return diagnostics
}
}
continue
}
if diagnostics := s.visitPath(basePath, declaration, childDestination, depth+1); len(diagnostics) != 0 {
if diagnostics := s.visitPath(basePath, declaration, childDestination, declaration.destination, depth+1); len(diagnostics) != 0 {
return diagnostics
}
}
return nil
}
func (s *importTraversal) visitPath(basePath string, declaration importDeclaration, destination Path, depth int) []Diagnostic {
func (s *importTraversal) visitPath(basePath string, declaration importDeclaration, destination, relativeDestination Path, depth int) []Diagnostic {
location := declaration.location
if depth > s.limits.depth {
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the active import-depth limit", &location)}
@@ -334,11 +342,13 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
var body *document.Node
var declarations []importDeclaration
var diagnostics []Diagnostic
var processing *sourceVersionProcessingResult
if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault {
processed, failures := s.versionState.process(root, sourceVersionContribution, destination, sourceID)
if len(failures) != 0 {
return failures
}
processing = &processed
body, declarations = processed.payload, processed.imports
} else {
body, declarations, diagnostics = splitImportDirectives(root)
@@ -368,7 +378,10 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
}
route := importRoute{canonical: canonical, locations: currentRoute.locations}
s.documents = append(s.documents, placed)
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
source: cloneSourceInfo(info), payload: body, document: placed,
destination: destination.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
})
s.active[identity] = len(s.stack)
s.visited[identity] = route
s.stack = append(s.stack, route)

View File

@@ -11,12 +11,58 @@ type assembledInput struct {
parsed parsedSource
document *document.Node
composition *composedDocument
acquisition []acquisitionScope
inheritance *inheritanceScope
sources []SourceInfo
contexts map[string]ResourceContext
versionState *sourceVersionState
}
// acquiredContribution keeps a source-local payload beside its placed document.
// The latter is used for composition; the former preserves the acquisition
// boundary for later scoped assembly and evidence.
type acquiredContribution struct {
source SourceInfo
payload *document.Node
document *document.Node
destination Path
relativeDestination Path
processing *sourceVersionProcessingResult
}
// acquisitionScope is the ordered strict mapping scope produced by one input
// and its ordinary imports.
type acquisitionScope struct {
destination Path
contributions []acquiredContribution
}
func cloneAcquisitionScope(scope acquisitionScope) acquisitionScope {
cloned := acquisitionScope{destination: scope.destination.clone()}
if scope.contributions != nil {
cloned.contributions = make([]acquiredContribution, len(scope.contributions))
for index, contribution := range scope.contributions {
cloned.contributions[index] = acquiredContribution{
source: cloneSourceInfo(contribution.source), payload: contribution.payload,
document: contribution.document, destination: contribution.destination.clone(),
relativeDestination: contribution.relativeDestination.clone(), processing: contribution.processing,
}
}
}
return cloned
}
func cloneAcquisitionScopes(scopes []acquisitionScope) []acquisitionScope {
if scopes == nil {
return nil
}
cloned := make([]acquisitionScope, len(scopes))
for index, scope := range scopes {
cloned[index] = cloneAcquisitionScope(scope)
}
return cloned
}
func assembleBaseInput[T any](source Source, options Options[T]) (assembledInput, []Diagnostic) {
return assembleBaseInputTracked(source, options, Path{}, &importAccounting{}, nil, nil)
}
@@ -76,6 +122,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
if len(diagnostics) != 0 {
return input, diagnostics
}
input.acquisition = cloneAcquisitionScopes(input.composition.acquisition)
input.document = input.composition.root
} else {
input.sources = sourceInfos(parsed.source)
@@ -93,7 +140,15 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
if err != nil {
return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the source depth or node limit after destination placement")}
}
localPayload := input.document
input.document = placed
input.acquisition = []acquisitionScope{{
destination: destination.clone(),
contributions: []acquiredContribution{{
source: cloneSourceInfo(*parsed.source), payload: localPayload,
document: placed, destination: destination.clone(), processing: versionProcessing,
}},
}}
rawNodes := countInputNodes(parsed.document)
placementNodes := destinationWrapperNodeCount(destination)
if accounting.sources+1 > limits.sources || accounting.bytes+parsed.bytes > limits.bytes || accounting.nodes+rawNodes+placementNodes > limits.nodes {
@@ -185,5 +240,6 @@ func compositionForInput(input assembledInput) (*composedDocument, []Diagnostic)
}
composition.inputBytes = input.parsed.bytes
composition.inputNodes = countInputNodes(input.document)
composition.acquisition = cloneAcquisitionScopes(input.acquisition)
return composition, nil
}

View File

@@ -161,7 +161,7 @@ func acquireUnderlayGroupWithVersion(group loadGroup, index int, rule *VersionRu
initialBytes, initialNodes := accounting.bytes, accounting.nodes
visited := make(map[resourceIdentity]importRoute)
hostToken := &namespaceToken{}
var documents []*document.Node
var scopes []acquisitionScope
var sources []SourceInfo
var contexts map[string]ResourceContext
for _, contribution := range group.inputs {
@@ -186,12 +186,12 @@ func acquireUnderlayGroupWithVersion(group loadGroup, index int, rule *VersionRu
Message: "underlay inputs cannot contribute to the schema-version path", Primary: &location,
}}
}
if acquired.composition != nil {
for _, application := range acquired.composition.applications {
documents = append(documents, application.value)
}
} else {
documents = append(documents, acquired.document)
scopes = append(scopes, cloneAcquisitionScopes(acquired.acquisition)...)
}
var documents []*document.Node
for _, scope := range scopes {
for _, contribution := range scope.contributions {
documents = append(documents, contribution.document)
}
}
if len(documents) == 0 {
@@ -203,6 +203,7 @@ func acquireUnderlayGroupWithVersion(group loadGroup, index int, rule *VersionRu
}
composition.inputBytes = accounting.bytes - initialBytes
composition.inputNodes = accounting.nodes - initialNodes
composition.acquisition = cloneAcquisitionScopes(scopes)
for pointer, origin := range composition.origins {
origin.Layer = LayerUnderlay
composition.origins[pointer] = origin