package engine import "go.maximumdirect.net/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 }