Explain descendants removed by sequence replacement

This commit is contained in:
2026-10-17 04:45:49 +00:00
parent 26720b98f0
commit 8a89806e44
3 changed files with 46 additions and 2 deletions

View File

@@ -92,7 +92,12 @@ items retain the same guarantees.
## 3. Record removals caused by registered-sequence replacement — P2 ## 3. Record removals caused by registered-sequence replacement — P2
- [ ] Addressed and regression-tested. - [x] Addressed and regression-tested.
Completion: replacement queries include missing descendants and use the replacing
container's origin. Public regressions verify shorter lists, empty lists, and
removed fields; existing append-relevance tests remain green. Focused facade
tests and the everyday check passed.
**Owner:** `applicationOperationPath` in **Owner:** `applicationOperationPath` in
[history.go](../../internal/engine/history.go), lines 134–149. [history.go](../../internal/engine/history.go), lines 134–149.

View File

@@ -71,6 +71,37 @@ func TestFacadeItemHelperHistoryPreservesTraversalOrder(t *testing.T) {
} }
} }
func TestFacadeRegisteredReplacementExplainsRemovedDescendants(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
upper []any
path configkit.Path
}{
{"shorter", []any{map[string]any{"id": "main"}}, configkit.Keys("records").Index(1).Key("id")},
{"empty", []any{}, configkit.Keys("records").Index(0).Key("id")},
{"removed field", []any{map[string]any{"id": "main"}}, configkit.Keys("records").Index(0).Key("text")},
} {
t.Run(test.name, func(t *testing.T) {
result, err := configkit.LoadLayered[map[string]any](configkit.Input{Source: configkit.Mapping("main", map[string]any{"records": test.upper})}, configkit.LayeredOptions[map[string]any]{
Definition: configkit.Definition[map[string]any]{SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}}},
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Mapping("lower", map[string]any{"records": []any{map[string]any{"id": "one", "text": "old"}, map[string]any{"id": "two"}}})}}}},
})
if err != nil {
t.Fatal(err)
}
explanation, ok := result.Explain(test.path)
if !ok || explanation.Effective.Present || len(explanation.History) < 2 {
t.Fatalf("removal not explained: %#v", explanation)
}
last := explanation.History[len(explanation.History)-1]
if !last.Before.Present || last.After.Present || last.Origin.Layer != configkit.LayerBase || len(last.Origin.Locations) != 1 || last.Origin.Locations[0].SourceID != result.Sources()[0].ID {
t.Fatalf("wrong removal transition/origin: %#v", last)
}
})
}
}
func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) { func TestFacadeSequencePlacementControlsAcrossLoadEntryPoints(t *testing.T) {
definition := configkit.Definition[map[string]any]{ definition := configkit.Definition[map[string]any]{
SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("entries"), IdentityField: "id"}}, SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("entries"), IdentityField: "id"}},

View File

@@ -139,7 +139,15 @@ func applicationOperationPath(application historyApplication, query Path, lookup
} }
sequencePathMatched = true sequencePathMatched = true
if pathContains(container, query) && len(query.segments) > len(container.segments) { if pathContains(container, query) && len(query.segments) > len(container.segments) {
if _, ok := historyApplicationLocalPath(application, query); !ok { local, ok := historyApplicationLocalPath(application, query)
if contribution.mode == sequenceHistoryReplace {
if _, present := documentAtPathWithLookup(application.value, local, lookup); !ok || !present {
// A replacement also changes descendants missing from the
// incoming list. Attribute their removal to its container.
return container, true
}
}
if !ok {
continue continue
} }
} }