288 lines
13 KiB
Go
288 lines
13 KiB
Go
package engine
|
|
|
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
|
|
func compositionTraversal(policy sequencePolicy, rule *VersionRule, versionState *sourceVersionState, accounting *importAccounting, graph importInclusionGraph) importTraversal {
|
|
return importTraversal{
|
|
request: importAcquisitionRequest{input: inputAcquisitionRequest{protectionRule: rule}},
|
|
policy: &acquisitionPolicy{sequencePolicy: policy, importLimits: defaultImportLimits()},
|
|
state: &acquisitionState{accounting: accounting, versionState: versionState},
|
|
tier: &tierAcquisitionState{},
|
|
importGraph: graph.clone(),
|
|
}
|
|
}
|
|
|
|
// composeAcquiredInputWithPolicy is the per-input adapter from retained
|
|
// acquisition scopes to the established strict-composition behavior. It uses
|
|
// the already acquired documents and graph; source handles are closed before
|
|
// this handoff.
|
|
func composeAcquiredInputWithPolicies(input assembledInput, lower collectionPolicyScope, policy sequencePolicy) (*composedDocument, []Diagnostic) {
|
|
if input.composition == nil || input.composition.root != nil || len(input.composition.acquisition) == 0 {
|
|
return input.composition, nil
|
|
}
|
|
acquired := input.composition
|
|
if len(acquired.acquisition) != 1 {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportInvalid, "acquired input has an invalid composition scope", nil)}
|
|
}
|
|
scope := cloneAcquisitionScope(acquired.acquisition[0])
|
|
var rule *VersionRule
|
|
if input.versionState != nil {
|
|
rule = input.versionState.effectiveRule
|
|
}
|
|
accounting := input.accounting
|
|
if accounting == nil {
|
|
accounting = &importAccounting{sources: len(input.sources), bytes: acquired.inputBytes, nodes: acquired.inputNodes}
|
|
}
|
|
traversal := compositionTraversal(policy, rule, input.versionState, accounting, acquired.importGraph)
|
|
if len(scope.contributions) == 1 && scope.contributions[0].role == importRoleMain && scope.contributions[0].pendingItem != nil {
|
|
main := scope.contributions[0]
|
|
localScope := cloneAcquisitionScope(*main.pendingItem)
|
|
if diagnostics := traversal.composePendingItems(&localScope); len(diagnostics) != 0 {
|
|
return nil, prependCompositionDiagnosticPaths(diagnostics, main.destination.Index(0))
|
|
}
|
|
documents := make([]*document.Node, 0, len(localScope.contributions))
|
|
for _, contribution := range localScope.contributions {
|
|
documents = append(documents, contribution.document)
|
|
}
|
|
completed, diagnostics := composeDocumentsWithCollectionPolicy(
|
|
policy, localScope.collectionPolicies,
|
|
acquiredSequenceDeclarations(localScope), documents...,
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
return nil, prependCompositionDiagnosticPaths(diagnostics, main.destination.Index(0))
|
|
}
|
|
if input.versionState != nil {
|
|
protectionRule := input.versionState.localRule
|
|
if input.versionState.mode == sourceVersionModeDefault && protectionRule != nil {
|
|
prefix := main.destination.Index(0)
|
|
relative, inside := relativeHistoryPath(prefix, protectionRule.Path)
|
|
if !inside || !matchesPathShape(Path{segments: protectionRule.Path.segments[:len(prefix.segments)]}, prefix) {
|
|
protectionRule = nil
|
|
} else {
|
|
scoped := *protectionRule
|
|
scoped.Path = relative
|
|
protectionRule = &scoped
|
|
}
|
|
}
|
|
if diagnostic := importedVersionClaim(documents, localScope, completed, protectionRule); diagnostic != nil {
|
|
if rule != nil {
|
|
diagnostic.Path = rule.Path.clone()
|
|
}
|
|
return nil, []Diagnostic{*diagnostic}
|
|
}
|
|
}
|
|
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(localScope)}
|
|
projectNestedEvidence(completed, localScope)
|
|
_, placed, err := placeDocumentWithMode(completed.root, main.destination, placementModeSequenceItem, main.payload.Location, maxDocumentDepth, maxDocumentNodes)
|
|
if err != nil {
|
|
return nil, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, main.source.ID, "configuration exceeds the source depth or node limit after destination placement")}
|
|
}
|
|
if !main.placementCharged {
|
|
placementNodes := destinationWrapperNodeCountForMode(main.destination, placementModeSequenceItem)
|
|
if accounting.nodes+placementNodes > traversal.policy.importLimits.nodes {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", main.source.ImportedFrom)}
|
|
}
|
|
accounting.nodes += placementNodes
|
|
}
|
|
composition, diagnostics := composeDocumentsWithPolicy(policy, placed)
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
synthetic := acquisitionScope{owner: main.scopeOwner, contributions: []acquiredContribution{{
|
|
source: cloneSourceInfo(main.source), payload: completed.root, document: placed,
|
|
identity: main.identity, role: importRoleMain, scopeOwner: main.scopeOwner,
|
|
destination: main.destination.clone(), processing: main.processing, nested: completed,
|
|
}}}
|
|
projectNestedEvidence(composition, synthetic)
|
|
projectAcquiredDestinations(composition, synthetic, input.sources)
|
|
composition.acquisition = []acquisitionScope{cloneAcquisitionScope(synthetic)}
|
|
composition.inputBytes = accounting.bytes
|
|
composition.inputNodes = accounting.nodes
|
|
composition.importGraph = acquired.importGraph.clone()
|
|
return composition, nil
|
|
}
|
|
return composeAcquiredScopeWithPolicy(
|
|
&scope, lower, policy, acquired.importGraph, input.sources, rule, input.versionState, accounting,
|
|
)
|
|
}
|
|
|
|
// composeAcquiredScopeWithPolicy is shared by per-input and underlay-group
|
|
// handoffs. Lower policy is carried separately from registered application
|
|
// constraints; current strict composition continues to use its existing rules.
|
|
func composeAcquiredScopeWithPolicy(
|
|
scope *acquisitionScope,
|
|
lower collectionPolicyScope,
|
|
policy sequencePolicy,
|
|
graph importInclusionGraph,
|
|
sources []SourceInfo,
|
|
rule *VersionRule,
|
|
versionState *sourceVersionState,
|
|
accounting *importAccounting,
|
|
) (*composedDocument, []Diagnostic) {
|
|
traversal := compositionTraversal(policy, rule, versionState, accounting, graph)
|
|
if diagnostics := traversal.composePendingItems(scope); len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
documents := make([]*document.Node, 0, len(scope.contributions))
|
|
for _, contribution := range scope.contributions {
|
|
documents = append(documents, contribution.document)
|
|
}
|
|
composition, diagnostics := composeDocumentsWithCollectionPolicy(
|
|
policy, lower, acquiredSequenceDeclarations(*scope), documents...,
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
if diagnostic := importedVersionClaim(documents, *scope, composition, rule); diagnostic != nil {
|
|
return nil, []Diagnostic{*diagnostic}
|
|
}
|
|
projectNestedEvidence(composition, *scope)
|
|
projectAcquiredDestinations(composition, *scope, sources)
|
|
composition.acquisition = []acquisitionScope{cloneAcquisitionScope(*scope)}
|
|
composition.inputBytes = accounting.bytes
|
|
composition.inputNodes = accounting.nodes
|
|
composition.importGraph = graph.clone()
|
|
return composition, nil
|
|
}
|
|
|
|
func reservePendingItemPlacements(scope *acquisitionScope, limits importLimits, accounting *importAccounting) *Diagnostic {
|
|
if scope == nil {
|
|
return nil
|
|
}
|
|
for index := range scope.contributions {
|
|
contribution := &scope.contributions[index]
|
|
if contribution.pendingItem == nil {
|
|
continue
|
|
}
|
|
placementNodes := destinationWrapperNodeCountForMode(contribution.destination, placementModeSequenceItem)
|
|
if accounting.nodes+placementNodes > limits.nodes {
|
|
diagnostic := importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", contribution.source.ImportedFrom)
|
|
return &diagnostic
|
|
}
|
|
accounting.nodes += placementNodes
|
|
contribution.placementCharged = true
|
|
if diagnostic := reservePendingItemPlacements(contribution.pendingItem, limits, accounting); diagnostic != nil {
|
|
return diagnostic
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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: admitted.nodeKey}
|
|
s.itemScope = true
|
|
if parentItemScope {
|
|
s.itemPrefix = appendDestination(parentPrefix, destination)
|
|
} else {
|
|
s.itemPrefix = destination.clone()
|
|
s.itemFallback = destination.clone()
|
|
}
|
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
|
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(admitted.canonical), admitted.declarations, depth, Path{}, admitted.sourceIndex)
|
|
itemScope := s.scope
|
|
s.scope, s.itemScope, s.itemPrefix, s.itemFallback = parentScope, parentItemScope, parentPrefix, parentFallback
|
|
if len(diagnostics) != 0 {
|
|
return diagnostics
|
|
}
|
|
itemScope.destination = Path{}
|
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
|
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
|
|
}
|
|
|
|
func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnostic {
|
|
for index := range scope.contributions {
|
|
contribution := &scope.contributions[index]
|
|
if contribution.pendingItem == nil {
|
|
continue
|
|
}
|
|
itemScope := contribution.pendingItem
|
|
if diagnostics := s.composePendingItems(itemScope); len(diagnostics) != 0 {
|
|
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination, index)
|
|
}
|
|
documents := make([]*document.Node, 0, len(itemScope.contributions))
|
|
for _, nested := range itemScope.contributions {
|
|
documents = append(documents, nested.document)
|
|
}
|
|
// An item scope is below a sequence index. Application selectors match
|
|
// mapping-only effective paths, so none can apply inside this scope.
|
|
// Its YAML declarations remain relative to the owning record.
|
|
completed, diagnostics := composeDocumentsWithCollectionPolicy(
|
|
sequencePolicy{}, itemScope.collectionPolicies,
|
|
acquiredSequenceDeclarations(*itemScope), documents...,
|
|
)
|
|
if len(diagnostics) != 0 {
|
|
return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination, index)
|
|
}
|
|
// Version ownership is checked by the enclosing composition once this
|
|
// item has its effective coordinates, not against its local field names.
|
|
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(*itemScope)}
|
|
projectNestedEvidence(completed, *itemScope)
|
|
_, placed, err := placeDocumentWithMode(completed.root, contribution.destination, placementModeSequenceItem, contribution.payload.Location, maxDocumentDepth, maxDocumentNodes)
|
|
if err != nil {
|
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the source depth or node limit after destination placement", contribution.source.ImportedFrom)}
|
|
}
|
|
placementNodes := destinationWrapperNodeCountForMode(contribution.destination, placementModeSequenceItem)
|
|
if !contribution.placementCharged {
|
|
if s.state.accounting.nodes+placementNodes > s.policy.importLimits.nodes {
|
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", contribution.source.ImportedFrom)}
|
|
}
|
|
s.state.accounting.nodes += placementNodes
|
|
}
|
|
contribution.payload = completed.root
|
|
contribution.document = placed
|
|
contribution.nested = completed
|
|
contribution.pendingItem = nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func acquiredSequenceDeclarations(scope acquisitionScope) []sequenceDeclarationSpec {
|
|
var declarations []sequenceDeclarationSpec
|
|
for _, contribution := range scope.contributions {
|
|
projected := cloneSequenceDeclarations(contribution.sequenceDeclarations)
|
|
for index := range projected {
|
|
projected[index].destination = appendDestination(contribution.relativeDestination, projected[index].destination)
|
|
}
|
|
declarations = append(declarations, projected...)
|
|
}
|
|
return declarations
|
|
}
|
|
|
|
func importedVersionClaim(documents []*document.Node, scope acquisitionScope, composed *composedDocument, rule *VersionRule) *Diagnostic {
|
|
if composed == nil || rule == nil {
|
|
return nil
|
|
}
|
|
mainIndex := -1
|
|
for index, contribution := range scope.contributions {
|
|
if contribution.role == importRoleMain {
|
|
mainIndex = index
|
|
break
|
|
}
|
|
}
|
|
ranges := indexSequenceRanges(composed.sequenceRanges)
|
|
for index, contribution := range documents {
|
|
if index == mainIndex {
|
|
continue
|
|
}
|
|
if node := fragmentVersionClaimWithIndex(contribution, rule, index, ranges); node != nil {
|
|
location := publicLocation(node.Location)
|
|
return &Diagnostic{
|
|
Code: CodeImportVersion, Severity: SeverityError, Path: rule.Path.clone(),
|
|
Message: "imported fragments cannot contribute to the schema-version path", Primary: &location,
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|