Replay explanations through shared history selections

This commit is contained in:
2026-10-26 17:01:32 +00:00
parent 4b4e15fc58
commit 738c8a9cc7
12 changed files with 386 additions and 135 deletions

View File

@@ -37,7 +37,15 @@ type Explanation struct {
// Explain returns safe effective and chronological values for a known final or
// historical path. Unknown and malformed paths return false.
func (r *Inspection) Explain(path Path) (Explanation, bool) {
if r == nil || r.final == nil || r.baseline == nil || !r.metadata.hasHistoricalPath(path) {
if r == nil || r.final == nil || !path.valid() {
return Explanation{}, false
}
model := r.metadata.historyModel
if model == nil {
return Explanation{}, false
}
selections := model.selectHistory(path)
if len(selections) == 0 && !model.hasObservedShape(path) {
return Explanation{}, false
}
explanation := Explanation{Path: path.clone(), Effective: valueSnapshotWithLookup(r.final, path, r.secrets, r.immutableLookup)}
@@ -45,20 +53,22 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
explanation.Origin = cloneOrigin(info.Origin)
}
working := document.Clone(r.baseline)
if _, present := documentAtPath(working, path); present {
working := document.Clone(model.baseline)
if baseline, found := historySelectionForPayload(selections, historyPayloadRef{phase: historyPhaseBaseline, id: 0}); found && baseline.after {
explanation.History = append(explanation.History, ValueChange{
After: valueSnapshot(working, path, r.secrets), Origin: Origin{Layer: LayerDefault},
After: valueSnapshot(working, path, r.secrets),
Origin: cloneOrigin(baseline.attribution.origin),
})
}
replayIndex := new(mappingIndex)
for _, application := range r.applications {
operationPath, relevant := applicationOperationPath(application, path, r.immutableLookup)
var before ValueSnapshot
if relevant {
before = valueSnapshot(working, path, r.secrets)
for payloadID, payload := range model.sourcePayloads {
if payload.kind != historySourceApplicationPayload {
continue
}
application := payload.application
before := valueSnapshot(working, path, r.secrets)
applyHistoryApplication(&working, application, replayIndex)
selected, relevant := historySelectionForApplication(model, selections, historyPhaseSources, historyPayloadRef{phase: historyPhaseSources, id: payloadID})
if !relevant {
continue
}
@@ -69,37 +79,38 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
explanation.History = append(explanation.History, ValueChange{
Before: before,
After: after,
Origin: historyApplicationOrigin(application, operationPath, r.immutableLookup),
Origin: cloneOrigin(selected.attribution.origin),
})
}
appendInheritanceExplanation(&explanation, &working, path, r.inheritance, r.secrets)
appendInheritanceExplanation(&explanation, &working, path, model, selections, r.secrets)
// Intermediate input replacements do not permanently suppress defaults:
// decoding applies them to the final input tree. Show that reconciliation
// explicitly before typed CLI contributions and domain resolution.
decodedInput := r.preCLI
decodedOrigins := r.preCLIOrigins
replayed, _ := documentAtPath(working, path)
decoded, _ := documentAtPathWithLookup(decodedInput, path, r.immutableLookup)
if !configurationValuesEqual(replayed, decoded) {
origin := cloneOrigin(decodedOrigins[path.String()])
origin.Operation = ChangeDecoding
explanation.History = append(explanation.History, ValueChange{
Before: valueSnapshot(working, path, r.secrets),
After: valueSnapshotWithLookup(decodedInput, path, r.secrets, r.immutableLookup),
Origin: origin,
Reasons: []string{"applied retained defaults and canonical typed decoding"},
})
if checkpoint := model.preCLICheckpoint; checkpoint != nil {
selection, selected := historySelectionForPayload(selections, historyPayloadRef{phase: historyPhasePreCLI, id: 0})
replayed, _ := documentAtPath(working, path)
decoded, _ := documentAtPathWithLookup(checkpoint.root, path, model.documentLookup)
if selected && !configurationValuesEqual(replayed, decoded) {
origin, reasons := checkpointExplanationAttribution(selection)
explanation.History = append(explanation.History, ValueChange{
Before: valueSnapshot(working, path, r.secrets),
After: valueSnapshotWithLookup(checkpoint.root, path, r.secrets, model.documentLookup),
Origin: origin,
Reasons: reasons,
})
}
working = document.Clone(checkpoint.root)
}
working = document.Clone(decodedInput)
for _, application := range r.postDecodeApps {
operationPath, relevant := applicationOperationPath(application, path, r.immutableLookup)
var before ValueSnapshot
if relevant {
before = valueSnapshot(working, path, r.secrets)
for payloadID, payload := range model.cliPayloads {
if payload.kind != historySourceApplicationPayload {
continue
}
applyHistoryApplication(&working, application, replayIndex)
applicationRef := historyPayloadRef{phase: historyPhaseCLI, id: payloadID}
before := valueSnapshot(working, path, r.secrets)
applyHistoryApplication(&working, payload.application, replayIndex)
selected, relevant := historySelectionForApplication(model, selections, historyPhaseCLI, applicationRef)
if !relevant {
continue
}
@@ -110,47 +121,45 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
explanation.History = append(explanation.History, ValueChange{
Before: before,
After: after,
Origin: historyApplicationOrigin(application, operationPath, r.immutableLookup),
Origin: cloneOrigin(selected.attribution.origin),
})
}
replayed, _ = documentAtPath(working, path)
effectiveInput, _ := documentAtPathWithLookup(r.input, path, r.immutableLookup)
if !configurationValuesEqual(replayed, effectiveInput) {
origin := cloneOrigin(r.inputOrigins[path.String()])
origin.Operation = ChangeDecoding
explanation.History = append(explanation.History, ValueChange{
Before: valueSnapshot(working, path, r.secrets),
After: valueSnapshotWithLookup(r.input, path, r.secrets, r.immutableLookup),
Origin: origin,
Reasons: []string{"applied the typed CLI assignment"},
})
if checkpoint := model.postCLICheckpoint; checkpoint != nil {
selection, selected := historySelectionForPayload(selections, historyPayloadRef{phase: historyPhasePostCLI, id: 0})
replayed, _ := documentAtPath(working, path)
effectiveInput, _ := documentAtPathWithLookup(checkpoint.root, path, model.documentLookup)
if selected && !configurationValuesEqual(replayed, effectiveInput) {
origin, reasons := checkpointExplanationAttribution(selection)
explanation.History = append(explanation.History, ValueChange{
Before: valueSnapshot(working, path, r.secrets),
After: valueSnapshotWithLookup(checkpoint.root, path, r.secrets, model.documentLookup),
Origin: origin,
Reasons: reasons,
})
}
working = document.Clone(checkpoint.root)
}
working = document.Clone(r.input)
for _, group := range r.resolutionReplay {
if group.coveringRecordID < 0 || group.coveringRecordID >= len(r.resolution) {
continue
if model.resolutionPayload != nil {
for _, group := range model.resolutionPayload.replayGroups {
selection, selected := historySelectionForResolutionGroup(selections, group)
if !selected || group.coveringRecordID < 0 || group.coveringRecordID >= len(model.resolutionPayload.snapshots) {
continue
}
covering := model.resolutionPayload.snapshots[group.coveringRecordID]
before := valueSnapshot(working, path, r.secrets)
replaceHistoryPath(&working, group.change.Path, covering.after)
after := valueSnapshot(working, path, r.secrets)
if !before.Present && !after.Present {
continue
}
explanation.History = append(explanation.History, ValueChange{
Before: before,
After: after,
Origin: cloneOrigin(selection.attribution.origin),
Reasons: append([]string(nil), group.change.Reasons...),
Dependencies: clonePaths(group.change.Dependencies),
})
}
covering := r.resolution[group.coveringRecordID]
relevant := pathsIntersect(group.change.Path, path)
var before ValueSnapshot
if relevant {
before = valueSnapshot(working, path, r.secrets)
}
replaceHistoryPath(&working, group.change.Path, covering.after)
if !relevant {
continue
}
after := valueSnapshot(working, path, r.secrets)
if !before.Present && !after.Present {
continue
}
explanation.History = append(explanation.History, ValueChange{
Before: before,
After: after,
Origin: cloneOrigin(group.change.Origin),
Reasons: append([]string(nil), group.change.Reasons...),
Dependencies: clonePaths(group.change.Dependencies),
})
}
if !explanation.Effective.Present && len(explanation.History) == 0 {
@@ -161,3 +170,63 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) {
}
return explanation, true
}
func historySelectionForPayload(selections []historySelection, ref historyPayloadRef) (historySelection, bool) {
for _, selection := range selections {
if selection.payload == ref {
return selection, true
}
}
return historySelection{}, false
}
func historySelectionForApplication(model *historyModel, selections []historySelection, phase historyPhase, applicationRef historyPayloadRef) (historySelection, bool) {
var selected historySelection
found := false
if direct, ok := historySelectionForPayload(selections, applicationRef); ok {
selected, found = direct, true
}
var payloads []historySourcePayload
switch phase {
case historyPhaseSources:
payloads = model.sourcePayloads
case historyPhaseCLI:
payloads = model.cliPayloads
default:
return selected, found
}
for payloadID, candidate := range payloads {
if candidate.kind != historySourceOverlayPayload || !candidate.hasApplicationRef || candidate.applicationRef != applicationRef {
continue
}
if operation, ok := historySelectionForPayload(selections, historyPayloadRef{phase: phase, id: payloadID}); ok {
selected, found = operation, true
}
}
return selected, found
}
func checkpointExplanationAttribution(selection historySelection) (Origin, []string) {
if selection.hasAttribution {
return cloneOrigin(selection.attribution.origin), append([]string(nil), selection.attribution.reasons...)
}
return Origin{Operation: ChangeDecoding}, nil
}
func historySelectionForResolutionGroup(selections []historySelection, group resolutionReplayGroup) (historySelection, bool) {
var fallback historySelection
found := false
for _, recordID := range group.originalRecordIDs {
selection, ok := historySelectionForPayload(selections, historyPayloadRef{phase: historyPhaseResolution, id: recordID})
if !ok {
continue
}
if recordID == group.coveringRecordID {
return selection, true
}
if !found {
fallback, found = selection, true
}
}
return fallback, found
}

View File

@@ -389,6 +389,11 @@ func TestExplanationReconcilesDefaultsAfterInputReplacement(t *testing.T) {
if last.Origin.Layer != info.Origin.Layer {
t.Fatal("decoding lost effective origin")
}
for _, metadataChange := range result.Metadata().History(path) {
if metadataChange.Origin.Operation == ChangeDecoding {
t.Fatal("typed reconciliation leaked into metadata history")
}
}
}
})
}
@@ -535,6 +540,25 @@ func TestExplanationReplaysResolutionAdditionsAndOverlappingIntent(t *testing.T)
if overlap && count != 1 {
t.Fatalf("overlapping records applied %d times", count)
}
if overlap && path.String() == "/m" {
metadataHistory := result.Metadata().History(path)
var sharedResolutionOrigin bool
for _, metadataChange := range metadataHistory {
if metadataChange.Origin.Layer != LayerResolution {
continue
}
for _, explanationChange := range x.History {
if explanationChange.Origin.Layer == metadataChange.Origin.Layer &&
explanationChange.Origin.Operation == metadataChange.Origin.Operation &&
reflect.DeepEqual(explanationChange.Origin.Locations, metadataChange.Origin.Locations) {
sharedResolutionOrigin = true
}
}
}
if !sharedResolutionOrigin {
t.Fatalf("metadata and explanation disagree on overlapping resolution attribution: metadata %#v, explanation %#v", metadataHistory, x.History)
}
}
last := x.History[len(x.History)-1]
if last.After != x.Effective {
t.Fatalf("incorrect replay for %s: %#v", path, x)

View File

@@ -76,14 +76,17 @@ func (builder *historyBuilder) observeCLIEvidence(applications []historyApplicat
}
index := new(mappingIndex)
for item := 0; item < count; item++ {
var applicationRef historyPayloadRef
hasApplication := false
if item < len(applications) {
application := applications[item]
payload := builder.addCLIPayload(&application, nil)
applicationRef = builder.addCLIPayload(&application, nil)
hasApplication = true
walkHistoryApplication(application, func(_, effective Path, node *document.Node) {
origin := sourceApplicationOrigin(application, node)
attribution := builder.addAttribution(origin, nil, nil)
builder.addObservation(historyObservation{
path: effective, phase: historyPhaseCLI, payload: payload,
path: effective, phase: historyPhaseCLI, payload: applicationRef,
reportedPath: effective, role: historyRoleContribution,
afterPresent: true, sensitive: node.Sensitive,
attributionRef: attribution,
@@ -94,6 +97,11 @@ func (builder *historyBuilder) observeCLIEvidence(applications []historyApplicat
if item < len(records) {
record := records[item]
payload := builder.addCLIPayload(nil, &record)
if hasApplication {
owned := &builder.model.cliPayloads[payload.id]
owned.applicationRef = applicationRef
owned.hasApplicationRef = true
}
attribution := builder.addAttribution(record.change.Origin, nil, nil)
for _, displaced := range record.displaced {
builder.addObservation(historyObservation{

View File

@@ -282,6 +282,13 @@ func resolveInheritanceAttribution(operation inheritanceOperation, destination P
return selections
}
func operationPathForQuery(root, query Path) Path {
if pathContains(query, root) {
return root
}
return query
}
func inheritanceChangesForQuery(query Path, operation inheritanceOperation) []Change {
if operation.control != nil {
for _, selection := range resolveInheritanceAttribution(operation, query) {

View File

@@ -130,6 +130,9 @@ func (builder *historyBuilder) addObservation(observation historyObservation) in
case historyRoleDefinition, historyRoleContribution, historyRoleCompositeDefinition, historyRoleCheckpoint:
pointer := observation.path.String()
builder.model.observationsByPointer[pointer] = append(builder.model.observationsByPointer[pointer], observationID)
if observation.role == historyRoleContribution {
builder.model.reportedPathIndex.insert(pointer, observationID)
}
case historyRoleOperation, historyRoleControl:
builder.model.reportedPathIndex.insert(observation.reportedPath.String(), observationID)
}

View File

@@ -36,6 +36,9 @@ func (model *historyModel) selectHistory(path Path) []historySelection {
model.reportedPathIndex.visitAncestors(path.String(), func(observationID int) {
candidateSet[observationID] = struct{}{}
})
model.reportedPathIndex.visitDescendants(path.String(), func(observationID int) {
candidateSet[observationID] = struct{}{}
})
candidates := make([]int, 0, len(candidateSet))
for observationID := range candidateSet {
candidates = append(candidates, observationID)
@@ -46,7 +49,32 @@ func (model *historyModel) selectHistory(path Path) []historySelection {
selectionByPayload := make(map[historyPayloadRef]int)
for _, observationID := range candidates {
observation := model.observations[observationID]
if observation.role == historyRoleInventory || !matchesPathShape(path, observation.path) {
if observation.role == historyRoleInventory {
continue
}
reportedPath := observation.reportedPath
attributionRef := observation.attributionRef
if observation.role == historyRoleContribution {
if !historyObservationMatchesQuery(path, observation.path) {
continue
}
application, found := model.applicationPayload(observation.payload)
if !found {
continue
}
if pathContains(observation.path, path) && len(observation.path.segments) < len(path.segments) {
local, ok := historyApplicationLocalPath(application, path)
if !ok {
continue
}
if _, present := documentAtPathWithLookup(application.value, local, model.documentLookup); !present {
continue
}
}
if operationPath, relevant := applicationOperationPath(application, path, model.documentLookup); relevant {
reportedPath = operationPath
}
} else if !historyObservationMatchesQuery(path, observation.path) {
continue
}
selectionIndex, found := selectionByPayload[observation.payload]
@@ -57,10 +85,10 @@ func (model *historyModel) selectHistory(path Path) []historySelection {
path: path.clone(),
phase: observation.phase,
payload: observation.payload,
reportedPath: observation.reportedPath.clone(),
reportedPath: reportedPath.clone(),
role: observation.role,
}
if attribution, ok := model.resolveAttribution(observation.attributionRef); ok {
if attribution, ok := model.resolveAttribution(attributionRef); ok {
selection.attribution = attribution
selection.hasAttribution = true
}
@@ -88,6 +116,13 @@ func (model *historyModel) selectHistory(path Path) []historySelection {
return selections
}
func historyObservationMatchesQuery(query, observed Path) bool {
if matchesPathShape(query, observed) {
return true
}
return pathContains(query, observed) && pathShapePrefixMatches(query, observed)
}
func (model *historyModel) hasObservedShape(path Path) bool {
if model == nil || !path.valid() {
return false
@@ -187,6 +222,32 @@ func (model *historyModel) resolveAttribution(ref historyAttributionRef) (histor
}, true
}
func (model *historyModel) applicationPayload(ref historyPayloadRef) (historyApplication, bool) {
if model == nil || ref.id < 0 {
return historyApplication{}, false
}
var payloads []historySourcePayload
switch ref.phase {
case historyPhaseSources:
payloads = model.sourcePayloads
case historyPhaseCLI:
payloads = model.cliPayloads
default:
return historyApplication{}, false
}
if ref.id >= len(payloads) || payloads[ref.id].kind != historySourceApplicationPayload {
return historyApplication{}, false
}
return payloads[ref.id].application, true
}
func pathShapePrefixMatches(left, right Path) bool {
length := min(len(left.segments), len(right.segments))
leftPrefix := Path{segments: clonePathSegments(left.segments[:length], 0)}
rightPrefix := Path{segments: clonePathSegments(right.segments[:length], 0)}
return matchesPathShape(leftPrefix, rightPrefix)
}
func metadataHistoryRole(role historyRole) bool {
switch role {
case historyRoleDefinition, historyRoleCompositeDefinition, historyRoleOperation, historyRoleControl:

View File

@@ -282,6 +282,7 @@ func TestProjectedSequenceHistoryAgreesAcrossMetadataAndExplanation(t *testing.T
metadata: metadata, baseline: &document.Node{Kind: document.Mapping},
input: composed.root, preCLI: composed.root, final: composed.root, applications: composed.applications,
}
inspection.metadata.historyModel = sourceHistoryModelForInspection(inspection.baseline, composed.root, composed.origins, composed.applications, composed.overlays)
inspection.secrets = classifySecretPaths(nil, func(candidate Path) bool {
return reflect.DeepEqual(candidate, path)
}, metadata, composed.root, composed.applications)
@@ -329,6 +330,7 @@ func TestReplacedProjectedSensitiveRecordRemainsRedacted(t *testing.T) {
metadata: metadata, baseline: &document.Node{Kind: document.Mapping},
input: final, preCLI: final, final: final, applications: applications,
}
inspection.metadata.historyModel = sourceHistoryModelForInspection(inspection.baseline, final, nil, applications, nil)
inspection.secrets = classifySecretPaths(nil, func(candidate Path) bool {
return reflect.DeepEqual(candidate, path)
}, metadata, final, applications)
@@ -345,6 +347,16 @@ func TestReplacedProjectedSensitiveRecordRemainsRedacted(t *testing.T) {
}
}
func sourceHistoryModelForInspection(baseline, final *document.Node, origins map[string]Origin, applications []historyApplication, records []overlayRecord) *historyModel {
builder := newHistoryBuilder()
builder.observeBaseline(baseline)
builder.setScratch(baseline)
builder.observeSourceEvidence(final, origins, applications, records)
builder.model.final = final
builder.registerDocumentRoot(final)
return builder.freeze()
}
func TestMappedUpsertProjectsSensitiveReplacementEvidence(t *testing.T) {
lowerRoot := parseCompositionDocument(t, "lower", `records:
- id: first

View File

@@ -19,12 +19,14 @@ const (
// historySourcePayload keeps source values and replacement facts under the
// shared model's ownership. Application documents remain shared when immutable.
type historySourcePayload struct {
kind historySourcePayloadKind
application historyApplication
overlay overlayRecord
path Path
origin Origin
contributions []historyPayloadRef
kind historySourcePayloadKind
application historyApplication
overlay overlayRecord
applicationRef historyPayloadRef
hasApplicationRef bool
path Path
origin Origin
contributions []historyPayloadRef
}
func (builder *historyBuilder) observeBaseline(root *document.Node) {
@@ -53,11 +55,22 @@ func (builder *historyBuilder) observeSourceEvidence(root *document.Node, origin
if len(applications) == 0 && len(records) == 0 {
return
}
applicationsBySource := make(map[string]historyPayloadRef)
applicationsByLayer := make(map[Layer][]historyPayloadRef)
for index := range applications {
application := applications[index]
payload := builder.addSourcePayload(&application, nil)
applicationsByLayer[application.layer] = append(applicationsByLayer[application.layer], payload)
walkHistoryApplication(application, func(_, effective Path, node *document.Node) {
if node.Location.SourceID != "" {
applicationsBySource[node.Location.SourceID] = payload
}
origin := sourceApplicationOrigin(application, node)
for _, location := range origin.Locations {
if location.SourceID != "" {
applicationsBySource[location.SourceID] = payload
}
}
attribution := builder.addAttribution(origin, nil, nil)
builder.addObservation(historyObservation{
path: effective, phase: historyPhaseSources, payload: payload,
@@ -70,6 +83,12 @@ func (builder *historyBuilder) observeSourceEvidence(root *document.Node, origin
for index := range records {
record := records[index]
payload := builder.addSourcePayload(nil, &record)
applicationRef, hasApplication := sourceApplicationForRecord(record, applicationsBySource, applicationsByLayer)
if hasApplication {
owned := &builder.model.sourcePayloads[payload.id]
owned.applicationRef = applicationRef
owned.hasApplicationRef = true
}
for displacedIndex, displaced := range record.displaced {
definitionPayload := payload
definitionPayload.substep = displacedIndex + 1
@@ -119,6 +138,19 @@ func (builder *historyBuilder) observeSourceEvidence(root *document.Node, origin
})
}
func sourceApplicationForRecord(record overlayRecord, bySource map[string]historyPayloadRef, byLayer map[Layer][]historyPayloadRef) (historyPayloadRef, bool) {
for _, location := range record.change.Origin.Locations {
if reference, found := bySource[location.SourceID]; found && location.SourceID != "" {
return reference, true
}
}
applications := byLayer[record.change.Origin.Layer]
if len(applications) == 1 {
return applications[0], true
}
return historyPayloadRef{}, false
}
func (builder *historyBuilder) addSourcePayload(application *historyApplication, overlay *overlayRecord) historyPayloadRef {
payload := builder.nextPayload(historyPhaseSources)
if payload.id != len(builder.model.sourcePayloads) {

View File

@@ -40,6 +40,8 @@ func TestHistorySourceObservationsPreserveRawAndCompositeDefinitions(t *testing.
builder := newHistoryBuilder()
builder.observeBaseline(baseline)
builder.observeSourceEvidence(assembly.current.root, assembly.current.origins, assembly.current.applications, assembly.current.overlays)
builder.model.final = assembly.current.root
builder.registerDocumentRoot(assembly.current.root)
model := builder.freeze()
defaultPath := Keys("defaults", "nullable")
@@ -87,6 +89,30 @@ func TestHistorySourceObservationsPreserveRawAndCompositeDefinitions(t *testing.
if emptySummary == nil || len(emptySummary.contributions) != 2 || emptySummary.contributions[0].id != 0 || emptySummary.contributions[1].id != 1 {
t.Fatalf("empty mapping summary contribution references = %#v", emptySummary)
}
metadata := Metadata{historyModel: model}
metadataHistory := metadata.History(Keys("section", "empty"))
var compositeSources []string
for _, change := range metadataHistory {
if len(change.Origin.Locations) > 1 {
compositeSources = locationSourceIDs(change.Origin.Locations)
}
}
inspection := &Inspection{
metadata: metadata, baseline: baseline, final: assembly.current.root,
immutableLookup: buildDocumentLookup(assembly.current.root),
}
explanation, explained := inspection.Explain(Keys("section", "empty"))
var contributionSources []string
for _, change := range explanation.History {
if change.Origin.Layer == LayerBase || change.Origin.Layer == LayerOverlay {
contributionSources = append(contributionSources, locationSourceIDs(change.Origin.Locations)...)
}
}
if !explained || !reflect.DeepEqual(compositeSources, []string{"base", "overlay"}) ||
!reflect.DeepEqual(contributionSources, []string{"base", "overlay"}) {
t.Fatalf("metadata composite attribution and explanation contributions differ: metadata %#v, explanation %#v", metadataHistory, explanation.History)
}
}
func TestHistorySourceReplacementTracksConcreteBeforeAndAfterShapes(t *testing.T) {

View File

@@ -2,64 +2,61 @@ package engine
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 {
selectionPath := query
if operation.transfer != nil {
selectionPath = operationPathForQuery(operation.transfer.to, query)
}
selections := resolveInheritanceAttribution(operation, selectionPath)
if operation.control != nil {
control := selectInheritanceEvidence(selections, inheritanceEvidenceControl, inheritanceEvidenceControl)
if control == nil || !pathsIntersect(operation.control.path, query) {
func appendInheritanceExplanation(explanation *Explanation, working **document.Node, query Path, model *historyModel, selections []historySelection, secrets map[string]bool) {
if model == nil {
return
}
for _, payload := range model.inheritancePayloads {
for operationIndex, operation := range payload.operations {
if operation.control != nil {
ref := historyPayloadRef{phase: historyPhaseInheritance, id: 0, substep: operationIndex * 2}
selection, selected := historySelectionForPayload(selections, ref)
if !selected {
continue
}
before := valueSnapshot(*working, query, secrets)
replaceHistoryPath(working, operation.control.path, nil)
after := valueSnapshot(*working, query, secrets)
appendInheritanceChange(explanation, selection, before, after)
continue
}
if operation.transfer == nil {
continue
}
parentRef := historyPayloadRef{phase: historyPhaseInheritance, id: 0, substep: operationIndex * 2}
childRef := historyPayloadRef{phase: historyPhaseInheritance, id: 0, substep: operationIndex*2 + 1}
parentSelection, hasParent := historySelectionForPayload(selections, parentRef)
childSelection, hasChild := historySelectionForPayload(selections, childRef)
before := valueSnapshot(*working, query, secrets)
replaceHistoryPath(working, operation.control.path, nil)
after := valueSnapshot(*working, query, secrets)
explanation.History = append(explanation.History, ValueChange{
Before: before, After: after, Origin: cloneOrigin(control.origin),
Dependencies: clonePaths(control.dependencies),
})
continue
}
if operation.transfer == nil {
continue
}
before := valueSnapshot(*working, query, secrets)
replaceHistoryPath(working, operation.transfer.to, operation.parent)
// A record keeps its own identity throughout inheritance. Restore it
// before taking enclosing snapshots as well as direct field snapshots.
for _, field := range operation.identityFields {
identity, _ := documentAtPath(operation.effective, Keys(field))
replaceHistoryPath(working, operation.transfer.to.Key(field), identity)
}
afterParent := valueSnapshot(*working, query, secrets)
parent := selectInheritanceEvidence(selections, inheritanceEvidenceParent, inheritanceEvidenceParent)
if pathsIntersect(operation.transfer.to, query) && parent != nil {
explanation.History = append(explanation.History, ValueChange{
Before: before, After: afterParent, Origin: cloneOrigin(parent.origin),
Dependencies: clonePaths(parent.dependencies),
})
}
replaceHistoryPath(working, operation.transfer.to, operation.effective)
if !pathsIntersect(operation.transfer.to, query) {
continue
}
afterChild := valueSnapshot(*working, query, secrets)
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),
})
replaceHistoryPath(working, operation.transfer.to, operation.parent)
// A record keeps its own identity throughout inheritance. Restore it
// before taking enclosing snapshots as well as direct field snapshots.
for _, field := range operation.identityFields {
identity, _ := documentAtPath(operation.effective, Keys(field))
replaceHistoryPath(working, operation.transfer.to.Key(field), identity)
}
afterParent := valueSnapshot(*working, query, secrets)
if hasParent {
appendInheritanceChange(explanation, parentSelection, before, afterParent)
}
replaceHistoryPath(working, operation.transfer.to, operation.effective)
afterChild := valueSnapshot(*working, query, secrets)
if hasChild {
appendInheritanceChange(explanation, childSelection, afterParent, afterChild)
}
}
}
}
func operationPathForQuery(root, query Path) Path {
if pathContains(query, root) {
return root
func appendInheritanceChange(explanation *Explanation, selection historySelection, before, after ValueSnapshot) {
if !selection.hasAttribution {
return
}
return query
explanation.History = append(explanation.History, ValueChange{
Before: before,
After: after,
Origin: cloneOrigin(selection.attribution.origin),
Reasons: append([]string(nil), selection.attribution.reasons...),
Dependencies: clonePaths(selection.attribution.dependencies),
})
}

View File

@@ -121,8 +121,17 @@ func matchesPathShape(query, candidate Path) bool {
return false
}
if actual.kind == pathSegmentIndex {
index, ok := schemaIndex(segment)
if !ok || index != actual.index {
switch segment.kind {
case pathSegmentIndex:
if segment.index != actual.index {
return false
}
case pathSegmentParsed:
index, ok := schemaIndex(segment)
if !ok || index != actual.index {
return false
}
default:
return false
}
} else if segment.kind == pathSegmentIndex {