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

@@ -1,6 +1,6 @@
# Implementation Plan: Sequence Placement and Composition # Implementation Plan: Sequence Placement and Composition
Status: implementation in progress; stages 1–5 completed. Status: implementation in progress; stages 1–6 completed.
## Purpose and authority ## Purpose and authority
@@ -414,6 +414,20 @@ Scope: `imports.go`, `load_input.go`, `load_request.go`, and their handoffs.
Validation: import/destination/grouped/source-version/resource-context tests and Validation: import/destination/grouped/source-version/resource-context tests and
`./scripts/check.sh`. Completion: this is a behavior-preserving private refactor. `./scripts/check.sh`. Completion: this is a behavior-preserving private refactor.
Completion evidence: acquisition now retains ordered ordinary-mapping scopes
with source-local payloads, placed contributions, destinations, source records,
and source-version processing results. Underlay groups compose those acquired
contributions at their boundary instead of treating retained history values as
complete roots. Existing filesystem contexts, import traversal/repetition
state, load-wide accounting, hooks, and lifecycle handling remain in their
existing paths. These checks passed with Go `go1.26.9 linux/amd64`:
- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'Destination|Import|Grouped|SourceVersion|Resource' -count=1`
- `GOTOOLCHAIN=local ./scripts/check.sh`
- `git diff --check`
No environmental limitation affected validation; checks ran on Linux/amd64.
## Stage 7 — Add the private item-placement operation and accounting ## Stage 7 — Add the private item-placement operation and accounting
Scope: placement primitives and engine per-input options; not public exposure. Scope: placement primitives and engine per-input options; not public exposure.

View File

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

View File

@@ -4,6 +4,8 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"testing/fstest" "testing/fstest"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
) )
type importDestinationConfig struct { 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 { if len(diagnostics) != 0 {
t.Fatalf("composition diagnostics = %#v", diagnostics) t.Fatalf("composition diagnostics = %#v", diagnostics)
} }

View File

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

View File

@@ -11,12 +11,58 @@ type assembledInput struct {
parsed parsedSource parsed parsedSource
document *document.Node document *document.Node
composition *composedDocument composition *composedDocument
acquisition []acquisitionScope
inheritance *inheritanceScope inheritance *inheritanceScope
sources []SourceInfo sources []SourceInfo
contexts map[string]ResourceContext contexts map[string]ResourceContext
versionState *sourceVersionState 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) { func assembleBaseInput[T any](source Source, options Options[T]) (assembledInput, []Diagnostic) {
return assembleBaseInputTracked(source, options, Path{}, &importAccounting{}, nil, nil) 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 { if len(diagnostics) != 0 {
return input, diagnostics return input, diagnostics
} }
input.acquisition = cloneAcquisitionScopes(input.composition.acquisition)
input.document = input.composition.root input.document = input.composition.root
} else { } else {
input.sources = sourceInfos(parsed.source) input.sources = sourceInfos(parsed.source)
@@ -93,7 +140,15 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
if err != nil { if err != nil {
return input, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, parsed.source.ID, "configuration exceeds the source depth or node limit after destination placement")} 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.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) rawNodes := countInputNodes(parsed.document)
placementNodes := destinationWrapperNodeCount(destination) placementNodes := destinationWrapperNodeCount(destination)
if accounting.sources+1 > limits.sources || accounting.bytes+parsed.bytes > limits.bytes || accounting.nodes+rawNodes+placementNodes > limits.nodes { 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.inputBytes = input.parsed.bytes
composition.inputNodes = countInputNodes(input.document) composition.inputNodes = countInputNodes(input.document)
composition.acquisition = cloneAcquisitionScopes(input.acquisition)
return composition, nil return composition, nil
} }

View File

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