From cd730d5c99f6a62ec779cbe28be7e6402c7c5a06 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 7 Sep 2026 03:06:14 +0000 Subject: [PATCH] Prepare ConfigKit v0.1.1 --- directives.go | 12 ++- docs/config.md | 16 +++- docs/policy/release.md | 13 ++- docs/releases/v0.1.1.md | 43 +++++++++ .../consumers/testdata/narratio/pipeline.yml | 2 +- explain_test.go | 2 +- glob_test.go | 10 +- imports_test.go | 91 ++++++++++++------- load_test.go | 4 +- override.go | 4 +- override_test.go | 6 +- profile_test.go | 8 +- provenance_test.go | 2 +- source_fifo_test.go | 2 +- 14 files changed, 147 insertions(+), 68 deletions(-) create mode 100644 docs/releases/v0.1.1.md diff --git a/directives.go b/directives.go index c27dd46..4e1c174 100644 --- a/directives.go +++ b/directives.go @@ -7,6 +7,8 @@ import ( "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) +const directiveKey = "configkit" + type importDeclaration struct { path string glob string @@ -19,11 +21,11 @@ func splitImportDirectives(root *document.Node) (*document.Node, []importDeclara var declarations []importDeclaration directiveIndex := -1 for index, entry := range body.Mapping { - if entry.Key != "_configkit" { + if entry.Key != directiveKey { continue } directiveIndex = index - parsed, diagnostics := parseImportDirective(entry.Value, Keys("_configkit")) + parsed, diagnostics := parseImportDirective(entry.Value, Keys(directiveKey)) if len(diagnostics) != 0 { return nil, nil, diagnostics } @@ -102,7 +104,7 @@ func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic if node.Kind == document.Mapping { for _, entry := range node.Mapping { childPath := current.Key(entry.Key) - if entry.Key == "_configkit" { + if entry.Key == directiveKey { location := publicLocation(entry.KeyLocation) return []Diagnostic{{Code: CodeImportInvalid, Severity: SeverityError, Path: childPath, Message: "configuration directives are permitted only at the document root", Primary: &location}} } @@ -183,7 +185,7 @@ func profileDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic if node.Kind == document.Mapping { for _, entry := range node.Mapping { childPath := current.Key(entry.Key) - if entry.Key == "_configkit" { + if entry.Key == directiveKey { location := publicLocation(entry.KeyLocation) return []Diagnostic{{ Code: CodeProfileDirective, Severity: SeverityError, Path: childPath, @@ -209,7 +211,7 @@ func disabledDirectiveDiagnostics(node *document.Node, path Path) []Diagnostic { if node.Kind == document.Mapping { for _, entry := range node.Mapping { childPath := path.Key(entry.Key) - if entry.Key == "_configkit" { + if entry.Key == directiveKey { location := Location{SourceID: entry.KeyLocation.SourceID, Line: entry.KeyLocation.Line, Column: entry.KeyLocation.Column} diagnostics = append(diagnostics, Diagnostic{ Code: CodeImportDisabled, Severity: SeverityError, Path: childPath, diff --git a/docs/config.md b/docs/config.md index 42419bb..4ed4bc4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -61,7 +61,7 @@ semantic validator, explicit path and glob imports, one selected profile, and filesystem context. It also accepts explicit CLI override records, static secret tags, and a dynamic path predicate. `NoSource` rejects imports, active profiles, and filesystem context as invalid combinations but accepts overrides. Input -containing a reserved `_configkit` mapping key is rejected while imports are +containing a reserved `configkit` mapping key is rejected while imports are disabled. Every source, options, schema, version, defaults, decode, resolution, or validation @@ -419,7 +419,7 @@ origin data; history retains the displaced definitions and identifies the parent replacement. Surviving profile paths have profile origins, while recursively shared mapping containers retain their contributing locations. -A profile may not contain `_configkit` anywhere, contribute the configured schema +A profile may not contain `configkit` anywhere, contribute the configured schema version, or replace an ancestor of that version with a scalar or null. These rules apply even if the supplied version text would be unchanged. Profiles cannot mask a base parse, composition, or version failure because those checks run first. @@ -452,7 +452,7 @@ All records are detached and validated before any is applied. Root and explicit sequence-index paths are invalid; parsed numeric tokens remain mapping keys. A path may create missing mapping ancestors but cannot traverse an existing scalar, null, or sequence. Duplicate and ancestor/descendant-overlapping records are rejected. -Neither paths nor values may create `_configkit`, and no record may overlap the +Neither paths nor values may create `configkit`, and no record may overlap the configured schema-version path. Unknown fields and effective type mismatches remain ordinary strict-decoder errors. @@ -475,12 +475,18 @@ zero. ## Explicit imports Imports are opt-in. With `Options.Imports` enabled, a document may reserve its root -`_configkit` mapping for an `imports` sequence. ConfigKit removes that mapping before +`configkit` mapping for an `imports` sequence. ConfigKit removes that mapping before application decoding. Each sequence item is a mapping with exactly one nonempty string `path` or `glob` plus an optional boolean `optional`; all other shapes and -fields are `import.invalid`. A `_configkit` key below the root is invalid. With +fields are `import.invalid`. A `configkit` key below the root is invalid. With imports disabled, any occurrence remains `import.disabled`. +The directive key is `configkit`, not the `_configkit` spelling used in v0.1.0. +There is no legacy alias: `_configkit` is ordinary application data, subject to +normal schema validation, and does not trigger imports. When upgrading, rename +directive keys in root files and imported fragments, and rename any application +fields previously named `configkit` to avoid the newly reserved key. + Path declarations use slash-separated relative paths. Absolute paths and network URLs are invalid. Each path resolves from its declaring file's canonical directory, and files are traversed depth-first in declaration order: a file contributes its own diff --git a/docs/policy/release.md b/docs/policy/release.md index e83bdb0..32cde90 100644 --- a/docs/policy/release.md +++ b/docs/policy/release.md @@ -2,7 +2,7 @@ **Base Policy Version:** 0.1.0 -**Last Updated:** 2026-09-06 +**Last Updated:** 2026-09-07 ## Purpose @@ -220,5 +220,12 @@ restating or redefining its requirements. ## Policy Exceptions -None. Candidate selection, publication, and post-publication verification remain -mandatory steps in the [release procedure](../release.md), not policy exceptions. +For v0.1.1 only, the maintainer approved renaming the reserved `_configkit` key to +`configkit` as an error-correcting point release because there are no current +consumers. This intentionally changes the v0.1.0 configuration contract without a +minor-version increment. The release note must explain the renamed directive, +the lack of a legacy alias, and the newly reserved application key. This exception +does not apply to subsequent releases or waive any validation or publication gate. + +Candidate selection, publication, and post-publication verification remain +mandatory steps in the [release procedure](../release.md). diff --git a/docs/releases/v0.1.1.md b/docs/releases/v0.1.1.md new file mode 100644 index 0000000..e1b20e2 --- /dev/null +++ b/docs/releases/v0.1.1.md @@ -0,0 +1,43 @@ +# ConfigKit v0.1.1 + +This release corrects the spelling of ConfigKit's reserved configuration key. + +## Summary + +The reserved directive key is now `configkit` instead of `_configkit`. Import +declarations continue to use the same `imports` sequence and path/glob syntax. +See the [configuration contract](../config.md#explicit-imports). + +## Compatibility + +This is an intentional configuration compatibility change, approved as a one-time +[patch-release exception](../policy/release.md#policy-exceptions) because there are +no current consumers. `_configkit` is no longer a directive or a reserved key; it +is ordinary application data and is subject to normal schema validation. There is +no legacy alias. `configkit` is now reserved, including in nested mappings, profiles, +and override paths and values. + +The Go API, diagnostic codes, Go minimum version, and supported platforms are +unchanged. The private YAML dependency remains pinned to +`go.yaml.in/yaml/v4 v4.0.0-rc.6`, confirmed as the newest available v4 release +during candidate preparation. + +## Upgrade + +Rename `_configkit` directive keys to `configkit` in root configuration files and +imported fragments. Rename any application-owned `configkit` fields to avoid the +newly reserved key. Update diagnostic-path expectations to the new spelling. +Files retaining `_configkit` will not load their declared imports. + +Consumers can select this release with: + +```sh +go get gitea.maximumdirect.net/eric/ConfigKit@v0.1.1 +``` + +## Changes + +- Renamed the directive key consistently across imports, profiles, and overrides. +- Updated current configuration documentation and the composed consumer fixture. +- Updated behavioral tests and added coverage proving the old spelling is not an + import alias. diff --git a/examples/consumers/testdata/narratio/pipeline.yml b/examples/consumers/testdata/narratio/pipeline.yml index 26f2c0b..a89d7b4 100644 --- a/examples/consumers/testdata/narratio/pipeline.yml +++ b/examples/consumers/testdata/narratio/pipeline.yml @@ -1,4 +1,4 @@ -_configkit: +configkit: imports: - path: artifacts.yml poll_interval: 1m diff --git a/explain_test.go b/explain_test.go index c3a395e..b000421 100644 --- a/explain_test.go +++ b/explain_test.go @@ -23,7 +23,7 @@ type explanationConcurrency struct { func TestExplainShowsDefaultBaseAndEffectiveProfileValue(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "config.yml", "_configkit:\n imports: [{path: transcription.yml}]\n") + mainPath := writeImportFile(t, directory, "config.yml", "configkit:\n imports: [{path: transcription.yml}]\n") writeImportFile(t, directory, "transcription.yml", "transcription:\n concurrency: 8\n") writeImportFile(t, directory, filepath.Join("profiles", "testing.yml"), "transcription:\n concurrency: 1\n") result, err := Load[explanationConcurrency](File(mainPath), Options[explanationConcurrency]{ diff --git a/glob_test.go b/glob_test.go index c13c4cc..28360e8 100644 --- a/glob_test.go +++ b/glob_test.go @@ -10,13 +10,13 @@ import ( func TestLoadGlobImportsUseLexicalDeclarationOrderAndNestedDFS(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", `_configkit: + mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - glob: fragments/*.yml - path: after.yml items: {root: root} `) - aPath := writeImportFile(t, directory, "fragments/a.yml", `_configkit: + aPath := writeImportFile(t, directory, "fragments/a.yml", `configkit: imports: [{path: nested/child.yml}] items: {alpha: alpha} `) @@ -56,7 +56,7 @@ items: {alpha: alpha} func TestLoadGlobImportsSupportEscapesClassesAndExplicitConfD(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", `_configkit: + mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - glob: 'conf.d/literal\*.yml' - glob: 'conf.d/item-[ab].yml' @@ -126,7 +126,7 @@ func TestLoadGlobImportsPreserveIdentityAndFileRules(t *testing.T) { t.Run("overlapping declarations", func(t *testing.T) { directory := t.TempDir() writeImportFile(t, directory, "conf/value.yml", "items: {value: value}\n") - mainPath := writeImportFile(t, directory, "main.yml", `_configkit: + mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - glob: conf/*.yml - glob: conf/value.* @@ -269,5 +269,5 @@ func writeGlobRoot(t *testing.T, directory, pattern string, optional bool) strin if optional { optionalLine = "\n optional: true" } - return writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - glob: '"+pattern+"'"+optionalLine+"\n") + return writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - glob: '"+pattern+"'"+optionalLine+"\n") } diff --git a/imports_test.go b/imports_test.go index 5327958..7340ceb 100644 --- a/imports_test.go +++ b/imports_test.go @@ -22,16 +22,37 @@ type importTestConfig struct { Items map[string]string `yaml:"items"` } +func TestLoadFormerDirectiveKeyIsApplicationData(t *testing.T) { + type config struct { + Legacy struct { + Imports []struct { + Path string `yaml:"path"` + } `yaml:"imports"` + } `yaml:"_configkit"` + } + for _, imports := range []bool{false, true} { + directory := t.TempDir() + mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: nonexistent.yml\n") + result, err := Load[config](File(mainPath), Options[config]{Imports: imports}) + if err != nil { + t.Fatalf("Imports=%v: %v", imports, err) + } + if len(result.Config.Legacy.Imports) != 1 || result.Config.Legacy.Imports[0].Path != "nonexistent.yml" || len(result.Sources()) != 1 { + t.Fatalf("Imports=%v: former directive was not retained as application data: %#v", imports, result) + } + } +} + func TestLoadTraversesPathImportsDepthFirstAndComposesOrigins(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", `_configkit: + mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: first.yml - path: second.yml settings: root: root `) - firstPath := writeImportFile(t, directory, "first.yml", `_configkit: + firstPath := writeImportFile(t, directory, "first.yml", `configkit: imports: - path: nested/third.yml settings: @@ -71,7 +92,7 @@ settings: func TestLoadRejectsImportConflictsAndFragmentVersions(t *testing.T) { t.Run("all duplicate claimants", func(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", `_configkit: + mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: one.yml - path: two.yml @@ -97,7 +118,7 @@ func TestLoadRejectsImportConflictsAndFragmentVersions(t *testing.T) { } { t.Run(test.name, func(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: fragment.yml\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: fragment.yml\n") writeImportFile(t, directory, "fragment.yml", test.fragment) loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true, Definition: Definition[importTestConfig]{Version: versionRule}}, CodeImportVersion) diagnostic := loadError.Diagnostics()[0] @@ -114,15 +135,15 @@ func TestLoadImportDirectiveValidationAndTemporaryGlobGate(t *testing.T) { body string code Code }{ - {name: "directive kind", body: "_configkit: []\n", code: CodeImportInvalid}, - {name: "unknown directive field", body: "_configkit: {unknown: true}\n", code: CodeImportInvalid}, - {name: "imports kind", body: "_configkit: {imports: {}}\n", code: CodeImportInvalid}, - {name: "declaration kind", body: "_configkit: {imports: [file.yml]}\n", code: CodeImportInvalid}, - {name: "missing target", body: "_configkit: {imports: [{optional: true}]}\n", code: CodeImportInvalid}, - {name: "two targets", body: "_configkit: {imports: [{path: a.yml, glob: '*.yml'}]}\n", code: CodeImportInvalid}, - {name: "unknown declaration field", body: "_configkit: {imports: [{path: a.yml, extra: true}]}\n", code: CodeImportInvalid}, - {name: "optional kind", body: "_configkit: {imports: [{path: a.yml, optional: 'yes'}]}\n", code: CodeImportInvalid}, - {name: "nested directive", body: "nested: {_configkit: {}}\n", code: CodeImportInvalid}, + {name: "directive kind", body: "configkit: []\n", code: CodeImportInvalid}, + {name: "unknown directive field", body: "configkit: {unknown: true}\n", code: CodeImportInvalid}, + {name: "imports kind", body: "configkit: {imports: {}}\n", code: CodeImportInvalid}, + {name: "declaration kind", body: "configkit: {imports: [file.yml]}\n", code: CodeImportInvalid}, + {name: "missing target", body: "configkit: {imports: [{optional: true}]}\n", code: CodeImportInvalid}, + {name: "two targets", body: "configkit: {imports: [{path: a.yml, glob: '*.yml'}]}\n", code: CodeImportInvalid}, + {name: "unknown declaration field", body: "configkit: {imports: [{path: a.yml, extra: true}]}\n", code: CodeImportInvalid}, + {name: "optional kind", body: "configkit: {imports: [{path: a.yml, optional: 'yes'}]}\n", code: CodeImportInvalid}, + {name: "nested directive", body: "nested: {configkit: {}}\n", code: CodeImportInvalid}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { @@ -136,7 +157,7 @@ func TestLoadImportDirectiveValidationAndTemporaryGlobGate(t *testing.T) { func TestLoadImportMissingContainmentAndRegularFileRules(t *testing.T) { t.Run("optional missing", func(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", `_configkit: + mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: - path: missing.yml optional: true @@ -172,7 +193,7 @@ settings: {root: retained} if test.setup != nil { test.setup(t, directory) } - mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: \""+test.path+"\"\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports:\n - path: \""+test.path+"\"\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, test.code) }) } @@ -181,9 +202,9 @@ settings: {root: retained} func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) { t.Run("cycle", func(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports: [{path: a.yml}]\n") - writeImportFile(t, directory, "a.yml", "_configkit:\n imports: [{path: b.yml}]\n") - writeImportFile(t, directory, "b.yml", "_configkit:\n imports: [{path: main.yml}]\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: a.yml}]\n") + writeImportFile(t, directory, "a.yml", "configkit:\n imports: [{path: b.yml}]\n") + writeImportFile(t, directory, "b.yml", "configkit:\n imports: [{path: main.yml}]\n") loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportCycle) diagnostic := loadError.Diagnostics()[0] if diagnostic.Primary == nil || len(diagnostic.Related) != 2 || !strings.Contains(diagnostic.Message, "main.yml") || !strings.Contains(diagnostic.Message, "a.yml") || !strings.Contains(diagnostic.Message, "b.yml") { @@ -193,9 +214,9 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) { t.Run("diamond", func(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n") - writeImportFile(t, directory, "left.yml", "_configkit:\n imports: [{path: shared.yml}]\n") - writeImportFile(t, directory, "right.yml", "_configkit:\n imports: [{path: shared.yml}]\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: left.yml}, {path: right.yml}]\n") + writeImportFile(t, directory, "left.yml", "configkit:\n imports: [{path: shared.yml}]\n") + writeImportFile(t, directory, "right.yml", "configkit:\n imports: [{path: shared.yml}]\n") writeImportFile(t, directory, "shared.yml", "settings: {root: shared}\n") loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated) diagnostic := loadError.Diagnostics()[0] @@ -216,7 +237,7 @@ func TestLoadImportIdentityReportsCyclesAndRepeatedRoutes(t *testing.T) { if err := os.Symlink("target.yml", filepath.Join(directory, "second-link.yml")); err != nil { t.Skipf("symlinks unavailable: %v", err) } - mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: first-link.yml}, {path: second-link.yml}]}\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: first-link.yml}, {path: second-link.yml}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportRepeated) }) } @@ -231,7 +252,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) { if err := os.Symlink(target, filepath.Join(directory, "link.yml")); err != nil { t.Skipf("symlinks unavailable: %v", err) } - mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: link.yml}]}\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: link.yml}]}\n") result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true}) if err != nil || result.Config.Settings.First != "linked" || result.Sources()[1].Path != canonicalTestPath(t, target) { t.Fatalf("symlink result = %#v, %v", result, err) @@ -245,7 +266,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) { if err := os.Symlink(target, filepath.Join(root, "escape.yml")); err != nil { t.Skipf("symlinks unavailable: %v", err) } - mainPath := writeImportFile(t, root, "main.yml", "_configkit: {imports: [{path: escape.yml}]}\n") + mainPath := writeImportFile(t, root, "main.yml", "configkit: {imports: [{path: escape.yml}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot) }) @@ -255,7 +276,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) { if err := os.Link(one, filepath.Join(directory, "two.yml")); err != nil { t.Skipf("hard links unavailable: %v", err) } - mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeComposeConflict) }) @@ -264,7 +285,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) { if err := os.Symlink("loop.yml", filepath.Join(directory, "loop.yml")); err != nil { t.Skipf("symlinks unavailable: %v", err) } - mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: loop.yml}]}\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: loop.yml}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportInvalid) }) } @@ -272,7 +293,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) { func TestLoadByteAndReaderImportsRequireExplicitFilesystemContext(t *testing.T) { directory := t.TempDir() writeImportFile(t, directory, "fragment.yml", "settings: {first: imported}\n") - input := []byte("_configkit: {imports: [{path: fragment.yml}]}\n") + input := []byte("configkit: {imports: [{path: fragment.yml}]}\n") context := &FilesystemContext{BaseDir: directory, Root: directory} for _, test := range []struct { name string @@ -295,7 +316,7 @@ func TestLoadByteAndReaderImportsRequireExplicitFilesystemContext(t *testing.T) func TestFileImportRootCanContainTheSelectedFileDirectory(t *testing.T) { root := t.TempDir() - mainPath := writeImportFile(t, root, "configs/main.yml", "_configkit: {imports: [{path: ../shared.yml}]}\n") + mainPath := writeImportFile(t, root, "configs/main.yml", "configkit: {imports: [{path: ../shared.yml}]}\n") writeImportFile(t, root, "shared.yml", "settings: {root: shared}\n") result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{ Imports: true, Filesystem: &FilesystemContext{Root: root}, @@ -309,7 +330,7 @@ func TestFileImportRootCanContainTheSelectedFileDirectory(t *testing.T) { func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) { t.Run("parse", func(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: broken.yml}]}\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: broken.yml}]}\n") writeImportFile(t, directory, "broken.yml", "settings: [\n") loadError := requireAnyLoadError(t, File(mainPath), Options[importTestConfig]{Imports: true}) diagnostics := loadError.Diagnostics() @@ -320,7 +341,7 @@ func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) { t.Run("decode", func(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: unknown.yml}]}\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: unknown.yml}]}\n") writeImportFile(t, directory, "unknown.yml", "unknown: value\n") loadError := requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeDecodeUnknownField) diagnostic := loadError.Diagnostics()[0] @@ -332,7 +353,7 @@ func TestImportedParseAndDecodeDiagnosticsKeepTheirSource(t *testing.T) { func TestOptionalImportDoesNotSuppressUnsafeOrUnreadableTargets(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "outside.yml", "_configkit: {imports: [{path: ../missing.yml, optional: true}]}\n") + mainPath := writeImportFile(t, directory, "outside.yml", "configkit: {imports: [{path: ../missing.yml, optional: true}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeImportOutsideRoot) locked := writeImportFile(t, directory, "locked.yml", "settings: {root: locked}\n") @@ -344,13 +365,13 @@ func TestOptionalImportDoesNotSuppressUnsafeOrUnreadableTargets(t *testing.T) { _ = file.Close() t.Skip("filesystem does not enforce the unreadable-file fixture") } - mainPath = writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: locked.yml, optional: true}]}\n") + mainPath = writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: locked.yml, optional: true}]}\n") requireLoadCode(t, File(mainPath), Options[importTestConfig]{Imports: true}, CodeSourcePermission) } func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: one.yml}, {path: two.yml}]}\n") writeImportFile(t, directory, "one.yml", "settings: {first: one}\n") writeImportFile(t, directory, "two.yml", "settings: {second: two}\n") main := parseImportMain(t, mainPath) @@ -370,7 +391,7 @@ func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) { }) } - writeImportFile(t, directory, "one.yml", "_configkit: {imports: [{path: nested.yml}]}\n") + writeImportFile(t, directory, "one.yml", "configkit: {imports: [{path: nested.yml}]}\n") writeImportFile(t, directory, "nested.yml", "settings: {nested: nested}\n") main = parseImportMain(t, mainPath) _, _, diagnostics := loadImportedDocuments(File(mainPath), nil, main, nil, importLimits{depth: 1, sources: 8, bytes: maxTotalBytes, nodes: maxDocumentNodes}) @@ -381,7 +402,7 @@ func TestImportTraversalEnforcesPrivateBudgets(t *testing.T) { func TestImportTraversalAcceptsExactPrivateBudgets(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: imported.yml}]}\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: imported.yml}]}\n") importedPath := writeImportFile(t, directory, "imported.yml", "settings: {first: imported}\n") main := parseImportMain(t, mainPath) imported := parseImportMain(t, importedPath) diff --git a/load_test.go b/load_test.go index 33b3789..06a7cb6 100644 --- a/load_test.go +++ b/load_test.go @@ -240,7 +240,7 @@ func TestLoadRejectsUnavailableOptionsAndDisabledDirectives(t *testing.T) { }) } - result, err := Load[config](Bytes("directive.yml", []byte("nested:\n _configkit: {}\n"))) + result, err := Load[config](Bytes("directive.yml", []byte("nested:\n configkit: {}\n"))) if result != nil || err == nil { t.Fatalf("result, err = %#v, %v", result, err) } @@ -249,7 +249,7 @@ func TestLoadRejectsUnavailableOptionsAndDisabledDirectives(t *testing.T) { t.Fatal(err) } diagnostic := loadError.Diagnostics()[0] - if diagnostic.Code != CodeImportDisabled || diagnostic.Path.String() != "/nested/_configkit" || diagnostic.Primary == nil || diagnostic.Primary.Line != 2 { + if diagnostic.Code != CodeImportDisabled || diagnostic.Path.String() != "/nested/configkit" || diagnostic.Primary == nil || diagnostic.Primary.Line != 2 { t.Fatalf("diagnostic = %#v", diagnostic) } } diff --git a/override.go b/override.go index fe37fb4..81a4b8a 100644 --- a/override.go +++ b/override.go @@ -188,7 +188,7 @@ func overlappingOverride(previous []Override, valid []bool, path Path) int { func pathContainsDirective(path Path) bool { for _, segment := range path.segments { - if segment.key == "_configkit" { + if segment.key == directiveKey { return true } } @@ -205,7 +205,7 @@ func overrideDirectivePath(node *document.Node, path Path) (Path, bool) { if node.Kind == document.Mapping { for _, entry := range node.Mapping { child := path.Key(entry.Key) - if entry.Key == "_configkit" { + if entry.Key == directiveKey { return child, true } if found, ok := overrideDirectivePath(entry.Value, child); ok { diff --git a/override_test.go b/override_test.go index 2b4023b..69bf6f0 100644 --- a/override_test.go +++ b/override_test.go @@ -30,7 +30,7 @@ type overrideLifecycleConfig struct { func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports: [{path: base.yml}]\nversion: v1\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit:\n imports: [{path: base.yml}]\nversion: v1\n") writeImportFile(t, directory, "base.yml", `settings: target: base count: 4 @@ -187,8 +187,8 @@ func TestOverrideValidationRejectsPathsOverlapValuesAndProtectedTargets(t *testi {name: "nonfinite", overrides: []Override{{Path: Keys("scalar"), Value: math.Inf(1), Flag: "--scalar"}}, code: CodeOverrideInvalidValue}, {name: "depth", overrides: []Override{{Path: Keys("nested"), Value: deep, Flag: "--nested"}}, code: CodeOverrideLimitExceeded}, {name: "record bytes", overrides: []Override{{Path: Keys("scalar"), Value: strings.Repeat("x", maxSourceBytes+1), Flag: "--scalar"}}, code: CodeOverrideLimitExceeded}, - {name: "directive path", overrides: []Override{{Path: Keys("_configkit", "x"), Value: true, Flag: "--directive"}}, code: CodeOverrideDirective}, - {name: "directive value", overrides: []Override{{Path: Keys("nested"), Value: map[string]any{"_configkit": map[string]any{}}, Flag: "--nested"}}, code: CodeOverrideDirective}, + {name: "directive path", overrides: []Override{{Path: Keys("configkit", "x"), Value: true, Flag: "--directive"}}, code: CodeOverrideDirective}, + {name: "directive value", overrides: []Override{{Path: Keys("nested"), Value: map[string]any{"configkit": map[string]any{}}, Flag: "--nested"}}, code: CodeOverrideDirective}, {name: "version", overrides: []Override{{Path: Keys("version"), Value: "v2", Flag: "--version"}}, code: CodeOverrideVersion}, {name: "version ancestor", overrides: []Override{{Path: Keys("version"), Value: map[string]any{"child": "v2"}, Flag: "--version"}}, code: CodeOverrideVersion}, {name: "label", overrides: []Override{{Path: Keys("scalar"), Value: "x", Flag: "\nCANARY"}}, code: CodeOverrideInvalidLabel}, diff --git a/profile_test.go b/profile_test.go index ccde257..35f2545 100644 --- a/profile_test.go +++ b/profile_test.go @@ -23,7 +23,7 @@ type profileTestConfig struct { func TestLoadAppliesSelectedProfileAfterBaseAndBeforeDefaults(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", `_configkit: + mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: [{path: base.yml}] settings: kept: main @@ -133,8 +133,8 @@ func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) { profile string code Code }{ - {name: "root directive", profile: "_configkit: {}\n", code: CodeProfileDirective}, - {name: "nested directive", profile: "settings: {_configkit: {}}\n", code: CodeProfileDirective}, + {name: "root directive", profile: "configkit: {}\n", code: CodeProfileDirective}, + {name: "nested directive", profile: "settings: {configkit: {}}\n", code: CodeProfileDirective}, {name: "equal version", profile: "schema: {version: v1}\n", code: CodeProfileVersion}, {name: "ancestor scalar", profile: "schema: null\n", code: CodeProfileVersion}, } { @@ -158,7 +158,7 @@ func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) { t.Run("profile aliases imported base", func(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", "_configkit: {imports: [{path: profiles/testing.yml}]}\n") + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: profiles/testing.yml}]}\n") writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: value}\n") requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Imports: true, Profile: "testing"}, CodeProfileRepeated) }) diff --git a/provenance_test.go b/provenance_test.go index 386c09f..76f5127 100644 --- a/provenance_test.go +++ b/provenance_test.go @@ -27,7 +27,7 @@ type layeredConfig struct { func TestLayeredProvenanceAgreesWithResolvedSnapshot(t *testing.T) { directory := t.TempDir() - mainPath := writeImportFile(t, directory, "main.yml", `_configkit: + mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: [{path: imported.yml}] shared: {equal: same} values: [1, 2] diff --git a/source_fifo_test.go b/source_fifo_test.go index ca6464d..4699f7f 100644 --- a/source_fifo_test.go +++ b/source_fifo_test.go @@ -43,7 +43,7 @@ func TestNonregularSourcesDoNotBlock(t *testing.T) { root := t.TempDir() input := "value: 1\n" if mode == "import" { - input = "_configkit:\n imports:\n - path: pipe.yml\n" + input = "configkit:\n imports:\n - path: pipe.yml\n" } if err := os.WriteFile(filepath.Join(root, "config.yml"), []byte(input), 0600); err != nil { t.Fatal(err)