Normalize inheritance history evidence
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Interface Contract Remediation Implementation Plan
|
# 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`.
|
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
|
||||||
Audience: a Luna coding agent executing the stages sequentially.
|
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
|
## Stage 6 — Normalize Inheritance And Control Evidence
|
||||||
|
|
||||||
Owners: new `history_inheritance.go`, `inheritance_history.go`,
|
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
|
1. Register one ordered inheritance payload. Produce observations for control
|
||||||
removal/cancellation, parent transfer, effective child contribution, and
|
removal/cancellation, parent transfer, effective child contribution, and
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
141
internal/engine/history_inheritance_test.go
Normal file
141
internal/engine/history_inheritance_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,10 +55,11 @@ type historyObservation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type historyModel struct {
|
type historyModel struct {
|
||||||
observations []historyObservation
|
observations []historyObservation
|
||||||
attributions []historyAttribution
|
attributions []historyAttribution
|
||||||
baseline *document.Node
|
baseline *document.Node
|
||||||
sourcePayloads []historySourcePayload
|
sourcePayloads []historySourcePayload
|
||||||
|
inheritancePayloads []historyInheritancePayload
|
||||||
|
|
||||||
shapesByPointer map[string][]Path
|
shapesByPointer map[string][]Path
|
||||||
shapeOrder []Path
|
shapeOrder []Path
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type inheritanceOperation struct {
|
|||||||
controlOrigin Origin
|
controlOrigin Origin
|
||||||
parent *document.Node
|
parent *document.Node
|
||||||
effective *document.Node
|
effective *document.Node
|
||||||
|
childOrigins map[string]Origin
|
||||||
parentOrigins map[string]Origin
|
parentOrigins map[string]Origin
|
||||||
effectiveOrigins map[string]Origin
|
effectiveOrigins map[string]Origin
|
||||||
parentPolicies []collectionPolicyScope
|
parentPolicies []collectionPolicyScope
|
||||||
@@ -102,6 +103,7 @@ func cloneInheritanceOperations(operations []inheritanceOperation) []inheritance
|
|||||||
}
|
}
|
||||||
cloned[index].parent = document.Clone(operation.parent)
|
cloned[index].parent = document.Clone(operation.parent)
|
||||||
cloned[index].effective = document.Clone(operation.effective)
|
cloned[index].effective = document.Clone(operation.effective)
|
||||||
|
cloned[index].childOrigins = cloneOriginIndex(operation.childOrigins)
|
||||||
cloned[index].parentOrigins = cloneOriginIndex(operation.parentOrigins)
|
cloned[index].parentOrigins = cloneOriginIndex(operation.parentOrigins)
|
||||||
cloned[index].effectiveOrigins = cloneOriginIndex(operation.effectiveOrigins)
|
cloned[index].effectiveOrigins = cloneOriginIndex(operation.effectiveOrigins)
|
||||||
cloned[index].parentPolicies = cloneInheritancePolicyTemplates(operation.parentPolicies)
|
cloned[index].parentPolicies = cloneInheritancePolicyTemplates(operation.parentPolicies)
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ func combineInheritanceTemplate(
|
|||||||
result.transfers = append(result.transfers, parent.transfer)
|
result.transfers = append(result.transfers, parent.transfer)
|
||||||
result.operations = append(result.operations, inheritanceOperation{
|
result.operations = append(result.operations, inheritanceOperation{
|
||||||
transfer: &parent.transfer, parent: parent.historyParent, effective: parent.payload,
|
transfer: &parent.transfer, parent: parent.historyParent, effective: parent.payload,
|
||||||
|
childOrigins: child.origins,
|
||||||
parentOrigins: parentOrigins, effectiveOrigins: mergedOrigins,
|
parentOrigins: parentOrigins, effectiveOrigins: mergedOrigins,
|
||||||
parentPolicies: cloneInheritancePolicyTemplates(parent.policyTemplates),
|
parentPolicies: cloneInheritancePolicyTemplates(parent.policyTemplates),
|
||||||
templatePolicies: cloneCollectionPolicyScopeForInheritance(child.policyTemplates),
|
templatePolicies: cloneCollectionPolicyScopeForInheritance(child.policyTemplates),
|
||||||
|
|||||||
@@ -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) {
|
func appendInheritanceExplanation(explanation *Explanation, working **document.Node, query Path, operations []inheritanceOperation, secrets map[string]bool) {
|
||||||
for _, operation := range operations {
|
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 operation.control != nil {
|
||||||
if !pathsIntersect(operation.control.path, query) {
|
control := selectInheritanceEvidence(selections, inheritanceEvidenceControl, inheritanceEvidenceControl)
|
||||||
|
if control == nil || !pathsIntersect(operation.control.path, query) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
before := valueSnapshot(*working, query, secrets)
|
before := valueSnapshot(*working, query, secrets)
|
||||||
replaceHistoryPath(working, operation.control.path, nil)
|
replaceHistoryPath(working, operation.control.path, nil)
|
||||||
after := valueSnapshot(*working, query, secrets)
|
after := valueSnapshot(*working, query, secrets)
|
||||||
if len(changes) != 0 {
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
explanation.History = append(explanation.History, ValueChange{
|
Before: before, After: after, Origin: cloneOrigin(control.origin),
|
||||||
Before: before, After: after, Origin: cloneOrigin(changes[0].Origin),
|
Dependencies: clonePaths(control.dependencies),
|
||||||
Dependencies: clonePaths(changes[0].Dependencies),
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if operation.transfer == nil {
|
if operation.transfer == nil {
|
||||||
@@ -32,13 +35,11 @@ func appendInheritanceExplanation(explanation *Explanation, working **document.N
|
|||||||
replaceHistoryPath(working, operation.transfer.to.Key(field), identity)
|
replaceHistoryPath(working, operation.transfer.to.Key(field), identity)
|
||||||
}
|
}
|
||||||
afterParent := valueSnapshot(*working, query, secrets)
|
afterParent := valueSnapshot(*working, query, secrets)
|
||||||
parent, parentPresent := transferredOperationOrigin(operation, operationPathForQuery(operation.transfer.to, query))
|
parent := selectInheritanceEvidence(selections, inheritanceEvidenceParent, inheritanceEvidenceParent)
|
||||||
if pathsIntersect(operation.transfer.to, query) && parentPresent {
|
if pathsIntersect(operation.transfer.to, query) && parent != nil {
|
||||||
parent.Operation = ChangeInheritance
|
|
||||||
parent.Inheritance = projectInheritanceSteps(parent)
|
|
||||||
explanation.History = append(explanation.History, ValueChange{
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
Before: before, After: afterParent, Origin: parent,
|
Before: before, After: afterParent, Origin: cloneOrigin(parent.origin),
|
||||||
Dependencies: []Path{operation.transfer.from.clone(), operation.transfer.reference.clone()},
|
Dependencies: clonePaths(parent.dependencies),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
replaceHistoryPath(working, operation.transfer.to, operation.effective)
|
replaceHistoryPath(working, operation.transfer.to, operation.effective)
|
||||||
@@ -46,14 +47,12 @@ func appendInheritanceExplanation(explanation *Explanation, working **document.N
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
afterChild := valueSnapshot(*working, query, secrets)
|
afterChild := valueSnapshot(*working, query, secrets)
|
||||||
if len(changes) != 0 {
|
child := selectInheritanceEvidence(selections, inheritanceEvidenceChild, inheritanceEvidenceRemoval, inheritanceEvidenceDisplaced)
|
||||||
last := changes[len(changes)-1]
|
if child != nil && (parent == nil || len(selections) > 1) {
|
||||||
if !parentPresent || len(changes) > 1 {
|
explanation.History = append(explanation.History, ValueChange{
|
||||||
explanation.History = append(explanation.History, ValueChange{
|
Before: afterParent, After: afterChild, Origin: cloneOrigin(child.origin),
|
||||||
Before: afterParent, After: afterChild, Origin: cloneOrigin(last.Origin),
|
Dependencies: clonePaths(child.dependencies),
|
||||||
Dependencies: clonePaths(last.Dependencies),
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,54 +104,7 @@ func historyWithInheritance(path Path, stored []Change, operations []inheritance
|
|||||||
}
|
}
|
||||||
|
|
||||||
func projectedInheritanceChanges(query Path, operation inheritanceOperation) []Change {
|
func projectedInheritanceChanges(query Path, operation inheritanceOperation) []Change {
|
||||||
if operation.control != nil {
|
return inheritanceChangesForQuery(query, operation)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func transferredOperationOrigin(operation inheritanceOperation, destination Path) (Origin, bool) {
|
func transferredOperationOrigin(operation inheritanceOperation, destination Path) (Origin, bool) {
|
||||||
|
|||||||
Reference in New Issue
Block a user