278 lines
11 KiB
Go
278 lines
11 KiB
Go
package engine
|
|
|
|
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
|
|
func prependCompositionDiagnosticPaths(diagnostics []Diagnostic, prefix Path) []Diagnostic {
|
|
cloned := append([]Diagnostic(nil), diagnostics...)
|
|
for index := range cloned {
|
|
if !isCompositionDiagnostic(cloned[index]) {
|
|
continue
|
|
}
|
|
cloned[index].Path = appendDestination(prefix, cloned[index].Path)
|
|
cloned[index].Primary = cloneLocation(cloned[index].Primary)
|
|
cloned[index].Related = cloneLocations(cloned[index].Related)
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func isCompositionDiagnostic(diagnostic Diagnostic) bool {
|
|
return diagnostic.Code == CodeComposeConflict || diagnostic.Code == CodeComposeInvalid
|
|
}
|
|
|
|
// On a composition failure only the earlier contributions are needed to locate
|
|
// this item. Project one enclosing scope at a time as traversal unwinds; syntax
|
|
// and access diagnostics keep their original source-local coordinates.
|
|
func projectItemDiagnosticPaths(diagnostics []Diagnostic, parent acquisitionScope, destination Path, beforeContribution int) []Diagnostic {
|
|
for _, diagnostic := range diagnostics {
|
|
if isCompositionDiagnostic(diagnostic) {
|
|
prefix := parent
|
|
prefix.contributions = parent.contributions[:beforeContribution]
|
|
index := priorSequenceLength([]acquisitionScope{prefix}, destination)
|
|
return prependCompositionDiagnosticPaths(diagnostics, destination.Index(index))
|
|
}
|
|
}
|
|
return diagnostics
|
|
}
|
|
|
|
// This scan is confined to failed loads, not the successful construction path.
|
|
func priorSequenceLength(scopes []acquisitionScope, path Path) int {
|
|
length := 0
|
|
for _, scope := range scopes {
|
|
for _, contribution := range scope.contributions {
|
|
if node, present := documentAtPath(contribution.document, path); present && node.Kind == document.Sequence {
|
|
length += len(node.Sequence)
|
|
}
|
|
}
|
|
}
|
|
return length
|
|
}
|
|
|
|
func projectPriorInputDiagnosticPaths(diagnostics []Diagnostic, scopes []acquisitionScope) []Diagnostic {
|
|
cloned := cloneDiagnostics(diagnostics)
|
|
for index := range cloned {
|
|
if !isCompositionDiagnostic(cloned[index]) {
|
|
continue
|
|
}
|
|
for position, segment := range cloned[index].Path.segments {
|
|
if segment.kind != pathSegmentIndex {
|
|
continue
|
|
}
|
|
container := Path{segments: cloned[index].Path.segments[:position]}
|
|
cloned[index].Path.segments[position].index += priorSequenceLength(scopes, container)
|
|
// Registration does not address paths through item indexes. A prior
|
|
// input shifts the outer collection, not its item-local sequences.
|
|
break
|
|
}
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func projectNestedEvidence(composed *composedDocument, scope acquisitionScope) {
|
|
if composed == nil {
|
|
return
|
|
}
|
|
applications := composed.applications
|
|
projectedApplications := make([]historyApplication, 0, len(applications))
|
|
ranges := indexSequenceRanges(composed.sequenceRanges)
|
|
for sourceIndex, contribution := range scope.contributions {
|
|
if contribution.nested == nil {
|
|
projectedApplications = append(projectedApplications, applications[sourceIndex])
|
|
continue
|
|
}
|
|
itemIndex := 0
|
|
if assigned, found := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: contribution.destination.String()}]; found {
|
|
if projected, ok := assigned.outputForInput(0); ok {
|
|
itemIndex = projected
|
|
}
|
|
}
|
|
prefix := contribution.destination.Index(itemIndex)
|
|
if sourceIndex < len(applications) {
|
|
outer := applications[sourceIndex]
|
|
structural := document.Clone(outer.value)
|
|
if node, found := documentAtPath(structural, contribution.destination); found && node.Kind == document.Sequence && len(node.Sequence) > 0 {
|
|
// The retained fragment is still a local singleton. Its assigned
|
|
// outer index belongs to the projection, not this source tree.
|
|
node.Sequence[0] = &document.Node{Kind: document.Mapping, Location: node.Sequence[0].Location}
|
|
}
|
|
outer.value = structural
|
|
projectedApplications = append(projectedApplications, outer)
|
|
for _, nested := range contribution.nested.applications {
|
|
projected := nested
|
|
projected.path = appendDestination(prefix, nested.path)
|
|
projected.sequences = cloneSequenceHistoryContributions(nested.sequences)
|
|
projected.sequencesByPath = indexSequenceHistoryContributions(projected.sequences)
|
|
projectedApplications = append(projectedApplications, projected)
|
|
}
|
|
}
|
|
projectNestedOrigins(composed.origins, contribution.nested.root, contribution.nested.origins, prefix)
|
|
}
|
|
composed.applications = projectedApplications
|
|
projectNestedCollectionPolicies(composed, scope, ranges)
|
|
}
|
|
|
|
func projectNestedCollectionPolicies(composed *composedDocument, scope acquisitionScope, ranges map[sequenceRangeKey]sequenceRange) {
|
|
if composed == nil {
|
|
return
|
|
}
|
|
type itemPolicyProjection struct {
|
|
destination Path
|
|
policy collectionPolicy
|
|
base int
|
|
}
|
|
projected := make(map[string]*itemPolicyProjection)
|
|
var prefixLengths map[string]int
|
|
for sourceIndex, contribution := range scope.contributions {
|
|
if prefixLengths != nil && sourceIndex > 0 {
|
|
addSequencePrefixLengths(scope.contributions[sourceIndex-1].document, Path{}, prefixLengths)
|
|
}
|
|
if contribution.nested == nil || len(contribution.nested.collectionPolicies.collections) == 0 {
|
|
continue
|
|
}
|
|
key := contribution.destination.String()
|
|
assigned := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: key}]
|
|
itemIndex, found := assigned.outputForInput(0)
|
|
if !found {
|
|
// Normal composition supplies the assignment directly. For a
|
|
// handoff without ranges, scan each preceding tree only once.
|
|
if prefixLengths == nil {
|
|
prefixLengths = make(map[string]int)
|
|
for _, previous := range scope.contributions[:sourceIndex] {
|
|
addSequencePrefixLengths(previous.document, Path{}, prefixLengths)
|
|
}
|
|
}
|
|
itemIndex = prefixLengths[key]
|
|
}
|
|
working, initialized := projected[key]
|
|
if !initialized {
|
|
// Read one owned copy per destination, then add each incoming
|
|
// record without recopying earlier records and their scopes.
|
|
policy, _ := composed.collectionPolicies.at(contribution.destination)
|
|
working = &itemPolicyProjection{destination: contribution.destination, policy: policy, base: len(policy.recordOrder)}
|
|
projected[key] = working
|
|
}
|
|
working.policy.setRecordPolicyAt(working.base+itemIndex, collectionRecordPolicy{
|
|
owner: newCollectionRecordPolicy().owner,
|
|
scope: contribution.nested.collectionPolicies,
|
|
})
|
|
}
|
|
for _, working := range projected {
|
|
composed.collectionPolicies.set(working.destination, working.policy)
|
|
}
|
|
}
|
|
|
|
func addSequencePrefixLengths(node *document.Node, path Path, lengths map[string]int) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
if node.Kind == document.Sequence {
|
|
lengths[path.String()] += len(node.Sequence)
|
|
return
|
|
}
|
|
// Contribution destinations select mapping paths, not sequence members.
|
|
for _, entry := range node.Mapping {
|
|
addSequencePrefixLengths(entry.Value, path.Key(entry.Key), lengths)
|
|
}
|
|
}
|
|
|
|
func projectNestedOrigins(target map[string]Origin, root *document.Node, origins map[string]Origin, prefix Path) {
|
|
var visit func(*document.Node, Path)
|
|
visit = func(node *document.Node, path Path) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
if origin, ok := origins[path.String()]; ok {
|
|
target[appendDestination(prefix, path).String()] = cloneOrigin(origin)
|
|
}
|
|
for _, entry := range node.Mapping {
|
|
visit(entry.Value, path.Key(entry.Key))
|
|
}
|
|
for index, child := range node.Sequence {
|
|
visit(child, path.Index(index))
|
|
}
|
|
}
|
|
visit(root, Path{})
|
|
}
|
|
|
|
func projectAcquiredDestinations(composed *composedDocument, scope acquisitionScope, sources []SourceInfo) {
|
|
byID := make(map[string]int, len(sources))
|
|
for index := range sources {
|
|
byID[sources[index].ID] = index
|
|
}
|
|
var projectScope func(*composedDocument, acquisitionScope, Path, bool, string)
|
|
projectScope = func(currentComposition *composedDocument, current acquisitionScope, prefix Path, insideItem bool, parentSourceID string) {
|
|
ranges := indexSequenceRanges(currentComposition.sequenceRanges)
|
|
for sourceIndex, contribution := range current.contributions {
|
|
if contribution.source.ID == parentSourceID {
|
|
continue
|
|
}
|
|
contributionPrefix := prefix
|
|
contributionInsideItem := insideItem
|
|
itemIndex := 0
|
|
if assigned, found := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: contribution.destination.String()}]; found {
|
|
if projected, ok := assigned.outputForInput(0); ok {
|
|
itemIndex = projected
|
|
}
|
|
}
|
|
if contribution.source.placementMode == PlacementModeSequenceItem {
|
|
container := appendDestination(contributionPrefix, contribution.destination)
|
|
contribution.source.Destination = container.clone()
|
|
contribution.source.destinationUnresolved = false
|
|
current.contributions[sourceIndex].source = cloneSourceInfo(contribution.source)
|
|
if position, ok := byID[contribution.source.ID]; ok {
|
|
sources[position].Destination = container
|
|
sources[position].destinationUnresolved = false
|
|
}
|
|
contributionPrefix = appendDestination(container, Path{}.Index(itemIndex))
|
|
contributionInsideItem = true
|
|
projectSourceVersionEvidence(contribution.processing, contributionPrefix, nil, sourceIndex)
|
|
} else if contributionInsideItem {
|
|
exactDestination := appendDestination(contributionPrefix, contribution.destination)
|
|
contribution.source.Destination = exactDestination.clone()
|
|
contribution.source.destinationUnresolved = false
|
|
current.contributions[sourceIndex].source = cloneSourceInfo(contribution.source)
|
|
if position, ok := byID[contribution.source.ID]; ok {
|
|
sources[position].Destination = exactDestination
|
|
sources[position].destinationUnresolved = false
|
|
}
|
|
projectSourceVersionEvidence(contribution.processing, exactDestination, nil, sourceIndex)
|
|
} else {
|
|
projectSourceVersionEvidence(contribution.processing, contribution.destination, ranges, sourceIndex)
|
|
}
|
|
if contribution.nested != nil && contribution.nested.acquisition != nil {
|
|
for _, nestedScope := range contribution.nested.acquisition {
|
|
projectScope(contribution.nested, nestedScope, contributionPrefix, contributionInsideItem, contribution.source.ID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
projectScope(composed, scope, Path{}, false, "")
|
|
}
|
|
|
|
func projectSourceVersionEvidence(processing *sourceVersionProcessingResult, destination Path, ranges sequenceRangeIndex, sourceIndex int) {
|
|
if processing != nil && processing.evidence != nil {
|
|
evidence := processing.evidence
|
|
evidence.destination = destination.clone()
|
|
// Rebuild from source-local paths on each handoff: an input can be
|
|
// composed first on its own and then at a different offset in its tier.
|
|
evidence.projectedPaths = nil
|
|
paths := evidence.sensitivityPaths()
|
|
for index, path := range paths {
|
|
for position := range path.segments {
|
|
prefix := Path{segments: path.segments[:position]}
|
|
assigned, found := ranges[sequenceRangeKey{sourceIndex: sourceIndex, path: prefix.String()}]
|
|
if !found {
|
|
continue
|
|
}
|
|
item, ok := schemaIndex(path.segments[position])
|
|
if ok && path.segments[position].kind != pathSegmentKey {
|
|
if outputIndex, projected := assigned.outputForInput(item); projected {
|
|
path.segments[position] = pathSegment{kind: pathSegmentIndex, index: outputIndex}
|
|
}
|
|
}
|
|
}
|
|
paths[index] = path
|
|
}
|
|
evidence.projectedPaths = paths
|
|
}
|
|
}
|