441 lines
13 KiB
Go
441 lines
13 KiB
Go
package engine
|
|
|
|
import (
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
const historyNodeLimit = 4_000_000
|
|
|
|
type retainedNodeCounter struct {
|
|
seen map[*document.Node]bool
|
|
count int
|
|
limit int
|
|
}
|
|
|
|
func newRetainedNodeCounter(limit int) *retainedNodeCounter {
|
|
return &retainedNodeCounter{seen: make(map[*document.Node]bool), limit: limit}
|
|
}
|
|
|
|
func (c *retainedNodeCounter) add(root *document.Node) bool {
|
|
if c == nil {
|
|
return true
|
|
}
|
|
stack := []*document.Node{root}
|
|
for len(stack) != 0 {
|
|
node := stack[len(stack)-1]
|
|
stack = stack[:len(stack)-1]
|
|
if node == nil || c.seen[node] {
|
|
continue
|
|
}
|
|
c.seen[node] = true
|
|
c.count++
|
|
if c.limit >= 0 && c.count > c.limit {
|
|
return false
|
|
}
|
|
for _, entry := range node.Mapping {
|
|
stack = append(stack, entry.Value)
|
|
}
|
|
stack = append(stack, node.Sequence...)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// History applications borrow immutable policy-validated input trees. Metadata
|
|
// records answer presence and origin queries; replay reconstructs value changes.
|
|
// Replay reconciles with canonical decoding before domain resolution so both
|
|
// contracts agree on the effective result without retaining a tree per field.
|
|
type historyApplication struct {
|
|
layer Layer
|
|
path Path
|
|
value *document.Node
|
|
origin Origin
|
|
sequences []sequenceHistoryContribution
|
|
}
|
|
|
|
func cloneHistoryApplications(values []historyApplication) []historyApplication {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
cloned := make([]historyApplication, len(values))
|
|
for index, value := range values {
|
|
cloned[index] = historyApplication{
|
|
layer: value.layer, path: value.path.clone(), value: value.value,
|
|
origin: cloneOrigin(value.origin), sequences: cloneSequenceHistoryContributions(value.sequences),
|
|
}
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func applicationOperationPath(application historyApplication, query Path, lookup *documentLookup) (Path, bool) {
|
|
if !application.path.valid() || application.value == nil {
|
|
return Path{}, false
|
|
}
|
|
if application.path.String() != "" {
|
|
if pathsIntersect(application.path, query) {
|
|
return application.path.clone(), true
|
|
}
|
|
return Path{}, false
|
|
}
|
|
sequencePathMatched := false
|
|
for _, contribution := range application.sequences {
|
|
if !pathContains(contribution.path, query) && !pathContains(query, contribution.path) {
|
|
continue
|
|
}
|
|
sequencePathMatched = true
|
|
if pathContains(contribution.path, query) && len(query.segments) > len(contribution.path.segments) {
|
|
segment := query.segments[len(contribution.path.segments)]
|
|
itemIndex, ok := schemaIndex(segment)
|
|
if !ok || itemIndex < contribution.offset || itemIndex >= contribution.offset+contribution.length {
|
|
continue
|
|
}
|
|
}
|
|
return query.clone(), true
|
|
}
|
|
if sequencePathMatched {
|
|
return Path{}, false
|
|
}
|
|
if len(query.segments) == 0 {
|
|
return Path{}, true
|
|
}
|
|
for length := 1; length <= len(query.segments); length++ {
|
|
prefix := Path{segments: clonePathSegments(query.segments[:length], 0)}
|
|
node, present := documentAtPathWithLookup(application.value, prefix, lookup)
|
|
if !present {
|
|
return Path{}, false
|
|
}
|
|
if length == len(query.segments) || node.Kind != document.Mapping {
|
|
return prefix, true
|
|
}
|
|
}
|
|
return Path{}, false
|
|
}
|
|
|
|
func historyApplicationOrigin(application historyApplication, operationPath Path, lookup *documentLookup) Origin {
|
|
if application.origin.Layer != "" {
|
|
return cloneOrigin(application.origin)
|
|
}
|
|
origin := Origin{Layer: application.layer}
|
|
relative, ok := relativeHistoryPath(application.path, operationPath)
|
|
if !ok {
|
|
return origin
|
|
}
|
|
for _, contribution := range application.sequences {
|
|
if !pathContains(contribution.path, operationPath) {
|
|
continue
|
|
}
|
|
relative, ok = relativeHistoryPath(Path{}, operationPath)
|
|
if !ok || len(relative.segments) < len(contribution.path.segments)+1 {
|
|
break
|
|
}
|
|
itemPosition := len(contribution.path.segments)
|
|
itemIndex, valid := schemaIndex(relative.segments[itemPosition])
|
|
if !valid || itemIndex < contribution.offset || itemIndex >= contribution.offset+contribution.length {
|
|
break
|
|
}
|
|
relative.segments[itemPosition] = pathSegment{kind: pathSegmentIndex, index: itemIndex - contribution.offset}
|
|
break
|
|
}
|
|
if node, present := documentAtPathWithLookup(application.value, relative, lookup); present {
|
|
origin.Locations = []Location{publicLocation(node.Location)}
|
|
}
|
|
return origin
|
|
}
|
|
|
|
func relativeHistoryPath(root, path Path) (Path, bool) {
|
|
if !pathContains(root, path) {
|
|
return Path{}, false
|
|
}
|
|
return Path{segments: clonePathSegments(path.segments[len(root.segments):], 0)}, true
|
|
}
|
|
|
|
func pathsIntersect(left, right Path) bool {
|
|
return pathContains(left, right) || pathContains(right, left)
|
|
}
|
|
|
|
func applyHistoryApplication(root **document.Node, application historyApplication, index *mappingIndex) {
|
|
if application.path.String() == "" {
|
|
mergeHistoryDocumentAtPath(root, application.value, Path{}, index, application.sequences)
|
|
return
|
|
}
|
|
mergeHistoryAtPath(root, application.path, application.value, index)
|
|
}
|
|
|
|
func mergeHistoryDocument(target **document.Node, incoming *document.Node, index *mappingIndex) {
|
|
mergeHistoryDocumentAtPath(target, incoming, Path{}, index, nil)
|
|
}
|
|
|
|
func mergeHistoryDocumentAtPath(target **document.Node, incoming *document.Node, path Path, index *mappingIndex, sequences []sequenceHistoryContribution) {
|
|
if incoming == nil {
|
|
return
|
|
}
|
|
if *target == nil || (*target).Kind != document.Mapping || incoming.Kind != document.Mapping {
|
|
*target = document.Clone(incoming)
|
|
return
|
|
}
|
|
for _, entry := range incoming.Mapping {
|
|
position := index.find(*target, entry.Key)
|
|
if position < 0 {
|
|
cloned := entry
|
|
cloned.Value = document.Clone(entry.Value)
|
|
index.append(*target, cloned)
|
|
continue
|
|
}
|
|
entryPath := path.Key(entry.Key)
|
|
if contribution, found := sequenceHistoryAt(sequences, entryPath); found {
|
|
applySequenceHistory(&(*target).Mapping[position].Value, entry.Value, contribution)
|
|
continue
|
|
}
|
|
mergeHistoryDocumentAtPath(&(*target).Mapping[position].Value, entry.Value, entryPath, index, sequences)
|
|
}
|
|
}
|
|
|
|
func sequenceHistoryAt(contributions []sequenceHistoryContribution, path Path) (sequenceHistoryContribution, bool) {
|
|
for _, contribution := range contributions {
|
|
if contribution.path.String() == path.String() {
|
|
return contribution, true
|
|
}
|
|
}
|
|
return sequenceHistoryContribution{}, false
|
|
}
|
|
|
|
func applySequenceHistory(target **document.Node, incoming *document.Node, contribution sequenceHistoryContribution) {
|
|
if incoming == nil || incoming.Kind != document.Sequence {
|
|
return
|
|
}
|
|
if contribution.mode == sequenceHistoryReplace || *target == nil || (*target).Kind != document.Sequence {
|
|
*target = document.Clone(incoming)
|
|
return
|
|
}
|
|
for _, item := range incoming.Sequence {
|
|
(*target).Sequence = append((*target).Sequence, document.Clone(item))
|
|
}
|
|
}
|
|
|
|
func cloneSequenceHistoryContributions(values []sequenceHistoryContribution) []sequenceHistoryContribution {
|
|
if values == nil {
|
|
return nil
|
|
}
|
|
cloned := make([]sequenceHistoryContribution, len(values))
|
|
for index, value := range values {
|
|
cloned[index] = value
|
|
cloned[index].path = value.path.clone()
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func mergeHistoryAtPath(root **document.Node, path Path, incoming *document.Node, index *mappingIndex) {
|
|
if root == nil || *root == nil || len(path.segments) == 0 {
|
|
mergeHistoryDocument(root, incoming, index)
|
|
return
|
|
}
|
|
current := *root
|
|
for segmentIndex, segment := range path.segments {
|
|
if segment.kind == pathSegmentIndex {
|
|
position, ok := schemaIndex(segment)
|
|
if current.Kind != document.Sequence || !ok || position >= len(current.Sequence) {
|
|
return
|
|
}
|
|
if segmentIndex == len(path.segments)-1 {
|
|
mergeHistoryDocument(¤t.Sequence[position], incoming, index)
|
|
return
|
|
}
|
|
current = current.Sequence[position]
|
|
continue
|
|
}
|
|
if current.Kind != document.Mapping {
|
|
return
|
|
}
|
|
position := index.find(current, segment.key)
|
|
if segmentIndex == len(path.segments)-1 {
|
|
if position < 0 {
|
|
index.append(current, document.Entry{Key: segment.key, Value: document.Clone(incoming)})
|
|
} else {
|
|
mergeHistoryDocument(¤t.Mapping[position].Value, incoming, index)
|
|
}
|
|
return
|
|
}
|
|
if position < 0 {
|
|
child := &document.Node{Kind: document.Mapping}
|
|
index.append(current, document.Entry{Key: segment.key, Value: child})
|
|
current = child
|
|
} else {
|
|
current = current.Mapping[position].Value
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resolution records describe one before/final transformation, not assignment
|
|
// order. A parent already carries its descendants' final values; merge overlapping
|
|
// intent into that event rather than replay a child assignment a second time.
|
|
func coalescedResolutionSnapshots(snapshots []resolutionSnapshot) []resolutionSnapshot {
|
|
groups := make([]resolutionSnapshot, 0, len(snapshots))
|
|
reasons := make([]map[string]struct{}, 0, len(snapshots))
|
|
dependencies := make([]map[string]struct{}, 0, len(snapshots))
|
|
var coverage pathPrefixIndex
|
|
for _, snapshot := range snapshots {
|
|
pointer := snapshot.change.Path.String()
|
|
groupIndex := -1
|
|
coverage.visitAncestors(pointer, func(id int) {
|
|
if groupIndex < 0 || id < groupIndex {
|
|
groupIndex = id
|
|
}
|
|
})
|
|
if groupIndex >= 0 {
|
|
group := &groups[groupIndex]
|
|
for _, reason := range snapshot.change.Reasons {
|
|
if _, found := reasons[groupIndex][reason]; !found {
|
|
reasons[groupIndex][reason] = struct{}{}
|
|
group.change.Reasons = append(group.change.Reasons, reason)
|
|
}
|
|
}
|
|
for _, dependency := range snapshot.change.Dependencies {
|
|
dependencyPointer := dependency.String()
|
|
if _, found := dependencies[groupIndex][dependencyPointer]; !found {
|
|
dependencies[groupIndex][dependencyPointer] = struct{}{}
|
|
group.change.Dependencies = append(group.change.Dependencies, dependency.clone())
|
|
}
|
|
}
|
|
if snapshot.change.Origin.Operation == ChangeNormalization {
|
|
group.change.Origin.Operation = ChangeNormalization
|
|
}
|
|
continue
|
|
}
|
|
snapshot.change = cloneChange(snapshot.change)
|
|
groups = append(groups, snapshot)
|
|
reasonSet := make(map[string]struct{}, len(snapshot.change.Reasons))
|
|
for _, reason := range snapshot.change.Reasons {
|
|
reasonSet[reason] = struct{}{}
|
|
}
|
|
reasons = append(reasons, reasonSet)
|
|
dependencySet := make(map[string]struct{}, len(snapshot.change.Dependencies))
|
|
for _, dependency := range snapshot.change.Dependencies {
|
|
dependencySet[dependency.String()] = struct{}{}
|
|
}
|
|
dependencies = append(dependencies, dependencySet)
|
|
coverage.insert(pointer, len(groups)-1)
|
|
}
|
|
return groups
|
|
}
|
|
|
|
func replaceHistoryPath(root **document.Node, path Path, replacement *document.Node) {
|
|
if root == nil || *root == nil {
|
|
return
|
|
}
|
|
if len(path.segments) == 0 {
|
|
*root = document.Clone(replacement)
|
|
return
|
|
}
|
|
current := *root
|
|
for index, segment := range path.segments {
|
|
last := index == len(path.segments)-1
|
|
switch current.Kind {
|
|
case document.Mapping:
|
|
if segment.kind == pathSegmentIndex {
|
|
return
|
|
}
|
|
position := documentEntryIndex(current.Mapping, segment.key)
|
|
if position < 0 {
|
|
if replacement == nil {
|
|
return
|
|
}
|
|
child := &document.Node{Kind: document.Mapping}
|
|
if last {
|
|
child = document.Clone(replacement)
|
|
}
|
|
current.Mapping = append(current.Mapping, document.Entry{Key: segment.key, Value: child})
|
|
if last {
|
|
return
|
|
}
|
|
current = child
|
|
continue
|
|
}
|
|
if last {
|
|
if replacement == nil {
|
|
current.Mapping = append(current.Mapping[:position], current.Mapping[position+1:]...)
|
|
} else {
|
|
current.Mapping[position].Value = document.Clone(replacement)
|
|
}
|
|
return
|
|
}
|
|
current = current.Mapping[position].Value
|
|
case document.Sequence:
|
|
position, ok := schemaIndex(segment)
|
|
if !ok || position >= len(current.Sequence) {
|
|
return
|
|
}
|
|
if last {
|
|
if replacement != nil {
|
|
current.Sequence[position] = document.Clone(replacement)
|
|
}
|
|
return
|
|
}
|
|
current = current.Sequence[position]
|
|
default:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func historyNodesWithinLimit(baseline, input, final *document.Node, applications []historyApplication, resolutions []resolutionSnapshot, inherited ...*inheritanceScope) bool {
|
|
return historyNodesWithin(historyNodeLimit, baseline, input, final, applications, resolutions, inherited...)
|
|
}
|
|
|
|
func historyNodesWithin(limit int, baseline, input, final *document.Node, applications []historyApplication, resolutions []resolutionSnapshot, inherited ...*inheritanceScope) bool {
|
|
seen := make(map[*document.Node]bool)
|
|
count := 0
|
|
var visit func(*document.Node) bool
|
|
visit = func(node *document.Node) bool {
|
|
if node == nil || seen[node] {
|
|
return true
|
|
}
|
|
seen[node] = true
|
|
count++
|
|
if count > limit {
|
|
return false
|
|
}
|
|
for _, entry := range node.Mapping {
|
|
if !visit(entry.Value) {
|
|
return false
|
|
}
|
|
}
|
|
for _, child := range node.Sequence {
|
|
if !visit(child) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
if !visit(baseline) || !visit(input) || !visit(final) {
|
|
return false
|
|
}
|
|
for _, application := range applications {
|
|
if !visit(application.value) {
|
|
return false
|
|
}
|
|
}
|
|
for _, resolution := range resolutions {
|
|
if !visit(resolution.before) || !visit(resolution.after) {
|
|
return false
|
|
}
|
|
}
|
|
for _, expansion := range inherited {
|
|
if expansion == nil {
|
|
continue
|
|
}
|
|
if !visit(expansion.root) {
|
|
return false
|
|
}
|
|
for _, operation := range expansion.operations {
|
|
if !visit(operation.parent) || !visit(operation.effective) {
|
|
return false
|
|
}
|
|
}
|
|
for _, replacement := range expansion.replacements {
|
|
if !visit(replacement.before) || !visit(replacement.after) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|