Retain ordered acquisition scopes
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user