2026-11-02 14:40:04 +00:00
|
|
|
package engine
|
|
|
|
|
|
|
|
|
|
import "go.maximumdirect.net/configkit/internal/document"
|
|
|
|
|
|
|
|
|
|
// collectionCorrespondence describes matching at one concrete registered path.
|
|
|
|
|
// A negative index denotes an identity present only at the other endpoint.
|
|
|
|
|
type collectionCorrespondence struct {
|
|
|
|
|
path Path
|
|
|
|
|
eligible bool
|
|
|
|
|
structural bool
|
|
|
|
|
entryToFinal []int
|
|
|
|
|
finalToEntry []int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type resolutionCorrespondence struct {
|
|
|
|
|
collections map[string]collectionCorrespondence
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:46:13 +00:00
|
|
|
func (c resolutionCorrespondence) clone() resolutionCorrespondence {
|
|
|
|
|
result := resolutionCorrespondence{collections: make(map[string]collectionCorrespondence, len(c.collections))}
|
|
|
|
|
for key, collection := range c.collections {
|
|
|
|
|
collection.path = collection.path.clone()
|
|
|
|
|
collection.entryToFinal = append([]int(nil), collection.entryToFinal...)
|
|
|
|
|
collection.finalToEntry = append([]int(nil), collection.finalToEntry...)
|
|
|
|
|
result.collections[key] = collection
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 14:40:04 +00:00
|
|
|
func buildResolutionCorrespondence(entry, final *document.Node, policy sequencePolicy) resolutionCorrespondence {
|
|
|
|
|
result := resolutionCorrespondence{collections: make(map[string]collectionCorrespondence)}
|
|
|
|
|
var indexes mappingIndex
|
|
|
|
|
var visit func(*document.Node, *document.Node, Path)
|
|
|
|
|
visit = func(before, after *document.Node, path Path) {
|
|
|
|
|
if fields, registered := policy.identityFieldsAt(path); registered &&
|
|
|
|
|
((before != nil && before.Kind == document.Sequence) || (after != nil && after.Kind == document.Sequence)) {
|
|
|
|
|
result.collections[path.String()] = matchResolutionCollection(before, after, path, fields)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if before != nil && before.Kind == document.Mapping {
|
|
|
|
|
for _, item := range before.Mapping {
|
|
|
|
|
var next *document.Node
|
|
|
|
|
if after != nil && after.Kind == document.Mapping {
|
|
|
|
|
if position := indexes.find(after, item.Key); position >= 0 {
|
|
|
|
|
next = after.Mapping[position].Value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
visit(item.Value, next, path.Key(item.Key))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if after != nil && after.Kind == document.Mapping {
|
|
|
|
|
for _, item := range after.Mapping {
|
|
|
|
|
if before == nil || before.Kind != document.Mapping || indexes.find(before, item.Key) < 0 {
|
|
|
|
|
visit(nil, item.Value, path.Key(item.Key))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
visit(entry, final, Path{})
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func matchResolutionCollection(before, after *document.Node, path Path, fields []string) collectionCorrespondence {
|
|
|
|
|
result := collectionCorrespondence{path: path.clone()}
|
|
|
|
|
prior, priorIssues := validateNormalizedSequenceIdentityFrom(before, path, fields, sequenceIdentityDiagnosticCodes{}, 0, sequencePayloadRecord)
|
|
|
|
|
later, laterIssues := validateNormalizedSequenceIdentityFrom(after, path, fields, sequenceIdentityDiagnosticCodes{}, 0, sequencePayloadRecord)
|
|
|
|
|
if len(priorIssues) != 0 || len(laterIssues) != 0 {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
result.eligible = true
|
|
|
|
|
result.entryToFinal = make([]int, len(before.Sequence))
|
|
|
|
|
result.finalToEntry = make([]int, len(after.Sequence))
|
|
|
|
|
for i := range result.entryToFinal {
|
|
|
|
|
result.entryToFinal[i] = -1
|
|
|
|
|
}
|
|
|
|
|
for i := range result.finalToEntry {
|
|
|
|
|
result.finalToEntry[i] = -1
|
|
|
|
|
}
|
|
|
|
|
result.structural = len(prior) != len(later)
|
|
|
|
|
for tuple, previous := range prior {
|
|
|
|
|
if current, found := later[tuple]; found {
|
|
|
|
|
result.entryToFinal[previous.index] = current.index
|
|
|
|
|
result.finalToEntry[current.index] = previous.index
|
|
|
|
|
if previous.index != current.index {
|
|
|
|
|
result.structural = true
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
result.structural = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// entryPath translates a surviving final record prefix; additions have no entry.
|
|
|
|
|
// Paths outside eligible collections and wholly absent final positions stay positional.
|
|
|
|
|
func (c resolutionCorrespondence) entryPath(path Path) (Path, bool) {
|
|
|
|
|
if !path.valid() {
|
|
|
|
|
return Path{}, false
|
|
|
|
|
}
|
|
|
|
|
for depth := 0; depth < len(path.segments); depth++ {
|
|
|
|
|
prefix := Path{segments: path.segments[:depth]}
|
|
|
|
|
collection, found := c.collections[prefix.String()]
|
|
|
|
|
if !found || !collection.eligible {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
compatible := true
|
|
|
|
|
for i, segment := range collection.path.segments {
|
|
|
|
|
if segment.kind == pathSegmentKey && path.segments[i].kind == pathSegmentIndex {
|
|
|
|
|
compatible = false
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
index, ok := schemaIndex(path.segments[depth])
|
|
|
|
|
if !compatible || !ok || index >= len(collection.finalToEntry) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
previous := collection.finalToEntry[index]
|
|
|
|
|
if previous < 0 {
|
|
|
|
|
return Path{}, false
|
|
|
|
|
}
|
|
|
|
|
translated := path.clone()
|
|
|
|
|
translated.segments[depth] = pathSegment{kind: pathSegmentIndex, index: previous}
|
|
|
|
|
return translated, true
|
|
|
|
|
}
|
|
|
|
|
return path.clone(), true
|
|
|
|
|
}
|
2026-11-02 14:56:38 +00:00
|
|
|
|
|
|
|
|
// finalPath translates historical record descendants without matching removals.
|
|
|
|
|
func (c resolutionCorrespondence) finalPath(path Path) (Path, bool) {
|
|
|
|
|
if !path.valid() {
|
|
|
|
|
return Path{}, false
|
|
|
|
|
}
|
|
|
|
|
for depth := 0; depth < len(path.segments); depth++ {
|
|
|
|
|
prefix := Path{segments: path.segments[:depth]}
|
|
|
|
|
collection, found := c.collections[prefix.String()]
|
|
|
|
|
if !found || !collection.eligible {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for i, segment := range collection.path.segments {
|
|
|
|
|
if segment.kind == pathSegmentKey && path.segments[i].kind == pathSegmentIndex {
|
|
|
|
|
return path.clone(), true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
index, ok := schemaIndex(path.segments[depth])
|
|
|
|
|
if !ok || index >= len(collection.entryToFinal) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
current := collection.entryToFinal[index]
|
|
|
|
|
if current < 0 {
|
|
|
|
|
return Path{}, false
|
|
|
|
|
}
|
|
|
|
|
translated := path.clone()
|
|
|
|
|
translated.segments[depth] = pathSegment{kind: pathSegmentIndex, index: current}
|
|
|
|
|
return translated, true
|
|
|
|
|
}
|
|
|
|
|
return path.clone(), true
|
|
|
|
|
}
|