diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 8509359..95eeab0 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -124,7 +124,13 @@ appends do not falsely claim to change earlier records. ## 4. Honor contextual parsed version-path indexes — P2 -- [ ] Addressed and regression-tested. +- [x] Addressed and regression-tested. + +Completion: protected-path sequence traversal uses the existing canonical +contextual index interpreter. Public import and grouped-underlay parity tests +cover parsed and explicitly indexed rules; existing version-path tests preserve +numeric-key intent and invalid index behavior. Focused tests and the everyday +check passed. **Owner:** `fragmentVersionClaimProjected` in [directives.go](../../internal/engine/directives.go), lines 190–194. diff --git a/facade_source_version_mode_test.go b/facade_source_version_mode_test.go index 90a730d..efc260b 100644 --- a/facade_source_version_mode_test.go +++ b/facade_source_version_mode_test.go @@ -63,6 +63,44 @@ func TestFacadeSourceVersionEvidenceFollowsAuthoredSequenceRanges(t *testing.T) } } +func TestFacadeSourceVersionOwnershipAcceptsContextualIndexes(t *testing.T) { + t.Parallel() + parsed, err := configkit.ParsePath("/records/0/version") + if err != nil { + t.Fatal(err) + } + for _, grouped := range []bool{false, true} { + for _, path := range []configkit.Path{configkit.Keys("records").Index(0).Key("version"), parsed} { + definition := configkit.Definition[map[string]any]{ + SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}}, + Version: &configkit.VersionRule{Path: path, Check: func(configkit.Version, *configkit.Validation) error { return nil }}, + } + first := "records: [{id: first, version: first}]\n" + child := "records: [{id: child, version: child}]\n" + var result *configkit.Result[map[string]any] + if grouped { + result, err = configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte(first))}, configkit.LayeredOptions[map[string]any]{ + Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired, + Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: configkit.Bytes("lower", []byte(child))}}}}, + }) + } else { + filesystem := configkit.NewFilesystem("versions", fstest.MapFS{"main.yml": {Data: []byte("configkit: {imports: [{path: child.yml}]}\n" + first)}, "child.yml": {Data: []byte(child)}}) + result, err = configkit.Load(filesystem.File("main.yml"), configkit.Options[map[string]any]{Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired, Imports: true}) + } + if err != nil || result == nil { + t.Fatalf("grouped=%t path=%#v: %v", grouped, path, err) + } + want := 2 + if grouped { + want = 1 + } + if len(result.Config["records"].([]any)) != want { + t.Fatal("version path changed composition") + } + } + } +} + type sourceVersionModeReader struct { *strings.Reader reads int diff --git a/internal/engine/directives.go b/internal/engine/directives.go index 206c337..73ababb 100644 --- a/internal/engine/directives.go +++ b/internal/engine/directives.go @@ -188,10 +188,10 @@ func fragmentVersionClaimProjected(root *document.Node, rule *VersionRule, sourc current = current.Mapping[index].Value currentPath = currentPath.Key(segment.key) case document.Sequence: - if segment.kind != pathSegmentIndex { + index, valid := schemaIndex(segment) + if !valid { return current } - index := segment.index for _, assigned := range ranges { if assigned.sourceIndex != sourceIndex || assigned.path.String() != currentPath.String() { continue