Separate explanation replay snapshots and rendering
This commit is contained in:
276
history.go
Normal file
276
history.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
const historyNodeLimit = 4_000_000
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
func applicationOperationPath(application historyApplication, query Path) (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
|
||||
}
|
||||
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 := documentAtPath(application.value, prefix)
|
||||
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) Origin {
|
||||
if application.origin.Layer != "" {
|
||||
return cloneOrigin(application.origin)
|
||||
}
|
||||
origin := Origin{Layer: application.layer}
|
||||
relative, ok := relativeHistoryPath(application.path, operationPath)
|
||||
if !ok {
|
||||
return origin
|
||||
}
|
||||
if node, present := documentAtPath(application.value, relative); 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) {
|
||||
if application.path.String() == "" {
|
||||
mergeHistoryDocument(root, application.value)
|
||||
return
|
||||
}
|
||||
mergeHistoryAtPath(root, application.path, application.value)
|
||||
}
|
||||
|
||||
func mergeHistoryDocument(target **document.Node, incoming *document.Node) {
|
||||
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 := documentEntryIndex((*target).Mapping, entry.Key)
|
||||
if position < 0 {
|
||||
cloned := entry
|
||||
cloned.Value = document.Clone(entry.Value)
|
||||
(*target).Mapping = append((*target).Mapping, cloned)
|
||||
continue
|
||||
}
|
||||
mergeHistoryDocument(&(*target).Mapping[position].Value, entry.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func mergeHistoryAtPath(root **document.Node, path Path, incoming *document.Node) {
|
||||
if root == nil || *root == nil || len(path.segments) == 0 {
|
||||
mergeHistoryDocument(root, incoming)
|
||||
return
|
||||
}
|
||||
current := *root
|
||||
for index, segment := range path.segments {
|
||||
if current.Kind != document.Mapping || segment.kind == pathSegmentIndex {
|
||||
return
|
||||
}
|
||||
position := documentEntryIndex(current.Mapping, segment.key)
|
||||
if index == len(path.segments)-1 {
|
||||
if position < 0 {
|
||||
current.Mapping = append(current.Mapping, document.Entry{Key: segment.key, Value: document.Clone(incoming)})
|
||||
} else {
|
||||
mergeHistoryDocument(¤t.Mapping[position].Value, incoming)
|
||||
}
|
||||
return
|
||||
}
|
||||
if position < 0 {
|
||||
child := &document.Node{Kind: document.Mapping}
|
||||
current.Mapping = append(current.Mapping, 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))
|
||||
for _, snapshot := range snapshots {
|
||||
merged := false
|
||||
for i := range groups {
|
||||
if !pathContains(groups[i].change.Path, snapshot.change.Path) {
|
||||
continue
|
||||
}
|
||||
for _, reason := range snapshot.change.Reasons {
|
||||
groups[i].change.Reasons = appendUniqueString(groups[i].change.Reasons, reason)
|
||||
}
|
||||
for _, dependency := range snapshot.change.Dependencies {
|
||||
groups[i].change.Dependencies = appendUniquePath(groups[i].change.Dependencies, dependency)
|
||||
}
|
||||
if snapshot.change.Origin.Operation == ChangeNormalization {
|
||||
groups[i].change.Origin.Operation = ChangeNormalization
|
||||
}
|
||||
merged = true
|
||||
break
|
||||
}
|
||||
if !merged {
|
||||
snapshot.change = cloneChange(snapshot.change)
|
||||
groups = append(groups, snapshot)
|
||||
}
|
||||
}
|
||||
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) bool {
|
||||
return historyNodesWithin(historyNodeLimit, baseline, input, final, applications, resolutions)
|
||||
}
|
||||
|
||||
func historyNodesWithin(limit int, baseline, input, final *document.Node, applications []historyApplication, resolutions []resolutionSnapshot) 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
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user