From da74d4190d7ca77e02a38302537983fda5b0a48f Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 26 Oct 2026 15:25:02 +0000 Subject: [PATCH] Normalize inheritance history evidence --- docs/roadmap/implementation.md | 8 +- internal/engine/history_inheritance.go | 305 ++++++++++++++++++++ internal/engine/history_inheritance_test.go | 141 +++++++++ internal/engine/history_model.go | 9 +- internal/engine/inheritance.go | 2 + internal/engine/inheritance_combine.go | 1 + internal/engine/inheritance_explain.go | 43 ++- internal/engine/inheritance_history.go | 49 +--- 8 files changed, 482 insertions(+), 76 deletions(-) create mode 100644 internal/engine/history_inheritance.go create mode 100644 internal/engine/history_inheritance_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 4029f3d..a5fcd7d 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Interface Contract Remediation Implementation Plan -Status: in progress. Stages 1–5 are complete; Stages 6–18 remain pending. +Status: in progress. Stages 1–6 are complete; Stages 7–18 remain pending. Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`. Audience: a Luna coding agent executing the stages sequentially. @@ -420,7 +420,11 @@ and correct displacement attribution. Focused check: ## Stage 6 — Normalize Inheritance And Control Evidence Owners: new `history_inheritance.go`, `inheritance_history.go`, -`inheritance_explain.go`, inheritance operation producers. Status: pending. +`inheritance_explain.go`, inheritance operation producers. Status: complete. +Validation: +`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Inheritance.*History|History.*Inheritance|RecordInheritance|NestedInheritance' -count=1`, +`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`, +and `git diff --check`. 1. Register one ordered inheritance payload. Produce observations for control removal/cancellation, parent transfer, effective child contribution, and diff --git a/internal/engine/history_inheritance.go b/internal/engine/history_inheritance.go new file mode 100644 index 0000000..8f31d90 --- /dev/null +++ b/internal/engine/history_inheritance.go @@ -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 +} diff --git a/internal/engine/history_inheritance_test.go b/internal/engine/history_inheritance_test.go new file mode 100644 index 0000000..56bcfc7 --- /dev/null +++ b/internal/engine/history_inheritance_test.go @@ -0,0 +1,141 @@ +package engine + +import "testing" + +func TestInheritanceEvidenceUsesOrderedPayloadAndConcreteTransferShapes(t *testing.T) { + t.Parallel() + + controlPath := Keys("child", inheritanceKey) + transfer := inheritanceTransfer{ + from: Keys("parent"), to: Keys("child"), reference: Keys("parent"), + locations: []Location{{SourceID: "directive"}}, + } + parent := parseDocumentForDecode(t, "id: parent\nbranch: {deep: {leaf: inherited}}\n") + effective := parseDocumentForDecode(t, "id: child\nbranch: replaced\n") + root := parseDocumentForDecode(t, "parent: {unused: true}\nchild: {id: child, extends: parent, branch: {local: stale}}\n") + parentOrigins := map[string]Origin{ + Keys("parent", "branch").String(): {Layer: LayerBase, Locations: []Location{{SourceID: "parent"}}}, + Keys("parent", "branch", "deep").String(): {Layer: LayerBase, Locations: []Location{{SourceID: "parent"}}}, + Keys("parent", "branch", "deep", "leaf").String(): {Layer: LayerBase, Locations: []Location{{SourceID: "parent"}}}, + } + effectiveOrigins := map[string]Origin{ + Keys("child", "branch").String(): {Layer: LayerOverlay, Locations: []Location{{SourceID: "child"}}}, + } + childOrigins := map[string]Origin{ + Keys("child", "id").String(): {Layer: LayerOverlay, Locations: []Location{{SourceID: "child"}}}, + Keys("child", "branch").String(): {Layer: LayerOverlay, Locations: []Location{{SourceID: "child"}}}, + Keys("child", "branch", "local").String(): {Layer: LayerOverlay, Locations: []Location{{SourceID: "child"}}}, + } + operation := inheritanceOperation{ + transfer: &transfer, parent: parent, effective: effective, + parentOrigins: parentOrigins, effectiveOrigins: effectiveOrigins, + childOrigins: childOrigins, identityFields: []string{"id"}, + } + controlOrigin := Origin{Layer: LayerOverlay, Locations: []Location{{SourceID: "directive"}}} + controlOperation := inheritanceOperation{ + control: &inheritanceControl{path: controlPath}, controlOrigin: controlOrigin, + } + builder := newHistoryBuilder() + builder.setScratch(root) + builder.observeInheritanceEvidence([]inheritanceOperation{controlOperation, operation}) + model := builder.freeze() + + if len(model.inheritancePayloads) != 1 || len(model.inheritancePayloads[0].operations) != 2 { + t.Fatalf("ordered inheritance payload = %#v", model.inheritancePayloads) + } + controlSelections := model.selectHistory(controlPath) + if len(controlSelections) != 1 || controlSelections[0].role != historyRoleControl || !controlSelections[0].before || controlSelections[0].after || + !controlSelections[0].hasAttribution || len(controlSelections[0].attribution.dependencies) != 1 || controlSelections[0].attribution.dependencies[0].String() != controlPath.String() { + t.Fatalf("consumed control selection = %#v", controlSelections) + } + + inheritedPath := Keys("child", "branch", "deep", "leaf") + inherited := model.selectHistory(inheritedPath) + if len(inherited) != 2 || inherited[0].phase != historyPhaseInheritance || inherited[0].payload.id != 0 || inherited[0].payload.substep != 2 || + inherited[0].before || !inherited[0].after || !inherited[0].hasAttribution || inherited[0].attribution.origin.Locations[0].SourceID != "parent" || + len(inherited[0].attribution.dependencies) != 2 || inherited[1].payload.substep != 3 || !inherited[1].before || inherited[1].after { + t.Fatalf("parent transfer selection = %#v", inherited) + } + + displacedPath := Keys("child", "branch", "local") + displaced := model.selectHistory(displacedPath) + if len(displaced) != 1 || displaced[0].payload.substep != 2 || !displaced[0].before || displaced[0].after || + displaced[0].attribution.origin.Locations[0].SourceID != "child" { + t.Fatalf("pre-transfer child shape was not retained: %#v", displaced) + } + if public := projectedInheritanceChanges(displacedPath, model.inheritancePayloads[0].operations[1]); len(public) != 0 { + t.Fatalf("displaced child path unexpectedly projected publicly: %#v", public) + } + if builder.scratch != nil { + t.Fatal("frozen builder unexpectedly retained scratch") + } + if identity := model.selectHistory(Keys("child", "id")); len(identity) != 0 { + t.Fatalf("child identity received inheritance evidence: %#v", identity) + } +} + +func TestInheritanceResolverSharesProjectionAttribution(t *testing.T) { + t.Parallel() + + from, to, reference := Keys("parents", "base"), Keys("children", "item"), Keys("parents", "base") + path := to.Key("nested").Key("value") + parent := Origin{Layer: LayerBase, Locations: []Location{{SourceID: "base"}}} + child := Origin{Layer: LayerOverlay, Locations: []Location{{SourceID: "child"}}} + operation := inheritanceOperation{ + transfer: &inheritanceTransfer{from: from, to: to, reference: reference}, + parentOrigins: map[string]Origin{from.Key("nested").Key("value").String(): parent}, + effectiveOrigins: map[string]Origin{path.String(): child}, + } + selections := resolveInheritanceAttribution(operation, path) + if len(selections) != 2 || selections[0].kind != inheritanceEvidenceParent || selections[1].kind != inheritanceEvidenceChild { + t.Fatalf("shared resolver selections = %#v", selections) + } + if selections[0].path.String() != selections[1].path.String() || selections[0].origin.Operation != ChangeInheritance || selections[1].origin.Operation != ChangeInheritance { + t.Fatalf("parent and child attributions did not share one resolver: %#v", selections) + } + changes := projectedInheritanceChanges(path, operation) + if len(changes) != 2 || changes[0].Origin.Locations[0].SourceID != selections[0].origin.Locations[0].SourceID || + changes[1].Origin.Locations[0].SourceID != selections[1].origin.Locations[0].SourceID || + changes[0].Dependencies[0].String() != from.String() || changes[0].Dependencies[1].String() != reference.String() || + changes[1].Dependencies[0].String() != reference.String() { + t.Fatalf("metadata projection attribution = %#v; resolver = %#v", changes, selections) + } +} + +func TestInheritanceEvidenceSeparatesCollidingKeyAndIndexShapes(t *testing.T) { + t.Parallel() + + from, to := Keys("parent"), Keys("child") + transfer := inheritanceTransfer{from: from, to: to, reference: from} + parent := parseDocumentForDecode(t, "items: [{value: inherited}]\n") + effective := parseDocumentForDecode(t, "items: {'0': {value: child}}\n") + root := parseDocumentForDecode(t, "child: {items: {'0': {value: old}}}\n") + parentPath := from.Key("items").Index(0).Key("value") + childPath := to.Key("items").Key("0").Key("value") + operation := inheritanceOperation{ + transfer: &transfer, parent: parent, effective: effective, + parentOrigins: map[string]Origin{parentPath.String(): {Layer: LayerBase, Locations: []Location{{SourceID: "parent"}}}}, + effectiveOrigins: map[string]Origin{childPath.String(): {Layer: LayerOverlay, Locations: []Location{{SourceID: "child"}}}}, + childOrigins: map[string]Origin{childPath.String(): {Layer: LayerOverlay, Locations: []Location{{SourceID: "child"}}}}, + } + builder := newHistoryBuilder() + builder.setScratch(root) + builder.observeInheritanceEvidence([]inheritanceOperation{operation}) + model := builder.freeze() + + keyPath := to.Key("items").Key("0").Key("value") + keySelections := model.selectHistory(keyPath) + if len(keySelections) != 2 || keySelections[0].payload.substep != 0 || !keySelections[0].before || keySelections[0].after || + keySelections[0].attribution.origin.Locations[0].SourceID != "child" || keySelections[1].payload.substep != 1 || + keySelections[1].before || !keySelections[1].after || keySelections[1].attribution.origin.Locations[0].SourceID != "child" { + t.Fatalf("mapping-key inheritance selections = %#v", keySelections) + } + + indexPath := to.Key("items").Index(0).Key("value") + indexSelections := model.selectHistory(indexPath) + if len(indexSelections) != 2 || indexSelections[0].payload.substep != 0 || indexSelections[0].before || !indexSelections[0].after || + indexSelections[0].attribution.origin.Locations[0].SourceID != "parent" || indexSelections[1].payload.substep != 1 || + !indexSelections[1].before || indexSelections[1].after { + t.Fatalf("sequence-index inheritance selections = %#v", indexSelections) + } +} diff --git a/internal/engine/history_model.go b/internal/engine/history_model.go index f27c854..a79573a 100644 --- a/internal/engine/history_model.go +++ b/internal/engine/history_model.go @@ -55,10 +55,11 @@ type historyObservation struct { } type historyModel struct { - observations []historyObservation - attributions []historyAttribution - baseline *document.Node - sourcePayloads []historySourcePayload + observations []historyObservation + attributions []historyAttribution + baseline *document.Node + sourcePayloads []historySourcePayload + inheritancePayloads []historyInheritancePayload shapesByPointer map[string][]Path shapeOrder []Path diff --git a/internal/engine/inheritance.go b/internal/engine/inheritance.go index eb32d8f..1dfafb8 100644 --- a/internal/engine/inheritance.go +++ b/internal/engine/inheritance.go @@ -38,6 +38,7 @@ type inheritanceOperation struct { controlOrigin Origin parent *document.Node effective *document.Node + childOrigins map[string]Origin parentOrigins map[string]Origin effectiveOrigins map[string]Origin parentPolicies []collectionPolicyScope @@ -102,6 +103,7 @@ func cloneInheritanceOperations(operations []inheritanceOperation) []inheritance } cloned[index].parent = document.Clone(operation.parent) cloned[index].effective = document.Clone(operation.effective) + cloned[index].childOrigins = cloneOriginIndex(operation.childOrigins) cloned[index].parentOrigins = cloneOriginIndex(operation.parentOrigins) cloned[index].effectiveOrigins = cloneOriginIndex(operation.effectiveOrigins) cloned[index].parentPolicies = cloneInheritancePolicyTemplates(operation.parentPolicies) diff --git a/internal/engine/inheritance_combine.go b/internal/engine/inheritance_combine.go index 6c65d5b..31dd257 100644 --- a/internal/engine/inheritance_combine.go +++ b/internal/engine/inheritance_combine.go @@ -106,6 +106,7 @@ func combineInheritanceTemplate( result.transfers = append(result.transfers, parent.transfer) result.operations = append(result.operations, inheritanceOperation{ transfer: &parent.transfer, parent: parent.historyParent, effective: parent.payload, + childOrigins: child.origins, parentOrigins: parentOrigins, effectiveOrigins: mergedOrigins, parentPolicies: cloneInheritancePolicyTemplates(parent.policyTemplates), templatePolicies: cloneCollectionPolicyScopeForInheritance(child.policyTemplates), diff --git a/internal/engine/inheritance_explain.go b/internal/engine/inheritance_explain.go index c978f04..a8be72f 100644 --- a/internal/engine/inheritance_explain.go +++ b/internal/engine/inheritance_explain.go @@ -4,20 +4,23 @@ import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" func appendInheritanceExplanation(explanation *Explanation, working **document.Node, query Path, operations []inheritanceOperation, secrets map[string]bool) { for _, operation := range operations { - changes := projectedInheritanceChanges(query, operation) + selectionPath := query + if operation.transfer != nil { + selectionPath = operationPathForQuery(operation.transfer.to, query) + } + selections := resolveInheritanceAttribution(operation, selectionPath) if operation.control != nil { - if !pathsIntersect(operation.control.path, query) { + control := selectInheritanceEvidence(selections, inheritanceEvidenceControl, inheritanceEvidenceControl) + if control == nil || !pathsIntersect(operation.control.path, query) { continue } before := valueSnapshot(*working, query, secrets) replaceHistoryPath(working, operation.control.path, nil) after := valueSnapshot(*working, query, secrets) - if len(changes) != 0 { - explanation.History = append(explanation.History, ValueChange{ - Before: before, After: after, Origin: cloneOrigin(changes[0].Origin), - Dependencies: clonePaths(changes[0].Dependencies), - }) - } + explanation.History = append(explanation.History, ValueChange{ + Before: before, After: after, Origin: cloneOrigin(control.origin), + Dependencies: clonePaths(control.dependencies), + }) continue } if operation.transfer == nil { @@ -32,13 +35,11 @@ func appendInheritanceExplanation(explanation *Explanation, working **document.N replaceHistoryPath(working, operation.transfer.to.Key(field), identity) } afterParent := valueSnapshot(*working, query, secrets) - parent, parentPresent := transferredOperationOrigin(operation, operationPathForQuery(operation.transfer.to, query)) - if pathsIntersect(operation.transfer.to, query) && parentPresent { - parent.Operation = ChangeInheritance - parent.Inheritance = projectInheritanceSteps(parent) + parent := selectInheritanceEvidence(selections, inheritanceEvidenceParent, inheritanceEvidenceParent) + if pathsIntersect(operation.transfer.to, query) && parent != nil { explanation.History = append(explanation.History, ValueChange{ - Before: before, After: afterParent, Origin: parent, - Dependencies: []Path{operation.transfer.from.clone(), operation.transfer.reference.clone()}, + Before: before, After: afterParent, Origin: cloneOrigin(parent.origin), + Dependencies: clonePaths(parent.dependencies), }) } replaceHistoryPath(working, operation.transfer.to, operation.effective) @@ -46,14 +47,12 @@ func appendInheritanceExplanation(explanation *Explanation, working **document.N continue } afterChild := valueSnapshot(*working, query, secrets) - if len(changes) != 0 { - last := changes[len(changes)-1] - if !parentPresent || len(changes) > 1 { - explanation.History = append(explanation.History, ValueChange{ - Before: afterParent, After: afterChild, Origin: cloneOrigin(last.Origin), - Dependencies: clonePaths(last.Dependencies), - }) - } + child := selectInheritanceEvidence(selections, inheritanceEvidenceChild, inheritanceEvidenceRemoval, inheritanceEvidenceDisplaced) + if child != nil && (parent == nil || len(selections) > 1) { + explanation.History = append(explanation.History, ValueChange{ + Before: afterParent, After: afterChild, Origin: cloneOrigin(child.origin), + Dependencies: clonePaths(child.dependencies), + }) } } } diff --git a/internal/engine/inheritance_history.go b/internal/engine/inheritance_history.go index 64c5259..2302c59 100644 --- a/internal/engine/inheritance_history.go +++ b/internal/engine/inheritance_history.go @@ -104,54 +104,7 @@ func historyWithInheritance(path Path, stored []Change, operations []inheritance } func projectedInheritanceChanges(query Path, operation inheritanceOperation) []Change { - if operation.control != nil { - if !pathsIntersect(operation.control.path, query) { - return nil - } - origin := cloneOrigin(operation.controlOrigin) - origin.Operation = ChangeInheritance - origin.Inheritance = projectInheritanceSteps(origin) - return []Change{{Path: operation.control.path.clone(), Origin: origin, - Dependencies: []Path{operation.control.path.clone()}}} - } - if operation.transfer == nil || !pathsIntersect(operation.transfer.to, query) || inheritanceOperationIdentity(operation, query) { - return nil - } - path := query - if pathContains(query, operation.transfer.to) { - path = operation.transfer.to - } - var changes []Change - parent, parentFound := transferredOperationOrigin(operation, path) - if parentFound { - parent.Operation = ChangeInheritance - parent.Inheritance = projectInheritanceSteps(parent) - changes = append(changes, Change{ - Path: path.clone(), Origin: parent, - Dependencies: []Path{operation.transfer.from.clone(), operation.transfer.reference.clone()}, - }) - } - effective, effectiveFound := operation.effectiveOrigins[path.String()] - if effectiveFound { - effective = cloneOrigin(effective) - effective.Operation = ChangeInheritance - } - if effectiveFound && (!parentFound || !originContainedBy(effective, parent)) { - effective.Inheritance = projectInheritanceSteps(effective) - changes = append(changes, Change{ - Path: path.clone(), Origin: effective, - Dependencies: []Path{operation.transfer.reference.clone()}, - }) - } else if parentFound && !effectiveFound { - removed := cloneOrigin(operation.effectiveOrigins[operation.transfer.to.String()]) - removed.Operation = ChangeInheritance - removed.Inheritance = projectInheritanceSteps(removed) - changes = append(changes, Change{ - Path: operation.transfer.to.clone(), Origin: removed, - Dependencies: []Path{operation.transfer.reference.clone()}, - }) - } - return changes + return inheritanceChangesForQuery(query, operation) } func transferredOperationOrigin(operation inheritanceOperation, destination Path) (Origin, bool) {