Preserve inherited reference path origins

This commit is contained in:
2026-10-03 17:58:48 +00:00
parent 01c08ec8e4
commit f4779fede9
4 changed files with 117 additions and 8 deletions

View File

@@ -80,10 +80,12 @@ umask, setgid inheritance, and default ACLs; see [Operations](operations.md#cach
Every nonempty relative configured destination or reference path resolves from Every nonempty relative configured destination or reference path resolves from
the canonical directory of the file that supplies its surviving declaration. the canonical directory of the file that supplies its surviving declaration.
This includes imported fragments and selected configuration overlays. Built-in This includes imported fragments, selected configuration overlays, and values
relative destinations use the selected main configuration file's directory; copied through inheritance: the copied value keeps its original declaring
explicit `--output-dir` and `--debug-dir` values use the invocation working file. A child-supplied replacement resolves from the child's declaring file.
directory. Absolute values remain unchanged. Built-in relative destinations use the selected main configuration file's
directory; explicit `--output-dir` and `--debug-dir` values use the invocation
working directory. Absolute values remain unchanged.
The root file can use ConfigKit imports to compose disjoint definitions. Imports The root file can use ConfigKit imports to compose disjoint definitions. Imports
and selected overlays remain under the configuration root; they do not merge or and selected overlays remain under the configuration root; they do not merge or

View File

@@ -28,10 +28,13 @@ at their respective [CLI](cli.md), [pipeline](pipeline.md), and
## Loading And Validation ## Loading And Validation
ConfigKit acquires the selected root, imports, optional selected overlay, and ConfigKit composes the selected root, imports, optional selected overlay, and
explicit CLI overrides in precedence order. Its document definition supplies explicit CLI overrides in precedence order, then expands native sibling
typed defaults, source-aware path normalization, and structural validation inheritance before typed decoding. The document definition supplies typed
before conversion to the independently owned runtime **Config**. Only defaults and structural validation; Notarius's recorded resolution changes
normalize configured paths after decode. Path normalization follows the source
of the surviving value, including a value copied through inheritance, rather
than the inheritance directive's location. Only
`NOTARIUS_CONFIG` participates in discovery; retired field-level environment `NOTARIUS_CONFIG` participates in discovery; retired field-level environment
variables do not participate in loading. Version 5 decoding rejects scalar variables do not participate in loading. Version 5 decoding rejects scalar
module bindings and scalar external references. ConfigKit retains redacted, module bindings and scalar external references. ConfigKit retains redacted,

View File

@@ -227,6 +227,8 @@ not change defaults or add CLI flags for arbitrary configuration overrides.
## Stage 4 — Verify Declaration-Relative Paths Through Inheritance ## Stage 4 — Verify Declaration-Relative Paths Through Inheritance
> Completion: complete.
Goal: keep inherited resource paths anchored to the original declaration. Goal: keep inherited resource paths anchored to the original declaration.
Inspect `document_paths.go`, `document_paths_test.go`, the original-path map in Inspect `document_paths.go`, `document_paths_test.go`, the original-path map in

View File

@@ -331,3 +331,105 @@ pipelines:
t.Fatalf("override-inherited repair budget = %d, want 3", got) t.Fatalf("override-inherited repair budget = %d, want 3", got)
} }
} }
func TestInheritedReferencePathsKeepDeclaringSource(t *testing.T) {
t.Parallel()
root := t.TempDir()
parentDirectory := filepath.Join(root, "parents")
childDirectory := filepath.Join(root, "children")
overlayDirectory := filepath.Join(root, "overlays")
fixtureDirectory := filepath.Join(root, "fixtures")
for _, directory := range []string{parentDirectory, childDirectory, overlayDirectory, fixtureDirectory} {
if err := os.MkdirAll(directory, 0o700); err != nil {
t.Fatal(err)
}
}
for _, name := range []string{"shared.txt", "parent.txt", "child.txt", "step.txt", "overlay.txt"} {
if err := os.WriteFile(filepath.Join(fixtureDirectory, name), []byte("fixture\n"), 0o600); err != nil {
t.Fatal(err)
}
}
parent := filepath.Join(parentDirectory, "pipelines.yml")
if err := os.WriteFile(parent, []byte(`pipelines:
" parent ":
input: {module: input}
references:
" shared ": {path: ../fixtures/shared.txt}
nested:
extends: " shared "
source: {path: ../fixtures/parent.txt}
steps:
- id: first
references:
step-source: {path: ../fixtures/step.txt}
artifacts:
summary:
extract: {module: extract}
`), 0o600); err != nil {
t.Fatal(err)
}
child := filepath.Join(childDirectory, "pipelines.yml")
if err := os.WriteFile(child, []byte(`pipelines:
child:
extends: " parent "
references:
source: {path: ../fixtures/child.txt}
`), 0o600); err != nil {
t.Fatal(err)
}
main := filepath.Join(root, "notarius.yml")
if err := os.WriteFile(main, []byte(`version: 5
configkit:
imports:
- path: parents/pipelines.yml
- path: children/pipelines.yml
`), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(overlayDirectory, "development.yml"), []byte(`pipelines:
" parent ":
references:
" shared ": {path: ../fixtures/overlay.txt}
`), 0o600); err != nil {
t.Fatal(err)
}
loaded, err := loadConfig(main, LoadOptions{})
if err != nil {
t.Fatalf("loadConfig() error = %v", err)
}
assertReferencePath := func(pipelineID, referenceID, want string) {
t.Helper()
if got := loaded.Config.Pipelines[pipelineID].References[referenceID].Path; got != want {
t.Errorf("%s reference %s path = %q, want %q", pipelineID, referenceID, got, want)
}
}
assertReferencePath("parent", "shared", filepath.Join(fixtureDirectory, "shared.txt"))
assertReferencePath("child", "shared", filepath.Join(fixtureDirectory, "shared.txt"))
assertReferencePath("child", "nested", filepath.Join(fixtureDirectory, "shared.txt"))
assertReferencePath("parent", "source", filepath.Join(fixtureDirectory, "parent.txt"))
assertReferencePath("child", "source", filepath.Join(fixtureDirectory, "child.txt"))
if got := loaded.Config.Pipelines["child"].Steps[0].References["step-source"].Path; got != filepath.Join(fixtureDirectory, "step.txt") {
t.Errorf("inherited step reference path = %q", got)
}
overlaid, err := loadConfig(main, LoadOptions{Overlay: "development"})
if err != nil {
t.Fatalf("loadConfig() with overlay error = %v", err)
}
for _, pipelineID := range []string{"parent", "child"} {
for _, referenceID := range []string{"shared", "nested"} {
if got, want := overlaid.Config.Pipelines[pipelineID].References[referenceID].Path, filepath.Join(fixtureDirectory, "overlay.txt"); got != want {
t.Errorf("overlaid %s reference %s path = %q, want %q", pipelineID, referenceID, got, want)
}
}
}
if got, want := overlaid.Config.Pipelines["child"].References["source"].Path, filepath.Join(fixtureDirectory, "child.txt"); got != want {
t.Errorf("child replacement path = %q, want %q", got, want)
}
if got, want := overlaid.Config.Pipelines["child"].Steps[0].References["step-source"].Path, filepath.Join(fixtureDirectory, "step.txt"); got != want {
t.Errorf("overlaid inherited step reference path = %q, want %q", got, want)
}
}