diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 7eb38db..56171dc 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -394,7 +394,7 @@ Roadmap candidate 7. Owners: publication, inspection, document/snapshot/history Validate: `go test ./internal/engine -run 'Path|History|Explain|Effective|Independent|Parallel'`, engine race tests, and the everyday check. -## Stage 16: Index materialized inheritance-step membership +## Stage 16: Index materialized inheritance-step membership — Complete Roadmap candidate 8. Owner: engine `inheritance_origin.go`. diff --git a/internal/engine/inheritance_origin.go b/internal/engine/inheritance_origin.go index e4e2ba9..617fad9 100644 --- a/internal/engine/inheritance_origin.go +++ b/internal/engine/inheritance_origin.go @@ -2,6 +2,7 @@ package engine import ( "slices" + "strconv" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) @@ -74,10 +75,44 @@ func copyInheritanceSubtree(target, incoming map[string]Origin, node *document.N }) } +type inheritanceStepKey struct { + from string + to string + reference string + locations string +} + +func inheritanceStepKeyForPaths(from, to, reference Path, locations []Location) inheritanceStepKey { + return inheritanceStepKey{ + from: from.String(), to: to.String(), reference: reference.String(), + locations: inheritanceLocationsKey(locations), + } +} + +func inheritanceLocationsKey(locations []Location) string { + encoded := strconv.AppendInt(nil, int64(len(locations)), 10) + encoded = append(encoded, ';') + for _, location := range locations { + encoded = strconv.AppendInt(encoded, int64(len(location.SourceID)), 10) + encoded = append(encoded, ':') + encoded = append(encoded, location.SourceID...) + encoded = append(encoded, ';') + encoded = strconv.AppendInt(encoded, int64(location.Line), 10) + encoded = append(encoded, ';') + encoded = strconv.AppendInt(encoded, int64(location.Column), 10) + encoded = append(encoded, ';') + } + return string(encoded) +} + // Materialize concrete steps only for an inspection record. The linked private // traces remain compact when one parent supplies many descendant values. func projectInheritanceSteps(origin Origin) []InheritanceStep { steps := cloneInheritanceSteps(origin.Inheritance) + seen := make(map[inheritanceStepKey]struct{}, len(steps)) + for _, step := range steps { + seen[inheritanceStepKeyForPaths(step.From, step.To, step.Reference, step.Locations)] = struct{}{} + } for _, contributor := range origin.contributors { var reverse []*inheritanceTrace for link := contributor.trace; link != nil; link = link.previous { @@ -91,17 +126,13 @@ func projectInheritanceSteps(origin Origin) []InheritanceStep { continue } next := appendInheritancePath(transfer.to, relative) - step := InheritanceStep{ - From: current.clone(), To: next.clone(), Reference: transfer.reference.clone(), - Locations: cloneLocations(transfer.locations), - } - if !slices.ContainsFunc(steps, func(existing InheritanceStep) bool { - return existing.From.String() == step.From.String() && - existing.To.String() == step.To.String() && - existing.Reference.String() == step.Reference.String() && - slices.Equal(existing.Locations, step.Locations) - }) { - steps = append(steps, step) + key := inheritanceStepKeyForPaths(current, next, transfer.reference, transfer.locations) + if _, found := seen[key]; !found { + seen[key] = struct{}{} + steps = append(steps, InheritanceStep{ + From: current.clone(), To: next.clone(), Reference: transfer.reference.clone(), + Locations: cloneLocations(transfer.locations), + }) } current = next } diff --git a/internal/engine/inheritance_origin_test.go b/internal/engine/inheritance_origin_test.go index f2067d5..2f872c4 100644 --- a/internal/engine/inheritance_origin_test.go +++ b/internal/engine/inheritance_origin_test.go @@ -2,6 +2,8 @@ package engine import ( "io/fs" + "reflect" + "slices" "testing" "testing/fstest" @@ -85,6 +87,120 @@ func TestInheritedOriginsKeepSourcePathsPresenceAndIndependentReplacement(t *tes } } +func TestProjectedInheritanceStepsMatchScanAndRetainOwnership(t *testing.T) { + sourcePath := Keys("source", "a/b", "01") + firstLocations := []Location{{SourceID: "first;:source", Line: -1, Column: 2}} + secondLocations := []Location{ + {SourceID: "second:;source", Line: 3, Column: -4}, + {SourceID: "", Line: -5, Column: 6}, + } + firstTransfer := inheritanceTransfer{ + from: Keys("source"), to: Keys("middle"), reference: Keys("middle", "extends"), locations: firstLocations, + } + secondTransfer := inheritanceTransfer{ + from: Keys("middle"), to: Keys("target"), reference: Keys("target", "extends"), locations: secondLocations, + } + firstTrace := &inheritanceTrace{transfer: &firstTransfer} + chain := &inheritanceTrace{previous: firstTrace, transfer: &secondTransfer} + firstStep := InheritanceStep{ + From: sourcePath, To: Keys("middle", "a/b", "01"), Reference: firstTransfer.reference, Locations: firstLocations, + } + + duplicateFirstTransfer := firstTransfer + duplicateSecondTransfer := secondTransfer + duplicateChain := &inheritanceTrace{ + previous: &inheritanceTrace{transfer: &duplicateFirstTransfer}, + transfer: &duplicateSecondTransfer, + } + reversedLocations := []Location{secondLocations[1], secondLocations[0]} + reorderedSecondTransfer := secondTransfer + reorderedSecondTransfer.locations = reversedLocations + reorderedChain := &inheritanceTrace{ + previous: &inheritanceTrace{transfer: &duplicateFirstTransfer}, + transfer: &reorderedSecondTransfer, + } + origin := Origin{ + Inheritance: []InheritanceStep{firstStep, firstStep}, + contributors: []inheritanceContributor{ + {source: sourcePath, trace: chain}, + {source: sourcePath, trace: duplicateChain}, + {source: sourcePath, trace: reorderedChain}, + {source: sourcePath, trace: chain}, + }, + } + + want := projectInheritanceStepsByScan(origin) + got := projectInheritanceSteps(origin) + if !reflect.DeepEqual(got, want) { + t.Fatalf("projected steps differ from ordered scan:\n got: %#v\nwant: %#v", got, want) + } + if len(got) != 4 || got[0].From.String() != "/source/a~1b/01" || got[0].To.String() != "/middle/a~1b/01" || + !reflect.DeepEqual(got[0], got[1]) || got[2].To.String() != "/target/a~1b/01" || + !reflect.DeepEqual(got[2].Locations, secondLocations) || !reflect.DeepEqual(got[3].Locations, reversedLocations) { + t.Fatalf("unexpected duplicate/order/escape projection: %#v", got) + } + + got[0].From.segments[0].key = "mutated" + got[2].Locations[0].SourceID = "mutated" + if repeated := projectInheritanceSteps(origin); !reflect.DeepEqual(repeated, want) { + t.Fatalf("returned steps mutated retained evidence:\n got: %#v\nwant: %#v", repeated, want) + } +} + +func TestInheritanceLocationsKeyEncodesOrderedLengthPrefixedValues(t *testing.T) { + if inheritanceLocationsKey(nil) != inheritanceLocationsKey([]Location{}) { + t.Fatal("nil and empty location lists should encode equally") + } + values := [][]Location{ + {{SourceID: "a;1;-2;", Line: -3, Column: 4}}, + {{SourceID: "a", Line: 1, Column: -2}, {SourceID: ";1;-2;", Line: -3, Column: 4}}, + {{SourceID: ";1;-2;", Line: -3, Column: 4}, {SourceID: "a", Line: 1, Column: -2}}, + {{SourceID: "é", Line: -3, Column: 4}}, + {{SourceID: "e", Line: -3, Column: 4}}, + } + keys := make(map[string]int, len(values)) + for index, locations := range values { + key := inheritanceLocationsKey(locations) + if previous, found := keys[key]; found { + t.Fatalf("location lists %d and %d encoded identically", previous, index) + } + keys[key] = index + } +} + +func projectInheritanceStepsByScan(origin Origin) []InheritanceStep { + steps := cloneInheritanceSteps(origin.Inheritance) + for _, contributor := range origin.contributors { + var reverse []*inheritanceTrace + for link := contributor.trace; link != nil; link = link.previous { + reverse = append(reverse, link) + } + current := contributor.source + for index := len(reverse) - 1; index >= 0; index-- { + transfer := reverse[index].transfer + relative, ok := relativeHistoryPath(transfer.from, current) + if !ok { + continue + } + next := appendInheritancePath(transfer.to, relative) + step := InheritanceStep{ + From: current.clone(), To: next.clone(), Reference: transfer.reference.clone(), + Locations: cloneLocations(transfer.locations), + } + if !slices.ContainsFunc(steps, func(existing InheritanceStep) bool { + return existing.From.String() == step.From.String() && + existing.To.String() == step.To.String() && + existing.Reference.String() == step.Reference.String() && + slices.Equal(existing.Locations, step.Locations) + }) { + steps = append(steps, step) + } + current = next + } + } + return steps +} + func TestInheritedOriginsRetainChainsAndNestedDestinations(t *testing.T) { first := parseCompositionDocument(t, "source-1", `first: enabled: false