diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 0ecb6a6..f5ee3fc 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -351,7 +351,7 @@ Roadmap candidate 7. Owners: engine `history.go`, `load.go`, `load_result.go`. Validate: `go test ./internal/engine -run 'Resolution|History|Explain|Ownership'`, engine race tests, and the everyday check. Full replay semantics stay intact. -## Stage 14: Index relevant inheritance history operations +## Stage 14: Index relevant inheritance history operations — Complete Roadmap candidate 7. Owners: engine metadata, metadata cloning, inheritance history. diff --git a/internal/engine/inheritance_history.go b/internal/engine/inheritance_history.go index b68b065..5c31ddc 100644 --- a/internal/engine/inheritance_history.go +++ b/internal/engine/inheritance_history.go @@ -1,6 +1,57 @@ package engine -import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" +import ( + "sort" + + "gitea.maximumdirect.net/eric/ConfigKit/internal/document" +) + +type inheritanceHistoryIndex struct { + paths pathPrefixIndex +} + +func newInheritanceHistoryIndex(operations []inheritanceOperation) *inheritanceHistoryIndex { + if len(operations) == 0 { + return nil + } + index := &inheritanceHistoryIndex{} + for operationID, operation := range operations { + if operation.control != nil { + index.paths.insert(operation.control.path.String(), operationID) + continue + } + if operation.transfer != nil { + index.paths.insert(operation.transfer.to.String(), operationID) + } + } + return index +} + +func (index *inheritanceHistoryIndex) matchingOperations(path Path, operations []inheritanceOperation) []inheritanceOperation { + if index == nil { + return operations + } + var ids []int + seen := make(map[int]struct{}) + collect := func(id int) { + if _, found := seen[id]; found { + return + } + seen[id] = struct{}{} + ids = append(ids, id) + } + pointer := path.String() + index.paths.visitAncestors(pointer, collect) + index.paths.visitDescendants(pointer, collect) + sort.Ints(ids) + matching := make([]inheritanceOperation, 0, len(ids)) + for _, id := range ids { + if id >= 0 && id < len(operations) { + matching = append(matching, operations[id]) + } + } + return matching +} // Retain source history at original paths. Structural operations are held once // per relationship and projected onto a requested destination path only when diff --git a/internal/engine/inheritance_history_test.go b/internal/engine/inheritance_history_test.go index e81d52f..0f408bc 100644 --- a/internal/engine/inheritance_history_test.go +++ b/internal/engine/inheritance_history_test.go @@ -2,6 +2,7 @@ package engine import ( "bytes" + "reflect" "strings" "testing" @@ -311,6 +312,51 @@ func TestInheritanceHistoryRetainsControlsAndDisplacedPaths(t *testing.T) { } } +func TestIndexedInheritanceHistoryMatchesOperationScan(t *testing.T) { + inspection, _ := inheritanceInspectionForTest(t, + parseCompositionDocument(t, "source-1", "parent: {branch: {detail: inherited, retained: present}}\n"), + 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)) + } + queries := []Path{ + Keys("child"), + Keys("child", inheritanceKey), + Keys("child", "branch", "detail"), + Keys("child", "branch"), + Keys("sibling", "branch", "detail"), + Path{}, + } + for _, path := range queries { + if !metadata.hasHistoricalPath(path) { + continue + } + stored := metadata.history[path.String()] + want := historyWithInheritance(path, stored, metadata.inheritance) + 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} + 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) + } + } + + removed := Keys("child", "branch", "detail") + first := metadata.History(removed) + if len(first) == 0 || len(first[0].Path.segments) == 0 { + t.Fatalf("removed-path history = %#v", first) + } + first[0].Path.segments[0].key = "mutated" + second := cloneMetadata(metadata).History(removed) + want := historyWithInheritance(removed, metadata.history[removed.String()], metadata.inheritance) + if !reflect.DeepEqual(second, want) { + t.Fatalf("returned history mutation affected a repeated or cloned query:\n got: %#v\nwant: %#v", second, want) + } +} + func TestInheritanceHistoryPrecedesApplicationResolution(t *testing.T) { inspection, _ := inheritanceInspectionForTest(t, parseCompositionDocument(t, "source-1", "parent: {value: original}\nchild: {extends: parent}\n")) diff --git a/internal/engine/load_metadata.go b/internal/engine/load_metadata.go index ee760af..2a8bf8a 100644 --- a/internal/engine/load_metadata.go +++ b/internal/engine/load_metadata.go @@ -254,9 +254,10 @@ func cloneMetadata(metadata Metadata) Metadata { 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...), + fields: make(map[string]FieldInfo, len(metadata.fields)), + history: make(map[string][]Change, len(metadata.history)), + inheritance: append([]inheritanceOperation(nil), metadata.inheritance...), + inheritanceIndex: metadata.inheritanceIndex, } cloned.effectivePaths = make(map[string]Path, len(metadata.effectivePaths)) for pointer, path := range metadata.effectivePaths { diff --git a/internal/engine/metadata.go b/internal/engine/metadata.go index f12a61b..2b98b9f 100644 --- a/internal/engine/metadata.go +++ b/internal/engine/metadata.go @@ -9,11 +9,12 @@ import ( // 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 + effectivePaths map[string]Path + historicalPaths map[string][]Path + fields map[string]FieldInfo + history map[string][]Change + inheritance []inheritanceOperation + inheritanceIndex *inheritanceHistoryIndex } // Lookup returns information for an effective path. Unknown and invalid paths @@ -57,7 +58,11 @@ func (m Metadata) History(path Path) []Change { return nil } history := m.history[path.String()] - return historyWithInheritance(path, history, m.inheritance) + operations := m.inheritance + if m.inheritanceIndex != nil { + operations = m.inheritanceIndex.matchingOperations(path, m.inheritance) + } + return historyWithInheritance(path, history, operations) } // HistoricalPaths returns the paths observed during loading, including effective @@ -161,6 +166,7 @@ func finalizeMetadataPaths(m *Metadata, final *document.Node, applications []his walkDocument(operation.effective, operation.transfer.to, add) } } + m.inheritanceIndex = newInheritanceHistoryIndex(m.inheritance) } // effectivePathIndex records document-derived paths with their contextual segment