Retain ordered acquisition scopes
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -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
|
||||
`./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
|
||||
|
||||
Scope: placement primitives and engine per-input options; not public exposure.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ type importTraversal struct {
|
||||
rule *VersionRule
|
||||
versionState *sourceVersionState
|
||||
|
||||
documents []*document.Node
|
||||
scope acquisitionScope
|
||||
sources []SourceInfo
|
||||
active map[resourceIdentity]int
|
||||
visited map[resourceIdentity]importRoute
|
||||
@@ -144,7 +144,7 @@ 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),
|
||||
scope: acquisitionScope{destination: destination.clone()},
|
||||
sources: []SourceInfo{mainInfo},
|
||||
active: make(map[resourceIdentity]int), visited: visited,
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
scopes = append(scopes, cloneAcquisitionScopes(acquired.acquisition)...)
|
||||
}
|
||||
} else {
|
||||
documents = append(documents, acquired.document)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user