Normalize inheritance history evidence
This commit is contained in:
305
internal/engine/history_inheritance.go
Normal file
305
internal/engine/history_inheritance.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package engine
|
||||
|
||||
import "gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
|
||||
type historyInheritancePayload struct {
|
||||
operations []inheritanceOperation
|
||||
}
|
||||
|
||||
type inheritanceEvidenceKind uint8
|
||||
|
||||
const (
|
||||
inheritanceEvidenceControl inheritanceEvidenceKind = iota
|
||||
inheritanceEvidenceParent
|
||||
inheritanceEvidenceChild
|
||||
inheritanceEvidenceDisplaced
|
||||
inheritanceEvidenceRemoval
|
||||
)
|
||||
|
||||
type inheritanceAttributionSelection struct {
|
||||
kind inheritanceEvidenceKind
|
||||
path Path
|
||||
origin Origin
|
||||
dependencies []Path
|
||||
}
|
||||
|
||||
type inheritanceShapeFacts struct {
|
||||
child bool
|
||||
parent bool
|
||||
effective bool
|
||||
}
|
||||
|
||||
// observeInheritanceEvidence owns the ordered inheritance operation list and
|
||||
// records its concrete before/after shapes against the builder's one scratch
|
||||
// projection. Parent and effective-child trees alone cannot establish which
|
||||
// concrete paths an operation displaced from its child or enclosing tree.
|
||||
func (builder *historyBuilder) observeInheritanceEvidence(operations []inheritanceOperation) {
|
||||
builder.requireOpen()
|
||||
if len(operations) == 0 {
|
||||
return
|
||||
}
|
||||
payload := builder.nextPayload(historyPhaseInheritance)
|
||||
if payload.id != len(builder.model.inheritancePayloads) {
|
||||
panic("configkit: inheritance payload identifiers are out of order")
|
||||
}
|
||||
owned := cloneInheritanceOperations(operations)
|
||||
builder.model.inheritancePayloads = append(builder.model.inheritancePayloads, historyInheritancePayload{operations: owned})
|
||||
|
||||
for operationIndex, operation := range owned {
|
||||
if operation.control != nil {
|
||||
builder.observeInheritanceControl(payload, operationIndex, operation)
|
||||
continue
|
||||
}
|
||||
if operation.transfer == nil {
|
||||
continue
|
||||
}
|
||||
builder.observeInheritanceTransfer(payload, operationIndex, operation)
|
||||
}
|
||||
}
|
||||
|
||||
func (builder *historyBuilder) observeInheritanceControl(payload historyPayloadRef, operationIndex int, operation inheritanceOperation) {
|
||||
control := operation.control
|
||||
if control == nil {
|
||||
return
|
||||
}
|
||||
node, before := builder.scratchAt(control.path)
|
||||
selections := resolveInheritanceAttribution(operation, control.path)
|
||||
var attributionRef historyAttributionRef = noHistoryAttribution
|
||||
var sensitive bool
|
||||
if before && node != nil {
|
||||
sensitive = node.Sensitive
|
||||
}
|
||||
if len(selections) != 0 {
|
||||
selection := selections[0]
|
||||
attributionRef = builder.addAttribution(selection.origin, nil, selection.dependencies)
|
||||
}
|
||||
ref := payload
|
||||
ref.substep = operationIndex * 2
|
||||
builder.addObservation(historyObservation{
|
||||
path: control.path, phase: historyPhaseInheritance, payload: ref,
|
||||
reportedPath: control.path, role: historyRoleControl,
|
||||
beforePresent: before, sensitive: sensitive,
|
||||
attributionRef: attributionRef,
|
||||
})
|
||||
replaceHistoryPath(&builder.scratch, control.path, nil)
|
||||
}
|
||||
|
||||
func (builder *historyBuilder) observeInheritanceTransfer(payload historyPayloadRef, operationIndex int, operation inheritanceOperation) {
|
||||
transfer := operation.transfer
|
||||
if transfer == nil {
|
||||
return
|
||||
}
|
||||
beforeChild, _ := documentAtPath(builder.scratch, transfer.to)
|
||||
parentPaths := inheritanceTreePaths(operation.parent, transfer.to)
|
||||
beforePaths := inheritanceTreePaths(beforeChild, transfer.to)
|
||||
parentSide := unionInheritancePaths(beforePaths, parentPaths)
|
||||
parentRef := payload
|
||||
parentRef.substep = operationIndex * 2
|
||||
for _, path := range parentSide {
|
||||
if inheritanceOperationIdentity(operation, path) {
|
||||
continue
|
||||
}
|
||||
beforeNode, before := documentAtInheritancePath(beforeChild, transfer.to, path)
|
||||
parentNode, after := documentAtInheritancePath(operation.parent, transfer.to, path)
|
||||
selection := selectInheritanceEvidence(resolveInheritanceAttribution(operation, path, inheritanceShapeFacts{
|
||||
child: before, parent: after,
|
||||
}), inheritanceEvidenceParent, inheritanceEvidenceDisplaced)
|
||||
builder.addInheritanceObservation(parentRef, transfer.to, path, before, after, beforeNode, parentNode, selection)
|
||||
}
|
||||
|
||||
parentWorking := document.Clone(operation.parent)
|
||||
if parentWorking != nil {
|
||||
replaceHistoryPath(&builder.scratch, transfer.to, parentWorking)
|
||||
for _, field := range operation.identityFields {
|
||||
identity, found := documentAtPath(operation.effective, Keys(field))
|
||||
if found {
|
||||
replaceHistoryPath(&builder.scratch, transfer.to.Key(field), document.Clone(identity))
|
||||
}
|
||||
}
|
||||
}
|
||||
parentState, _ := documentAtPath(builder.scratch, transfer.to)
|
||||
effectivePaths := inheritanceTreePaths(operation.effective, transfer.to)
|
||||
parentStatePaths := inheritanceTreePaths(parentState, transfer.to)
|
||||
childSide := unionInheritancePaths(parentStatePaths, effectivePaths)
|
||||
childRef := payload
|
||||
childRef.substep = operationIndex*2 + 1
|
||||
for _, path := range childSide {
|
||||
if inheritanceOperationIdentity(operation, path) {
|
||||
continue
|
||||
}
|
||||
parentNode, before := documentAtInheritancePath(parentState, transfer.to, path)
|
||||
effectiveNode, after := documentAtInheritancePath(operation.effective, transfer.to, path)
|
||||
selection := selectInheritanceEvidence(resolveInheritanceAttribution(operation, path, inheritanceShapeFacts{
|
||||
parent: before, effective: after,
|
||||
}), inheritanceEvidenceChild, inheritanceEvidenceRemoval, inheritanceEvidenceDisplaced)
|
||||
builder.addInheritanceObservation(childRef, transfer.to, path, before, after, parentNode, effectiveNode, selection)
|
||||
}
|
||||
|
||||
if operation.effective != nil {
|
||||
replaceHistoryPath(&builder.scratch, transfer.to, document.Clone(operation.effective))
|
||||
}
|
||||
}
|
||||
|
||||
func (builder *historyBuilder) addInheritanceObservation(payload historyPayloadRef, reportedPath, path Path, before, after bool, beforeNode, afterNode *document.Node, selection *inheritanceAttributionSelection) {
|
||||
ref := noHistoryAttribution
|
||||
if selection != nil {
|
||||
ref = builder.addAttribution(selection.origin, nil, selection.dependencies)
|
||||
}
|
||||
sensitive := beforeNode != nil && beforeNode.Sensitive || afterNode != nil && afterNode.Sensitive
|
||||
builder.addObservation(historyObservation{
|
||||
path: path, phase: historyPhaseInheritance, payload: payload,
|
||||
reportedPath: reportedPath, role: historyRoleOperation,
|
||||
beforePresent: before, afterPresent: after, sensitive: sensitive,
|
||||
attributionRef: ref,
|
||||
})
|
||||
}
|
||||
|
||||
func selectInheritanceEvidence(selections []inheritanceAttributionSelection, preferred inheritanceEvidenceKind, fallbacks ...inheritanceEvidenceKind) *inheritanceAttributionSelection {
|
||||
for index := range selections {
|
||||
if selections[index].kind == preferred {
|
||||
return &selections[index]
|
||||
}
|
||||
}
|
||||
for _, fallback := range fallbacks {
|
||||
for index := range selections {
|
||||
if selections[index].kind == fallback {
|
||||
return &selections[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inheritanceTreePaths(root *document.Node, destination Path) []Path {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
var paths []Path
|
||||
walkDocument(root, Path{}, func(local Path, _ *document.Node) {
|
||||
paths = append(paths, appendInheritancePath(destination, local))
|
||||
})
|
||||
return paths
|
||||
}
|
||||
|
||||
func unionInheritancePaths(groups ...[]Path) []Path {
|
||||
var paths []Path
|
||||
seen := make(map[string][]Path)
|
||||
for _, group := range groups {
|
||||
for _, path := range group {
|
||||
found := false
|
||||
for _, existing := range seen[path.String()] {
|
||||
if sameConcreteHistoryPath(existing, path) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
owned := path.clone()
|
||||
paths = append(paths, owned)
|
||||
seen[path.String()] = append(seen[path.String()], owned)
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func documentAtInheritancePath(root *document.Node, rootPath, path Path) (*document.Node, bool) {
|
||||
relative, within := relativeHistoryPath(rootPath, path)
|
||||
if !within {
|
||||
return nil, false
|
||||
}
|
||||
return documentAtPath(root, relative)
|
||||
}
|
||||
|
||||
func resolveInheritanceAttribution(operation inheritanceOperation, destination Path, shapes ...inheritanceShapeFacts) []inheritanceAttributionSelection {
|
||||
if operation.control != nil {
|
||||
if !pathsIntersect(operation.control.path, destination) {
|
||||
return nil
|
||||
}
|
||||
origin := cloneOrigin(operation.controlOrigin)
|
||||
origin.Operation = ChangeInheritance
|
||||
origin.Inheritance = projectInheritanceSteps(origin)
|
||||
return []inheritanceAttributionSelection{{
|
||||
kind: inheritanceEvidenceControl, path: operation.control.path.clone(), origin: origin,
|
||||
dependencies: []Path{operation.control.path.clone()},
|
||||
}}
|
||||
}
|
||||
if operation.transfer == nil || !pathsIntersect(operation.transfer.to, destination) || inheritanceOperationIdentity(operation, destination) {
|
||||
return nil
|
||||
}
|
||||
path := destination
|
||||
if pathContains(destination, operation.transfer.to) {
|
||||
path = operation.transfer.to
|
||||
}
|
||||
var selections []inheritanceAttributionSelection
|
||||
parent, parentFound := transferredOperationOrigin(operation, path)
|
||||
effective, effectiveFound := operation.effectiveOrigins[path.String()]
|
||||
child, childFound := operation.childOrigins[path.String()]
|
||||
if len(shapes) != 0 {
|
||||
parentFound = parentFound && shapes[0].parent
|
||||
effectiveFound = effectiveFound && shapes[0].effective
|
||||
childFound = childFound && shapes[0].child
|
||||
}
|
||||
if parentFound {
|
||||
parent.Operation = ChangeInheritance
|
||||
parent.Inheritance = projectInheritanceSteps(parent)
|
||||
selections = append(selections, inheritanceAttributionSelection{
|
||||
kind: inheritanceEvidenceParent, path: path.clone(), origin: parent,
|
||||
dependencies: []Path{operation.transfer.from.clone(), operation.transfer.reference.clone()},
|
||||
})
|
||||
}
|
||||
if effectiveFound {
|
||||
effective = cloneOrigin(effective)
|
||||
effective.Operation = ChangeInheritance
|
||||
}
|
||||
if effectiveFound && (!parentFound || !originContainedBy(effective, parent)) {
|
||||
effective.Inheritance = projectInheritanceSteps(effective)
|
||||
selections = append(selections, inheritanceAttributionSelection{
|
||||
kind: inheritanceEvidenceChild, path: path.clone(), origin: effective,
|
||||
dependencies: []Path{operation.transfer.reference.clone()},
|
||||
})
|
||||
} else if parentFound && !effectiveFound {
|
||||
removed, found := operation.effectiveOrigins[operation.transfer.to.String()]
|
||||
if found {
|
||||
removed = cloneOrigin(removed)
|
||||
removed.Operation = ChangeInheritance
|
||||
removed.Inheritance = projectInheritanceSteps(removed)
|
||||
selections = append(selections, inheritanceAttributionSelection{
|
||||
kind: inheritanceEvidenceRemoval, path: operation.transfer.to.clone(), origin: removed,
|
||||
dependencies: []Path{operation.transfer.reference.clone()},
|
||||
})
|
||||
}
|
||||
}
|
||||
if childFound && !parentFound {
|
||||
child = cloneOrigin(child)
|
||||
child.Operation = ChangeInheritance
|
||||
child.Inheritance = projectInheritanceSteps(child)
|
||||
selections = append(selections, inheritanceAttributionSelection{
|
||||
kind: inheritanceEvidenceDisplaced, path: path.clone(), origin: child,
|
||||
dependencies: []Path{operation.transfer.reference.clone()},
|
||||
})
|
||||
}
|
||||
return selections
|
||||
}
|
||||
|
||||
func inheritanceChangesForQuery(query Path, operation inheritanceOperation) []Change {
|
||||
if operation.control != nil {
|
||||
for _, selection := range resolveInheritanceAttribution(operation, query) {
|
||||
return []Change{{Path: selection.path.clone(), Origin: cloneOrigin(selection.origin), Dependencies: clonePaths(selection.dependencies)}}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if operation.transfer == nil {
|
||||
return nil
|
||||
}
|
||||
path := operationPathForQuery(operation.transfer.to, query)
|
||||
selections := resolveInheritanceAttribution(operation, path)
|
||||
changes := make([]Change, 0, len(selections))
|
||||
for _, selection := range selections {
|
||||
if selection.kind == inheritanceEvidenceDisplaced {
|
||||
continue
|
||||
}
|
||||
changes = append(changes, Change{Path: selection.path.clone(), Origin: cloneOrigin(selection.origin), Dependencies: clonePaths(selection.dependencies)})
|
||||
}
|
||||
return changes
|
||||
}
|
||||
Reference in New Issue
Block a user