diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index a0ed38e..d89d7d2 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–15 are complete; Stages 16–18 remain pending. +Status: in progress. Stages 1–16 are complete; Stages 17–18 remain pending. Baseline: `f6d54a094a2b5ada5748686620659399dce7ebf0`. Audience: a Luna coding agent executing the stages sequentially. @@ -692,7 +692,9 @@ quota or representation-specific node total is promised. Focused check: ## Stage 16 — Remove Superseded Attribution And Repair Test Seams Owners: all entries in the migration map and tests constructing private inspection -or metadata values. Status: pending. +or metadata values. Status: complete. Validation: `GOTOOLCHAIN=local +GOPROXY=off GOTELEMETRY=off go test ./internal/engine` and +`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...` passed. 1. Trace remaining production reads/writes of old history/inheritance fields and helpers. Remove history-building portions of `metadataFromDecoded`, diff --git a/internal/engine/history_budget_test.go b/internal/engine/history_budget_test.go index b82534e..ea3c47c 100644 --- a/internal/engine/history_budget_test.go +++ b/internal/engine/history_budget_test.go @@ -231,11 +231,12 @@ func TestPreparedInheritanceEvidenceSharesOwnerAndDetachesPublicViews(t *testing builder.observeBaseline(baseline) builder.setScratch(input) builder.observeInheritanceEvidence(expanded.operations) + builder.observePreCLICheckpoint(expanded.root, expanded.origins) + builder.observePostCLICheckpoint(expanded.root, expanded.origins) + builder.prefixReady = true metadata := Metadata{historyBuilder: builder} resolved := resolvedLoad[historyBudgetSimpleConfig]{ - metadata: metadata, historyBuilder: builder, final: expanded.root, - decoded: decodedConfiguration{baseline: baseline}, structuralInput: input, - inheritance: expanded, + metadata: metadata, final: expanded.root, } counter := newRetainedNodeCounter(100) inspection, ok := prepareInspectionHistory(resolved, counter) diff --git a/internal/engine/history_inheritance.go b/internal/engine/history_inheritance.go index 70c2001..db1ac55 100644 --- a/internal/engine/history_inheritance.go +++ b/internal/engine/history_inheritance.go @@ -281,32 +281,3 @@ 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) { - return []Change{{Path: selection.path.clone(), Origin: cloneOrigin(selection.origin), Dependencies: clonePaths(selection.dependencies)}} - } - return nil - } - if operation.transfer == nil { - return nil - } - path := operationPathForQuery(operation.transfer.to, query) - selections := resolveInheritanceAttribution(operation, path) - changes := make([]Change, 0, len(selections)) - for _, selection := range selections { - if selection.kind == inheritanceEvidenceDisplaced { - continue - } - changes = append(changes, Change{Path: selection.path.clone(), Origin: cloneOrigin(selection.origin), Dependencies: clonePaths(selection.dependencies)}) - } - return changes -} diff --git a/internal/engine/history_inheritance_test.go b/internal/engine/history_inheritance_test.go index 56bcfc7..87edac8 100644 --- a/internal/engine/history_inheritance_test.go +++ b/internal/engine/history_inheritance_test.go @@ -63,9 +63,6 @@ func TestInheritanceEvidenceUsesOrderedPayloadAndConcreteTransferShapes(t *testi displaced[0].attribution.origin.Locations[0].SourceID != "child" { t.Fatalf("pre-transfer child shape was not retained: %#v", displaced) } - if public := projectedInheritanceChanges(displacedPath, model.inheritancePayloads[0].operations[1]); len(public) != 0 { - t.Fatalf("displaced child path unexpectedly projected publicly: %#v", public) - } if builder.scratch != nil { t.Fatal("frozen builder unexpectedly retained scratch") } @@ -74,31 +71,41 @@ func TestInheritanceEvidenceUsesOrderedPayloadAndConcreteTransferShapes(t *testi } } -func TestInheritanceResolverSharesProjectionAttribution(t *testing.T) { +func TestInheritanceModelSharesProjectionAttribution(t *testing.T) { t.Parallel() from, to, reference := Keys("parents", "base"), Keys("children", "item"), Keys("parents", "base") path := to.Key("nested").Key("value") parent := Origin{Layer: LayerBase, Locations: []Location{{SourceID: "base"}}} child := Origin{Layer: LayerOverlay, Locations: []Location{{SourceID: "child"}}} + parentDocument := parseDocumentForDecode(t, "nested: {value: parent}\n") + effectiveDocument := parseDocumentForDecode(t, "nested: {value: child}\n") operation := inheritanceOperation{ transfer: &inheritanceTransfer{from: from, to: to, reference: reference}, + parent: parentDocument, + effective: effectiveDocument, parentOrigins: map[string]Origin{from.Key("nested").Key("value").String(): parent}, effectiveOrigins: map[string]Origin{path.String(): child}, } - selections := resolveInheritanceAttribution(operation, path) - if len(selections) != 2 || selections[0].kind != inheritanceEvidenceParent || selections[1].kind != inheritanceEvidenceChild { - t.Fatalf("shared resolver selections = %#v", selections) + root := parseDocumentForDecode(t, "children: {item: {nested: {value: old}}}\n") + builder := newHistoryBuilder() + builder.setScratch(root) + builder.observeInheritanceEvidence([]inheritanceOperation{operation}) + model := builder.freeze() + selections := model.selectHistory(path) + if len(selections) != 2 || selections[0].role != historyRoleOperation || selections[1].role != historyRoleOperation { + t.Fatalf("model inheritance selections = %#v", selections) } - if selections[0].path.String() != selections[1].path.String() || selections[0].origin.Operation != ChangeInheritance || selections[1].origin.Operation != ChangeInheritance { - t.Fatalf("parent and child attributions did not share one resolver: %#v", selections) + if selections[0].reportedPath.String() != to.String() || selections[1].reportedPath.String() != to.String() || + selections[0].attribution.origin.Operation != ChangeInheritance || selections[1].attribution.origin.Operation != ChangeInheritance { + t.Fatalf("parent and child attributions did not share the model owner: %#v", selections) } - changes := projectedInheritanceChanges(path, operation) - if len(changes) != 2 || changes[0].Origin.Locations[0].SourceID != selections[0].origin.Locations[0].SourceID || - changes[1].Origin.Locations[0].SourceID != selections[1].origin.Locations[0].SourceID || - changes[0].Dependencies[0].String() != from.String() || changes[0].Dependencies[1].String() != reference.String() || - changes[1].Dependencies[0].String() != reference.String() { - t.Fatalf("metadata projection attribution = %#v; resolver = %#v", changes, selections) + if selections[0].attribution.origin.Locations[0].SourceID != "base" || + selections[1].attribution.origin.Locations[0].SourceID != "child" || + len(selections[0].attribution.dependencies) != 2 || selections[0].attribution.dependencies[0].String() != from.String() || + selections[0].attribution.dependencies[1].String() != reference.String() || + len(selections[1].attribution.dependencies) != 1 || selections[1].attribution.dependencies[0].String() != reference.String() { + t.Fatalf("model attribution dependencies or origins differ: %#v", selections) } } diff --git a/internal/engine/history_resolution_test.go b/internal/engine/history_resolution_test.go index be0a972..7049b77 100644 --- a/internal/engine/history_resolution_test.go +++ b/internal/engine/history_resolution_test.go @@ -124,7 +124,14 @@ func TestMetadataFinalizationRegistersResolutionInventoryAndRoots(t *testing.T) var metadata Metadata snapshots := []resolutionSnapshot{parentSnapshot, snapshot} - finalizeMetadataPathsWithResolution(&metadata, final, nil, snapshots, before, before) + builder := newHistoryBuilder() + builder.setScratch(before) + builder.observeDocument(before, historyPhaseBaseline) + builder.observeDocument(final, historyPhasePostCLI) + builder.prefixReady = true + builder.observeResolutionEvidence(snapshots) + metadata.historyBuilder = builder + freezeMetadataHistory(&metadata, final) 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) } diff --git a/internal/engine/history_sequence_test.go b/internal/engine/history_sequence_test.go index ad2f6ce..7dd09ef 100644 --- a/internal/engine/history_sequence_test.go +++ b/internal/engine/history_sequence_test.go @@ -272,19 +272,18 @@ func TestProjectedSequenceHistoryAgreesAcrossMetadataAndExplanation(t *testing.T t.Fatalf("compose diagnostics = %#v", diagnostics) } path := Keys("records").Index(1).Key("token") - var metadata Metadata - finalizeMetadataPaths(&metadata, composed.root, composed.applications) + baseline := &document.Node{Kind: document.Mapping} + model := sourceHistoryModelForInspection(baseline, composed.root, composed.origins, composed.applications, composed.overlays) + metadata := Metadata{historyModel: model} + finalizeEffectiveMetadataPaths(&metadata, composed.root) if !metadata.hasHistoricalPath(path) { t.Fatalf("projected metadata omitted shifted path %s: %#v", path, metadata.HistoricalPaths()) } - 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, nil) + }, metadata) explanation, ok := inspection.Explain(path) if !ok || !explanation.Effective.Redacted { t.Fatalf("shifted explanation = %#v, %v", explanation, ok) @@ -323,15 +322,14 @@ func TestReplacedProjectedSensitiveRecordRemainsRedacted(t *testing.T) { }) final := applications[len(applications)-1].value path := Keys("records").Index(1).Key("token") - var metadata Metadata - finalizeMetadataPaths(&metadata, final, applications) baseline := &document.Node{Kind: document.Mapping} model := sourceHistoryModelForInspection(baseline, final, nil, applications, nil) - metadata.historyModel = model + metadata := Metadata{historyModel: model} + finalizeEffectiveMetadataPaths(&metadata, final) inspection := &Inspection{metadata: metadata, history: model, final: final} inspection.secrets = classifySecretPaths(nil, func(candidate Path) bool { return reflect.DeepEqual(candidate, path) - }, metadata, final, nil) + }, metadata) explanation, ok := inspection.Explain(path) if !ok || explanation.Effective.Present { t.Fatalf("replaced shifted record explanation = %#v, %v", explanation, ok) @@ -404,14 +402,13 @@ func TestMappedUpsertProjectsSensitiveReplacementEvidence(t *testing.T) { finalRecords, _ := documentAtPath(finalRoot, Keys("records")) finalRecords.Sequence = document.Clone(combined.value).Sequence - var metadata Metadata - finalizeMetadataPaths(&metadata, finalRoot, applications) baseline := &document.Node{Kind: document.Mapping} model := sourceHistoryModelForInspection(baseline, finalRoot, nil, applications, nil) - metadata.historyModel = model + metadata := Metadata{historyModel: model} + finalizeEffectiveMetadataPaths(&metadata, finalRoot) 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, nil) + inspection.secrets = classifySecretPaths(nil, secretRule, metadata) explanation, ok := inspection.Explain(secretPath) if !ok || !explanation.Effective.Redacted { t.Fatalf("mapped replacement explanation = %#v, %v", explanation, ok) diff --git a/internal/engine/inheritance_history.go b/internal/engine/inheritance_history.go index b4b21ed..3704f5e 100644 --- a/internal/engine/inheritance_history.go +++ b/internal/engine/inheritance_history.go @@ -1,63 +1,6 @@ package engine -import ( - "slices" - - "gitea.maximumdirect.net/eric/ConfigKit/internal/document" -) - -// Retain source history at original paths. Structural operations are held once -// per relationship and projected onto a requested destination path only when -// History or Explain is queried. -func applyInheritanceMetadata(metadata *Metadata, raw *document.Node, rawOrigins map[string]Origin, overlays []overlayRecord, expansion *inheritanceScope) { - if metadata == nil || expansion == nil { - return - } - sourceView := Metadata{ - fields: make(map[string]FieldInfo), - history: metadata.history, - } - walkDocument(raw, Path{}, func(path Path, node *document.Node) { - origin, found := rawOrigins[path.String()] - if !found { - origin = Origin{Layer: LayerBase, Locations: []Location{publicLocation(node.Location)}} - } - sourceView.fields[path.String()] = FieldInfo{Explicit: true, Null: node.Kind == document.Null, Origin: cloneOrigin(origin)} - }) - applyOverlayMetadata(&sourceView, overlays) - metadata.history = sourceView.history -} - -func historyWithInheritance(path Path, stored []Change, operations []inheritanceOperation) []Change { - split := len(stored) - for index, change := range stored { - if change.Origin.Layer == LayerCLI || change.Origin.Layer == LayerResolution { - split = index - break - } - } - history := cloneChanges(stored[:split]) - for _, operation := range operations { - for _, change := range projectedInheritanceChanges(path, operation) { - if len(history) != 0 { - change.Previous = []Origin{cloneOrigin(history[len(history)-1].Origin)} - } - history = append(history, change) - } - } - for _, change := range stored[split:] { - cloned := cloneChange(change) - if len(history) != 0 { - cloned.Previous = []Origin{cloneOrigin(history[len(history)-1].Origin)} - } - history = append(history, cloned) - } - return history -} - -func projectedInheritanceChanges(query Path, operation inheritanceOperation) []Change { - return inheritanceChangesForQuery(query, operation) -} +import "slices" func transferredOperationOrigin(operation inheritanceOperation, destination Path) (Origin, bool) { if inheritanceOperationIdentity(operation, destination) { diff --git a/internal/engine/inheritance_history_test.go b/internal/engine/inheritance_history_test.go index ed8a150..c8c1ae1 100644 --- a/internal/engine/inheritance_history_test.go +++ b/internal/engine/inheritance_history_test.go @@ -125,9 +125,11 @@ func inheritanceInspectionFromComposition(t *testing.T, composed *composedDocume metadata := metadataFromDecoded(decoded) metadata.historyBuilder.setScratch(composed.root) metadata.historyBuilder.observeSourceEvidence(composed.root, composed.origins, composed.applications, composed.overlays) - applyInheritanceMetadata(&metadata, composed.root, composed.origins, composed.overlays, expanded) metadata.historyBuilder.setScratch(composed.root) metadata.historyBuilder.observeInheritanceEvidence(expanded.operations) + metadata.historyBuilder.observePreCLICheckpoint(expanded.root, expanded.origins) + metadata.historyBuilder.observePostCLICheckpoint(expanded.root, expanded.origins) + metadata.historyBuilder.prefixReady = true final := expanded.root if len(snapshots) != 0 { final = document.Clone(expanded.root) @@ -137,7 +139,10 @@ func inheritanceInspectionFromComposition(t *testing.T, composed *composedDocume } } } - finalizeMetadataPathsWithResolution(&metadata, final, composed.applications, snapshots, baseline, composed.root, expanded.root) + if len(snapshots) != 0 { + metadata.historyBuilder.observeResolutionEvidence(snapshots) + } + freezeMetadataHistory(&metadata, final) return &Inspection{ metadata: metadata, history: metadata.historyModel, final: final, }, expanded diff --git a/internal/engine/inheritance_secret_test.go b/internal/engine/inheritance_secret_test.go index b85a0e4..f5b8aa2 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, nil) + }, inspection.metadata) 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, nil) + }, inspection.metadata) 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, nil) + inspection.secrets = classifySecretPaths(schema, nil, inspection.metadata) 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, nil, []*sourceVersionEvidence{evidence}) + }, inspection.metadata, []*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, nil) + inspection.secrets = classifySecretPaths(schema, nil, inspection.metadata) 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_metadata.go b/internal/engine/load_metadata.go index 0266ce8..4a87bda 100644 --- a/internal/engine/load_metadata.go +++ b/internal/engine/load_metadata.go @@ -60,108 +60,13 @@ func (input resolutionInput) sourceContext(id string) (ResourceContext, bool) { func metadataFromDecoded(decoded decodedConfiguration) Metadata { metadata := Metadata{ fields: make(map[string]FieldInfo), - history: make(map[string][]Change), historyBuilder: newHistoryBuilder(), } metadata.historyBuilder.observeBaseline(decoded.baseline) - baseline := make(map[string]*document.Node) - indexDocument(decoded.baseline, Path{}, baseline) - for pointer := range baseline { - path, err := ParsePath(pointer) - if err != nil { - continue - } - metadata.history[pointer] = append(metadata.history[pointer], Change{ - Path: path, Origin: Origin{Layer: LayerDefault}, - }) - } - for pointer := range decoded.presence { - path, err := ParsePath(pointer) - if err != nil { - continue - } - origin := cloneOrigin(decoded.origins[pointer]) - change := Change{Path: path, Origin: origin} - if _, ok := baseline[pointer]; ok { - change.Previous = []Origin{{Layer: LayerDefault}} - } - metadata.history[pointer] = append(metadata.history[pointer], change) - } collectFieldInfo(decoded.final, Path{}, decoded, metadata.fields) return metadata } -func applyOverlayMetadata(metadata *Metadata, records []overlayRecord) { - baseline := make(map[string][]Change, len(metadata.history)) - for pointer, history := range metadata.history { - for _, change := range history { - if change.Origin.Layer == LayerDefault { - baseline[pointer] = append(baseline[pointer], cloneChange(change)) - } - } - } - events := make(map[string][]Change) - for _, record := range records { - exact := false - for _, displaced := range record.displaced { - pointer := displaced.path.String() - appendInputOriginEvent(events, baseline, pointer, displaced.path, displaced.origin) - appendInputChangeEvent(events, baseline, pointer, record.change) - if pointer == record.change.Path.String() { - exact = true - } - } - if !exact { - pointer := record.change.Path.String() - for _, previous := range record.change.Previous { - appendInputOriginEvent(events, baseline, pointer, record.change.Path, previous) - } - appendInputChangeEvent(events, baseline, pointer, record.change) - } - } - for pointer, info := range metadata.fields { - if !info.Explicit { - continue - } - path, err := ParsePath(pointer) - if err != nil { - continue - } - appendInputOriginEvent(events, baseline, pointer, path, info.Origin) - } - metadata.history = make(map[string][]Change, len(baseline)+len(events)) - for pointer, history := range baseline { - metadata.history[pointer] = cloneChanges(history) - } - for pointer, history := range events { - metadata.history[pointer] = append(metadata.history[pointer], cloneChanges(history)...) - } -} - -func appendInputOriginEvent(events map[string][]Change, baseline map[string][]Change, pointer string, path Path, origin Origin) { - if origin.Layer == "" { - return - } - history := events[pointer] - if len(history) != 0 && originRepresentedByChange(path, origin, history[len(history)-1]) { - return - } - appendInputChangeEvent(events, baseline, pointer, Change{Path: path.clone(), Origin: cloneOrigin(origin)}) -} - -func appendInputChangeEvent(events map[string][]Change, baseline map[string][]Change, pointer string, value Change) { - change := cloneChange(value) - history := events[pointer] - if len(history) != 0 { - change.Previous = []Origin{cloneOrigin(history[len(history)-1].Origin)} - } else if defaults := baseline[pointer]; len(defaults) != 0 { - change.Previous = []Origin{cloneOrigin(defaults[len(defaults)-1].Origin)} - } else { - change.Previous = nil - } - events[pointer] = append(history, change) -} - func originContainedBy(candidate, container Origin) bool { if candidate.Layer != container.Layer || candidate.Operation != container.Operation { return false @@ -195,14 +100,6 @@ func originContainedBy(candidate, container Origin) bool { return true } -func cloneChanges(changes []Change) []Change { - cloned := make([]Change, len(changes)) - for index, change := range changes { - cloned[index] = cloneChange(change) - } - return cloned -} - func collectFieldInfo(node *document.Node, path Path, decoded decodedConfiguration, fields map[string]FieldInfo) { pointer := path.String() _, explicit := decoded.presence[pointer] @@ -224,13 +121,11 @@ func collectFieldInfo(node *document.Node, path Path, decoded decodedConfigurati } func cloneMetadata(metadata Metadata) Metadata { - if metadata.fields == nil && metadata.history == nil && metadata.historyModel == nil { + if metadata.fields == nil && metadata.historyModel == nil { return Metadata{} } cloned := Metadata{ fields: make(map[string]FieldInfo, len(metadata.fields)), - history: make(map[string][]Change, len(metadata.history)), - inheritance: append([]inheritanceOperation(nil), metadata.inheritance...), historyModel: metadata.historyModel, } cloned.effectivePaths = make(map[string]Path, len(metadata.effectivePaths)) @@ -240,13 +135,6 @@ func cloneMetadata(metadata Metadata) Metadata { for path, info := range metadata.fields { cloned.fields[path] = cloneFieldInfo(info) } - for path, changes := range metadata.history { - clonedChanges := make([]Change, len(changes)) - for index, change := range changes { - clonedChanges[index] = cloneChange(change) - } - cloned.history[path] = clonedChanges - } return cloned } @@ -268,56 +156,16 @@ func applyResolutionMetadata(metadata *Metadata, resolved resolutionResult, entr for snapshotID, snapshot := range resolved.snapshots { pointer := snapshotPointers[snapshotID] - change := cloneChange(snapshot.change) if info, ok := metadata.fields[pointer]; ok { - change.Previous = []Origin{cloneOrigin(info.Origin)} - info.Origin = cloneOrigin(change.Origin) + info.Origin = cloneOrigin(snapshot.change.Origin) metadata.fields[pointer] = info } - metadata.history[pointer] = append(metadata.history[pointer], change) } - appendResolutionDescendantHistory(metadata, resolved.snapshots, snapshotPointers, finalPaths, &deltaCoverage, snapshotIDs) applyResolutionFieldOrigins(metadata.fields, resolved.snapshots, &deltaCoverage, snapshotIDs) refreshResolutionFields(metadata.fields, finalPaths, resolved.final, entry, resolved.snapshots, snapshotCoverage) } -func appendResolutionDescendantHistory(metadata *Metadata, snapshots []resolutionSnapshot, snapshotPointers []string, finalPaths map[string]*document.Node, deltaIndex *pathPrefixIndex, snapshotIDs []int) { - allPointers := make(map[string]struct{}, len(metadata.history)+len(finalPaths)) - for pointer := range metadata.history { - if _, err := ParsePath(pointer); err == nil { - allPointers[pointer] = struct{}{} - } - } - for pointer := range finalPaths { - allPointers[pointer] = struct{}{} - } - buckets := make([][]string, len(snapshotIDs)) - for pointer := range allPointers { - deltaIndex.visitAncestors(pointer, func(deltaID int) { - snapshotID := snapshotIDs[deltaID] - if snapshotID < 0 || pointer == snapshotPointers[snapshotID] { - return - } - buckets[deltaID] = append(buckets[deltaID], pointer) - }) - } - for deltaID, pointers := range buckets { - snapshotID := snapshotIDs[deltaID] - if snapshotID < 0 { - continue - } - slices.Sort(pointers) - for _, pointer := range pointers { - change := cloneChange(snapshots[snapshotID].change) - if info, ok := metadata.fields[pointer]; ok { - change.Previous = []Origin{cloneOrigin(info.Origin)} - } - metadata.history[pointer] = append(metadata.history[pointer], change) - } - } -} - func applyResolutionFieldOrigins(fields map[string]FieldInfo, snapshots []resolutionSnapshot, deltaIndex *pathPrefixIndex, snapshotIDs []int) { buckets := make([][]string, len(snapshotIDs)) for pointer := range fields { diff --git a/internal/engine/load_result.go b/internal/engine/load_result.go index bde5827..358e312 100644 --- a/internal/engine/load_result.go +++ b/internal/engine/load_result.go @@ -10,19 +10,10 @@ import ( // it owns no source access, options, or long-lived schema state. type resolvedLoad[T any] struct { configuration T - decoded decodedConfiguration metadata Metadata - historyBuilder *historyBuilder final *document.Node - snapshots []resolutionSnapshot - applications []historyApplication - postDecodeApps []historyApplication - structuralInput *document.Node - preCLI *document.Node - preCLIOrigins map[string]Origin referenceInput *document.Node versionEvidence []*sourceVersionEvidence - inheritance *inheritanceScope contexts map[string]ResourceContext } @@ -57,10 +48,7 @@ func resolveLoadInput[T any](schema *schemaDescriptor, definition Definition[T], decoded := prepared.decoded configuration := decoded.value.Interface().(T) metadata := prepared.metadata - builder := prepared.historyBuilder - if builder == nil { - builder = metadata.historyBuilder - } + builder := metadata.historyBuilder final := decoded.final referenceInput := prepared.referenceInput var resolutionSnapshots []resolutionSnapshot @@ -86,22 +74,13 @@ func resolveLoadInput[T any](schema *schemaDescriptor, definition Definition[T], if refreshReferenceInput { referenceInput = documentWithMetadataLocations(final, metadata, input.document) } - applications := []historyApplication(nil) - if input.composition != nil { - applications = input.composition.applications - } else if input.document != nil { - applications = []historyApplication{{layer: LayerBase, value: input.document}} - } var versionEvidence []*sourceVersionEvidence if input.versionState != nil { versionEvidence = append(versionEvidence, input.versionState.evidence...) } return resolvedLoad[T]{ - configuration: configuration, decoded: decoded, metadata: metadata, historyBuilder: builder, final: final, - snapshots: resolutionSnapshots, applications: applications, - postDecodeApps: prepared.postDecodeApps, structuralInput: document.Clone(prepared.structuralInput), - preCLI: prepared.preCLI, preCLIOrigins: prepared.preCLIOrigins, referenceInput: referenceInput, - versionEvidence: versionEvidence, inheritance: input.inheritance, contexts: input.contexts, + configuration: configuration, metadata: metadata, final: final, referenceInput: referenceInput, + versionEvidence: versionEvidence, contexts: input.contexts, }, diagnostics } @@ -117,21 +96,7 @@ func detachLoadConfiguration[T any](schema *schemaDescriptor, configuration T, m func prepareInspectionHistory[T any](resolved resolvedLoad[T], counter *retainedNodeCounter) (*Inspection, bool) { 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 - } - finalizeMetadataPathsWithResolution( - &metadata, resolved.final, resolved.applications, resolved.snapshots, - resolved.decoded.baseline, resolved.structuralInput, resolved.preCLI, resolved.decoded.final, - ) + freezeMetadataHistory(&metadata, resolved.final) model := metadata.historyModel inspection := &Inspection{ metadata: metadata, @@ -152,8 +117,8 @@ func publishLoadResult[T any](schema *schemaDescriptor, definition Definition[T] accumulator.add(*issue) return nil, accumulator.loadError() } - metadata, final := inspection.metadata, inspection.final - secrets := classifySecretPathsWithEvidence(schema, definition.Secret, metadata, final, nil, resolved.versionEvidence) + metadata := inspection.metadata + secrets := classifySecretPathsWithEvidence(schema, definition.Secret, metadata, resolved.versionEvidence) sources := sourceInfosWithVersionEvidence(accumulator.sources, resolved.versionEvidence, secrets, false) inspection.metadata = metadata inspection.sources = sources diff --git a/internal/engine/metadata.go b/internal/engine/metadata.go index bb7a074..c675c22 100644 --- a/internal/engine/metadata.go +++ b/internal/engine/metadata.go @@ -7,22 +7,10 @@ import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" type Metadata struct { effectivePaths map[string]Path fields map[string]FieldInfo - history map[string][]Change - inheritance []inheritanceOperation historyBuilder *historyBuilder historyModel *historyModel } -func (m Metadata) currentHistoryModel() *historyModel { - if m.historyModel != nil { - return m.historyModel - } - if m.historyBuilder != nil { - return m.historyBuilder.model - } - return nil -} - // Lookup returns information for an effective path. Unknown and invalid paths // return false. func (m Metadata) Lookup(path Path) (FieldInfo, bool) { @@ -155,80 +143,13 @@ func (m Metadata) hasHistoricalPath(path Path) bool { return m.historyEvidence().hasObservedShape(path) } -func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []historyApplication, earlier ...*document.Node) { - finalizeMetadataPathsWithResolution(m, final, applications, nil, earlier...) -} - -func finalizeMetadataPathsWithResolution(m *Metadata, final *document.Node, applications []historyApplication, snapshots []resolutionSnapshot, earlier ...*document.Node) { +func freezeMetadataHistory(m *Metadata, final *document.Node) { finalizeEffectiveMetadataPaths(m, final) builder := m.historyBuilder - if builder != nil && builder.prefixReady { - if len(snapshots) != 0 && builder.model.resolutionPayload == nil { - if len(earlier) != 0 { - builder.setScratch(earlier[len(earlier)-1]) - } - builder.observeResolutionEvidence(snapshots) - } - if builder.model.final != final { - builder.model.final = final - } - m.historyModel = builder.freeze() - m.historyBuilder = nil - return - } - if builder == nil { - builder = newHistoryBuilder() - } - if len(earlier) != 0 { - builder.model.baseline = earlier[0] + if builder == nil || !builder.prefixReady { + panic("configkit: metadata history prefix is not ready") } 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 - } - builder.observeDocument(tree, phase) - } - if final != nil { - builder.observeDocument(final, historyPhasePostCLI) - } - for _, application := range applications { - 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 { - builder.addInventoryObservation(operation.control.path, operation.control.path, payload, historyPhaseInheritance, false) - continue - } - if operation.transfer == nil { - continue - } - 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.historyBuilder = nil } diff --git a/internal/engine/metadata_test.go b/internal/engine/metadata_test.go index 5ab8ef7..87361fc 100644 --- a/internal/engine/metadata_test.go +++ b/internal/engine/metadata_test.go @@ -47,12 +47,16 @@ func TestMetadataHistoricalPaths(t *testing.T) { } } -func TestMetadataPathIndexesOwnIndependentSegments(t *testing.T) { +func TestEffectivePathIndexOwnsIndependentSegments(t *testing.T) { t.Parallel() final := parseDocumentForDecode(t, "service: {workers: [1]}\n") var metadata Metadata - finalizeMetadataPaths(&metadata, final, nil) + finalizeEffectiveMetadataPaths(&metadata, final) + builder := newHistoryBuilder() + builder.observePostCLICheckpoint(final, nil) + builder.model.final = final + metadata.historyModel = builder.freeze() pointer := "/service/workers/0" effective := metadata.effectivePaths[pointer] historical := metadata.historyModel.concreteShapes(pointer) @@ -75,9 +79,13 @@ func TestMetadataPathIndexesRetainDistinctHistoricalShapes(t *testing.T) { final := parseDocumentForDecode(t, "shape: [replacement]\n") earlier := parseDocumentForDecode(t, "shape: {'0': base}\n") - var metadata Metadata - // Repeated trees must not add duplicate shapes to the historical index. - finalizeMetadataPaths(&metadata, final, nil, earlier, earlier, final) + builder := newHistoryBuilder() + builder.observeBaseline(earlier) + builder.setScratch(earlier) + builder.observePostCLICheckpoint(final, nil) + builder.model.final = final + metadata := Metadata{historyModel: builder.freeze()} + finalizeEffectiveMetadataPaths(&metadata, final) keyPath := Keys("shape", "0") indexPath := Keys("shape").Index(0) pointer := indexPath.String() diff --git a/internal/engine/override_context.go b/internal/engine/override_context.go index b221979..83f7f82 100644 --- a/internal/engine/override_context.go +++ b/internal/engine/override_context.go @@ -2,19 +2,13 @@ package engine import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" -// postDecodeOverrideContext keeps the structural, typed pre-CLI, and effective -// post-CLI views distinct while preparing the existing inspection machinery. +// postDecodeOverrideContext carries the typed post-CLI state and detached +// resolution views after the history builder has taken ownership of evidence. type postDecodeOverrideContext struct { - structuralInput *document.Node - preCLI *document.Node - preCLIOrigins map[string]Origin - decoded decodedConfiguration - metadata Metadata - historyBuilder *historyBuilder - resolution resolutionInput - applications []historyApplication - postDecodeApps []historyApplication - referenceInput *document.Node + decoded decodedConfiguration + metadata Metadata + resolution resolutionInput + referenceInput *document.Node } func preparePostDecodeOverrideContext(input assembledInput, before decodedConfiguration, assigned decodedOverrideResult) postDecodeOverrideContext { @@ -32,14 +26,12 @@ func preparePostDecodeOverrideContext(input assembledInput, before decodedConfig structuralOverlays = input.composition.overlays metadata.historyBuilder.setScratch(input.composition.root) metadata.historyBuilder.observeSourceEvidence(input.composition.root, input.composition.origins, applications, structuralOverlays) - applyInheritanceMetadata(&metadata, input.composition.root, input.composition.origins, structuralOverlays, input.inheritance) metadata.historyBuilder.setScratch(input.composition.root) metadata.historyBuilder.observeInheritanceEvidence(input.inheritance.operations) } else if input.composition != nil { structuralOverlays = input.composition.overlays metadata.historyBuilder.setScratch(input.composition.root) metadata.historyBuilder.observeSourceEvidence(input.composition.root, input.composition.origins, applications, structuralOverlays) - applyOverlayMetadata(&metadata, structuralOverlays) } else if len(applications) != 0 { metadata.historyBuilder.observeSourceEvidence(input.document, nil, applications, nil) } @@ -58,7 +50,8 @@ func preparePostDecodeOverrideContext(input assembledInput, before decodedConfig collectFieldInfo(after.final, Path{}, after, metadata.fields) } if len(cliRecords) != 0 || len(postDecodeApps) != 0 { - appendPostDecodeCLIHistory(&metadata, cliRecords, postDecodeApps, before.final) + metadata.historyBuilder.setScratch(before.final) + metadata.historyBuilder.observeCLIEvidence(postDecodeApps, cliRecords) } metadata.historyBuilder.observePostCLICheckpoint(after.final, after.origins) metadata.historyBuilder.prefixReady = true @@ -82,33 +75,10 @@ func preparePostDecodeOverrideContext(input assembledInput, before decodedConfig finalizeEffectiveMetadataPaths(&metadata, assigned.final) return postDecodeOverrideContext{ - structuralInput: document.Clone(input.document), - preCLI: document.Clone(before.final), - preCLIOrigins: cloneOrigins(before.origins), - decoded: after, - metadata: metadata, - historyBuilder: metadata.historyBuilder, - resolution: resolution, - applications: applications, - postDecodeApps: postDecodeApps, - referenceInput: documentWithMetadataLocations(assigned.final, metadata, input.document), - } -} - -// Source history is already assembled, with inheritance projected at query time. -// Reuse overlay event construction for CLI changes without reapplying displaced -// source definitions after those later assignments. -func appendPostDecodeCLIHistory(metadata *Metadata, records []overlayRecord, applications []historyApplication, before *document.Node) { - metadata.historyBuilder.setScratch(before) - metadata.historyBuilder.observeCLIEvidence(applications, records) - cli := Metadata{fields: metadata.fields} - applyOverlayMetadata(&cli, records) - for pointer, history := range cli.history { - for _, change := range history { - if change.Origin.Layer == LayerCLI { - appendInputChangeEvent(metadata.history, nil, pointer, change) - } - } + decoded: after, + metadata: metadata, + resolution: resolution, + referenceInput: documentWithMetadataLocations(assigned.final, metadata, input.document), } } @@ -169,19 +139,14 @@ func collectAffectedOverrideOrigins(displaced *[]displacedDefinition, existing, } } -func (context postDecodeOverrideContext) historyApplications() []historyApplication { - applications := cloneHistoryApplications(context.applications) - return append(applications, cloneHistoryApplications(context.postDecodeApps)...) -} - func structuralHistoryApplications(input assembledInput) []historyApplication { if input.composition != nil { - return cloneHistoryApplications(input.composition.applications) + return input.composition.applications } if input.document == nil { return nil } - return []historyApplication{{layer: LayerBase, value: document.Clone(input.document)}} + return []historyApplication{{layer: LayerBase, value: input.document}} } // documentWithMetadataLocations attaches effective origin coordinates to a @@ -289,7 +254,7 @@ func metadataFieldLocation(metadata Metadata, path Path, origin Origin) (documen if location, ok := latestOriginDocumentLocation(origin); ok { return location, true } - if model := metadata.currentHistoryModel(); model != nil { + if model := metadata.historyEvidence(); model != nil { location, ok := model.latestHistoricalLocation(path) if ok { return document.Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}, true diff --git a/internal/engine/override_context_test.go b/internal/engine/override_context_test.go index 7cc7e04..ce54f40 100644 --- a/internal/engine/override_context_test.go +++ b/internal/engine/override_context_test.go @@ -50,21 +50,14 @@ func TestPostDecodeOverrideContextOrdersDecodeCLIAndResolutionEvidence(t *testin t.Fatalf("applyDecodedOverrides() issues = %#v", issues) } prepared := preparePostDecodeOverrideContext(input, decoded, assigned) - if prepared.historyBuilder == nil || prepared.historyBuilder != prepared.metadata.historyBuilder || !prepared.historyBuilder.prefixReady { + if prepared.metadata.historyBuilder == nil || !prepared.metadata.historyBuilder.prefixReady { t.Fatal("post-decode handoff did not retain the completed history prefix") } if prepared.resolution.metadata.historyBuilder != nil || prepared.resolution.metadata.historyModel != nil { t.Fatal("resolution entry view borrowed mutable or historical state") } - if prepared.structuralInput == structural || prepared.preCLI == decoded.final || - prepared.decoded.final == assigned.final || prepared.referenceInput == assigned.final { - t.Fatal("structural, pre-CLI, post-CLI, and located snapshots are not detached") - } - if raw, ok := documentAtPath(prepared.structuralInput, Keys("version")); !ok || raw.Kind != document.Number || raw.Text != "1" { - t.Fatalf("structural version = %#v", raw) - } - if typed, ok := documentAtPath(prepared.preCLI, Keys("version")); !ok || typed.Kind != document.String || typed.Text != "1" { - t.Fatalf("pre-CLI typed version = %#v", typed) + if prepared.decoded.final == assigned.final || prepared.referenceInput == assigned.final { + t.Fatal("post-CLI configuration and located resolution views are not detached") } if prepared.metadata.fields["/count"].Origin.Layer != LayerCLI || !prepared.metadata.fields["/count"].Explicit { t.Fatalf("equal-value CLI metadata = %#v", prepared.metadata.fields["/count"]) @@ -75,9 +68,6 @@ func TestPostDecodeOverrideContextOrdersDecodeCLIAndResolutionEvidence(t *testin if _, explicit := prepared.decoded.presence["/items/0/enabled"]; explicit { t.Fatal("replaced list retained discarded-child presence") } - if len(prepared.applications) != 1 || len(prepared.postDecodeApps) != len(batch.overrides) || len(prepared.historyApplications()) != 1+len(batch.overrides) { - t.Fatalf("history applications = structural:%d CLI:%d all:%d", len(prepared.applications), len(prepared.postDecodeApps), len(prepared.historyApplications())) - } if prepared.resolution.contexts["source-1"].BaseDir != "/config" { t.Fatalf("resolution source contexts = %#v", prepared.resolution.contexts) } @@ -101,8 +91,8 @@ func TestPostDecodeOverrideContextOrdersDecodeCLIAndResolutionEvidence(t *testin if len(resolveDiagnostics) != 0 { t.Fatalf("resolveLoadInput() diagnostics = %#v", resolveDiagnostics) } - if resolvedContext.historyBuilder == nil || resolvedContext.historyBuilder != resolvedContext.metadata.historyBuilder || - resolvedContext.historyBuilder.model.resolutionPayload == nil || len(resolvedContext.historyBuilder.model.resolutionPayload.snapshots) != 1 { + if resolvedContext.metadata.historyBuilder == nil || resolvedContext.metadata.historyBuilder.model.resolutionPayload == nil || + len(resolvedContext.metadata.historyBuilder.model.resolutionPayload.snapshots) != 1 { t.Fatal("resolution evidence was not appended once to the handed-off model") } if location, ok := metadataFieldLocation(resolvedContext.metadata, Keys("count"), Origin{Layer: LayerResolution}); !ok || location.SourceID != countSource { @@ -115,7 +105,7 @@ func TestPostDecodeOverrideContextOrdersDecodeCLIAndResolutionEvidence(t *testin t.Fatalf("post-resolution reference location = %#v, %v", value, ok) } - finalizeMetadataPaths(&prepared.metadata, assigned.final, prepared.historyApplications(), decoded.baseline, prepared.structuralInput, prepared.preCLI, assigned.final) + freezeMetadataHistory(&prepared.metadata, assigned.final) inspection := Inspection{ metadata: prepared.metadata, history: prepared.metadata.historyModel, final: assigned.final, } diff --git a/internal/engine/resolution_index_test.go b/internal/engine/resolution_index_test.go index 711a204..23b449a 100644 --- a/internal/engine/resolution_index_test.go +++ b/internal/engine/resolution_index_test.go @@ -211,47 +211,51 @@ func TestApplyResolutionMetadataPreservesOverlappingDeltaOrder(t *testing.T) { oldOrigin := Origin{Layer: LayerBase} parentOrigin := Origin{Layer: LayerResolution, Operation: ChangeNormalization} childOrigin := Origin{Layer: LayerResolution, Operation: ChangeDerivedDefault} - leaf := &document.Node{Kind: document.String, Text: "value"} - final := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{ - Key: "group", - Value: &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{ - Key: "item", - Value: &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "leaf", Value: leaf}}}, - }}}, - }}} + beforeLeaf := &document.Node{Kind: document.String, Text: "before"} + afterLeaf := &document.Node{Kind: document.String, Text: "after"} + beforeItem := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "leaf", Value: beforeLeaf}}} + afterItem := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "leaf", Value: afterLeaf}}} + beforeGroup := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "item", Value: beforeItem}}} + afterGroup := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "item", Value: afterItem}}} + before := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "group", Value: beforeGroup}}} + final := &document.Node{Kind: document.Mapping, Mapping: []document.Entry{{Key: "group", Value: afterGroup}}} + path := Keys("group", "item", "leaf") + builder := newHistoryBuilder() + builder.setScratch(before) + snapshots := []resolutionSnapshot{ + {change: Change{Path: Keys("group"), Origin: parentOrigin}, before: beforeGroup, after: afterGroup}, + {change: Change{Path: Keys("group", "item"), Origin: childOrigin}, before: beforeItem, after: afterItem}, + } + builder.observeResolutionEvidence(snapshots) + builder.model.final = final metadata := &Metadata{ - fields: map[string]FieldInfo{"/group/item/leaf": {Origin: oldOrigin}}, - history: make(map[string][]Change), + fields: map[string]FieldInfo{path.String(): {Explicit: true, Origin: oldOrigin}}, + historyBuilder: builder, } entry := resolutionInput{metadata: Metadata{ - fields: map[string]FieldInfo{"/group/item/leaf": {Explicit: true, Origin: oldOrigin}}, - effectivePaths: effectivePathIndex(final), + fields: map[string]FieldInfo{path.String(): {Explicit: true, Origin: oldOrigin}}, + effectivePaths: effectivePathIndex(before), }} resolved := resolutionResult{ final: final, deltas: []documentDelta{{path: Keys("group")}, {path: Keys("group", "item")}}, - snapshots: []resolutionSnapshot{ - {change: Change{Path: Keys("group"), Origin: parentOrigin}}, - {change: Change{Path: Keys("group", "item"), Origin: childOrigin}}, - }, + snapshots: snapshots, } applyResolutionMetadata(metadata, resolved, entry) + metadata.effectivePaths = effectivePathIndex(final) + metadata.historyModel = builder.freeze() + metadata.historyBuilder = nil - field := metadata.fields["/group/item/leaf"] + field := metadata.fields[path.String()] if field.Origin.Operation != childOrigin.Operation { t.Fatalf("final leaf origin operation = %q, want %q", field.Origin.Operation, childOrigin.Operation) } - changes := metadata.history["/group/item/leaf"] + changes := metadata.History(path) if len(changes) != 2 { t.Fatalf("leaf history has %d entries, want 2: %#v", len(changes), changes) } if changes[0].Origin.Operation != parentOrigin.Operation || changes[1].Origin.Operation != childOrigin.Operation { t.Fatalf("leaf history operations = %q, %q; want %q, %q", changes[0].Origin.Operation, changes[1].Origin.Operation, parentOrigin.Operation, childOrigin.Operation) } - for index, change := range changes { - if len(change.Previous) != 1 || change.Previous[0].Layer != oldOrigin.Layer { - t.Errorf("leaf history[%d] Previous = %#v, want original origin", index, change.Previous) - } - } } diff --git a/internal/engine/secret.go b/internal/engine/secret.go index 9f85171..0edb70d 100644 --- a/internal/engine/secret.go +++ b/internal/engine/secret.go @@ -11,11 +11,11 @@ type secretDependency struct { dependency Path } -func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata, final *document.Node, applications []historyApplication, projections ...*document.Node) map[string]bool { - return classifySecretPathsWithEvidence(schema, predicate, metadata, final, applications, nil, projections...) +func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata) map[string]bool { + return classifySecretPathsWithEvidence(schema, predicate, metadata, nil) } -func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata, final *document.Node, applications []historyApplication, evidence []*sourceVersionEvidence, projections ...*document.Node) map[string]bool { +func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata, evidence []*sourceVersionEvidence) map[string]bool { paths := make(map[string]Path) candidateShapes := make(map[string][]Path) var candidates []Path @@ -30,19 +30,9 @@ func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Pa candidateShapes[pointer] = append(candidateShapes[pointer], cloned) candidates = append(candidates, cloned) } - for _, application := range applications { - walkHistoryApplication(application, func(_ Path, path Path, _ *document.Node) { - if len(application.sequences) != 0 { - addCandidate(path) - } else if parsed, err := ParsePath(path.String()); err == nil { - addCandidate(parsed) - } - addSecretPath(paths, path.String()) - }) - } secret := make(map[string]bool) var dependencies, inherited []secretDependency - if model := metadata.currentHistoryModel(); model != nil { + if model := metadata.historyEvidence(); model != nil { dependencies, inherited = model.sensitivityDependencies() for _, candidate := range model.sensitivityCandidates() { pointer := candidate.path.String() @@ -71,22 +61,6 @@ func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Pa } } - classifyProjection := func(tree *document.Node) { - walkDocument(tree, Path{}, func(path Path, node *document.Node) { - pointer := path.String() - addSecretPath(paths, pointer) - if parsed, err := ParsePath(pointer); err == nil { - addCandidate(parsed) - } - if node.Sensitive { - secret[pointer] = true - } - }) - } - for _, tree := range projections { - classifyProjection(tree) - } - classifyProjection(final) classifyPath := func(path Path) { if root, ok := schemaSecretRoot(schema, path); ok { secret[root.String()] = true diff --git a/internal/engine/secret_classification_test.go b/internal/engine/secret_classification_test.go index a4177a9..42d5674 100644 --- a/internal/engine/secret_classification_test.go +++ b/internal/engine/secret_classification_test.go @@ -42,6 +42,7 @@ func TestSecretClassificationParsesTypedHistoryAfterNumericShapeChanges(t *testi pointer := test.historical.String() builder := newHistoryBuilder() builder.observeDocument(earlier, historyPhaseSources) + builder.observeDocument(final, historyPhasePostCLI) metadata := Metadata{historyModel: builder.freeze()} seen := false classified := classifySecretPaths(nil, func(path Path) bool { @@ -55,7 +56,7 @@ func TestSecretClassificationParsesTypedHistoryAfterNumericShapeChanges(t *testi } } return true - }, metadata, final, []historyApplication{{value: earlier}}, earlier) + }, metadata) if !seen || !classified[pointer] { t.Fatalf("historical/final numeric path was not classified: %#v", classified) } @@ -77,7 +78,7 @@ func TestSecretPredicateReceivesDetachedHistoricalPath(t *testing.T) { seen = true candidate.segments[0].key = "changed" return true - }, metadata, nil, nil) + }, metadata) if !seen || !classified[path.String()] || classified["/changed/1"] { t.Fatalf("classification after predicate mutation = %#v, predicate called: %v", classified, seen) @@ -133,6 +134,9 @@ func TestSourceVersionEvidenceSensitivityUsesOriginalMountedPaths(t *testing.T) final := parseCompositionDocument(t, "final", "tenant:\n groups:\n blue:\n - public: shifted-neighbor\n labels:\n public: visible\n") var sawOriginalSequencePath, sawAncestorPredicate bool + builder := newHistoryBuilder() + builder.observeDocument(final, historyPhaseSources) + metadata := Metadata{historyModel: builder.freeze()} classified := classifySecretPathsWithEvidence(schema, func(path Path) bool { if path.String() == appendDestination(destination, predicatePath).String() { return true @@ -151,7 +155,7 @@ func TestSourceVersionEvidenceSensitivityUsesOriginalMountedPaths(t *testing.T) sawOriginalSequencePath = len(path.segments) == 5 && path.segments[3].kind == pathSegmentIndex && path.segments[3].index == 2 } return false - }, Metadata{}, final, []historyApplication{{path: destination, value: final}}, evidence) + }, metadata, evidence) for _, path := range []Path{ appendDestination(destination, sequencePath), @@ -197,7 +201,7 @@ func TestSourceVersionEvidenceClassificationKeepsCollidingShapes(t *testing.T) { sawKey = sawKey || reflect.DeepEqual(path, keyPath) sawIndex = sawIndex || reflect.DeepEqual(path, indexPath) return false - }, Metadata{}, nil, nil, evidence) + }, Metadata{}, evidence) if !classified[keyPath.String()] || !sawKey || !sawIndex { t.Errorf("reverse=%v: classified=%v, key seen=%v, index seen=%v", reverse, classified, sawKey, sawIndex) }