Replay explanations through shared history selections
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Interface Contract Remediation Implementation Plan
|
||||
|
||||
Status: in progress. Stages 1–10 are complete; Stages 11–18 remain pending.
|
||||
Status: in progress. Stages 1–11 are complete; Stages 12–18 remain pending.
|
||||
Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`.
|
||||
Audience: a Luna coding agent executing the stages sequentially.
|
||||
|
||||
@@ -555,7 +555,10 @@ root history regressions.
|
||||
## Stage 11 — Cut Explanation Over To Common Selections
|
||||
|
||||
Owners: `explain.go`, `inheritance_explain.go`, `snapshot.go`, model replay helpers.
|
||||
Status: pending.
|
||||
Status: complete. Validation:
|
||||
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Explain|Explanation|History|Render|Diff' -count=1`,
|
||||
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`,
|
||||
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...`, and `git diff --check`.
|
||||
|
||||
1. Drive phase chronology and event relevance through model selections. Remove
|
||||
independent relevance/origin interpretation from `Inspection.Explain` and
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user