From 12fd0a71917bac14078c4d2572f29cebf5d0ba74 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 26 Oct 2026 17:36:36 +0000 Subject: [PATCH] Publish inspection through one frozen history model --- docs/roadmap/implementation.md | 5 +- internal/engine/diff.go | 4 +- internal/engine/diff_operation_test.go | 10 +-- internal/engine/diff_test.go | 5 +- internal/engine/explain.go | 4 +- internal/engine/explain_test.go | 24 ++++-- internal/engine/history.go | 26 +----- internal/engine/history_budget_test.go | 85 +++++++++++++------ internal/engine/history_model.go | 33 ++++---- internal/engine/history_select_test.go | 13 ++- internal/engine/history_sequence_test.go | 33 ++++---- internal/engine/history_source.go | 1 - internal/engine/history_source_test.go | 4 +- internal/engine/inheritance_history_test.go | 42 ++++++---- internal/engine/inheritance_secret_test.go | 10 +-- internal/engine/load.go | 29 ++----- internal/engine/load_result.go | 93 +++++---------------- internal/engine/metadata.go | 4 - internal/engine/override_context_test.go | 8 +- internal/engine/resolution_test.go | 17 ++-- 20 files changed, 203 insertions(+), 247 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index cdacaa8..c19f786 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -635,7 +635,10 @@ secret/diff integration tests. ## Stage 14 — Publish One Frozen History Owner Owners: `Inspection`, `prepareInspectionHistory`, `publishLoadResult`, -`Inspection.Metadata`, cloning and lookup setup. Status: pending. +`Inspection.Metadata`, cloning and lookup setup. Status: complete. Validation: +`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Detached|Independent|ConcurrentReadOnly|History|Explain' -count=1`, +`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test -race ./internal/engine -run 'Detached|Independent|ConcurrentReadOnly|History|Explain' -count=1`, +`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...`, and `git diff --check`. 1. Finish the model before publication, with eager indexes and final observations. Attach the same frozen owner to inspection and metadata. Remove independent diff --git a/internal/engine/diff.go b/internal/engine/diff.go index 19f4a54..5a608e4 100644 --- a/internal/engine/diff.go +++ b/internal/engine/diff.go @@ -34,11 +34,11 @@ func prepareDiffRegions(left, right *Inspection) ([]diffRegion, error) { deltas := compareConfigurationValues(left.final, right.final) regions := make([]diffRegion, 0, len(deltas)) for _, delta := range deltas { - leftNode, leftPresent := documentAtPathWithLookup(left.final, delta.path, left.immutableLookup) + leftNode, leftPresent := documentAtPathWithLookup(left.final, delta.path, left.documentLookup()) if !leftPresent { leftNode = nil } - rightNode, rightPresent := documentAtPathWithLookup(right.final, delta.path, right.immutableLookup) + rightNode, rightPresent := documentAtPathWithLookup(right.final, delta.path, right.documentLookup()) if !rightPresent { rightNode = nil } diff --git a/internal/engine/diff_operation_test.go b/internal/engine/diff_operation_test.go index ab463d5..4b7e0b1 100644 --- a/internal/engine/diff_operation_test.go +++ b/internal/engine/diff_operation_test.go @@ -53,7 +53,7 @@ func TestDiffUsesFinalValuesAndDependencyDerivedSensitivity(t *testing.T) { !right.Inspection.secrets["/credential"] || !right.Inspection.secrets["/derived"] { t.Fatalf("typed or dependency-derived sensitivity was not retained: left=%#v right=%#v", left.Inspection.secrets, right.Inspection.secrets) } - if node, present := documentAtPathWithLookup(left.Inspection.final, Keys("derived"), left.Inspection.immutableLookup); !present || node.Text != "before" { + if node, present := documentAtPathWithLookup(left.Inspection.final, Keys("derived"), left.Inspection.documentLookup()); !present || node.Text != "before" { t.Fatalf("left frozen derived value = %#v, present=%v", node, present) } @@ -138,10 +138,10 @@ func TestDiffUsesReferenceNamesAndFrozenSecretState(t *testing.T) { if err != nil || len(differences) != 0 { t.Fatalf("credential rotation behind one reference = %#v, err = %v", differences, err) } - if node, present := documentAtPathWithLookup(left.Inspection.final, Keys("optional"), left.Inspection.immutableLookup); !present || node.Text != "DIFF_ROTATING_SECRET" { + if node, present := documentAtPathWithLookup(left.Inspection.final, Keys("optional"), left.Inspection.documentLookup()); !present || node.Text != "DIFF_ROTATING_SECRET" { t.Fatalf("frozen reference = %#v, present=%v", node, present) } - if node, present := documentAtPathWithLookup(right.Inspection.final, Keys("optional"), right.Inspection.immutableLookup); !present || node.Text != "DIFF_ROTATING_SECRET" { + if node, present := documentAtPathWithLookup(right.Inspection.final, Keys("optional"), right.Inspection.documentLookup()); !present || node.Text != "DIFF_ROTATING_SECRET" { t.Fatalf("right frozen reference = %#v, present=%v", node, present) } @@ -164,7 +164,7 @@ func TestDiffUsesReferenceNamesAndFrozenSecretState(t *testing.T) { if absent.Config.Optional != nil { t.Fatalf("missing optional reference decoded as %#v", absent.Config.Optional) } - if node, present := documentAtPathWithLookup(absent.Inspection.final, Keys("optional"), absent.Inspection.immutableLookup); !present || node.Kind != document.Null { + if node, present := documentAtPathWithLookup(absent.Inspection.final, Keys("optional"), absent.Inspection.documentLookup()); !present || node.Kind != document.Null { t.Fatalf("optional nil projection = %#v, present=%v", node, present) } differences, err = Diff(absent.Inspection, left.Inspection) @@ -210,7 +210,7 @@ func TestDiffProtectsHistoricalSensitiveRemovalAndReplacement(t *testing.T) { if !left.Inspection.secrets[secretPath.String()] { t.Fatal("removed historical selector was not retained") } - if _, present := documentAtPathWithLookup(left.Inspection.final, secretPath, left.Inspection.immutableLookup); present { + if _, present := documentAtPathWithLookup(left.Inspection.final, secretPath, left.Inspection.documentLookup()); present { t.Fatal("removed token remains in the effective left tree") } differences, err := Diff(left.Inspection, right.Inspection) diff --git a/internal/engine/diff_test.go b/internal/engine/diff_test.go index 6b3cbe4..567d29e 100644 --- a/internal/engine/diff_test.go +++ b/internal/engine/diff_test.go @@ -127,7 +127,10 @@ type expectedDiffRegion struct { } func diffTestInspection(root *document.Node) *Inspection { - return &Inspection{final: root, immutableLookup: buildDocumentLookup(root)} + builder := newHistoryBuilder() + builder.model.final = root + model := builder.freeze() + return &Inspection{final: root, history: model, metadata: Metadata{historyModel: model}} } func assertDiffRegionNode(t *testing.T, side string, node *document.Node, wantPresent bool, wantKind document.Kind) { diff --git a/internal/engine/explain.go b/internal/engine/explain.go index d9609c0..6aca451 100644 --- a/internal/engine/explain.go +++ b/internal/engine/explain.go @@ -40,7 +40,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) { if r == nil || r.final == nil || !path.valid() { return Explanation{}, false } - model := r.metadata.historyModel + model := r.history if model == nil { return Explanation{}, false } @@ -48,7 +48,7 @@ func (r *Inspection) Explain(path Path) (Explanation, bool) { if len(selections) == 0 && !model.hasObservedShape(path) { return Explanation{}, false } - explanation := Explanation{Path: path.clone(), Effective: valueSnapshotWithLookup(r.final, path, r.secrets, r.immutableLookup)} + explanation := Explanation{Path: path.clone(), Effective: valueSnapshotWithLookup(r.final, path, r.secrets, model.documentLookup)} if info, ok := r.metadata.Lookup(path); ok { explanation.Origin = cloneOrigin(info.Origin) } diff --git a/internal/engine/explain_test.go b/internal/engine/explain_test.go index d4023ba..e6ab902 100644 --- a/internal/engine/explain_test.go +++ b/internal/engine/explain_test.go @@ -308,16 +308,28 @@ func TestSecretsBootstrapLoadAndExplanationRemainSeparateAndSafe(t *testing.T) { func TestHistoryNodeBudgetCountsSharedTreesOnce(t *testing.T) { leaf := &document.Node{Kind: document.String, Text: "value"} root := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "value", Value: leaf}}} - inspection := &Inspection{ - baseline: root, input: root, final: root, - applications: []historyApplication{{layer: LayerBase, value: root}}, - } + builder := newHistoryBuilder() + builder.model.baseline = root + builder.model.final = root + builder.model.sourcePayloads = []historySourcePayload{{ + kind: historySourceApplicationPayload, application: historyApplication{layer: LayerBase, value: root}, + }} + model := builder.freeze() + inspection := &Inspection{history: model, metadata: Metadata{historyModel: model}, final: root} counter := newRetainedNodeCounter(2) if !addInspectionHistory(counter, inspection) || counter.count != 2 { t.Fatal("structurally shared history was counted more than once") } other := document.Clone(root) - inspection.input = other + builder = newHistoryBuilder() + builder.model.baseline = root + builder.model.final = root + builder.model.sourcePayloads = []historySourcePayload{{ + kind: historySourceApplicationPayload, application: historyApplication{layer: LayerBase, value: other}, + }} + model = builder.freeze() + inspection.history = model + inspection.metadata.historyModel = model counter = newRetainedNodeCounter(3) if addInspectionHistory(counter, inspection) { t.Fatal("distinct retained nodes did not consume the budget") @@ -425,7 +437,7 @@ func TestIntegerVersionInspectionRetainsInputAndReconcilesTypedValue(t *testing. if first.Config.Version != "4" || second.Config.Version != "4" { t.Fatalf("configurations = %#v / %#v", first.Config, second.Config) } - inputVersion, inputOK := documentAtPath(first.applications[0].value, Keys("version")) + inputVersion, inputOK := documentAtPath(first.history.sourcePayloads[0].application.value, Keys("version")) finalVersion, finalOK := documentAtPath(first.final, Keys("version")) if !inputOK || inputVersion.Kind != document.Number || inputVersion.Text != "4" || !finalOK || finalVersion.Kind != document.String || finalVersion.Text != "4" { diff --git a/internal/engine/history.go b/internal/engine/history.go index 76ecc0e..7287b7c 100644 --- a/internal/engine/history.go +++ b/internal/engine/history.go @@ -85,36 +85,14 @@ func documentCloneNodeCount(root *document.Node, limit int) (int, bool) { } func addInspectionHistory(counter *retainedNodeCounter, inspection *Inspection) bool { - if inspection == nil { + if inspection == nil || inspection.history == nil { return false } - for _, root := range []*document.Node{inspection.baseline, inspection.input, inspection.final, inspection.preCLI} { + for _, root := range inspection.history.documentRoots() { if !counter.add(root) { return false } } - for _, application := range inspection.applications { - if !counter.add(application.value) { - return false - } - } - for _, application := range inspection.postDecodeApps { - if !counter.add(application.value) { - return false - } - } - for _, snapshot := range inspection.resolution { - if !counter.add(snapshot.before) || !counter.add(snapshot.after) { - return false - } - } - for _, operations := range [][]inheritanceOperation{inspection.inheritance, inspection.metadata.inheritance} { - for _, operation := range operations { - if !counter.add(operation.parent) || !counter.add(operation.effective) { - return false - } - } - } return true } diff --git a/internal/engine/history_budget_test.go b/internal/engine/history_budget_test.go index 99f87a3..ef2fe01 100644 --- a/internal/engine/history_budget_test.go +++ b/internal/engine/history_budget_test.go @@ -22,21 +22,33 @@ type historyBudgetLayeredConfig struct { func retainedInspectionNodeCountForTest(inspection *Inspection) int { seen := make(map[*document.Node]struct{}) - stack := []*document.Node{inspection.baseline, inspection.input, inspection.final, inspection.preCLI} - for _, application := range inspection.applications { - stack = append(stack, application.value) + model := inspection.history + stack := []*document.Node{model.baseline, model.final} + for _, payload := range model.sourcePayloads { + if payload.kind == historySourceApplicationPayload { + stack = append(stack, payload.application.value) + } } - for _, application := range inspection.postDecodeApps { - stack = append(stack, application.value) + for _, payload := range model.inheritancePayloads { + for _, operation := range payload.operations { + stack = append(stack, operation.parent, operation.effective) + } } - for _, snapshot := range inspection.resolution { - stack = append(stack, snapshot.before, snapshot.after) + for _, payload := range model.cliPayloads { + if payload.kind == historySourceApplicationPayload { + stack = append(stack, payload.application.value) + } } - for _, operation := range inspection.inheritance { - stack = append(stack, operation.parent, operation.effective) + if model.preCLICheckpoint != nil { + stack = append(stack, model.preCLICheckpoint.root) } - for _, operation := range inspection.metadata.inheritance { - stack = append(stack, operation.parent, operation.effective) + if model.postCLICheckpoint != nil { + stack = append(stack, model.postCLICheckpoint.root) + } + if model.resolutionPayload != nil { + for _, snapshot := range model.resolutionPayload.snapshots { + stack = append(stack, snapshot.before, snapshot.after) + } } for len(stack) != 0 { @@ -144,8 +156,8 @@ func TestLayeredHistoryBudgetRejectsBeforeSecretsAndValidation(t *testing.T) { if want == 0 { t.Fatal("independent retained-tree count was empty") } - if len(baseline.Inspection.inheritance) == 0 || len(baseline.Inspection.metadata.inheritance) == 0 { - t.Fatal("layered load did not retain both inheritance evidence copies") + if baseline.Inspection.history == nil || baseline.Inspection.metadata.historyModel != baseline.Inspection.history || len(baseline.Inspection.history.inheritancePayloads) == 0 { + t.Fatal("layered load did not publish its shared inheritance evidence owner") } exactCounts := &callbackCounts{} loaded, err := loadGroupedWithSecretLookupLimit(makeRequest(exactCounts), testSecretBinding(), lookup(exactCounts), want) @@ -169,23 +181,46 @@ func TestLayeredHistoryBudgetRejectsBeforeSecretsAndValidation(t *testing.T) { } } -func TestPreparedInheritanceEvidenceOwnsDistinctTrees(t *testing.T) { - root := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "value", Value: &document.Node{Kind: document.String, Text: "shared"}}}} - resolved := resolvedLoad[historyBudgetSimpleConfig]{ - inheritance: &inheritanceScope{operations: []inheritanceOperation{{parent: root, effective: root}}}, +func TestPreparedInheritanceEvidenceSharesOwnerAndDetachesPublicViews(t *testing.T) { + input := parseCompositionDocument(t, "inheritance", "parent: {value: shared}\nchild: {extends: parent}\n") + expanded, diagnostics := expandInheritanceWithOrigins(input, nil, defaultProjectionLimits()) + if len(diagnostics) != 0 { + t.Fatal(diagnostics) } - counter := newRetainedNodeCounter(8) + baseline := &document.Node{Kind: document.Mapping} + builder := newHistoryBuilder() + builder.observeBaseline(baseline) + builder.setScratch(input) + builder.observeInheritanceEvidence(expanded.operations) + metadata := Metadata{historyBuilder: builder} + resolved := resolvedLoad[historyBudgetSimpleConfig]{ + metadata: metadata, historyBuilder: builder, final: expanded.root, + decoded: decodedConfiguration{baseline: baseline}, structuralInput: input, + inheritance: expanded, + } + counter := newRetainedNodeCounter(100) inspection, ok := prepareInspectionHistory(resolved, counter) if !ok { - t.Fatal("preparation rejected the exact size of the distinct retained copies") + t.Fatal("preparation rejected the shared evidence owner") } - if len(inspection.inheritance) != 1 || len(inspection.metadata.inheritance) != 1 { - t.Fatalf("inheritance copies = inspection %#v, metadata %#v", inspection.inheritance, inspection.metadata.inheritance) + if inspection.history == nil || inspection.metadata.historyModel != inspection.history || len(inspection.history.inheritancePayloads) != 1 { + t.Fatalf("history owners = inspection %p, metadata %p, inheritance payloads %d", inspection.history, inspection.metadata.historyModel, len(inspection.history.inheritancePayloads)) } - inspectionOperation := inspection.inheritance[0] - metadataOperation := inspection.metadata.inheritance[0] - if inspectionOperation.parent == metadataOperation.parent || inspectionOperation.effective == metadataOperation.effective || counter.count != 8 { - t.Fatalf("separate inheritance copies were not counted independently: inspection=(%p, %p) metadata=(%p, %p) count=%d", inspectionOperation.parent, inspectionOperation.effective, metadataOperation.parent, metadataOperation.effective, counter.count) + path := Keys("child", "value") + first := inspection.Metadata().History(path) + if len(first) == 0 { + t.Fatal("metadata projection omitted inherited history") + } + first[0].Origin.Locations[0].SourceID = "mutated metadata result" + first[0].Dependencies[0].segments[0].key = "mutated metadata dependency" + if explanation, found := inspection.Explain(path); !found || len(explanation.History) == 0 || explanation.History[0].Origin.Locations[0].SourceID == "mutated metadata result" { + t.Fatalf("metadata result mutation leaked into explanation: %#v, %v", explanation, found) + } + explanation, _ := inspection.Explain(path) + explanation.History[0].Origin.Locations[0].SourceID = "mutated explanation result" + second := inspection.Metadata().History(path) + if second[0].Origin.Locations[0].SourceID == "mutated explanation result" || second[0].Dependencies[0].String() == "/mutated metadata dependency" { + t.Fatalf("explanation mutation leaked into metadata: %#v", second) } } diff --git a/internal/engine/history_model.go b/internal/engine/history_model.go index c02baa7..5a44b81 100644 --- a/internal/engine/history_model.go +++ b/internal/engine/history_model.go @@ -59,7 +59,6 @@ type historyModel struct { attributions []historyAttribution baseline *document.Node final *document.Node - inventoryRoots []*document.Node sourcePayloads []historySourcePayload inheritancePayloads []historyInheritancePayload cliPayloads []historySourcePayload @@ -149,20 +148,12 @@ func (builder *historyBuilder) observeDocumentAt(root *document.Node, prefix Pat if root == nil { return } - builder.registerDocumentRoot(root) payload := builder.nextPayload(phase) walkDocument(root, prefix, func(path Path, node *document.Node) { builder.addInventoryObservation(path, path, payload, phase, node.Sensitive) }) } -func (builder *historyBuilder) registerDocumentRoot(root *document.Node) { - builder.requireOpen() - if root != nil { - builder.model.inventoryRoots = append(builder.model.inventoryRoots, root) - } -} - func (builder *historyBuilder) addInventoryObservation(path, reportedPath Path, payload historyPayloadRef, phase historyPhase, sensitive bool) int { return builder.addObservation(historyObservation{ path: path, phase: phase, payload: payload, @@ -204,32 +195,40 @@ func (model *historyModel) documentRoots() []*document.Node { if model == nil { return nil } - roots := append([]*document.Node(nil), model.inventoryRoots...) - roots = append(roots, model.baseline, model.final) + var roots []*document.Node + appendRoot := func(root *document.Node) { + if root != nil { + roots = append(roots, root) + } + } + appendRoot(model.baseline) + appendRoot(model.final) for _, payload := range model.sourcePayloads { if payload.kind == historySourceApplicationPayload { - roots = append(roots, payload.application.value) + appendRoot(payload.application.value) } } for _, payload := range model.inheritancePayloads { for _, operation := range payload.operations { - roots = append(roots, operation.parent, operation.effective) + appendRoot(operation.parent) + appendRoot(operation.effective) } } for _, payload := range model.cliPayloads { if payload.kind == historySourceApplicationPayload { - roots = append(roots, payload.application.value) + appendRoot(payload.application.value) } } if model.preCLICheckpoint != nil { - roots = append(roots, model.preCLICheckpoint.root) + appendRoot(model.preCLICheckpoint.root) } if model.postCLICheckpoint != nil { - roots = append(roots, model.postCLICheckpoint.root) + appendRoot(model.postCLICheckpoint.root) } if model.resolutionPayload != nil { for _, snapshot := range model.resolutionPayload.snapshots { - roots = append(roots, snapshot.before, snapshot.after) + appendRoot(snapshot.before) + appendRoot(snapshot.after) } } return roots diff --git a/internal/engine/history_select_test.go b/internal/engine/history_select_test.go index 0c1e92a..989691e 100644 --- a/internal/engine/history_select_test.go +++ b/internal/engine/history_select_test.go @@ -156,7 +156,7 @@ func TestHistorySelectionMatchesDeepSidesOfAncestorShapeChange(t *testing.T) { } } -func TestHistoryModelFinalizesLocationSensitivityAndDocumentReaders(t *testing.T) { +func TestHistoryModelFinalizesLocationAndSensitivityWithoutRetainingInventoryTrees(t *testing.T) { t.Parallel() root := parseDocumentForDecode(t, "shape: {'0': {value: old}}\n") @@ -189,11 +189,11 @@ func TestHistoryModelFinalizesLocationSensitivityAndDocumentReaders(t *testing.T }) model := builder.freeze() - if got := model.documentRoots(); len(got) == 0 || got[0] != root { - t.Fatalf("document roots = %#v, want registered root", got) + if got := model.documentRoots(); len(got) != 0 { + t.Fatalf("inventory-only document was retained: %#v", got) } - if got, ok := model.documentAt(root, Keys("shape", "0")); !ok || got.Kind != document.Mapping { - t.Fatalf("indexed document lookup = %#v, %v", got, ok) + if got, ok := model.documentAt(root, Keys("shape", "0")); ok || got != nil { + t.Fatalf("lookup retained an inventory-only document = %#v, %v", got, ok) } if got, ok := model.latestHistoricalLocation(keyPath); !ok || got.SourceID != "base.yml" || got.Line != 4 || got.Column != 2 { t.Fatalf("latest historical location = %#v, %v", got, ok) @@ -213,9 +213,6 @@ func TestHistoryModelFinalizesLocationSensitivityAndDocumentReaders(t *testing.T if !sawCandidate { t.Fatal("sensitivity candidate omitted the concrete historical path") } - if got, found := model.documentAt(root, Keys("shape", "missing")); found || got != nil { - t.Fatalf("lookup for missing value = %#v, %v", got, found) - } } func TestHistoryBuilderScratchUsesOneDetachedWorkingDocument(t *testing.T) { diff --git a/internal/engine/history_sequence_test.go b/internal/engine/history_sequence_test.go index 630f0b7..ad2f6ce 100644 --- a/internal/engine/history_sequence_test.go +++ b/internal/engine/history_sequence_test.go @@ -278,14 +278,13 @@ func TestProjectedSequenceHistoryAgreesAcrossMetadataAndExplanation(t *testing.T t.Fatalf("projected metadata omitted shifted path %s: %#v", path, metadata.HistoricalPaths()) } - inspection := &Inspection{ - 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) + baseline := &document.Node{Kind: document.Mapping} + model := sourceHistoryModelForInspection(baseline, composed.root, composed.origins, composed.applications, composed.overlays) + metadata.historyModel = model + inspection := &Inspection{metadata: metadata, history: model, final: composed.root} inspection.secrets = classifySecretPaths(nil, func(candidate Path) bool { return reflect.DeepEqual(candidate, path) - }, metadata, composed.root, composed.applications) + }, metadata, composed.root, nil) explanation, ok := inspection.Explain(path) if !ok || !explanation.Effective.Redacted { t.Fatalf("shifted explanation = %#v, %v", explanation, ok) @@ -326,14 +325,13 @@ func TestReplacedProjectedSensitiveRecordRemainsRedacted(t *testing.T) { path := Keys("records").Index(1).Key("token") var metadata Metadata finalizeMetadataPaths(&metadata, final, applications) - inspection := &Inspection{ - 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) + baseline := &document.Node{Kind: document.Mapping} + model := sourceHistoryModelForInspection(baseline, final, nil, applications, nil) + metadata.historyModel = model + inspection := &Inspection{metadata: metadata, history: model, final: final} inspection.secrets = classifySecretPaths(nil, func(candidate Path) bool { return reflect.DeepEqual(candidate, path) - }, metadata, final, applications) + }, metadata, final, nil) explanation, ok := inspection.Explain(path) if !ok || explanation.Effective.Present { t.Fatalf("replaced shifted record explanation = %#v, %v", explanation, ok) @@ -353,7 +351,6 @@ func sourceHistoryModelForInspection(baseline, final *document.Node, origins map builder.setScratch(baseline) builder.observeSourceEvidence(final, origins, applications, records) builder.model.final = final - builder.registerDocumentRoot(final) return builder.freeze() } @@ -409,12 +406,12 @@ func TestMappedUpsertProjectsSensitiveReplacementEvidence(t *testing.T) { var metadata Metadata finalizeMetadataPaths(&metadata, finalRoot, applications) - inspection := &Inspection{ - metadata: metadata, baseline: &document.Node{Kind: document.Mapping}, - input: finalRoot, preCLI: finalRoot, final: finalRoot, applications: applications, - } + baseline := &document.Node{Kind: document.Mapping} + model := sourceHistoryModelForInspection(baseline, finalRoot, nil, applications, nil) + metadata.historyModel = model + inspection := &Inspection{metadata: metadata, history: model, final: finalRoot} secretRule := func(candidate Path) bool { return reflect.DeepEqual(candidate, secretPath) } - inspection.secrets = classifySecretPaths(nil, secretRule, metadata, finalRoot, applications) + inspection.secrets = classifySecretPaths(nil, secretRule, metadata, finalRoot, nil) explanation, ok := inspection.Explain(secretPath) if !ok || !explanation.Effective.Redacted { t.Fatalf("mapped replacement explanation = %#v, %v", explanation, ok) diff --git a/internal/engine/history_source.go b/internal/engine/history_source.go index e192e42..401aa12 100644 --- a/internal/engine/history_source.go +++ b/internal/engine/history_source.go @@ -35,7 +35,6 @@ func (builder *historyBuilder) observeBaseline(root *document.Node) { return } builder.model.baseline = root - builder.registerDocumentRoot(root) payload := builder.nextPayload(historyPhaseBaseline) defaultAttribution := builder.addAttribution(Origin{Layer: LayerDefault}, nil, nil) walkDocument(root, Path{}, func(path Path, node *document.Node) { diff --git a/internal/engine/history_source_test.go b/internal/engine/history_source_test.go index 96ddf5a..28d3a21 100644 --- a/internal/engine/history_source_test.go +++ b/internal/engine/history_source_test.go @@ -41,7 +41,6 @@ func TestHistorySourceObservationsPreserveRawAndCompositeDefinitions(t *testing. 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") @@ -99,8 +98,7 @@ func TestHistorySourceObservationsPreserveRawAndCompositeDefinitions(t *testing. } } inspection := &Inspection{ - metadata: metadata, baseline: baseline, final: assembly.current.root, - immutableLookup: buildDocumentLookup(assembly.current.root), + metadata: metadata, history: model, final: assembly.current.root, } explanation, explained := inspection.Explain(Keys("section", "empty")) var contributionSources []string diff --git a/internal/engine/inheritance_history_test.go b/internal/engine/inheritance_history_test.go index 71e2a58..2ec5e9f 100644 --- a/internal/engine/inheritance_history_test.go +++ b/internal/engine/inheritance_history_test.go @@ -128,12 +128,18 @@ func inheritanceInspectionFromComposition(t *testing.T, composed *composedDocume applyInheritanceMetadata(&metadata, composed.root, composed.origins, composed.overlays, expanded) metadata.historyBuilder.setScratch(composed.root) metadata.historyBuilder.observeInheritanceEvidence(expanded.operations) - metadata.inheritance = cloneInheritanceOperations(expanded.operations) - finalizeMetadataPathsWithResolution(&metadata, expanded.root, composed.applications, snapshots, baseline, composed.root, expanded.root) + final := expanded.root + if len(snapshots) != 0 { + final = document.Clone(expanded.root) + for _, group := range coalescedResolutionSnapshots(snapshots) { + if group.coveringRecordID >= 0 && group.coveringRecordID < len(snapshots) { + replaceHistoryPath(&final, group.change.Path, snapshots[group.coveringRecordID].after) + } + } + } + finalizeMetadataPathsWithResolution(&metadata, final, composed.applications, snapshots, baseline, composed.root, expanded.root) return &Inspection{ - metadata: metadata, baseline: baseline, input: expanded.root, preCLI: expanded.root, - inputOrigins: expanded.origins, final: expanded.root, - applications: composed.applications, inheritance: expanded.operations, + metadata: metadata, history: metadata.historyModel, final: final, }, expanded } @@ -323,8 +329,8 @@ func TestIndexedInheritanceHistoryMatchesOperationScan(t *testing.T) { parseCompositionDocument(t, "source-2", "child: {extends: parent, branch: replaced}\n"), parseCompositionDocument(t, "source-3", "sibling: {extends: parent}\n")) metadata := inspection.Metadata() - if len(metadata.inheritance) < 4 { - t.Fatalf("inheritance operations = %d, want multiple control and transfer scopes", len(metadata.inheritance)) + if inspection.history == nil || len(inspection.history.inheritancePayloads) == 0 || len(inspection.history.inheritancePayloads[0].operations) < 4 { + t.Fatalf("inheritance operations = %#v, want multiple control and transfer scopes", inspection.history) } queries := []Path{ Keys("child"), @@ -372,11 +378,6 @@ func TestInheritanceHistoryPrecedesApplicationResolution(t *testing.T) { after: replacement, } inspection, _ := inheritanceInspectionFromComposition(t, composed, snapshot) - final := document.Clone(inspection.final) - replaceHistoryPath(&final, path, replacement) - inspection.final = final - inspection.resolution = []resolutionSnapshot{snapshot} - inspection.resolutionReplay = coalescedResolutionSnapshots(inspection.resolution) inspection.metadata.fields[path.String()] = FieldInfo{Explicit: true, Origin: snapshot.change.Origin} history := inspection.Metadata().History(path) if len(history) < 2 || history[len(history)-2].Origin.Operation != ChangeInheritance || @@ -390,17 +391,24 @@ func TestInheritanceHistoryPrecedesApplicationResolution(t *testing.T) { } } -func TestHistoryNodeBudgetCountsSharedInheritanceEvidenceOnce(t *testing.T) { +func TestHistoryNodeBudgetCountsSharedInheritanceRootsOnce(t *testing.T) { shared := &document.Node{Kind: document.String, Text: "shared"} extra := &document.Node{Kind: document.String, Text: "extra"} - inspection := &Inspection{ - inheritance: []inheritanceOperation{{parent: shared, effective: shared}}, - metadata: Metadata{inheritance: []inheritanceOperation{{parent: shared, effective: extra}}}, - } + operation := inheritanceOperation{parent: shared, effective: shared} + builder := newHistoryBuilder() + builder.observeInheritanceEvidence([]inheritanceOperation{operation}) + model := builder.freeze() + inspection := &Inspection{history: model, metadata: Metadata{historyModel: model}} counter := newRetainedNodeCounter(2) if !addInspectionHistory(counter, inspection) || counter.count != 2 { t.Fatal("shared evidence node accounting is incorrect") } + operation.effective = extra + builder = newHistoryBuilder() + builder.observeInheritanceEvidence([]inheritanceOperation{operation}) + model = builder.freeze() + inspection.history = model + inspection.metadata.historyModel = model counter = newRetainedNodeCounter(1) if addInspectionHistory(counter, inspection) { t.Fatal("distinct inheritance evidence did not consume the budget") diff --git a/internal/engine/inheritance_secret_test.go b/internal/engine/inheritance_secret_test.go index 31492df..b85a0e4 100644 --- a/internal/engine/inheritance_secret_test.go +++ b/internal/engine/inheritance_secret_test.go @@ -15,7 +15,7 @@ func TestInheritanceSensitivityFollowsCopiesWithoutMaskingNeighbors(t *testing.T inspection, _ := inheritanceInspectionForTest(t, input) inspection.secrets = classifySecretPaths(nil, func(path Path) bool { return path.String() == classified.String() - }, inspection.metadata, inspection.final, inspection.applications, inspection.input) + }, inspection.metadata, inspection.final, nil) for _, path := range []Path{Keys("parent", "token"), Keys("child", "token"), Keys("next", "token"), Keys("other", "token")} { explanation, found := inspection.Explain(path) if !found || !explanation.Effective.Redacted || strings.Contains(explanation.Effective.YAML, canary) { @@ -44,7 +44,7 @@ func TestInheritanceSensitivityPreservesDisplacedHistoryAndControl(t *testing.T) inspection, _ := inheritanceInspectionForTest(t, parseCompositionDocument(t, "source-1", "parent: {branch: {token: "+canary+"}}\nchild: {extends: parent, branch: public}\n")) inspection.secrets = classifySecretPaths(nil, func(path Path) bool { return path.String() == Keys("parent", "branch", "token").String() || path.String() == Keys("child", inheritanceKey).String() - }, inspection.metadata, inspection.final, inspection.applications, inspection.input) + }, inspection.metadata, inspection.final, nil) path := Keys("child", "branch", "token") explanation, found := inspection.Explain(path) if !found || explanation.Effective.Present { @@ -85,7 +85,7 @@ func TestInheritanceSensitivityIncludesSchemaTaggedSource(t *testing.T) { t.Fatal(issue) } inspection, _ := inheritanceInspectionForTest(t, parseCompositionDocument(t, "source-1", "parent: {token: TAGGED-CANARY}\nchild: {extends: parent}\n")) - inspection.secrets = classifySecretPaths(schema, nil, inspection.metadata, inspection.final, inspection.applications, inspection.input) + inspection.secrets = classifySecretPaths(schema, nil, inspection.metadata, inspection.final, nil) child, found := inspection.Explain(Keys("child", "token")) if !found || !child.Effective.Redacted || strings.Contains(child.Effective.YAML, "TAGGED-CANARY") { t.Fatalf("tagged source copy = %#v, %v", child, found) @@ -104,7 +104,7 @@ func TestSourceVersionEvidenceSensitivityFollowsInheritanceAndDisplacedHistory(t } inspection.secrets = classifySecretPathsWithEvidence(nil, func(path Path) bool { return path.String() == secretPath.String() - }, inspection.metadata, inspection.final, inspection.applications, []*sourceVersionEvidence{evidence}, inspection.input) + }, inspection.metadata, inspection.final, nil, []*sourceVersionEvidence{evidence}) path := Keys("child", "branch", "token") explanation, found := inspection.Explain(path) @@ -162,7 +162,7 @@ func TestInheritedSecretReferencesResolveOnlyAfterExpansion(t *testing.T) { t.Fatalf("inherited reference resolution = %#v, %d", resolved.diagnostics, lookupCount) } inspection, _ := inheritanceInspectionFromComposition(t, composed) - inspection.secrets = classifySecretPaths(schema, nil, inspection.metadata, inspection.final, inspection.applications, inspection.input) + inspection.secrets = classifySecretPaths(schema, nil, inspection.metadata, inspection.final, nil) var rendered bytes.Buffer if err := inspection.WriteEffective(&rendered); err != nil || strings.Contains(rendered.String(), "RESOLVED-CREDENTIAL-CANARY") { t.Fatalf("reference rendering = %q, %v", rendered.String(), err) diff --git a/internal/engine/load.go b/internal/engine/load.go index 2b0ba6f..71735e4 100644 --- a/internal/engine/load.go +++ b/internal/engine/load.go @@ -87,25 +87,22 @@ type Loaded[T any] struct { // Inspection owns frozen load snapshots and detached query state. type Inspection struct { metadata Metadata + history *historyModel sources []SourceInfo contexts map[string]ResourceContext diagnostics []Diagnostic - baseline *document.Node - input *document.Node - inputOrigins map[string]Origin - preCLI *document.Node - preCLIOrigins map[string]Origin - postDecodeApps []historyApplication final *document.Node - immutableLookup *documentLookup - resolution []resolutionSnapshot - resolutionReplay []resolutionReplayGroup - applications []historyApplication - inheritance []inheritanceOperation secrets map[string]bool secretReferences map[string]string } +func (r *Inspection) documentLookup() *documentLookup { + if r == nil || r.history == nil { + return nil + } + return r.history.documentLookup +} + // Metadata returns detached effective presence, origin, and history records. func (r *Inspection) Metadata() Metadata { if r == nil { @@ -321,14 +318,6 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo accumulator.add(Diagnostic{Code: CodeExplainLimitExceeded, Severity: SeverityError, Message: "retained configuration history exceeds the node limit"}) return nil, accumulator.loadError() } - historyApplications := make([]historyApplication, 0, len(inspection.applications)+len(inspection.postDecodeApps)) - historyApplications = append(historyApplications, inspection.applications...) - historyApplications = append(historyApplications, inspection.postDecodeApps...) - if !addInspectionHistory(counter, inspection) { - accumulator.add(Diagnostic{Code: CodeExplainLimitExceeded, Severity: SeverityError, Message: "retained configuration history exceeds the node limit"}) - return nil, accumulator.loadError() - } - // Detach resolver-borrowed values before reference mutation or validation. configuration, ownershipIssue := detachLoadConfiguration(schema, resolved.configuration, cloneUnresolvedSecrets) if ownershipIssue != nil { @@ -355,5 +344,5 @@ func executeLoadRequest[T any](request loadRequest[T], secrets SecretBinding, lo resolved.configuration = configuration // Publication detaches equal-valued validator replacements, preserving the // validated secret state, then finalizes metadata and sensitivity. - return publishLoadResult(schema, request.definition, resolved, inspection, historyApplications, secretResolution.references, &accumulator) + return publishLoadResult(schema, request.definition, resolved, inspection, secretResolution.references, &accumulator) } diff --git a/internal/engine/load_result.go b/internal/engine/load_result.go index 27699ff..bde5827 100644 --- a/internal/engine/load_result.go +++ b/internal/engine/load_result.go @@ -82,7 +82,6 @@ func resolveLoadInput[T any](schema *schemaDescriptor, definition Definition[T], } if builder != nil { builder.model.final = final - builder.registerDocumentRoot(final) } if refreshReferenceInput { referenceInput = documentWithMetadataLocations(final, metadata, input.document) @@ -120,44 +119,24 @@ func prepareInspectionHistory[T any](resolved resolvedLoad[T], counter *retained metadata := resolved.metadata if resolved.historyBuilder != nil { metadata.historyBuilder = resolved.historyBuilder + } else if resolved.inheritance != nil { + builder := newHistoryBuilder() + if resolved.decoded.baseline != nil { + builder.observeBaseline(resolved.decoded.baseline) + } + builder.setScratch(resolved.final) + builder.observeInheritanceEvidence(resolved.inheritance.operations) + metadata.historyBuilder = builder } - metadata.inheritance = nil + finalizeMetadataPathsWithResolution( + &metadata, resolved.final, resolved.applications, resolved.snapshots, + resolved.decoded.baseline, resolved.structuralInput, resolved.preCLI, resolved.decoded.final, + ) + model := metadata.historyModel inspection := &Inspection{ - metadata: metadata, - baseline: resolved.decoded.baseline, - input: resolved.decoded.final, - inputOrigins: cloneOriginIndex(resolved.decoded.origins), - preCLI: resolved.preCLI, - preCLIOrigins: cloneOrigins(resolved.preCLIOrigins), - final: resolved.final, - resolution: resolved.snapshots, - resolutionReplay: coalescedResolutionSnapshots(resolved.snapshots), - } - if !addInspectionHistory(counter, inspection) { - return nil, false - } - var ok bool - inspection.applications, ok = cloneHistoryApplicationsWithinLimit(resolved.applications, counter) - if !ok { - return nil, false - } - inspection.postDecodeApps, ok = cloneHistoryApplicationsWithinLimit(resolved.postDecodeApps, counter) - if !ok { - return nil, false - } - if resolved.inheritance != nil { - roots := inheritanceOperationDocumentRoots(resolved.inheritance.operations) - if !counter.canAddClones(roots, 2) { - return nil, false - } - inspection.metadata.inheritance = cloneInheritanceOperations(resolved.inheritance.operations) - if !addInheritanceOperationDocuments(counter, inspection.metadata.inheritance) { - return nil, false - } - inspection.inheritance = cloneInheritanceOperations(resolved.inheritance.operations) - if !addInheritanceOperationDocuments(counter, inspection.inheritance) { - return nil, false - } + metadata: metadata, + history: model, + final: resolved.final, } if !addInspectionHistory(counter, inspection) { return nil, false @@ -165,33 +144,7 @@ func prepareInspectionHistory[T any](resolved resolvedLoad[T], counter *retained return inspection, true } -func cloneHistoryApplicationsWithinLimit(values []historyApplication, counter *retainedNodeCounter) ([]historyApplication, bool) { - for _, value := range values { - if !counter.add(value.value) { - return nil, false - } - } - return cloneHistoryApplications(values), true -} - -func inheritanceOperationDocumentRoots(operations []inheritanceOperation) []*document.Node { - roots := make([]*document.Node, 0, len(operations)*2) - for _, operation := range operations { - roots = append(roots, operation.parent, operation.effective) - } - return roots -} - -func addInheritanceOperationDocuments(counter *retainedNodeCounter, operations []inheritanceOperation) bool { - for _, operation := range operations { - if !counter.add(operation.parent) || !counter.add(operation.effective) { - return false - } - } - return true -} - -func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T], resolved resolvedLoad[T], inspection *Inspection, applications []historyApplication, references map[string]string, accumulator *diagnosticAccumulator) (*Loaded[T], error) { +func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T], resolved resolvedLoad[T], inspection *Inspection, references map[string]string, accumulator *diagnosticAccumulator) (*Loaded[T], error) { // A read-only validator can replace a value with an equal borrowed object. // Detach without re-reading the environment or discarding validated state. configuration, issue := detachLoadConfiguration(schema, resolved.configuration, cloneValidatedSecrets) @@ -199,21 +152,13 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T] accumulator.add(*issue) return nil, accumulator.loadError() } - decoded := resolved.decoded - metadata, final, preCLI := inspection.metadata, inspection.final, inspection.preCLI - finalizeMetadataPathsWithResolution(&metadata, final, applications, resolved.snapshots, decoded.baseline, resolved.structuralInput, resolved.preCLI, decoded.final) - secrets := classifySecretPathsWithEvidence(schema, definition.Secret, metadata, final, applications, resolved.versionEvidence, decoded.baseline, resolved.structuralInput, resolved.preCLI, decoded.final) + metadata, final := inspection.metadata, inspection.final + secrets := classifySecretPathsWithEvidence(schema, definition.Secret, metadata, final, nil, resolved.versionEvidence) sources := sourceInfosWithVersionEvidence(accumulator.sources, resolved.versionEvidence, secrets, false) - retainedTrees := make([]*document.Node, 0, len(applications)+4) - retainedTrees = append(retainedTrees, decoded.final, final, preCLI) - for _, application := range applications { - retainedTrees = append(retainedTrees, application.value) - } inspection.metadata = metadata inspection.sources = sources inspection.contexts = cloneResourceContexts(resolved.contexts) inspection.diagnostics = canonicalDiagnostics(accumulator.diagnostics) - inspection.immutableLookup = buildDocumentLookup(retainedTrees...) inspection.secrets = secrets inspection.secretReferences = cloneSecretReferences(references) return &Loaded[T]{Config: configuration, Inspection: inspection}, nil diff --git a/internal/engine/metadata.go b/internal/engine/metadata.go index 66e2b31..bb7a074 100644 --- a/internal/engine/metadata.go +++ b/internal/engine/metadata.go @@ -171,7 +171,6 @@ func finalizeMetadataPathsWithResolution(m *Metadata, final *document.Node, appl } if builder.model.final != final { builder.model.final = final - builder.registerDocumentRoot(final) } m.historyModel = builder.freeze() m.historyBuilder = nil @@ -200,7 +199,6 @@ func finalizeMetadataPathsWithResolution(m *Metadata, final *document.Node, appl builder.observeDocument(final, historyPhasePostCLI) } for _, application := range applications { - builder.registerDocumentRoot(application.value) phase := historyPhaseSources if application.layer == LayerCLI { phase = historyPhaseCLI @@ -219,8 +217,6 @@ func finalizeMetadataPathsWithResolution(m *Metadata, final *document.Node, appl if operation.transfer == nil { continue } - builder.registerDocumentRoot(operation.parent) - builder.registerDocumentRoot(operation.effective) for _, root := range []*document.Node{operation.parent, operation.effective} { walkDocument(root, operation.transfer.to, func(path Path, node *document.Node) { builder.addInventoryObservation(path, path, payload, historyPhaseInheritance, node.Sensitive) diff --git a/internal/engine/override_context_test.go b/internal/engine/override_context_test.go index 2fc48f4..7cc7e04 100644 --- a/internal/engine/override_context_test.go +++ b/internal/engine/override_context_test.go @@ -117,13 +117,7 @@ func TestPostDecodeOverrideContextOrdersDecodeCLIAndResolutionEvidence(t *testin finalizeMetadataPaths(&prepared.metadata, assigned.final, prepared.historyApplications(), decoded.baseline, prepared.structuralInput, prepared.preCLI, assigned.final) inspection := Inspection{ - metadata: prepared.metadata, baseline: decoded.baseline, input: assigned.final, inputOrigins: assigned.origins, - preCLI: prepared.preCLI, preCLIOrigins: prepared.preCLIOrigins, - postDecodeApps: prepared.postDecodeApps, final: assigned.final, - applications: prepared.applications, - immutableLookup: buildDocumentLookup(prepared.preCLI, assigned.final, - prepared.applications[0].value, prepared.postDecodeApps[0].value, prepared.postDecodeApps[1].value, - prepared.postDecodeApps[2].value, prepared.postDecodeApps[3].value), + metadata: prepared.metadata, history: prepared.metadata.historyModel, final: assigned.final, } rateExplanation, ok := inspection.Explain(Keys("rate")) if !ok { diff --git a/internal/engine/resolution_test.go b/internal/engine/resolution_test.go index 82c1f5e..1b7eedc 100644 --- a/internal/engine/resolution_test.go +++ b/internal/engine/resolution_test.go @@ -76,8 +76,9 @@ func TestLoadRunsRecordedResolutionWithMapWriteback(t *testing.T) { "/artifacts/first/output", "/artifacts/first/timeout", "/artifacts/second/output", "/artifacts/second/timeout", "/timeout", } - gotOrder := make([]string, len(result.resolution)) - for index, snapshot := range result.resolution { + snapshots := result.history.resolutionPayload.snapshots + gotOrder := make([]string, len(snapshots)) + for index, snapshot := range snapshots { gotOrder[index] = snapshot.change.Path.String() } if !reflect.DeepEqual(gotOrder, wantOrder) { @@ -179,11 +180,12 @@ func TestResolutionContainerRecordCoversSubtreeRemovalAndSnapshotsAreDetached(t if _, ok := result.Metadata().Lookup(Keys("items", "remove")); ok { t.Fatal("removed entry remains effective metadata") } - if len(result.resolution) != 1 || result.resolution[0].before == nil || result.resolution[0].after == nil { - t.Fatalf("snapshots = %#v", result.resolution) + snapshots := result.history.resolutionPayload.snapshots + if len(snapshots) != 1 || snapshots[0].before == nil || snapshots[0].after == nil { + t.Fatalf("snapshots = %#v", snapshots) } result.Config.Items["keep"] = item{Value: "changed"} - if mappingValue(t, result.resolution[0].after, "keep").Mapping[0].Value.Text != "yes" { + if mappingValue(t, snapshots[0].after, "keep").Mapping[0].Value.Text != "yes" { t.Fatal("caller mutation changed private resolution snapshot") } } @@ -383,8 +385,9 @@ func TestResolutionPresenceUsesEntryShapeAcrossDynamicChanges(t *testing.T) { if err != nil { t.Fatal(err) } - if len(result.resolution) != 2 || result.resolution[1].change.Origin.Operation != ChangeDerivedDefault { - t.Fatalf("resolution records = %#v", result.resolution) + snapshots := result.history.resolutionPayload.snapshots + if len(snapshots) != 2 || snapshots[1].change.Origin.Operation != ChangeDerivedDefault { + t.Fatalf("resolution records = %#v", snapshots) } }) }