From 76521f7cd871c8fed71f42bcb655a679afba59e0 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 26 Oct 2026 16:09:32 +0000 Subject: [PATCH] Complete historical inventory and query services --- docs/roadmap/implementation.md | 7 +- internal/engine/history_model.go | 78 ++++++++++++-- internal/engine/history_resolution_test.go | 56 ++++++++++ internal/engine/history_select.go | 41 +++++++- internal/engine/history_select_test.go | 62 +++++++++++ internal/engine/history_sequence_test.go | 2 +- internal/engine/inheritance_history_test.go | 2 +- internal/engine/load_metadata.go | 7 +- internal/engine/load_result.go | 2 +- internal/engine/metadata.go | 108 +++++++++++--------- internal/engine/metadata_test.go | 20 +++- internal/engine/override_context.go | 3 +- internal/engine/override_context_test.go | 1 + 13 files changed, 311 insertions(+), 78 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 19d997d..67028bc 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Interface Contract Remediation Implementation Plan -Status: in progress. Stages 1–8 are complete; Stages 9–18 remain pending. +Status: in progress. Stages 1–9 are complete; Stages 10–18 remain pending. Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`. Audience: a Luna coding agent executing the stages sequentially. @@ -502,7 +502,10 @@ attribution is not lost to replay grouping. Focused check: ## Stage 9 — Complete Historical Inventory And Internal Query Services Owners: model finalization, `metadata.go`, history selection indexes. -Status: pending. +Status: complete. Validation: +`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'HistoricalPaths|Metadata|History.*Shape|SecretClassification' -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. Derive historical membership and enumeration from all model observations and registered payload roots, including inventory-only checkpoints and controls. diff --git a/internal/engine/history_model.go b/internal/engine/history_model.go index 880fbb7..7e34675 100644 --- a/internal/engine/history_model.go +++ b/internal/engine/history_model.go @@ -58,6 +58,8 @@ type historyModel struct { observations []historyObservation attributions []historyAttribution baseline *document.Node + final *document.Node + inventoryRoots []*document.Node sourcePayloads []historySourcePayload inheritancePayloads []historyInheritancePayload cliPayloads []historySourcePayload @@ -69,6 +71,7 @@ type historyModel struct { shapeOrder []Path observationsByPointer map[string][]int reportedPathIndex pathPrefixIndex + documentLookup *documentLookup frozen bool } @@ -134,21 +137,33 @@ func (builder *historyBuilder) addObservation(observation historyObservation) in } func (builder *historyBuilder) observeDocument(root *document.Node, phase historyPhase) { + builder.observeDocumentAt(root, Path{}, phase) +} + +func (builder *historyBuilder) observeDocumentAt(root *document.Node, prefix Path, phase historyPhase) { builder.requireOpen() if root == nil { return } + builder.registerDocumentRoot(root) payload := builder.nextPayload(phase) - walkDocument(root, Path{}, func(path Path, node *document.Node) { - builder.addObservation(historyObservation{ - path: path, - phase: phase, - payload: payload, - reportedPath: path, - role: historyRoleInventory, - sensitive: node.Sensitive, - attributionRef: noHistoryAttribution, - }) + 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, + reportedPath: reportedPath, role: historyRoleInventory, + sensitive: sensitive, attributionRef: noHistoryAttribution, }) } @@ -173,6 +188,7 @@ func (builder *historyBuilder) advanceScratch(root *document.Node) { func (builder *historyBuilder) freeze() *historyModel { builder.requireOpen() builder.scratch = nil + builder.model.documentLookup = buildDocumentLookup(builder.model.documentRoots()...) builder.model.frozen = true model := builder.model builder.model = nil @@ -180,6 +196,48 @@ func (builder *historyBuilder) freeze() *historyModel { return model } +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) + for _, payload := range model.sourcePayloads { + if payload.kind == historySourceApplicationPayload { + roots = append(roots, payload.application.value) + } + } + for _, payload := range model.inheritancePayloads { + for _, operation := range payload.operations { + roots = append(roots, operation.parent, operation.effective) + } + } + for _, payload := range model.cliPayloads { + if payload.kind == historySourceApplicationPayload { + roots = append(roots, payload.application.value) + } + } + if model.preCLICheckpoint != nil { + roots = append(roots, model.preCLICheckpoint.root) + } + if model.postCLICheckpoint != nil { + roots = append(roots, model.postCLICheckpoint.root) + } + if model.resolutionPayload != nil { + for _, snapshot := range model.resolutionPayload.snapshots { + roots = append(roots, snapshot.before, snapshot.after) + } + } + return roots +} + +func (model *historyModel) documentAt(root *document.Node, path Path) (*document.Node, bool) { + if model == nil || model.documentLookup == nil { + return nil, false + } + return documentAtPathWithLookup(root, path, model.documentLookup) +} + func (builder *historyBuilder) addShape(path Path) { pointer := path.String() for _, existing := range builder.model.shapesByPointer[pointer] { diff --git a/internal/engine/history_resolution_test.go b/internal/engine/history_resolution_test.go index 45e3d67..be0a972 100644 --- a/internal/engine/history_resolution_test.go +++ b/internal/engine/history_resolution_test.go @@ -92,3 +92,59 @@ func TestHistoryResolutionRetainsOriginalShapeAttributionBehindReplayGroup(t *te t.Fatal("original resolution subtree sensitivity was omitted from observations") } } + +func TestMetadataFinalizationRegistersResolutionInventoryAndRoots(t *testing.T) { + t.Parallel() + + beforeShape := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{ + Key: "0", Value: &document.Node{Kind: document.String, Text: "old"}, + }}} + afterShape := &document.Node{Kind: document.Sequence, Sequence: []*document.Node{{ + Kind: document.String, Text: "new", Sensitive: true, + }}} + before := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "shape", Value: beforeShape}}} + final := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "shape", Value: afterShape}}} + changePath := parseOverrideTestPath(t, "/shape/0") + snapshot := resolutionSnapshot{ + change: Change{ + Path: changePath, Origin: Origin{Layer: LayerResolution, Operation: ChangeNormalization}, + Reasons: []string{"changed dynamic shape"}, Dependencies: []Path{Keys("source")}, + }, + before: beforeShape.Mapping[0].Value, + after: afterShape.Sequence[0], + } + parentSnapshot := resolutionSnapshot{ + change: Change{ + Path: Keys("shape"), Origin: Origin{Layer: LayerResolution, Operation: ChangeNormalization}, + Reasons: []string{"replaced container"}, + }, + before: beforeShape, + after: afterShape, + } + + var metadata Metadata + snapshots := []resolutionSnapshot{parentSnapshot, snapshot} + finalizeMetadataPathsWithResolution(&metadata, final, nil, snapshots, before, before) + if got := metadata.HistoricalPaths(); len(got) != 3 || got[0].String() != "" || got[1].String() != "/shape" || got[2].String() != "/shape/0" { + t.Fatalf("historical paths = %#v", got) + } + keyPath := Keys("shape", "0") + indexPath := Keys("shape").Index(0) + if !metadata.hasHistoricalPath(keyPath) || !metadata.hasHistoricalPath(indexPath) { + t.Fatalf("resolution shape inventory omitted key or index: %#v", metadata.HistoricalPaths()) + } + if got := metadata.historyModel.selectHistory(keyPath); len(got) != 2 || !got[0].before || got[0].after || !got[1].before || got[1].after { + t.Fatalf("removed mapping-key resolution selection = %#v", got) + } + if got := metadata.historyModel.selectHistory(indexPath); len(got) != 2 || got[0].before || !got[0].after || got[1].before || !got[1].after { + t.Fatalf("introduced sequence-index resolution selection = %#v", got) + } + var retainedBefore, retainedAfter bool + for _, root := range metadata.historyModel.documentRoots() { + retainedBefore = retainedBefore || root == snapshot.before + retainedAfter = retainedAfter || root == snapshot.after + } + if !retainedBefore || !retainedAfter { + t.Fatalf("resolution roots not enumerated: before=%v after=%v", retainedBefore, retainedAfter) + } +} diff --git a/internal/engine/history_select.go b/internal/engine/history_select.go index 7c4ea6e..4c292c0 100644 --- a/internal/engine/history_select.go +++ b/internal/engine/history_select.go @@ -18,6 +18,12 @@ type historySelection struct { previous []Origin } +type historySensitivityCandidate struct { + path Path + sensitive bool + dependencies []Path +} + func (model *historyModel) selectHistory(path Path) []historySelection { if model == nil || !path.valid() || !model.hasObservedShape(path) { return nil @@ -119,7 +125,7 @@ func (model *historyModel) observedPaths() []Path { func (model *historyModel) historicalPaths() []Path { if model == nil { - return nil + return []Path{} } pointers := make([]string, 0, len(model.shapesByPointer)) for pointer := range model.shapesByPointer { @@ -136,6 +142,39 @@ func (model *historyModel) historicalPaths() []Path { return paths } +func (model *historyModel) latestHistoricalLocation(path Path) (Location, bool) { + selections := model.selectHistory(path) + for selectionIndex := len(selections) - 1; selectionIndex >= 0; selectionIndex-- { + selection := selections[selectionIndex] + if selection.hasAttribution { + if location, found := latestOriginDocumentLocation(selection.attribution.origin); found { + return Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}, true + } + } + for previousIndex := len(selection.previous) - 1; previousIndex >= 0; previousIndex-- { + if location, found := latestOriginDocumentLocation(selection.previous[previousIndex]); found { + return Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}, true + } + } + } + return Location{}, false +} + +func (model *historyModel) sensitivityCandidates() []historySensitivityCandidate { + if model == nil { + return nil + } + candidates := make([]historySensitivityCandidate, 0, len(model.observations)) + for _, observation := range model.observations { + candidate := historySensitivityCandidate{path: observation.path.clone(), sensitive: observation.sensitive} + if attribution, found := model.resolveAttribution(observation.attributionRef); found { + candidate.dependencies = attribution.dependencies + } + candidates = append(candidates, candidate) + } + return candidates +} + func (model *historyModel) resolveAttribution(ref historyAttributionRef) (historyAttribution, bool) { if model == nil || ref < 0 || int(ref) >= len(model.attributions) { return historyAttribution{}, false diff --git a/internal/engine/history_select_test.go b/internal/engine/history_select_test.go index 0d6265e..0c1e92a 100644 --- a/internal/engine/history_select_test.go +++ b/internal/engine/history_select_test.go @@ -156,6 +156,68 @@ func TestHistorySelectionMatchesDeepSidesOfAncestorShapeChange(t *testing.T) { } } +func TestHistoryModelFinalizesLocationSensitivityAndDocumentReaders(t *testing.T) { + t.Parallel() + + root := parseDocumentForDecode(t, "shape: {'0': {value: old}}\n") + value, _ := documentAtPath(root, Keys("shape", "0", "value")) + value.Sensitive = true + keyPath := Keys("shape", "0", "value") + dependency := Keys("credentials", "source") + + builder := newHistoryBuilder() + builder.observeDocument(root, historyPhaseSources) + definitionAttribution := builder.addAttribution(Origin{ + Layer: LayerBase, Locations: []Location{{SourceID: "base.yml", Line: 4, Column: 2}}, + }, nil, nil) + definition := builder.nextPayload(historyPhaseSources) + builder.addObservation(historyObservation{ + path: keyPath, phase: historyPhaseSources, payload: definition, + reportedPath: keyPath, role: historyRoleDefinition, + afterPresent: true, attributionRef: definitionAttribution, + }) + resolutionAttribution := builder.addAttribution( + Origin{Layer: LayerResolution, Operation: ChangeNormalization}, + []string{"normalized credential"}, []Path{dependency}, + ) + resolution := builder.nextPayload(historyPhaseResolution) + builder.addObservation(historyObservation{ + path: keyPath, phase: historyPhaseResolution, payload: resolution, + reportedPath: Keys("shape"), role: historyRoleOperation, + beforePresent: true, afterPresent: true, sensitive: true, + attributionRef: resolutionAttribution, + }) + model := builder.freeze() + + if got := model.documentRoots(); len(got) == 0 || got[0] != root { + t.Fatalf("document roots = %#v, want registered root", 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.latestHistoricalLocation(keyPath); !ok || got.SourceID != "base.yml" || got.Line != 4 || got.Column != 2 { + t.Fatalf("latest historical location = %#v, %v", got, ok) + } + + candidates := model.sensitivityCandidates() + var sawCandidate bool + for _, candidate := range candidates { + if !sameConcreteHistoryPath(candidate.path, keyPath) || !candidate.sensitive || len(candidate.dependencies) == 0 { + continue + } + if len(candidate.dependencies) != 1 || candidate.dependencies[0].String() != dependency.String() { + t.Fatalf("sensitivity dependencies = %#v", candidate.dependencies) + } + sawCandidate = true + } + 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) { t.Parallel() diff --git a/internal/engine/history_sequence_test.go b/internal/engine/history_sequence_test.go index 5179f66..6eb1ea7 100644 --- a/internal/engine/history_sequence_test.go +++ b/internal/engine/history_sequence_test.go @@ -275,7 +275,7 @@ func TestProjectedSequenceHistoryAgreesAcrossMetadataAndExplanation(t *testing.T var metadata Metadata finalizeMetadataPaths(&metadata, composed.root, composed.applications) if !metadata.hasHistoricalPath(path) { - t.Fatalf("projected metadata omitted shifted path %s: %#v", path, metadata.historicalPaths) + t.Fatalf("projected metadata omitted shifted path %s: %#v", path, metadata.HistoricalPaths()) } inspection := &Inspection{ diff --git a/internal/engine/inheritance_history_test.go b/internal/engine/inheritance_history_test.go index 55c17b6..04cd56b 100644 --- a/internal/engine/inheritance_history_test.go +++ b/internal/engine/inheritance_history_test.go @@ -339,7 +339,7 @@ func TestIndexedInheritanceHistoryMatchesOperationScan(t *testing.T) { if got := metadata.History(path); !reflect.DeepEqual(got, want) { t.Errorf("indexed history for %s differs from operation scan:\n got: %#v\nwant: %#v", path, got, want) } - unprepared := Metadata{history: metadata.history, historicalPaths: metadata.historicalPaths, inheritance: metadata.inheritance} + unprepared := Metadata{history: metadata.history, historyModel: metadata.historyModel, inheritance: metadata.inheritance} if got := unprepared.History(path); !reflect.DeepEqual(got, want) { t.Errorf("unprepared history for %s differs from operation scan:\n got: %#v\nwant: %#v", path, got, want) } diff --git a/internal/engine/load_metadata.go b/internal/engine/load_metadata.go index 948339b..b3888df 100644 --- a/internal/engine/load_metadata.go +++ b/internal/engine/load_metadata.go @@ -222,7 +222,7 @@ func collectFieldInfo(node *document.Node, path Path, decoded decodedConfigurati } func cloneMetadata(metadata Metadata) Metadata { - if metadata.fields == nil && metadata.history == nil { + if metadata.fields == nil && metadata.history == nil && metadata.historyModel == nil { return Metadata{} } cloned := Metadata{ @@ -230,15 +230,12 @@ func cloneMetadata(metadata Metadata) Metadata { history: make(map[string][]Change, len(metadata.history)), inheritance: append([]inheritanceOperation(nil), metadata.inheritance...), inheritanceIndex: metadata.inheritanceIndex, + historyModel: metadata.historyModel, } cloned.effectivePaths = make(map[string]Path, len(metadata.effectivePaths)) for pointer, path := range metadata.effectivePaths { cloned.effectivePaths[pointer] = path.clone() } - cloned.historicalPaths = make(map[string][]Path, len(metadata.historicalPaths)) - for pointer, paths := range metadata.historicalPaths { - cloned.historicalPaths[pointer] = clonePaths(paths) - } for path, info := range metadata.fields { cloned.fields[path] = cloneFieldInfo(info) } diff --git a/internal/engine/load_result.go b/internal/engine/load_result.go index 01e38d0..1a2a01f 100644 --- a/internal/engine/load_result.go +++ b/internal/engine/load_result.go @@ -181,7 +181,7 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T] } decoded := resolved.decoded metadata, final, preCLI := inspection.metadata, inspection.final, inspection.preCLI - finalizeMetadataPaths(&metadata, final, applications, decoded.baseline, resolved.structuralInput, resolved.preCLI, decoded.final) + 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) sources := sourceInfosWithVersionEvidence(accumulator.sources, resolved.versionEvidence, secrets, false) retainedTrees := make([]*document.Node, 0, len(applications)+4) diff --git a/internal/engine/metadata.go b/internal/engine/metadata.go index c9cf937..0d3ef7f 100644 --- a/internal/engine/metadata.go +++ b/internal/engine/metadata.go @@ -1,20 +1,16 @@ package engine -import ( - "sort" - - "gitea.maximumdirect.net/eric/ConfigKit/internal/document" -) +import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" // Metadata provides detached effective presence/origin and chronological history // queries for a load. Its zero value contains no paths. type Metadata struct { effectivePaths map[string]Path - historicalPaths map[string][]Path fields map[string]FieldInfo history map[string][]Change inheritance []inheritanceOperation inheritanceIndex *inheritanceHistoryIndex + historyModel *historyModel } // Lookup returns information for an effective path. Unknown and invalid paths @@ -77,18 +73,7 @@ func (m Metadata) History(path Path) []Change { // a path remains effective. Enumeration exposes no configuration values and does // not perform another load or resolve secrets. func (m Metadata) HistoricalPaths() []Path { - pointers := make([]string, 0, len(m.historicalPaths)) - for pointer := range m.historicalPaths { - pointers = append(pointers, pointer) - } - sort.Strings(pointers) - paths := make([]Path, 0, len(pointers)) - for _, pointer := range pointers { - // The index contains only canonical pointers produced by Path.String. - path, _ := ParsePath(pointer) - paths = append(paths, path) - } - return paths + return m.historyModel.historicalPaths() } // Textual pointers are lookup keys, but explicit key/index kinds remain part of @@ -115,54 +100,75 @@ func matchesPathShape(query, candidate Path) bool { } func (m Metadata) hasHistoricalPath(path Path) bool { - if !path.valid() { - return false - } - for _, candidate := range m.historicalPaths[path.String()] { - if matchesPathShape(path, candidate) { - return true - } - } - return false + return m.historyModel.hasObservedShape(path) } func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []historyApplication, earlier ...*document.Node) { - m.effectivePaths = make(map[string]Path) - m.historicalPaths = make(map[string][]Path) - addHistorical := func(path Path, pointer string) { - for _, prior := range m.historicalPaths[pointer] { - if matchesPathShape(path, prior) { - return - } + finalizeMetadataPathsWithResolution(m, final, applications, nil, earlier...) +} + +func finalizeMetadataPathsWithResolution(m *Metadata, final *document.Node, applications []historyApplication, snapshots []resolutionSnapshot, earlier ...*document.Node) { + finalizeEffectiveMetadataPaths(m, final) + builder := newHistoryBuilder() + if len(earlier) != 0 { + builder.model.baseline = earlier[0] + } + builder.model.final = final + for index, tree := range earlier { + phase := historyPhaseSources + switch index { + case 0: + phase = historyPhaseBaseline + case 2: + phase = historyPhasePreCLI + case 3: + phase = historyPhasePostCLI } - m.historicalPaths[pointer] = append(m.historicalPaths[pointer], path.clone()) + builder.observeDocument(tree, phase) } - walkDocument(final, Path{}, func(path Path, _ *document.Node) { - pointer := path.String() - m.effectivePaths[pointer] = path.clone() - addHistorical(path, pointer) - }) - add := func(path Path, _ *document.Node) { - addHistorical(path, path.String()) - } - for _, tree := range earlier { - walkDocument(tree, Path{}, add) + if final != nil { + builder.observeDocument(final, historyPhasePostCLI) } for _, application := range applications { - walkHistoryApplication(application, func(_ Path, effective Path, _ *document.Node) { - add(effective, nil) + builder.registerDocumentRoot(application.value) + phase := historyPhaseSources + if application.layer == LayerCLI { + phase = historyPhaseCLI + } + payload := builder.nextPayload(phase) + walkHistoryApplication(application, func(_, effective Path, node *document.Node) { + builder.addInventoryObservation(effective, effective, payload, phase, node.Sensitive) }) } for _, operation := range m.inheritance { + payload := builder.nextPayload(historyPhaseInheritance) if operation.control != nil { - add(operation.control.path, nil) + builder.addInventoryObservation(operation.control.path, operation.control.path, payload, historyPhaseInheritance, false) continue } - if operation.transfer != nil { - walkDocument(operation.parent, operation.transfer.to, add) - walkDocument(operation.effective, operation.transfer.to, add) + 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) + }) } } + if len(snapshots) != 0 { + if len(earlier) != 0 { + builder.setScratch(earlier[len(earlier)-1]) + } + builder.observeResolutionEvidence(snapshots) + } + m.historyModel = builder.freeze() + m.inheritanceIndex = newInheritanceHistoryIndex(m.inheritance) +} + +func finalizeEffectiveMetadataPaths(m *Metadata, final *document.Node) { + m.effectivePaths = effectivePathIndex(final) m.inheritanceIndex = newInheritanceHistoryIndex(m.inheritance) } diff --git a/internal/engine/metadata_test.go b/internal/engine/metadata_test.go index a798315..516fdee 100644 --- a/internal/engine/metadata_test.go +++ b/internal/engine/metadata_test.go @@ -55,7 +55,7 @@ func TestMetadataPathIndexesOwnIndependentSegments(t *testing.T) { finalizeMetadataPaths(&metadata, final, nil) pointer := "/service/workers/0" effective := metadata.effectivePaths[pointer] - historical := metadata.historicalPaths[pointer] + historical := metadata.historyModel.concreteShapes(pointer) if effective.String() != pointer || len(historical) != 1 || historical[0].String() != pointer { t.Fatalf("path indexes = %#v, %#v", effective, historical) } @@ -84,7 +84,7 @@ func TestMetadataPathIndexesRetainDistinctHistoricalShapes(t *testing.T) { if !reflect.DeepEqual(metadata.effectivePaths[pointer], indexPath) { t.Fatal("effective index does not reflect the final sequence") } - historical := metadata.historicalPaths[pointer] + historical := metadata.historyModel.concreteShapes(pointer) if len(historical) != 2 { t.Fatalf("historical shapes = %#v, want one key and one index", historical) } @@ -99,6 +99,16 @@ func TestMetadataPathIndexesRetainDistinctHistoricalShapes(t *testing.T) { t.Fatalf("historical index lost shape %#v", want) } } + paths := metadata.HistoricalPaths() + var pointers []string + for _, path := range paths { + pointers = append(pointers, path.String()) + } + if !reflect.DeepEqual(pointers, []string{"", "/shape", "/shape/0"}) || + len(paths[2].segments) != 2 || paths[2].segments[1].kind != pathSegmentParsed || + metadata.hasHistoricalPath(Keys("shape", "missing")) { + t.Fatalf("historical enumeration or membership = %#v", paths) + } } func TestHistoricalPathsExposeHiddenBaseOwnershipWithoutResolvingBaseSecrets(t *testing.T) { @@ -149,9 +159,11 @@ func TestMetadataQueriesReturnDetachedRecords(t *testing.T) { t.Parallel() path := Keys("service", "workers") + builder := newHistoryBuilder() + builder.observeDocument(parseDocumentForDecode(t, "service: {workers: null}\n"), historyPhaseBaseline) metadata := Metadata{ - effectivePaths: map[string]Path{path.String(): path}, - historicalPaths: map[string][]Path{path.String(): {path}}, + effectivePaths: map[string]Path{path.String(): path}, + historyModel: builder.freeze(), fields: map[string]FieldInfo{ path.String(): { Explicit: true, diff --git a/internal/engine/override_context.go b/internal/engine/override_context.go index c60baef..d33c244 100644 --- a/internal/engine/override_context.go +++ b/internal/engine/override_context.go @@ -65,8 +65,7 @@ func preparePostDecodeOverrideContext(input assembledInput, before decodedConfig origin: overlayOrigin(LayerCLI, document.Location{SourceID: override.source.ID}), }) } - allApplications := append(cloneHistoryApplications(applications), cloneHistoryApplications(postDecodeApps)...) - finalizeMetadataPaths(&metadata, assigned.final, allApplications, input.document, before.final) + finalizeEffectiveMetadataPaths(&metadata, assigned.final) return postDecodeOverrideContext{ structuralInput: document.Clone(input.document), diff --git a/internal/engine/override_context_test.go b/internal/engine/override_context_test.go index 002a849..e953277 100644 --- a/internal/engine/override_context_test.go +++ b/internal/engine/override_context_test.go @@ -83,6 +83,7 @@ func TestPostDecodeOverrideContextOrdersDecodeCLIAndResolutionEvidence(t *testin t.Fatalf("resolution CLI source = %#v, %v", source, ok) } + 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,