From bb9bde799aff9a71e23d95b6e70b5ecdfbbca335 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 16 Oct 2026 15:29:27 +0000 Subject: [PATCH] Project sequence origins onto appended indexes --- docs/roadmap/implementation.md | 15 ++++++ internal/engine/compose.go | 51 +++++++++++++++---- internal/engine/compose_test.go | 90 +++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 9 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 3903024..be26cd8 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -291,6 +291,21 @@ Scope: composition evidence, not import acquisition. Validation: composition/provenance/path tests and `./scripts/check.sh`. Completion: correct trees and effective origins share the same record ordering. +Completion evidence: composition origins now traverse each source document with +separate local and effective semantic paths. Stage 2's source ranges shift list +items and descendants to assigned indexes while retaining each field's physical +location and contributor order at sequence containers; conflict claims remain +source-local. Tests cover a later source's local index zero becoming effective +index one, mounted and escaped/numeric-key paths, multiple collections, and +detached origins, claims, ranges, and input trees. These checks passed with Go +`go1.26.9 linux/amd64`: + +- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'Compose|Provenance|Path' -count=1` +- `GOTOOLCHAIN=local ./scripts/check.sh` +- `git diff --check` + +No environmental limitation affected validation; checks ran on Linux/amd64. + ## Stage 4 — Preserve append history and whole-list tier replacement Scope: private history operations, replay, and precedence handoff. diff --git a/internal/engine/compose.go b/internal/engine/compose.go index 056f4f5..33ede28 100644 --- a/internal/engine/compose.go +++ b/internal/engine/compose.go @@ -116,7 +116,7 @@ func composeDocumentsWithPolicy(policy sequencePolicy, inputs ...*document.Node) for _, input := range inputs { applications = append(applications, historyApplication{layer: LayerBase, value: input}) } - origins := compositionOrigins(collected.ordered) + origins := projectedCompositionOrigins(inputs, ranges) completeCompositionOrigins(root, Path{}, origins) return &composedDocument{ root: root, @@ -337,18 +337,51 @@ func documentEntryIndex(entries []document.Entry, key string) int { return -1 } -func compositionOrigins(claims []compositionClaim) map[string]Origin { - origins := make(map[string]Origin, len(claims)) - for _, claim := range claims { - pointer := claim.path.String() - origin := origins[pointer] - origin.Layer = LayerBase - origin.Locations = append(origin.Locations, claim.location) - origins[pointer] = origin +func projectedCompositionOrigins(inputs []*document.Node, ranges []sequenceRange) map[string]Origin { + origins := make(map[string]Origin) + rangeBySourcePath := make(map[sequenceRangeKey]int, len(ranges)) + for index, assigned := range ranges { + rangeBySourcePath[sequenceRangeKey{sourceIndex: assigned.sourceIndex, path: assigned.path.String()}] = index + } + for sourceIndex, input := range inputs { + collectProjectedCompositionOrigins(input, Path{}, Path{}, sourceIndex, ranges, rangeBySourcePath, origins) } return origins } +type sequenceRangeKey struct { + sourceIndex int + path string +} + +// Keep local paths for matching Stage 2 ranges and effective paths for emitted +// evidence. The paths are transformed segment by segment; pointer strings are +// only used as map keys after projection is complete. +func collectProjectedCompositionOrigins(node *document.Node, localPath, effectivePath Path, sourceIndex int, ranges []sequenceRange, rangeBySourcePath map[sequenceRangeKey]int, origins map[string]Origin) { + pointer := effectivePath.String() + origin := origins[pointer] + origin.Layer = LayerBase + origin.Locations = append(origin.Locations, publicLocation(node.Location)) + origins[pointer] = origin + + switch node.Kind { + case document.Mapping: + for _, entry := range node.Mapping { + collectProjectedCompositionOrigins(entry.Value, localPath.Key(entry.Key), effectivePath.Key(entry.Key), sourceIndex, ranges, rangeBySourcePath, origins) + } + case document.Sequence: + start := 0 + if rangeIndex, ok := rangeBySourcePath[sequenceRangeKey{sourceIndex: sourceIndex, path: localPath.String()}]; ok { + start = ranges[rangeIndex].start + } + for index, child := range node.Sequence { + localChild := localPath.Index(index) + effectiveChild := effectivePath.Index(start + index) + collectProjectedCompositionOrigins(child, localChild, effectiveChild, sourceIndex, ranges, rangeBySourcePath, origins) + } + } +} + func applyCompositionOrigins(decoded *decodedConfiguration, origins map[string]Origin) { for pointer, origin := range origins { if _, present := decoded.presence[pointer]; present { diff --git a/internal/engine/compose_test.go b/internal/engine/compose_test.go index 4e81eb6..e635636 100644 --- a/internal/engine/compose_test.go +++ b/internal/engine/compose_test.go @@ -220,6 +220,96 @@ func TestComposeDocumentsAppendsRegisteredSequencesAndReturnsRanges(t *testing.T } } +func TestComposeDocumentsProjectsSequenceOriginsToAssignedIndexes(t *testing.T) { + first := parseCompositionDocument(t, "source-1", `mounted: + records: + - id: first + name: first-name +escaped/~: + "7": + - id: escaped-first + name: escaped-first-name +`) + second := parseCompositionDocument(t, "source-2", `mounted: + records: + - id: second + name: second-name +escaped/~: + "7": + - id: escaped-second + name: escaped-second-name +`) + firstBefore, secondBefore := document.Clone(first), document.Clone(second) + policy := sequencePolicy{identityFields: map[string]string{ + Keys("mounted", "records").String(): "id", + Keys("escaped/~", "7").String(): "id", + }} + + composed, diagnostics := composeDocumentsWithPolicy(policy, first, second) + if len(diagnostics) != 0 { + t.Fatalf("composeDocumentsWithPolicy() diagnostics = %#v", diagnostics) + } + for _, test := range []struct { + path Path + want Location + }{ + {path: Keys("mounted", "records").Index(1).Key("name"), want: Location{SourceID: "source-2", Line: 4, Column: 13}}, + {path: Keys("escaped/~", "7").Index(1).Key("name"), want: Location{SourceID: "source-2", Line: 8, Column: 13}}, + } { + path, want := test.path, test.want + origin, ok := composed.origins[path.String()] + if !ok || !reflect.DeepEqual(origin.Locations, []Location{want}) { + t.Errorf("origin at %s = %#v, want source-specific location %#v", path, origin, want) + } + } + for _, test := range []struct { + path Path + wantSources []string + }{ + {path: Keys("mounted", "records"), wantSources: []string{"source-1", "source-2"}}, + {path: Keys("escaped/~", "7"), wantSources: []string{"source-1", "source-2"}}, + } { + path, wantSources := test.path, test.wantSources + origin := composed.origins[path.String()] + gotSources := make([]string, len(origin.Locations)) + for index, location := range origin.Locations { + gotSources[index] = location.SourceID + } + if !reflect.DeepEqual(gotSources, wantSources) { + t.Errorf("container origin at %s = %#v, want contributors %#v", path, gotSources, wantSources) + } + } + if !reflect.DeepEqual(first, firstBefore) || !reflect.DeepEqual(second, secondBefore) { + t.Fatal("composition projection mutated an input") + } + + copy := cloneComposedDocument(composed) + copy.origins[Keys("mounted", "records").Index(1).Key("name").String()].Locations[0].SourceID = "changed" + for index := range copy.claims { + if copy.claims[index].path.String() == "/mounted/records" { + copy.claims[index].path.segments[0].key = "changed" + break + } + } + copy.sequenceRanges[0].path.segments[0].key = "changed" + if got := composed.origins[Keys("mounted", "records").Index(1).Key("name").String()].Locations[0].SourceID; got != "source-2" { + t.Fatalf("copied origin mutation changed source evidence: %q", got) + } + foundClaim := false + for _, claim := range composed.claims { + if claim.path.String() == "/mounted/records" { + foundClaim = true + break + } + } + if !foundClaim { + t.Fatal("copied claim mutation changed source path") + } + if got := composed.sequenceRanges[0].path.String(); got != "/mounted/records" { + t.Fatalf("copied range mutation changed assigned path: %q", got) + } +} + func TestComposeDocumentsValidatesRegisteredSequenceSingletons(t *testing.T) { tests := []struct { name string