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
the canonical directory of the file that supplies its surviving declaration.
This includes imported fragments and selected configuration overlays. 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.
This includes imported fragments, selected configuration overlays, and values
copied through inheritance: the copied value keeps its original declaring
file. A child-supplied replacement resolves from the child's declaring file.
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
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
ConfigKit acquires the selected root, imports, optional selected overlay, and
explicit CLI overrides in precedence order. Its document definition supplies
typed defaults, source-aware path normalization, and structural validation
before conversion to the independently owned runtime **Config**. Only
ConfigKit composes the selected root, imports, optional selected overlay, and
explicit CLI overrides in precedence order, then expands native sibling
inheritance before typed decoding. The document definition supplies typed
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
variables do not participate in loading. Version 5 decoding rejects scalar
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
> Completion: complete.
Goal: keep inherited resource paths anchored to the original declaration.
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)
}
}
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)
}
}