Project sequence origins onto appended indexes
This commit is contained in:
@@ -291,6 +291,21 @@ Scope: composition evidence, not import acquisition.
|
|||||||
Validation: composition/provenance/path tests and `./scripts/check.sh`.
|
Validation: composition/provenance/path tests and `./scripts/check.sh`.
|
||||||
Completion: correct trees and effective origins share the same record ordering.
|
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
|
## Stage 4 — Preserve append history and whole-list tier replacement
|
||||||
|
|
||||||
Scope: private history operations, replay, and precedence handoff.
|
Scope: private history operations, replay, and precedence handoff.
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func composeDocumentsWithPolicy(policy sequencePolicy, inputs ...*document.Node)
|
|||||||
for _, input := range inputs {
|
for _, input := range inputs {
|
||||||
applications = append(applications, historyApplication{layer: LayerBase, value: input})
|
applications = append(applications, historyApplication{layer: LayerBase, value: input})
|
||||||
}
|
}
|
||||||
origins := compositionOrigins(collected.ordered)
|
origins := projectedCompositionOrigins(inputs, ranges)
|
||||||
completeCompositionOrigins(root, Path{}, origins)
|
completeCompositionOrigins(root, Path{}, origins)
|
||||||
return &composedDocument{
|
return &composedDocument{
|
||||||
root: root,
|
root: root,
|
||||||
@@ -337,18 +337,51 @@ func documentEntryIndex(entries []document.Entry, key string) int {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func compositionOrigins(claims []compositionClaim) map[string]Origin {
|
func projectedCompositionOrigins(inputs []*document.Node, ranges []sequenceRange) map[string]Origin {
|
||||||
origins := make(map[string]Origin, len(claims))
|
origins := make(map[string]Origin)
|
||||||
for _, claim := range claims {
|
rangeBySourcePath := make(map[sequenceRangeKey]int, len(ranges))
|
||||||
pointer := claim.path.String()
|
for index, assigned := range ranges {
|
||||||
origin := origins[pointer]
|
rangeBySourcePath[sequenceRangeKey{sourceIndex: assigned.sourceIndex, path: assigned.path.String()}] = index
|
||||||
origin.Layer = LayerBase
|
}
|
||||||
origin.Locations = append(origin.Locations, claim.location)
|
for sourceIndex, input := range inputs {
|
||||||
origins[pointer] = origin
|
collectProjectedCompositionOrigins(input, Path{}, Path{}, sourceIndex, ranges, rangeBySourcePath, origins)
|
||||||
}
|
}
|
||||||
return 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) {
|
func applyCompositionOrigins(decoded *decodedConfiguration, origins map[string]Origin) {
|
||||||
for pointer, origin := range origins {
|
for pointer, origin := range origins {
|
||||||
if _, present := decoded.presence[pointer]; present {
|
if _, present := decoded.presence[pointer]; present {
|
||||||
|
|||||||
@@ -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) {
|
func TestComposeDocumentsValidatesRegisteredSequenceSingletons(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user