diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 698c863..aac17b2 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -1,6 +1,6 @@ # Import Discovery and Scheduling Review Findings -Status: in progress; issue 1 complete, five findings remain. +Status: in progress; issues 1–2 complete, four findings remain. Reviewed implementation: `aa93aed..15943e2`, ending at `15943e233ca3cd1286c03c0e7085871db344cd4b`, against the accepted @@ -57,7 +57,7 @@ source contexts, repeated paths, and distinct alias admission. The regression failed before the fix. Focused discovery/resource tests and `./scripts/check.sh` passed with Go 1.26.9 and offline dependency selection. -## 2. [ ] P1 — Scope main-version protection to the actual effective path +## 2. [x] P1 — Scope main-version protection to the actual effective path Owner: [composePendingItems](../../internal/engine/imports.go), around lines 694–695, and its call to `importedVersionClaim`. @@ -85,6 +85,13 @@ the effective paths really overlap. Cover imported/nested items, item helpers, main-item loading, underlays, and enabled source-version modes without adding blanket exemptions or banning ordinary domain version fields. +Completed: removed the global-rule check against item-local field names; enclosing +composition checks retain effective-path protection, including the existing +main-item scoped check. New public regressions cover item bodies, helpers, nested +items, underlays, and selected main items, and failed before the change. The +full everyday check passed, including existing default/enabled source-version +and assigned-index ownership regressions. + ## 3. [ ] P2 — Accept valid multi-component native filesystem roots Owner: [resolveNativeImportPath](../../internal/engine/filesystem_links.go), diff --git a/facade_item_version_test.go b/facade_item_version_test.go new file mode 100644 index 0000000..d20e1d4 --- /dev/null +++ b/facade_item_version_test.go @@ -0,0 +1,64 @@ +package configkit_test + +import ( + "testing" + "testing/fstest" + + configkit "gitea.maximumdirect.net/eric/ConfigKit" +) + +func TestFacadeItemDomainVersionsKeepEffectiveScope(t *testing.T) { + for _, test := range []struct { + name, item, child, helper string + underlay, mainItem bool + }{ + {name: "item", item: "id: item\nversion: domain\n"}, + {name: "helper", item: "id: item\nconfigkit: {imports: [{path: helper.yml}]}\n", helper: "version: domain\n"}, + {name: "nested", item: "id: parent\nconfigkit: {imports: [{path: child.yml, destination: [children], placement: sequence-item}]}\n", child: "id: child\nversion: domain\n"}, + {name: "underlay", item: "id: item\nversion: domain\n", underlay: true}, + {name: "main-item", item: "id: parent\nconfigkit: {imports: [{path: child.yml, destination: [children], placement: sequence-item}]}\n", child: "id: child\nversion: domain\n", mainItem: true}, + } { + t.Run(test.name, func(t *testing.T) { + files := fstest.MapFS{ + "main.yml": {Data: []byte("version: 1\nconfigkit: {imports: [{path: item.yml, destination: [items], placement: sequence-item}]}\n")}, + "item.yml": {Data: []byte(test.item)}, + "child.yml": {Data: []byte(test.child)}, + "helper.yml": {Data: []byte(test.helper)}, + "underlay.yml": {Data: []byte("configkit: {imports: [{path: item.yml, destination: [items], placement: sequence-item}]}\n")}, + } + filesystem := configkit.NewFilesystem("items", files) + definition := configkit.Definition[map[string]any]{Version: &configkit.VersionRule{ + Path: configkit.Keys("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }, + }} + var result *configkit.Result[map[string]any] + var err error + switch { + case test.underlay: + result, err = configkit.LoadLayered(configkit.Input{Source: configkit.Mapping("main", map[string]any{"version": "1"})}, configkit.LayeredOptions[map[string]any]{ + Definition: definition, Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: filesystem.File("underlay.yml"), Imports: true}}}}, + }) + case test.mainItem: + result, err = configkit.Load(filesystem.File("item.yml"), configkit.Options[map[string]any]{ + Imports: true, Destination: configkit.Keys("items"), PlacementMode: configkit.PlacementModeSequenceItem, Definition: definition, + }) + default: + result, err = configkit.Load(filesystem.File("main.yml"), configkit.Options[map[string]any]{Imports: true, Definition: definition}) + } + if err != nil { + t.Fatal(err) + } + item := result.Config["items"].([]any)[0].(map[string]any) + fieldPath := configkit.Keys("items").Index(0).Key("version") + if test.child != "" { + item = item["children"].([]any)[0].(map[string]any) + fieldPath = configkit.Keys("items").Index(0).Key("children").Index(0).Key("version") + } + if item["version"] != "domain" { + t.Fatalf("domain version = %#v", item) + } + if info, ok := result.Metadata().Lookup(fieldPath); !ok || !info.Explicit { + t.Fatalf("version metadata = %#v, %v", info, ok) + } + }) + } +} diff --git a/internal/engine/imports.go b/internal/engine/imports.go index 512c88a..d4110e7 100644 --- a/internal/engine/imports.go +++ b/internal/engine/imports.go @@ -691,9 +691,8 @@ func (s *importTraversal) composePendingItems(scope *acquisitionScope) []Diagnos if len(diagnostics) != 0 { return projectItemDiagnosticPaths(diagnostics, *scope, contribution.destination, index) } - if diagnostic := importedVersionClaim(documents, *itemScope, completed, s.rule); diagnostic != nil { - return []Diagnostic{*diagnostic} - } + // Version ownership is checked by the enclosing composition once this + // item has its effective coordinates, not against its local field names. completed.acquisition = []acquisitionScope{cloneAcquisitionScope(*itemScope)} projectNestedEvidence(completed, *itemScope) _, placed, err := placeDocumentWithMode(completed.root, contribution.destination, placementModeSequenceItem, contribution.payload.Location, maxDocumentDepth, maxDocumentNodes)