Files
configkit/internal/engine/history_inheritance.go

286 lines
10 KiB
Go

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 := copyInheritanceOperations(operations, false)
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
lookup := buildDocumentLookup(beforeChild, operation.parent)
for _, path := range parentSide {
if inheritanceOperationIdentity(operation, path) {
continue
}
beforeNode, before := documentAtInheritancePath(beforeChild, transfer.to, path, lookup)
parentNode, after := documentAtInheritancePath(operation.parent, transfer.to, path, lookup)
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
lookup = buildDocumentLookup(parentState, operation.effective)
for _, path := range childSide {
if inheritanceOperationIdentity(operation, path) {
continue
}
parentNode, before := documentAtInheritancePath(parentState, transfer.to, path, lookup)
effectiveNode, after := documentAtInheritancePath(operation.effective, transfer.to, path, lookup)
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, lookup *documentLookup) (*document.Node, bool) {
relative, within := relativeHistoryPath(rootPath, path)
if !within {
return nil, false
}
return documentAtPathWithLookup(root, relative, lookup)
}
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
}