diff --git a/README.md b/README.md index 4534195..f468bdb 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ Linux is release-validated; macOS support is best-effort. See the ConfigKit's initial loader accepts one strict YAML source (or explicit defaults-only input), optionally traverses explicit path and glob imports, strictly composes their disjoint definitions, applies typed defaults, and -can overlay one selected fixed profile and check an application schema version. It +can overlay one selected fixed overlay and check an application schema version. It returns detached metadata and safe diagnostics. Applications may perform audited defaulting and normalization through a resolver, then report domain errors and warnings through read-only semantic validation. Secret tags and a path predicate drive deterministic redacted effective YAML. Typed secret references resolve environment names before validation, and explicit secrets-directory bootstrap is available as a separate startup operation. Explicit -CLI override records apply above profiles. Value explanation is available through +CLI override records apply above overlays. Value explanation is available through detached structured records and deterministic redacted output. ```go diff --git a/doc.go b/doc.go index 3044bf6..d150bee 100644 --- a/doc.go +++ b/doc.go @@ -2,7 +2,7 @@ // loading for Go programs. // // Load supports file, byte, reader, and explicit no-input sources with strict YAML, -// typed defaults, imports, profiles, CLI overrides, schema-version checks, detached +// typed defaults, imports, overlays, CLI overrides, schema-version checks, detached // metadata, recorded application resolution, typed secret references, safe effective // rendering, read-only semantic validation, and structured load errors. The // configuration contract is documented in docs/config.md. diff --git a/docs/config.md b/docs/config.md index 7a2d6ae..fa85f2f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -20,7 +20,7 @@ The zero `Source` is invalid. Constructor argument failures are retained inside opaque source and become safe structured diagnostics when the source is consumed. File sources can observe file changes between consumptions; byte sources cannot. -Each source is limited to 8 MiB. A load using imports or an active profile is limited +Each source is limited to 8 MiB. A load using imports or an active overlay is limited to 64 MiB across all accepted sources, 1,024 accepted sources, 64 active import edges, and 1,000,000 input nodes in aggregate. Glob expansion separately permits 1,000,000 visited directory entries, including nonmatches. Reads request at most the @@ -33,7 +33,7 @@ truncated. Data returned together with a read error is not accepted as partial i root `T` must satisfy the application schema contract below. With no options it starts from the Go zero value. A `Definition[T].Defaults` factory, when supplied, runs exactly once after successful source parsing, schema construction, optional -schema-version checking, and selected-profile loading. Its result is detached before +schema-version checking, and selected-overlay loading. Its result is detached before input is applied. Resolver-provided mutable values are detached before secret lookup and validation, and a final detached copy prevents equal-valued aliases introduced by validators from escaping into the returned configuration. The publication copy @@ -43,7 +43,7 @@ The current lifecycle is: ```text validate options → parse the selected source → traverse/compose enabled imports - → build schema → check the optional application version → apply a selected profile + → build schema → check the optional application version → apply a selected overlay → apply explicit CLI overrides → create and clone defaults → strictly decode → recorded application resolution → resolve typed secret references → semantic validation → classify secrets @@ -57,9 +57,9 @@ contributes no source, and continues through defaults, decode, and projection. A missing file remains an error; it is never interpreted as no input. The current `Options` boundary accepts a defaults factory, `VersionRule`, resolver, -semantic validator, explicit path and glob imports, one selected profile, and +semantic validator, explicit path and glob imports, one selected overlay, and filesystem context. It also accepts explicit CLI override records, static secret -tags, and a dynamic path predicate. `NoSource` rejects imports, active profiles, +tags, and a dynamic path predicate. `NoSource` rejects imports, active overlays, and filesystem context as invalid combinations but accepts overrides. Input containing a reserved `configkit` mapping key is rejected while imports are disabled. @@ -93,7 +93,7 @@ This is a load-local exception: siblings and unrelated fields, dynamic values, integer and float destinations, durations, `Secret`, and text codecs retain ordinary strict decoding. The retained source and history stay numeric while typed projection and effective YAML contain the resulting string; explanation records this through -the normal decoding reconciliation event. Fragment, profile, and CLI ownership +the normal decoding reconciliation event. Fragment, overlay, and CLI ownership protections apply before this conversion. The checker reports safe, value-free errors and warnings through `Validation.Error` @@ -109,11 +109,11 @@ successful result. Callback panics remain programming failures. key/index distinctions using actual effective and historical container shapes; only parsed numeric tokens are resolved contextually. A descendant that never existed is not historical merely because an atomic ancestor was replaced. Exact surviving input paths -are explicit and have a base or profile origin; omitted paths have a default origin. +are explicit and have a base or overlay origin; omitted paths have a default origin. An explicit parent mapping does not make omitted children explicit. Null state and available source coordinates are retained. `History` records the Go-zero or application -default baseline followed by exact base or profile definitions, including -equal-valued input. Profile replacement removes displaced descendants from effective +default baseline followed by exact base or overlay definitions, including +equal-valued input. Overlay replacement removes displaced descendants from effective metadata while retaining their base definition and the parent replacement operation in history. All metadata queries and returned nested records are detached. @@ -125,21 +125,21 @@ documentation in [metadata.go](../metadata.go) defines enumeration and path-orde details. History is chronological: a path can have a synthetic default record, its exact base -definition, a profile replacement, and a recorded resolution operation. A descendant +definition, an overlay replacement, and a recorded resolution operation. A descendant query uses the ancestor's path for a whole-container or whole-sequence replacement; the descendant can therefore retain history after `Lookup` stops reporting it as effective. Equal-valued input is still an explicit base definition, while a derived value for absent input remains non-explicit. A mapping assembled from the selected file and imports has one composite base origin -whose locations retain source traversal order. A mapping also visited by a profile -has a profile-layer composite origin with the lower contributing locations followed -by the profile location; an empty mapping is still an explicit container even though +whose locations retain source traversal order. A mapping also visited by an overlay +has an overlay-layer composite origin with the lower contributing locations followed +by the overlay location; an empty mapping is still an explicit container even though it contributes no children. Resolution origins are synthetic: they have layer `resolution`, no fabricated file location, and a `derived_default` or `normalization` operation. Reasons and dependency paths retain first occurrence order with duplicates removed. Source records remain ordered as selected source, depth-first imports, the -selected profile, then synthetic CLI records in supplied order. +selected overlay, then synthetic CLI records in supplied order. ### Secret classification and effective rendering @@ -189,7 +189,7 @@ historical path. Its effective value and each chronological before/after value a parser values. `Present=false` with empty YAML means absence or deletion; explicit null is present and renders as `null`. The history starts with the canonical typed application-default or Go-zero baseline, then includes only relevant base sources in -traversal order, the selected profile, supplied CLI records, and audited net +traversal order, the selected overlay, supplied CLI records, and audited net resolution operations. Overlapping parent/child resolution records are rendered as one net subtree event with combined reasons and dependencies, rather than pretending that the recorded assignments ran in lexical order. Explicit equal @@ -199,7 +199,7 @@ before domain resolution. Its origin retains the decoded path's source/default attribution; its reason identifies static-default retention and canonical decoding. This built-in inspection operation is not accepted by `Resolution.Record`. -Base, profile, and CLI history preserves the policy-validated input representation, +Base, overlay, and CLI history preserves the policy-validated input representation, including a superseded value that could not have decoded into the final Go field. Baseline, effective, and resolution values use canonical typed projection. A decoding event bridges replayed input and canonical typed values, so intermediate @@ -338,7 +338,7 @@ found entry path. Resolution changes, additions, removals, and moves do not alte this entry view. `Resolution.Source(id)` returns a detached record for an already acquired source in -the same load: the main source, imports, selected profile, and supplied CLI records +the same load: the main source, imports, selected overlay, and supplied CLI records where applicable. Empty and unknown IDs return false. It never reloads input, reads the filesystem, resolves symlinks, or looks up credentials. A source path exists only when the acquired source has one; defaults and byte, reader, and CLI sources do @@ -436,41 +436,41 @@ traversal order. Traversal order never authorizes replacement. Composition clone the resulting tree and its ordered claim records rather than mutating or aliasing an input document. -## Selected profiles +## Selected overlays -`Options.Profile` selects at most one overlay by name. Names must match +`Options.Overlay` selects at most one overlay by name. Names must match `[a-zA-Z0-9_-]+`; they are not paths. The empty name and exact lowercase name -`default` select no profile, and `profiles/default.yml` is never loaded implicitly. -Every other name resolves only to `/profiles/.yml`, with -no extension fallback, case normalization, inheritance, or profile stack. A selected -missing profile is an error. +`default` select no overlay, and `overlays/default.yml` is never loaded implicitly. +Every other name resolves only to `/overlays/.yml`, with +no extension fallback, case normalization, inheritance, or overlay stack. A selected +missing overlay is an error. -The profile is a separately policy-validated YAML mapping. It is loaded after the +The overlay is a separately policy-validated YAML mapping. It is loaded after the base import graph is composed and its schema version is checked, but before defaults -and typed decoding. Profile bytes, nodes, and source records count toward the same +and typed decoding. Overlay bytes, nodes, and source records count toward the same aggregate limits as base inputs. Its canonical path must remain within the configured root and must not duplicate a file already present in the base graph. Overlay is intentionally distinct from strict composition: -| Existing lower node | Incoming profile node | Result | +| Existing lower node | Incoming overlay node | Result | | --- | --- | --- | -| Absent | Value | Retain the profile value | +| Absent | Value | Retain the overlay value | | Value | Absent | Retain the lower value | | Mapping | Mapping | Recurse by mapping key | | Any other pair of present nodes | Any other pair of present nodes | Replace the entire lower node | -Scalars, sequences, and null are atomic. Thus a profile sequence replaces rather +Scalars, sequences, and null are atomic. Thus an overlay sequence replaces rather than extends a base sequence, `[]` clears it, null is an explicit replacement, and cross-kind replacement is allowed when the final application schema accepts it. Replacing a subtree removes all displaced descendants from effective presence and origin data; history retains the displaced definitions and identifies the parent -replacement. Surviving profile paths have profile origins, while recursively shared +replacement. Surviving overlay paths have overlay origins, while recursively shared mapping containers retain their contributing locations. -A profile may not contain `configkit` anywhere, contribute the configured schema +An overlay 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 +apply even if the supplied version text would be unchanged. Overlays cannot mask a base parse, composition, or version failure because those checks run first. ## Explicit CLI overrides @@ -483,8 +483,8 @@ must never contain the raw argument or configured value. General environment variable overlays are an explicit non-goal: environment access is reserved for typed secret references. -Overrides apply after the selected profile and before typed defaults/decoding, so -the input precedence is application defaults < composed base < selected profile < +Overrides apply after the selected overlay and before typed defaults/decoding, so +the input precedence is application defaults < composed base < selected overlay < CLI. Recorded resolution, typed secret lookup, and semantic validation follow the effective override tree. Overrides also work with `NoSource`. @@ -506,7 +506,7 @@ configured schema-version path. Unknown fields and effective type mismatches rem ordinary strict-decoder errors. At most 1,024 records are accepted, and each record creates one synthetic CLI -source after file/profile sources in supplied order. Override sources share the +source after file/overlay sources in supplied order. Override sources share the 1,024-source ceiling. A record has an 8 MiB string-byte limit; override strings and source bytes share the 64 MiB aggregate limit. Synthetic mapping keys and values count toward the existing 1,000,000-node and depth-256 ceilings. Cycles, named or @@ -514,7 +514,7 @@ unsupported types, nonfinite floats, and limit excesses fail with safe `override diagnostics. Diagnostics and provenance identify the safe `Flag` source, never raw arguments or values. -Override origins are explicit CLI origins. History retains replaced base/profile +Override origins are explicit CLI origins. History retains replaced base/overlay definitions and records equal-value replacements as well as structural changes. Caller-owned maps and slices are snapshotted during loading and must not be mutated concurrently with the call. The executable `ExampleOverride` shows translating a diff --git a/docs/consumers/README.md b/docs/consumers/README.md index aaac8ac..8a67ad7 100644 --- a/docs/consumers/README.md +++ b/docs/consumers/README.md @@ -1,7 +1,7 @@ # Consumer Adoption Guide ConfigKit removes parser setup, strict decoding, presence bookkeeping, import and -profile traversal, precedence, provenance, secret lookup, and safe inspection from +selected-overlay traversal, precedence, provenance, secret lookup, and safe inspection from applications. Consumers still own their configuration structs, defaults, semantic validation, schema-version policy, CLI parsing, runtime matching and conversion, resource checks, remote acquisition, and service initialization. @@ -34,7 +34,7 @@ features appear only when the application needs them: result, err := configkit.Load[Config](configkit.File(path), configkit.Options[Config]{ Definition: rules, Imports: useDeclaredImports, - Profile: selectedConfigProfile, + Overlay: selectedConfigOverlay, Overrides: explicitlySetFlags, }) if err != nil { @@ -43,8 +43,8 @@ if err != nil { return start(result.Config) ``` -Simple consumers use neither imports nor profiles. A complex consumer can opt into -both without redefining their syntax. ConfigKit profiles are configuration overlays; +Simple consumers use neither imports nor selected overlays. A complex consumer can opt into +both without redefining their syntax. ConfigKit overlays are configuration overlays; they do not select or replace an application's LLM, model, pipeline, report, or execution profile concepts. @@ -100,6 +100,16 @@ The examples preserve domain meaning while adopting one configuration convention blocked empty variables remain blocked, and ordinary `Load` never changes the process environment. +### Selected-overlay migration + +To adopt the current selected-overlay API, rename `Options.Profile`, +`LayerProfile`, and `CodeProfile*` references to their `Overlay` equivalents; move +selected files from `profiles/.yml` to `overlays/.yml`; update any +handling of the `overlay` layer and `overlay.*` diagnostic strings; then rebuild. +The [configuration contract](../config.md#selected-overlays) is the canonical +behavioral reference. Application-owned profile concepts, such as `model_profile`, +are unchanged, and any CLI flag rename remains the application's decision. + These examples are migration evidence, not completed changes to Narratio, Notarius, WeatherReporter, or Seriatim. Updating those repositories, their deployed YAML, CLI wiring, runtime catalogs, and operational documentation remains separate downstream @@ -113,7 +123,7 @@ focused behavioral owner: | Roadmap surface | Maintained owner | | --- | --- | | `Load`, `NoSource`, validation, versions, resolution, `SecretEnv`, overrides, and explanation | Executable examples in [`example_test.go`](../../example_test.go) | -| Source/import/profile pipeline rules and artifact defaults | Narratio-shaped executable example and fixture | +| Source/import/selected-overlay pipeline rules and artifact defaults | Narratio-shaped executable example and fixture | | Canonical module objects and dynamic option handling | WeatherReporter- and Notarius-shaped examples and migration tests | | `LoadSecretsDir`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` | | Results, metadata, diagnostics, effective output, and writer failures | Focused root-package load, provenance, rendering, and explanation tests | diff --git a/docs/development.md b/docs/development.md index fe13d4c..61a6346 100644 --- a/docs/development.md +++ b/docs/development.md @@ -105,7 +105,7 @@ code changes. Use the component overview to find the corresponding implementatio | --- | --- | --- | | YAML syntax, diagnostics, parser limits | [Adapter tests](../internal/yaml/parse_test.go), [source translation](../internal/engine/parse_test.go) | `go test ./internal/yaml` and `go test ./internal/engine -run '^TestParseSource'` | | File/reader access, imports, globs, containment | [Sources](../internal/engine/source_test.go), [FIFO](../internal/engine/source_fifo_test.go), [imports](../internal/engine/imports_test.go), [globs](../internal/engine/glob_test.go) | `go test ./internal/engine -run 'Source\|Import\|Glob\|Filesystem\|Nonregular\|RegularFile'` | -| Composition, profiles, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [profiles](../internal/engine/profile_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Profile\|Override'` | +| Composition, selected overlays, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [selected overlays](../internal/engine/selected_overlay_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Override'` | | Schema, defaults, projection, cloning | [Schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection](../internal/engine/projection_test.go) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` | | Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test ./internal/engine -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` | | Paths, metadata, provenance | [Paths](../internal/engine/path_test.go), [metadata](../internal/engine/metadata_test.go), [provenance](../internal/engine/provenance_test.go) | `go test ./internal/engine -run 'Path\|Metadata\|Provenance\|Queries'` | diff --git a/docs/internal/extraction.md b/docs/internal/extraction.md index a4e064e..df86516 100644 --- a/docs/internal/extraction.md +++ b/docs/internal/extraction.md @@ -137,9 +137,11 @@ permission failures, explicit `conf.d` use, and entry/candidate budgets. This wo has no Narratio production equivalent; it uses the selected standard-library `path.Match` and bounded `File.ReadDir` adaptation rather than its flat loader. -### Profiles and overlay +### Upstream profiles and ConfigKit selected overlays -Assigned to selected-profile overlay: +The following upstream symbols and case names retain their original profile +terminology. ConfigKit adapts their selected-overlay behavior without renaming the +upstream evidence: - Production: `mergeOverlayComposition`, `mergeOverlayNodes`, `selectPipelineProfile`, `loadPipelineProfileOverlays`, and @@ -153,19 +155,21 @@ Assigned to selected-profile overlay: `TestLoadPipelineProfilePairResolvesIndependentEffectivePipelines`. - Preserve: recursive mapping overlay, atomic scalar/sequence replacement, deterministic selection failures, source ownership, and independent results. -- Adapt: load only the selected fixed profile path; lowercase `default` means no - overlay. ConfigKit permits null and cross-kind replacement and does not support - declared defaults, inheritance, arbitrary profile paths, or validating every +- Adapt: load only the selected fixed overlay path; lowercase `default` means no + selection. ConfigKit permits null and cross-kind replacement and does not support + declared defaults, inheritance, arbitrary overlay paths, or validating every unselected overlay. -Implementation record (2026-09-06): `overlayDocuments`, `mergeOverlayMappings`, -`loadProfileDocument`, and `profileDirectiveDiagnostics` adapt the assigned merge, -selection, loading, and ownership mechanics. `TestOverlayDocumentsRecursesMappingsAndReplacesAtomicSubtrees`, -`TestLoadAppliesSelectedProfileAfterBaseAndBeforeDefaults`, profile selection/error, -directive/version, containment, and budget cases adapt the assigned tests. ConfigKit +Implementation record (2026-09-06, updated 2026-09-23): `overlayDocuments`, +`mergeOverlayMappings`, `loadSelectedOverlayDocument`, and +`overlayDirectiveDiagnostics` adapt the assigned merge, selection, loading, and +ownership mechanics. `TestOverlayDocumentsRecursesMappingsAndReplacesAtomicSubtrees`, +`TestLoadAppliesSelectedOverlayAfterBaseAndBeforeDefaults`, selected-overlay +selection/error, directive/version, containment, and budget cases adapt the assigned +tests. ConfigKit uses exactly one fixed selected `.yml` source, permits null and cross-kind replacement, -retains displaced descendant history, rejects any profile directive or version-path -contribution, and counts the profile in aggregate budgets. Narratio's declared +retains displaced descendant history, rejects any overlay directive or version-path +contribution, and counts the overlay in aggregate budgets. Narratio's declared default, multi-overlay validation, inheritance-like application conventions, and null/kind restrictions were not retained. @@ -196,7 +200,7 @@ previously extracted source claims, overlay displacement records, typed baseline and audited resolution snapshots in `load_metadata.go`. The representative `TestLayeredProvenanceAgreesWithResolvedSnapshot` adapts Narratio's stable ownership, profile ownership, deterministic effective projection, and side-effect-free -inspection cases across one default/import/profile/resolution workflow. ConfigKit +inspection cases across one default/import/overlay/resolution workflow. ConfigKit adds chronological descendant history for parent and whole-sequence operations, composite empty-mapping origins, canonical resolution reasons/dependencies, private snapshot agreement, defensive nested copies, and concurrent load isolation. Value diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index 2d7ffbf..d3418fd 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -71,7 +71,7 @@ network provider into the library. ## Public API and dependency direction Keep one small, coherent public surface for the supported lifecycle. Simple -single-document consumers must not need import, profile, or provider machinery. +single-document consumers must not need import, overlay, or provider machinery. Do not expose parser-specific nodes, errors, or extension hooks through the public API. Parser integration belongs behind a private boundary, as recorded in [ADR 0002](../adr/0002-own-yaml-semantics.md). @@ -106,7 +106,7 @@ same inputs and options. Invalid input must not produce a successful usable configuration. Do not run application hooks on invalid typed configuration. User-controlled precedence is application defaults, composed base, selected -profile, then explicitly supplied CLI overrides. Applications own flag parsing; +overlay, then explicitly supplied CLI overrides. Applications own flag parsing; ConfigKit owns overlay semantics and provenance. Domain resolution follows these inputs and must remain recorded, not become a hidden override channel. diff --git a/docs/roadmap/deferred.md b/docs/roadmap/deferred.md index 708a856..be454fd 100644 --- a/docs/roadmap/deferred.md +++ b/docs/roadmap/deferred.md @@ -15,7 +15,7 @@ The initial release deliberately excludes: - automatic `conf.d` discovery; - remote imports, arbitrary providers, and additional configuration formats; -- custom merge strategies, sequence merging, reset operators, and profile inheritance; +- custom merge strategies, sequence merging, reset operators, and overlay inheritance; - parser-specific or custom structural-decoding extension APIs; - generic dynamic-subtree conversion and module-registry APIs; - automatic schema migrations; diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 7bc2604..3b053d2 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Configuration Overlay Rename Implementation Plan -Status: not started. Execute Stage 1, then Stage 2. +Status: Stage 1 complete. Execute Stage 2. ## Scope and operating instructions @@ -112,6 +112,15 @@ unchanged. Stage 1 is complete only with a compiling, passing repository and matching current documentation. It is not permission to ship before Stage 2's audit. +### Completion + +Completed 2026-09-23. Renamed the selected configuration feature across the +facade, engine, maintained consumer, fixtures, contract, and current guidance; +application-owned `ModelProfile` and `model_profile` remain unchanged. `gofmt` and +`./scripts/check.sh` passed on Go 1.26.8/linux. Review confirmed the new +`overlays/.yml` path, `overlay`/`overlay.*` public values, and no retained +legacy Go declarations or filesystem fallback. + ## Stage 2 — Verify the breaking migration and finalize the handoff ### Outcome diff --git a/docs/roadmap/overlays.md b/docs/roadmap/overlays.md index 14a73cf..dbfb7e5 100644 --- a/docs/roadmap/overlays.md +++ b/docs/roadmap/overlays.md @@ -17,7 +17,7 @@ not authorize publication or changes to downstream repositories. The baseline is ConfigKit at `aead20f`, following v0.3.1 and the release-workflow simplification. Current behavior is owned by the -[selected-profile contract](../config.md#selected-profiles). This roadmap owns +[selected-overlay contract](../config.md#selected-overlays). This roadmap owns the unimplemented rename; it is not a new current API reference. ## Target terminology and public surface diff --git a/examples/consumers/consumers.go b/examples/consumers/consumers.go index 922138a..6203db8 100644 --- a/examples/consumers/consumers.go +++ b/examples/consumers/consumers.go @@ -29,14 +29,14 @@ type NarratioConfig struct { // LoadNarratio loads the maintained import fixture with Narratio-shaped rules. func LoadNarratio(path string) (*configkit.Result[NarratioConfig], error) { - return LoadNarratioProfile(path, "") + return LoadNarratioOverlay(path, "") } -// LoadNarratioProfile selects a ConfigKit configuration overlay independently +// LoadNarratioOverlay selects a ConfigKit configuration overlay independently // from the application's ModelProfile field. -func LoadNarratioProfile(path, profile string) (*configkit.Result[NarratioConfig], error) { +func LoadNarratioOverlay(path, overlay string) (*configkit.Result[NarratioConfig], error) { return configkit.Load(configkit.File(path), configkit.Options[NarratioConfig]{ - Imports: true, Profile: profile, + Imports: true, Overlay: overlay, Definition: configkit.Definition[NarratioConfig]{ Defaults: func() NarratioConfig { return NarratioConfig{PollInterval: time.Minute} }, Resolve: resolveNarratio, diff --git a/examples/consumers/consumers_test.go b/examples/consumers/consumers_test.go index 6339741..d118ad8 100644 --- a/examples/consumers/consumers_test.go +++ b/examples/consumers/consumers_test.go @@ -92,10 +92,10 @@ func TestMaintainedConsumerFixturesPreserveDomainOutcomes(t *testing.T) { if origin, ok := narratio.Metadata().Lookup(configkit.Keys("artifacts", "transcript", "output")); !ok || origin.Origin.Layer != configkit.LayerResolution { t.Fatalf("artifact origin = %#v, %v", origin, ok) } - profiled, err := consumers.LoadNarratioProfile(fixture("narratio", "pipeline.yml"), "testing") - if err != nil || profiled.Config.PollInterval.String() != "2m0s" || - profiled.Config.ModelProfile != "concise" || len(profiled.Sources()) != 3 { - t.Fatalf("configuration/application profiles = %#v, %v", profiled, err) + overlaid, err := consumers.LoadNarratioOverlay(fixture("narratio", "pipeline.yml"), "testing") + if err != nil || overlaid.Config.PollInterval.String() != "2m0s" || + overlaid.Config.ModelProfile != "concise" || len(overlaid.Sources()) != 3 { + t.Fatalf("configuration overlay and application model profile = %#v, %v", overlaid, err) } notarius, err := consumers.LoadNotarius(fixture("notarius", "config.yml")) @@ -200,7 +200,7 @@ func TestMigrationFixturesFailWithStableCodesAndPaths(t *testing.T) { if len(diagnostics) == 0 || diagnostics[0].Code != test.code || diagnostics[0].Path.String() != test.path { t.Fatalf("diagnostics = %#v", diagnostics) } - if strings.Contains(diagnostics[0].Message, "wrong-before-profile") { + if strings.Contains(diagnostics[0].Message, "wrong-before-overlay") { t.Fatalf("diagnostic exposed raw parser input: %#v", diagnostics[0]) } }) diff --git a/examples/consumers/testdata/narratio/profiles/testing.yml b/examples/consumers/testdata/narratio/overlays/testing.yml similarity index 100% rename from examples/consumers/testdata/narratio/profiles/testing.yml rename to examples/consumers/testdata/narratio/overlays/testing.yml diff --git a/internal/engine/directives.go b/internal/engine/directives.go index 738acee..4a9399e 100644 --- a/internal/engine/directives.go +++ b/internal/engine/directives.go @@ -181,24 +181,24 @@ func nodeImportDiagnostic(code Code, path Path, message string, node *document.N return Diagnostic{Code: code, Severity: SeverityError, Path: path, Message: message, Primary: &location} } -func profileDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic { +func overlayDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic { if node.Kind == document.Mapping { for _, entry := range node.Mapping { childPath := current.Key(entry.Key) if entry.Key == directiveKey { location := publicLocation(entry.KeyLocation) return []Diagnostic{{ - Code: CodeProfileDirective, Severity: SeverityError, Path: childPath, - Message: "profiles cannot contain configuration directives", Primary: &location, + Code: CodeOverlayDirective, Severity: SeverityError, Path: childPath, + Message: "overlays cannot contain configuration directives", Primary: &location, }} } - if diagnostics := profileDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 { + if diagnostics := overlayDirectiveDiagnostics(entry.Value, childPath); len(diagnostics) != 0 { return diagnostics } } } else if node.Kind == document.Sequence { for index, child := range node.Sequence { - if diagnostics := profileDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 { + if diagnostics := overlayDirectiveDiagnostics(child, current.Index(index)); len(diagnostics) != 0 { return diagnostics } } diff --git a/internal/engine/explain_render.go b/internal/engine/explain_render.go index 457a9e4..ef5b1b9 100644 --- a/internal/engine/explain_render.go +++ b/internal/engine/explain_render.go @@ -38,7 +38,7 @@ func (r *Inspection) WriteExplanation(output io.Writer, path Path) error { rendered.WriteString(" - representation: ") if change.Origin.Operation == ChangeDecoding { rendered.WriteString("input to canonical typed\n") - } else if change.Origin.Layer == LayerBase || change.Origin.Layer == LayerProfile || change.Origin.Layer == LayerCLI { + } else if change.Origin.Layer == LayerBase || change.Origin.Layer == LayerOverlay || change.Origin.Layer == LayerCLI { rendered.WriteString("policy-validated input\n") } else { rendered.WriteString("canonical typed\n") diff --git a/internal/engine/explain_test.go b/internal/engine/explain_test.go index 18f0fb1..0f4766f 100644 --- a/internal/engine/explain_test.go +++ b/internal/engine/explain_test.go @@ -21,13 +21,13 @@ type explanationConcurrency struct { } `yaml:"transcription"` } -func TestExplainShowsDefaultBaseAndEffectiveProfileValue(t *testing.T) { +func TestExplainShowsDefaultBaseAndEffectiveOverlayValue(t *testing.T) { directory := t.TempDir() 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") + writeImportFile(t, directory, filepath.Join("overlays", "testing.yml"), "transcription:\n concurrency: 1\n") result, err := testLoad[explanationConcurrency](File(mainPath), Options[explanationConcurrency]{ - Imports: true, Profile: "testing", + Imports: true, Overlay: "testing", Definition: Definition[explanationConcurrency]{Defaults: func() explanationConcurrency { value := explanationConcurrency{} value.Transcription.Concurrency = 4 @@ -44,7 +44,7 @@ func TestExplainShowsDefaultBaseAndEffectiveProfileValue(t *testing.T) { if got := explanationValues(explanation); !reflect.DeepEqual(got, []string{"4\n", "8\n", "1\n"}) { t.Fatalf("history values = %#v", got) } - if explanation.Effective.YAML != "1\n" || explanation.Origin.Layer != LayerProfile || + if explanation.Effective.YAML != "1\n" || explanation.Origin.Layer != LayerOverlay || len(explanation.Origin.Locations) != 1 || explanation.Origin.Locations[0].SourceID != "source-3" || explanation.Origin.Locations[0].Line != 2 { t.Fatalf("effective explanation = %#v", explanation) @@ -70,18 +70,18 @@ func TestExplainReplaysCLIResolutionContainersAndSafeHistoricalValues(t *testing directory := t.TempDir() mainPath := writeImportFile(t, directory, "config.yml", `concurrency: 8 equal: same -limit: wrong-before-profile +limit: wrong-before-overlay items: [base, removed] labels: {base: retained} dynamic: {retired: HISTORICAL_EXPLAIN_CANARY, public: visible} credential: APP_SECRET classified: APP_SECRET `) - writeImportFile(t, directory, filepath.Join("profiles", "testing.yml"), `concurrency: 1 + writeImportFile(t, directory, filepath.Join("overlays", "testing.yml"), `concurrency: 1 equal: same limit: 7 -items: [profile] -labels: {profile: retained} +items: [overlay] +labels: {overlay: retained} dynamic: selected `) result, err := testLoadWithSecretLookup(File(mainPath), func(name string) (string, bool) { @@ -90,7 +90,7 @@ dynamic: selected } return "RESOLVED_EXPLAIN_CANARY", true }, Options[explanationConfig]{ - Profile: "testing", + Overlay: "testing", Overrides: []Override{ {Path: Keys("concurrency"), Value: 2, Flag: "--concurrency"}, {Path: Keys("equal"), Value: "same", Flag: "--equal"}, @@ -130,7 +130,7 @@ dynamic: selected if got := explanationValues(concurrency); !reflect.DeepEqual(got, []string{"4\n", "8\n", "1\n", "2\n", "3\n"}) { t.Fatalf("concurrency history = %#v", got) } - if got := historyValueLayers(concurrency); !reflect.DeepEqual(got, []Layer{LayerDefault, LayerBase, LayerProfile, LayerCLI, LayerResolution}) { + if got := historyValueLayers(concurrency); !reflect.DeepEqual(got, []Layer{LayerDefault, LayerBase, LayerOverlay, LayerCLI, LayerResolution}) { t.Fatalf("concurrency layers = %#v", got) } if concurrency.History[4].Before.YAML != "2\n" || concurrency.History[4].Reasons[0] != "normalized\nconcurrency" || @@ -149,7 +149,7 @@ dynamic: selected } limit, _ := result.Explain(Keys("limit")) - if got := explanationValues(limit); !reflect.DeepEqual(got, []string{"0\n", "wrong-before-profile\n", "7\n"}) { + if got := explanationValues(limit); !reflect.DeepEqual(got, []string{"0\n", "wrong-before-overlay\n", "7\n"}) { t.Fatalf("overridden invalid input was not retained: %#v", got) } removed, ok := result.Explain(Keys("dynamic", "retired")) @@ -182,7 +182,7 @@ dynamic: selected } labels, _ := result.Explain(Keys("labels")) if len(labels.History) != 4 || !strings.Contains(labels.Effective.YAML, "default: retained") || - !strings.Contains(labels.Effective.YAML, "profile: retained") || !strings.Contains(labels.Effective.YAML, "cli: retained") { + !strings.Contains(labels.Effective.YAML, "overlay: retained") || !strings.Contains(labels.Effective.YAML, "cli: retained") { t.Fatalf("composite mapping history = %#v", labels) } if labels.Origin.Layer != LayerCLI || len(labels.Origin.Locations) != 3 { @@ -258,7 +258,7 @@ dynamic: selected var invalidInput bytes.Buffer if err := result.WriteExplanation(&invalidInput, Keys("limit")); err != nil || !strings.Contains(invalidInput.String(), "policy-validated input") || - strings.Contains(invalidInput.String(), "wrong-before-profile\n source: layer=\"base\"\n before (canonical typed)") { + strings.Contains(invalidInput.String(), "wrong-before-overlay\n source: layer=\"base\"\n before (canonical typed)") { t.Fatalf("historical representation rendering = %v\n%s", err, invalidInput.String()) } } @@ -340,8 +340,8 @@ func TestExplanationReconcilesDefaultsAfterInputReplacement(t *testing.T) { if cli { opts.Overrides = []Override{{Path: Keys("m"), Value: map[string]any{"b": 2}, Flag: "--m"}} } else { - writeImportFile(t, root, "profiles/p.yml", "m: {b: 2}\n") - opts.Profile = "p" + writeImportFile(t, root, "overlays/p.yml", "m: {b: 2}\n") + opts.Overlay = "p" } result, err := testLoad(File(main), opts) if err != nil { diff --git a/internal/engine/glob_test.go b/internal/engine/glob_test.go index 54e88cb..2160c6e 100644 --- a/internal/engine/glob_test.go +++ b/internal/engine/glob_test.go @@ -229,11 +229,11 @@ func TestGlobTraversalBoundsNonmatchesAndAcceptedCandidates(t *testing.T) { } } -func TestFilesystemContextIsValidatedWithoutImportsOrActiveProfile(t *testing.T) { +func TestFilesystemContextIsValidatedWithoutImportsOrActiveOverlay(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "items: {root: root}\n") result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{ - Profile: "default", Filesystem: &FilesystemContext{Root: directory}, + Overlay: "default", Filesystem: &FilesystemContext{Root: directory}, }) if err != nil || result.Config.Items["root"] != "root" { t.Fatalf("file filesystem context = %#v, %v", result, err) @@ -247,9 +247,9 @@ func TestFilesystemContextIsValidatedWithoutImportsOrActiveProfile(t *testing.T) t.Fatalf("byte filesystem context = %#v, %v", result, err) } requireLoadCode(t, Bytes("memory.yml", []byte("{}\n")), Options[importTestConfig]{Filesystem: &FilesystemContext{BaseDir: directory}}, CodeOptionsInvalid) - defaultsOnly, err := testLoad[importTestConfig](NoSource(), Options[importTestConfig]{Profile: "default"}) + defaultsOnly, err := testLoad[importTestConfig](NoSource(), Options[importTestConfig]{Overlay: "default"}) if err != nil || defaultsOnly == nil { - t.Fatalf("default profile with NoSource = %#v, %v", defaultsOnly, err) + t.Fatalf("default overlay with NoSource = %#v, %v", defaultsOnly, err) } if runtime.GOOS == "linux" || runtime.GOOS == "darwin" { diff --git a/internal/engine/load.go b/internal/engine/load.go index f061444..0c0e896 100644 --- a/internal/engine/load.go +++ b/internal/engine/load.go @@ -19,17 +19,17 @@ type Definition[T any] struct { } // Options contains the application definition and per-load input selections. -// Imports enables explicit path and glob imports; Profile selects one fixed profile -// overlay; Overrides contains only values explicitly supplied by an application CLI. +// Imports enables explicit path and glob imports; Overlay selects one fixed overlay; +// Overrides contains only values explicitly supplied by an application CLI. type Options[T any] struct { Definition Definition[T] Imports bool - Profile string + Overlay string Filesystem *FilesystemContext Overrides []Override } -// FilesystemContext supplies filesystem roots for imports and profiles. +// FilesystemContext supplies filesystem roots for imports and overlays. type FilesystemContext struct { BaseDir string Root string @@ -113,7 +113,7 @@ func (r *Inspection) Diagnostics() []Diagnostic { } // Load parses and strictly decodes zero or one selected source, explicitly enabled -// imports, and one selected profile. It accepts at most one Options value and +// imports, and one selected overlay. It accepts at most one Options value and // returns no result when any error is reported. func Load[T any](source Source, secrets SecretBinding, supplied ...Options[T]) (*Loaded[T], error) { return loadWithSecretLookup(source, secrets, os.LookupEnv, supplied...) diff --git a/internal/engine/load_input.go b/internal/engine/load_input.go index c20cc47..4b6420f 100644 --- a/internal/engine/load_input.go +++ b/internal/engine/load_input.go @@ -25,7 +25,7 @@ func assembleBaseInput[T any](source Source, options Options[T]) (assembledInput input.document = input.composition.root } else { input.sources = sourceInfos(parsed.source) - if options.Filesystem != nil && !activeProfile(options.Profile) { + if options.Filesystem != nil && !activeOverlay(options.Overlay) { if diagnostic := validateFilesystemContext(source, options.Filesystem, parsed); diagnostic != nil { return input, []Diagnostic{*diagnostic} } @@ -40,17 +40,17 @@ func assembleBaseInput[T any](source Source, options Options[T]) (assembledInput } func applyInputLayers[T any](source Source, options Options[T], input assembledInput) (assembledInput, []Diagnostic) { - if !activeProfile(options.Profile) && len(options.Overrides) == 0 { + if !activeOverlay(options.Overlay) && len(options.Overrides) == 0 { return input, nil } composition, diagnostics := compositionForInput(input) if len(diagnostics) != 0 { return input, diagnostics } - if activeProfile(options.Profile) { - composition, input.sources, diagnostics = loadProfileDocument( + if activeOverlay(options.Overlay) { + composition, input.sources, diagnostics = loadSelectedOverlayDocument( source, options.Filesystem, input.parsed, composition, input.sources, - options.Profile, options.Definition.Version, defaultImportLimits(), + options.Overlay, options.Definition.Version, defaultImportLimits(), ) if len(diagnostics) != 0 { return input, diagnostics @@ -69,7 +69,7 @@ func applyInputLayers[T any](source Source, options Options[T], input assembledI return input, nil } -// compositionForInput initializes one handoff for profiles and CLI, including +// compositionForInput initializes one handoff for overlays and CLI, including // defaults-only CLI input. Raw source bytes and mapping keys retain their budgets. func compositionForInput(input assembledInput) (*composedDocument, []Diagnostic) { if input.composition != nil { diff --git a/internal/engine/metadata_test.go b/internal/engine/metadata_test.go index a615cb3..88fa605 100644 --- a/internal/engine/metadata_test.go +++ b/internal/engine/metadata_test.go @@ -60,8 +60,8 @@ func TestHistoricalPathsExposeHiddenBaseOwnershipWithoutResolvingBaseSecrets(t * root := writeImportFile(t, dir, "main.yml", "configkit:\n imports: [{path: first.yml}, {path: second.yml}]\ncredential: CONFIGKIT_HISTORY_UNUSED_PROD_KEY\n") writeImportFile(t, dir, "first.yml", "definitions: {recap: {prompt: recap}}\n") writeImportFile(t, dir, "second.yml", "definitions: {recap: {output: recap.md}}\n") - writeImportFile(t, dir, "profiles/testing.yml", "definitions: null\ncredential: "+credential+"\n") - result, err := testLoad[config](File(root), Options[config]{Imports: true, Profile: "testing"}) + writeImportFile(t, dir, "overlays/testing.yml", "definitions: null\ncredential: "+credential+"\n") + result, err := testLoad[config](File(root), Options[config]{Imports: true, Overlay: "testing"}) if err != nil { t.Fatal(err) } diff --git a/internal/engine/options.go b/internal/engine/options.go index dea8980..6461595 100644 --- a/internal/engine/options.go +++ b/internal/engine/options.go @@ -10,8 +10,8 @@ func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []D } var diagnostics []Diagnostic definition := options.Definition - if !validProfileName(options.Profile) { - diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "profile name must contain only ASCII letters, digits, underscore, or hyphen")) + if !validOverlayName(options.Overlay) { + diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "overlay name must contain only ASCII letters, digits, underscore, or hyphen")) } if options.Filesystem != nil { diagnostics = append(diagnostics, filesystemOptionDiagnostics(source, options.Filesystem)...) @@ -19,11 +19,11 @@ func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []D if source.kind == sourceNone && options.Imports { diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "NoSource cannot enable imports")) } - if source.kind == sourceNone && options.Profile != "" && options.Profile != "default" { - diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "NoSource cannot select an active profile")) + if source.kind == sourceNone && options.Overlay != "" && options.Overlay != "default" { + diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "NoSource cannot select an active overlay")) } - if options.Filesystem == nil && (source.kind == sourceBytes || source.kind == sourceReader) && (options.Imports || activeProfile(options.Profile)) { - diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "byte and reader imports or profiles require filesystem context")) + if options.Filesystem == nil && (source.kind == sourceBytes || source.kind == sourceReader) && (options.Imports || activeOverlay(options.Overlay)) { + diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "byte and reader imports or overlays require filesystem context")) } if definition.Version != nil { if !definition.Version.Path.valid() { diff --git a/internal/engine/overlay.go b/internal/engine/overlay.go index 9c7c20e..3c5b2f0 100644 --- a/internal/engine/overlay.go +++ b/internal/engine/overlay.go @@ -20,14 +20,14 @@ type displacedDefinition struct { func overlayDocuments(base *composedDocument, overlay *document.Node) (*composedDocument, []Diagnostic) { if base == nil || base.root == nil || base.root.Kind != document.Mapping || overlay == nil || overlay.Kind != document.Mapping { return nil, []Diagnostic{{ - Code: CodeProfileInvalid, Severity: SeverityError, - Message: "profile overlay requires mapping document roots", + Code: CodeOverlayInvalid, Severity: SeverityError, + Message: "overlay overlay requires mapping document roots", }} } result := cloneComposedDocument(base) - mergeOverlayOrigin(result.origins, Path{}, overlay.Location, LayerProfile) - mergeOverlayMappings(result, result.root, overlay, Path{}, LayerProfile, true) - result.applications = append(result.applications, historyApplication{layer: LayerProfile, value: overlay}) + mergeOverlayOrigin(result.origins, Path{}, overlay.Location, LayerOverlay) + mergeOverlayMappings(result, result.root, overlay, Path{}, LayerOverlay, true) + result.applications = append(result.applications, historyApplication{layer: LayerOverlay, value: overlay}) return result, nil } diff --git a/internal/engine/overlay_test.go b/internal/engine/overlay_test.go index 50aeb0f..3b64191 100644 --- a/internal/engine/overlay_test.go +++ b/internal/engine/overlay_test.go @@ -22,7 +22,7 @@ to_scalar: {child: old} } overlay := parseCompositionDocument(t, "source-2", `shared: changed: new - added: profile + added: overlay sequence: [] nullable: null to_map: {child: new} @@ -42,7 +42,7 @@ to_scalar: scalar }{ {path: Keys("shared", "kept"), kind: document.String, text: "base"}, {path: Keys("shared", "changed"), kind: document.String, text: "new"}, - {path: Keys("shared", "added"), kind: document.String, text: "profile"}, + {path: Keys("shared", "added"), kind: document.String, text: "overlay"}, {path: Keys("sequence"), kind: document.Sequence}, {path: Keys("nullable"), kind: document.Null}, {path: Keys("to_map"), kind: document.Mapping}, @@ -62,7 +62,7 @@ to_scalar: scalar t.Fatal("parent replacement retained a stale child origin") } shared := result.origins["/shared"] - if shared.Layer != LayerProfile || len(shared.Locations) != 2 { + if shared.Layer != LayerOverlay || len(shared.Locations) != 2 { t.Fatalf("shared mapping origin = %#v", shared) } if len(result.overlays) != 5 { diff --git a/internal/engine/override_test.go b/internal/engine/override_test.go index 3f519b2..738732d 100644 --- a/internal/engine/override_test.go +++ b/internal/engine/override_test.go @@ -41,9 +41,9 @@ func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *te labels: {base: retained} api_key: BASE_SECRET `) - writeImportFile(t, directory, filepath.Join("profiles", "testing.yml"), `settings: - target: profile - labels: {profile: retained} + writeImportFile(t, directory, filepath.Join("overlays", "testing.yml"), `settings: + target: overlay + labels: {overlay: retained} `) validated := false overrides := []Override{ @@ -62,7 +62,7 @@ func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *te } return "credential-value", true }, Options[overrideLifecycleConfig]{ - Imports: true, Profile: "testing", Overrides: overrides, + Imports: true, Overlay: "testing", Overrides: overrides, Definition: Definition[overrideLifecycleConfig]{ Defaults: func() overrideLifecycleConfig { return overrideLifecycleConfig{Settings: overrideLifecycleSettings{Target: "default", Count: 9, Text: "default", Items: []string{"default"}}} @@ -86,7 +86,7 @@ func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *te t.Fatalf("testLoadWithSecretLookup() error = %v", err) } want := overrideLifecycleSettings{ - Target: "cli", Labels: map[string]string{"base": "retained", "profile": "retained", "cli": "selected"}, + Target: "cli", Labels: map[string]string{"base": "retained", "overlay": "retained", "cli": "selected"}, APIKey: result.Config.Settings.APIKey, } if !validated || result.Config.Settings.Count != 0 || result.Config.Settings.Enabled || result.Config.Settings.Text != "" || @@ -95,7 +95,7 @@ func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *te t.Fatalf("effective settings = %#v", result.Config.Settings) } sources := result.Sources() - if len(sources) != 3+len(overrides) || sources[0].Layer != LayerBase || sources[1].Layer != LayerBase || sources[2].Layer != LayerProfile { + if len(sources) != 3+len(overrides) || sources[0].Layer != LayerBase || sources[1].Layer != LayerBase || sources[2].Layer != LayerOverlay { t.Fatalf("sources = %#v", sources) } for index, override := range overrides { @@ -104,7 +104,7 @@ func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *te } } history := result.Metadata().History(Keys("settings", "target")) - wantLayers := []Layer{LayerDefault, LayerBase, LayerProfile, LayerCLI, LayerResolution} + wantLayers := []Layer{LayerDefault, LayerBase, LayerOverlay, LayerCLI, LayerResolution} if got := historyLayers(history); !reflect.DeepEqual(got, wantLayers) { t.Fatalf("target history layers = %#v, want %#v; history %#v", got, wantLayers, history) } diff --git a/internal/engine/provenance_test.go b/internal/engine/provenance_test.go index c2c4a6e..71a0b59 100644 --- a/internal/engine/provenance_test.go +++ b/internal/engine/provenance_test.go @@ -16,7 +16,7 @@ type layeredConfig struct { Shared struct { Equal string `yaml:"equal"` Imported string `yaml:"imported"` - Profile string `yaml:"profile"` + Overlay string `yaml:"overlay"` } `yaml:"shared"` Values []int `yaml:"values"` Items map[string]layeredItem `yaml:"items"` @@ -40,7 +40,7 @@ nullable: base empty: {} `) importPath := writeImportFile(t, directory, "imported.yml", "shared: {imported: base}\n") - profilePath := writeImportFile(t, directory, "profiles/testing.yml", `shared: {profile: selected} + overlayPath := writeImportFile(t, directory, "overlays/testing.yml", `shared: {overlay: selected} values: [3, 4] value: selected nullable: null @@ -71,23 +71,23 @@ empty: {} } result, err := testLoad[layeredConfig](File(mainPath), Options[layeredConfig]{ - Imports: true, Profile: "testing", Definition: definition, + Imports: true, Overlay: "testing", Definition: definition, }) if err != nil { var loadError *LoadError _ = errors.As(err, &loadError) t.Fatalf("%v: %#v", err, loadError.Diagnostics()) } - wantSources := []string{canonicalTestPath(t, mainPath), canonicalTestPath(t, importPath), canonicalTestPath(t, profilePath)} + wantSources := []string{canonicalTestPath(t, mainPath), canonicalTestPath(t, importPath), canonicalTestPath(t, overlayPath)} sources := result.Sources() gotSources := make([]string, len(sources)) for index, source := range sources { gotSources[index] = source.Path } - if !reflect.DeepEqual(gotSources, wantSources) || sources[2].Layer != LayerProfile { + if !reflect.DeepEqual(gotSources, wantSources) || sources[2].Layer != LayerOverlay { t.Fatalf("sources = %#v", sources) } - if result.Config.Shared.Equal != "same" || result.Config.Shared.Imported != "base" || result.Config.Shared.Profile != "selected" || + if result.Config.Shared.Equal != "same" || result.Config.Shared.Imported != "base" || result.Config.Shared.Overlay != "selected" || !reflect.DeepEqual(result.Config.Values, []int{5}) || result.Config.Nullable != nil || result.Config.Derived != 7 || result.Config.Empty["retained"] != "default" { t.Fatalf("configuration = %#v", result.Config) @@ -100,13 +100,13 @@ empty: {} empty, _ := metadata.Lookup(Keys("empty")) firstValue, _ := metadata.Lookup(Keys("values").Index(0)) if !equal.Explicit || equal.Origin.Layer != LayerBase || derived.Explicit || derived.Origin.Operation != ChangeDerivedDefault || - !nullable.Explicit || !nullable.Null || nullable.Origin.Layer != LayerProfile || - !empty.Explicit || empty.Origin.Layer != LayerProfile || len(empty.Origin.Locations) != 2 || + !nullable.Explicit || !nullable.Null || nullable.Origin.Layer != LayerOverlay || + !empty.Explicit || empty.Origin.Layer != LayerOverlay || len(empty.Origin.Locations) != 2 || !firstValue.Explicit || firstValue.Origin.Operation != ChangeNormalization { t.Fatalf("metadata = equal %#v derived %#v nullable %#v empty %#v value %#v", equal, derived, nullable, empty, firstValue) } if _, ok := metadata.Lookup(Keys("value", "child")); ok { - t.Fatal("profile-removed child remains effective") + t.Fatal("overlay-removed child remains effective") } if _, ok := metadata.Lookup(Keys("values").Index(1)); ok { t.Fatal("resolution-removed sequence index remains effective") @@ -116,10 +116,10 @@ empty: {} } assertHistoryLayersAndPaths(t, metadata.History(Keys("values").Index(1)), - []Layer{LayerDefault, LayerBase, LayerProfile, LayerResolution}, + []Layer{LayerDefault, LayerBase, LayerOverlay, LayerResolution}, []string{"/values/1", "/values/1", "/values", "/values"}) assertHistoryLayersAndPaths(t, metadata.History(Keys("value", "child")), - []Layer{LayerDefault, LayerBase, LayerProfile}, + []Layer{LayerDefault, LayerBase, LayerOverlay}, []string{"/value/child", "/value/child", "/value"}) removedHistory := metadata.History(Keys("items", "remove", "value")) assertHistoryLayersAndPaths(t, removedHistory, @@ -159,11 +159,11 @@ empty: {} } } -func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) { +func TestSelectedOverlayLoadsRemainIndependentConcurrently(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "values: [1, 2]\n") - writeImportFile(t, directory, "profiles/testing.yml", "values: [3, 4]\n") - options := Options[layeredConfig]{Profile: "testing"} + writeImportFile(t, directory, "overlays/testing.yml", "values: [3, 4]\n") + options := Options[layeredConfig]{Overlay: "testing"} const loads = 16 results := make(chan *testResult[layeredConfig], loads) @@ -200,7 +200,7 @@ func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) { if !reflect.DeepEqual(result.Config.Values, []int{3, 4}) { t.Fatalf("concurrent result contaminated = %#v", result.Config.Values) } - if info, ok := result.Metadata().Lookup(Keys("values")); !ok || info.Origin.Layer != LayerProfile { + if info, ok := result.Metadata().Lookup(Keys("values")); !ok || info.Origin.Layer != LayerOverlay { t.Fatalf("concurrent metadata contaminated = %#v, %v", info, ok) } } @@ -220,7 +220,7 @@ sequence: [first, second] composite: {first: first} `) secondPath := writeImportFile(t, directory, "second.yml", "composite: {second: second}\n") - profilePath := writeImportFile(t, directory, "profiles/testing.yml", "equal: same\nprofile: profile\n") + overlayPath := writeImportFile(t, directory, "overlays/testing.yml", "equal: same\noverlay: overlay\n") compositeSources := map[string]bool{ canonicalTestPath(t, mainPath): true, canonicalTestPath(t, firstPath): true, @@ -232,12 +232,12 @@ composite: {first: first} Sequence []string `yaml:"sequence"` Composite map[string]string `yaml:"composite"` Equal string `yaml:"equal"` - Profile string `yaml:"profile"` + Overlay string `yaml:"overlay"` CLIEqual string `yaml:"cli_equal"` } var callbackErr error result, err := testLoad[config](File(mainPath), Options[config]{ - Imports: true, Profile: "testing", Overrides: []Override{{Path: Keys("cli_equal"), Value: "same", Flag: "--cli-equal"}}, + Imports: true, Overlay: "testing", Overrides: []Override{{Path: Keys("cli_equal"), Value: "same", Flag: "--cli-equal"}}, Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error { check := func(ok bool, message string) { if !ok && callbackErr == nil { @@ -257,9 +257,9 @@ composite: {first: first} fileOwner(Keys("main"), canonicalTestPath(t, mainPath)) fileOwner(Keys("imported"), canonicalTestPath(t, firstPath)) fileOwner(Keys("sequence").Index(1), canonicalTestPath(t, firstPath)) - profile := fileOwner(Keys("equal"), canonicalTestPath(t, profilePath)) - fileOwner(Keys("profile"), canonicalTestPath(t, profilePath)) - check(profile.Origin.Layer == LayerProfile, "profile replacement retained base ownership") + overlay := fileOwner(Keys("equal"), canonicalTestPath(t, overlayPath)) + fileOwner(Keys("overlay"), canonicalTestPath(t, overlayPath)) + check(overlay.Origin.Layer == LayerOverlay, "overlay replacement retained base ownership") cli, found := resolution.InputInfo(Keys("cli_equal")) check(found && cli.Origin.Layer == LayerCLI && len(cli.Origin.Locations) == 1, "CLI replacement lost ownership") if found && len(cli.Origin.Locations) == 1 { @@ -298,7 +298,7 @@ composite: {first: first} t.Fatal(err) } if result.Config.Main != "main" || result.Config.Imported != "imported" || !reflect.DeepEqual(result.Config.Sequence, []string{"first", "second"}) || - result.Config.Equal != "same" || result.Config.Profile != "profile" || result.Config.CLIEqual != "same" { + result.Config.Equal != "same" || result.Config.Overlay != "overlay" || result.Config.CLIEqual != "same" { t.Fatalf("configuration = %#v", result.Config) } diff --git a/internal/engine/records.go b/internal/engine/records.go index b0b3528..bbe2e8d 100644 --- a/internal/engine/records.go +++ b/internal/engine/records.go @@ -6,7 +6,7 @@ type Layer string const ( LayerDefault Layer = "default" LayerBase Layer = "base" - LayerProfile Layer = "profile" + LayerOverlay Layer = "overlay" LayerCLI Layer = "cli" LayerResolution Layer = "resolution" ) @@ -90,13 +90,13 @@ const ( CodeImportLimitExceeded Code = "import.limit_exceeded" CodeImportVersion Code = "import.version" - CodeProfileInvalid Code = "profile.invalid" - CodeProfileMissing Code = "profile.missing" - CodeProfileOutsideRoot Code = "profile.outside_root" - CodeProfileRepeated Code = "profile.repeated" - CodeProfileDirective Code = "profile.directive" - CodeProfileVersion Code = "profile.version" - CodeProfileLimitExceeded Code = "profile.limit_exceeded" + CodeOverlayInvalid Code = "overlay.invalid" + CodeOverlayMissing Code = "overlay.missing" + CodeOverlayOutsideRoot Code = "overlay.outside_root" + CodeOverlayRepeated Code = "overlay.repeated" + CodeOverlayDirective Code = "overlay.directive" + CodeOverlayVersion Code = "overlay.version" + CodeOverlayLimitExceeded Code = "overlay.limit_exceeded" CodeComposeInvalid Code = "compose.invalid" CodeComposeConflict Code = "compose.conflict" diff --git a/internal/engine/render_test.go b/internal/engine/render_test.go index 72b3303..42825a5 100644 --- a/internal/engine/render_test.go +++ b/internal/engine/render_test.go @@ -71,7 +71,7 @@ empty_sequence: [] labels: {hidden: DYNAMIC_CANARY_6a12, public: visible} sequence: [second, first] `) - writeImportFile(t, directory, "profiles/testing.yml", "dynamic: selected\n") + writeImportFile(t, directory, "overlays/testing.yml", "dynamic: selected\n") definition := Definition[renderConfig]{ Secret: func(path Path) bool { return path.String() == "/labels/hidden" || path.String() == "/dynamic/retired" @@ -85,7 +85,7 @@ sequence: [second, first] return nil }, } - result, err := testLoad[renderConfig](File(mainPath), Options[renderConfig]{Definition: definition, Profile: "testing"}) + result, err := testLoad[renderConfig](File(mainPath), Options[renderConfig]{Definition: definition, Overlay: "testing"}) if err != nil { t.Fatal(err) } diff --git a/internal/engine/profile.go b/internal/engine/selected_overlay.go similarity index 56% rename from internal/engine/profile.go rename to internal/engine/selected_overlay.go index a617fc0..ad565ee 100644 --- a/internal/engine/profile.go +++ b/internal/engine/selected_overlay.go @@ -12,12 +12,12 @@ import ( yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml" ) -func activeProfile(name string) bool { +func activeOverlay(name string) bool { return name != "" && name != "default" } -func validProfileName(name string) bool { - if !activeProfile(name) { +func validOverlayName(name string) bool { + if !activeOverlay(name) { return true } for _, character := range name { @@ -31,38 +31,38 @@ func validProfileName(name string) bool { return true } -func loadProfileDocument(source Source, context *FilesystemContext, main parsedSource, base *composedDocument, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) { +func loadSelectedOverlayDocument(source Source, context *FilesystemContext, main parsedSource, base *composedDocument, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) { rootPath, _, _, diagnostic := importFilesystem(source, context, main) if diagnostic != nil { return nil, cloneSourceInfos(sources), []Diagnostic{*diagnostic} } - profilePath := filepath.Join(rootPath, "profiles", name+".yml") - canonical, err := canonicalExistingPath(profilePath) + overlayPath := filepath.Join(rootPath, "overlays", name+".yml") + canonical, err := canonicalExistingPath(overlayPath) if err != nil { - code := CodeProfileInvalid - message := "selected profile could not be resolved" + code := CodeOverlayInvalid + message := "selected overlay could not be resolved" if errors.Is(err, fs.ErrNotExist) { - code, message = CodeProfileMissing, "selected profile does not exist" + code, message = CodeOverlayMissing, "selected overlay does not exist" } else if errors.Is(err, fs.ErrPermission) { - code, message = CodeImportPermission, "selected profile cannot be accessed" + code, message = CodeImportPermission, "selected overlay cannot be accessed" } return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(code, message, nil)} } if !containedPath(rootPath, canonical) { - return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeProfileOutsideRoot, "selected profile resolves outside the configuration root", nil)} + return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayOutsideRoot, "selected overlay resolves outside the configuration root", nil)} } for _, existing := range sources { if existing.Path != "" && existing.Path == canonical { - return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeProfileRepeated, "selected profile is already present in the base source graph", nil)} + return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayRepeated, "selected overlay is already present in the base source graph", nil)} } } if len(sources)+1 > limits.sources { - return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeProfileLimitExceeded, "selected profile exceeds the source-count limit", nil)} + return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayLimitExceeded, "selected overlay exceeds the source-count limit", nil)} } root, err := os.OpenRoot(rootPath) if err != nil { - value := globFilesystemDiagnostic(err, "configuration root could not be opened for the selected profile", Location{}) + value := globFilesystemDiagnostic(err, "configuration root could not be opened for the selected overlay", Location{}) value.Primary = nil return nil, cloneSourceInfos(sources), []Diagnostic{value} } @@ -74,57 +74,57 @@ func loadProfileDocument(source Source, context *FilesystemContext, main parsedS }() relative, err := filepath.Rel(rootPath, canonical) if err != nil || filepath.IsAbs(relative) || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) { - return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeProfileOutsideRoot, "selected profile could not be addressed inside the configuration root", nil)} + return nil, cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayOutsideRoot, "selected overlay could not be addressed inside the configuration root", nil)} } sourceID := "source-" + strconv.Itoa(len(sources)+1) - profileLocation := &Location{SourceID: sourceID} - profileSource := SourceInfo{ID: sourceID, Label: filepath.ToSlash(filepath.Join("profiles", name+".yml")), Path: canonical, Layer: LayerProfile} - sources = append(cloneSourceInfos(sources), profileSource) - data, diagnostic := readProfileFile(root, relative, sourceID, limits.bytes-base.inputBytes) + overlayLocation := &Location{SourceID: sourceID} + overlaySource := SourceInfo{ID: sourceID, Label: filepath.ToSlash(filepath.Join("overlays", name+".yml")), Path: canonical, Layer: LayerOverlay} + sources = append(cloneSourceInfos(sources), overlaySource) + data, diagnostic := readOverlayFile(root, relative, sourceID, limits.bytes-base.inputBytes) if diagnostic != nil { return nil, sources, []Diagnostic{*diagnostic} } - profileRoot, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{Depth: maxDocumentDepth, Nodes: maxDocumentNodes}) + overlayRoot, issues := yamladapter.Parse(bytes.NewReader(data), sourceID, yamladapter.Limits{Depth: maxDocumentDepth, Nodes: maxDocumentNodes}) if len(issues) != 0 { return nil, sources, parserDiagnostics(sourceID, issues) } - nodeCount := countInputNodes(profileRoot) + nodeCount := countInputNodes(overlayRoot) if base.inputNodes+nodeCount > limits.nodes { - return nil, sources, []Diagnostic{importDiagnostic(CodeProfileLimitExceeded, "selected profile exceeds the aggregate node limit", profileLocation)} + return nil, sources, []Diagnostic{importDiagnostic(CodeOverlayLimitExceeded, "selected overlay exceeds the aggregate node limit", overlayLocation)} } - if diagnostics := profileDirectiveDiagnostics(profileRoot, Path{}); len(diagnostics) != 0 { + if diagnostics := overlayDirectiveDiagnostics(overlayRoot, Path{}); len(diagnostics) != 0 { return nil, sources, diagnostics } - if versionNode := fragmentVersionClaim(profileRoot, rule); versionNode != nil { + if versionNode := fragmentVersionClaim(overlayRoot, rule); versionNode != nil { location := publicLocation(versionNode.Location) return nil, sources, []Diagnostic{{ - Code: CodeProfileVersion, Severity: SeverityError, Path: rule.Path.clone(), - Message: "profiles cannot contribute to or replace the schema-version path", Primary: &location, + Code: CodeOverlayVersion, Severity: SeverityError, Path: rule.Path.clone(), + Message: "overlays cannot contribute to or replace the schema-version path", Primary: &location, }} } closeErr := root.Close() rootClosed = true if closeErr != nil { - return nil, sources, []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed after reading the selected profile", nil)} + return nil, sources, []Diagnostic{importDiagnostic(CodeImportRead, "configuration root could not be closed after reading the selected overlay", nil)} } - profiled, diagnostics := overlayDocuments(base, profileRoot) - if profiled != nil { - profiled.inputBytes = base.inputBytes + int64(len(data)) - profiled.inputNodes = base.inputNodes + nodeCount + overlayd, diagnostics := overlayDocuments(base, overlayRoot) + if overlayd != nil { + overlayd.inputBytes = base.inputBytes + int64(len(data)) + overlayd.inputNodes = base.inputNodes + nodeCount } - return profiled, sources, diagnostics + return overlayd, sources, diagnostics } -func readProfileFile(root *os.Root, relative, sourceID string, remaining int64) ([]byte, *Diagnostic) { +func readOverlayFile(root *os.Root, relative, sourceID string, remaining int64) ([]byte, *Diagnostic) { limit := min(int64(maxSourceBytes), remaining) if limit < 0 { limit = 0 } data, failure := readRegularFile(relative, root.Stat, root.Open, limit) if failure != nil { - value := sourceReadDiagnostic(failure, sourceID, CodeProfileLimitExceeded, "selected profile exceeds a source or aggregate byte limit") + value := sourceReadDiagnostic(failure, sourceID, CodeOverlayLimitExceeded, "selected overlay exceeds a source or aggregate byte limit") return nil, &value } return data, nil diff --git a/internal/engine/profile_test.go b/internal/engine/selected_overlay_test.go similarity index 56% rename from internal/engine/profile_test.go rename to internal/engine/selected_overlay_test.go index 5659ae6..779284b 100644 --- a/internal/engine/profile_test.go +++ b/internal/engine/selected_overlay_test.go @@ -9,7 +9,7 @@ import ( "testing" ) -type profileTestConfig struct { +type overlayTestConfig struct { Settings struct { Kept string `yaml:"kept"` Changed string `yaml:"changed"` @@ -21,7 +21,7 @@ type profileTestConfig struct { Schema map[string]string `yaml:"schema"` } -func TestLoadAppliesSelectedProfileAfterBaseAndBeforeDefaults(t *testing.T) { +func TestLoadAppliesSelectedOverlayAfterBaseAndBeforeDefaults(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", `configkit: imports: [{path: base.yml}] @@ -33,19 +33,19 @@ nullable: base value: {child: base} `) basePath := writeImportFile(t, directory, "base.yml", "settings: {added: imported}\n") - profilePath := writeImportFile(t, directory, "profiles/testing.yml", `settings: - changed: profile -values: [profile] + overlayPath := writeImportFile(t, directory, "overlays/testing.yml", `settings: + changed: overlay +values: [overlay] nullable: null value: scalar `) - result, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{ + result, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{ Imports: true, - Profile: "testing", - Definition: Definition[profileTestConfig]{Defaults: func() profileTestConfig { + Overlay: "testing", + Definition: Definition[overlayTestConfig]{Defaults: func() overlayTestConfig { defaultValue := "default" - return profileTestConfig{ + return overlayTestConfig{ Values: []string{"default"}, Nullable: &defaultValue, Settings: struct { Kept string `yaml:"kept"` @@ -58,31 +58,31 @@ value: scalar if err != nil { t.Fatal(err) } - if result.Config.Settings.Kept != "main" || result.Config.Settings.Changed != "profile" || result.Config.Settings.Added != "imported" { + if result.Config.Settings.Kept != "main" || result.Config.Settings.Changed != "overlay" || result.Config.Settings.Added != "imported" { t.Fatalf("settings = %#v", result.Config.Settings) } - if !reflect.DeepEqual(result.Config.Values, []string{"profile"}) || result.Config.Nullable != nil || result.Config.Value != "scalar" { - t.Fatalf("profile values = values %#v nullable %#v value %#v", result.Config.Values, result.Config.Nullable, result.Config.Value) + if !reflect.DeepEqual(result.Config.Values, []string{"overlay"}) || result.Config.Nullable != nil || result.Config.Value != "scalar" { + t.Fatalf("overlay values = values %#v nullable %#v value %#v", result.Config.Values, result.Config.Nullable, result.Config.Value) } sources := result.Sources() - wantPaths := []string{canonicalTestPath(t, mainPath), canonicalTestPath(t, basePath), canonicalTestPath(t, profilePath)} + wantPaths := []string{canonicalTestPath(t, mainPath), canonicalTestPath(t, basePath), canonicalTestPath(t, overlayPath)} gotPaths := make([]string, len(sources)) for index, source := range sources { gotPaths[index] = source.Path } - if !reflect.DeepEqual(gotPaths, wantPaths) || sources[2].Layer != LayerProfile { + if !reflect.DeepEqual(gotPaths, wantPaths) || sources[2].Layer != LayerOverlay { t.Fatalf("sources = %#v", sources) } changed, _ := result.Metadata().Lookup(Keys("settings", "changed")) kept, _ := result.Metadata().Lookup(Keys("settings", "kept")) - if changed.Origin.Layer != LayerProfile || changed.Origin.Locations[0].SourceID != "source-3" || kept.Origin.Layer != LayerBase { + if changed.Origin.Layer != LayerOverlay || changed.Origin.Locations[0].SourceID != "source-3" || kept.Origin.Layer != LayerBase { t.Fatalf("origins = changed %#v kept %#v", changed, kept) } if _, ok := result.Metadata().Lookup(Keys("value", "child")); ok { t.Fatal("removed child remains effective") } childHistory := result.Metadata().History(Keys("value", "child")) - if len(childHistory) < 2 || childHistory[len(childHistory)-1].Path.String() != "/value" || childHistory[len(childHistory)-1].Origin.Layer != LayerProfile { + if len(childHistory) < 2 || childHistory[len(childHistory)-1].Path.String() != "/value" || childHistory[len(childHistory)-1].Origin.Layer != LayerOverlay { t.Fatalf("removed child history = %#v", childHistory) } if _, ok := result.Metadata().Lookup(Keys("values").Index(1)); ok { @@ -94,34 +94,34 @@ value: scalar } } -func TestLoadProfileSelectionUsesOneFixedYAMLFile(t *testing.T) { +func TestLoadOverlaySelectionUsesOneFixedYAMLFile(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "settings: {kept: base}\n") - writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: selected}\n") - writeImportFile(t, directory, "profiles/default.yml", "settings: {changed: forbidden}\n") + writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: selected}\n") + writeImportFile(t, directory, "overlays/default.yml", "settings: {changed: forbidden}\n") for _, name := range []string{"", "default"} { - result, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: name}) + result, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: name}) if err != nil || result.Config.Settings.Changed != "" || len(result.Sources()) != 1 { - t.Fatalf("inactive profile %q = %#v, %v", name, result, err) + t.Fatalf("inactive overlay %q = %#v, %v", name, result, err) } } - selected, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing"}) + selected, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: "testing"}) if err != nil || selected.Config.Settings.Changed != "selected" || len(selected.Sources()) != 2 { - t.Fatalf("selected profile = %#v, %v", selected, err) + t.Fatalf("selected overlay = %#v, %v", selected, err) } for _, name := range []string{"../testing", "testing.yml", "one,two", "with space", "é", "bad\x00name"} { t.Run(name, func(t *testing.T) { - requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: name}, CodeOptionsInvalid) + requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: name}, CodeOptionsInvalid) }) } - requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "missing"}, CodeProfileMissing) - writeImportFile(t, directory, "profiles/fallback.yaml", "settings: {changed: fallback}\n") - requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "fallback"}, CodeProfileMissing) + requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "missing"}, CodeOverlayMissing) + writeImportFile(t, directory, "overlays/fallback.yaml", "settings: {changed: fallback}\n") + requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "fallback"}, CodeOverlayMissing) } -func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) { +func TestLoadOverlayRejectsDirectivesVersionsAndBaseAliases(t *testing.T) { versionRule := &VersionRule{Path: Keys("schema", "version"), Check: func(version Version, _ *Validation) error { if !version.Present || version.Text != "4" { return errors.New("unexpected version") @@ -130,55 +130,55 @@ func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) { }} for _, test := range []struct { name string - profile string + overlay string code Code }{ - {name: "root directive", profile: "configkit: {}\n", code: CodeProfileDirective}, - {name: "nested directive", profile: "settings: {configkit: {}}\n", code: CodeProfileDirective}, - {name: "equal integer version", profile: "schema: {version: 4}\n", code: CodeProfileVersion}, - {name: "ancestor scalar", profile: "schema: null\n", code: CodeProfileVersion}, + {name: "root directive", overlay: "configkit: {}\n", code: CodeOverlayDirective}, + {name: "nested directive", overlay: "settings: {configkit: {}}\n", code: CodeOverlayDirective}, + {name: "equal integer version", overlay: "schema: {version: 4}\n", code: CodeOverlayVersion}, + {name: "ancestor scalar", overlay: "schema: null\n", code: CodeOverlayVersion}, } { t.Run(test.name, func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: 4}\n") - writeImportFile(t, directory, "profiles/testing.yml", test.profile) - requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "testing", Definition: Definition[profileTestConfig]{Version: versionRule}}, test.code) + writeImportFile(t, directory, "overlays/testing.yml", test.overlay) + requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing", Definition: Definition[overlayTestConfig]{Version: versionRule}}, test.code) }) } t.Run("mapping sibling is allowed", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: 4}\n") - writeImportFile(t, directory, "profiles/testing.yml", "schema: {other: allowed}\n") - result, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing", Definition: Definition[profileTestConfig]{Version: versionRule}}) + writeImportFile(t, directory, "overlays/testing.yml", "schema: {other: allowed}\n") + result, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: "testing", Definition: Definition[overlayTestConfig]{Version: versionRule}}) if err != nil || result.Config.Schema["other"] != "allowed" { t.Fatalf("mapping sibling = %#v, %v", result, err) } }) - t.Run("profile aliases imported base", func(t *testing.T) { + t.Run("overlay aliases imported base", func(t *testing.T) { directory := t.TempDir() - 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) + mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: overlays/testing.yml}]}\n") + writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: value}\n") + requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Imports: true, Overlay: "testing"}, CodeOverlayRepeated) }) } -func TestLoadProfileKeepsBaseFailuresAndFilesystemContainment(t *testing.T) { - t.Run("base parse precedes profile", func(t *testing.T) { +func TestLoadOverlayKeepsBaseFailuresAndFilesystemContainment(t *testing.T) { + t.Run("base parse precedes overlay", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "settings: [\n") - writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: profile}\n") - requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "testing"}, CodeYAMLInvalid) + writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: overlay}\n") + requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing"}, CodeYAMLInvalid) }) - t.Run("profile parse", func(t *testing.T) { + t.Run("overlay parse", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "{}\n") - writeImportFile(t, directory, "profiles/testing.yml", "settings: [\n") - loadError := requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "testing"}, CodeYAMLInvalid) + writeImportFile(t, directory, "overlays/testing.yml", "settings: [\n") + loadError := requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing"}, CodeYAMLInvalid) if loadError.Diagnostics()[0].Primary.SourceID != "source-2" || len(loadError.Sources()) != 2 { - t.Fatalf("profile parse attribution = %#v / %#v", loadError.Diagnostics(), loadError.Sources()) + t.Fatalf("overlay parse attribution = %#v / %#v", loadError.Diagnostics(), loadError.Sources()) } }) @@ -186,32 +186,32 @@ func TestLoadProfileKeepsBaseFailuresAndFilesystemContainment(t *testing.T) { t.Run("symlink escape", func(t *testing.T) { root := t.TempDir() outside := t.TempDir() - outsideProfile := writeImportFile(t, outside, "testing.yml", "settings: {changed: outside}\n") - if err := os.Mkdir(filepath.Join(root, "profiles"), 0o755); err != nil { + outsideOverlay := writeImportFile(t, outside, "testing.yml", "settings: {changed: outside}\n") + if err := os.Mkdir(filepath.Join(root, "overlays"), 0o755); err != nil { t.Fatal(err) } - if err := os.Symlink(outsideProfile, filepath.Join(root, "profiles/testing.yml")); err != nil { + if err := os.Symlink(outsideOverlay, filepath.Join(root, "overlays/testing.yml")); err != nil { t.Skipf("symlinks unavailable: %v", err) } mainPath := writeImportFile(t, root, "main.yml", "{}\n") - requireProfileLoadCode(t, File(mainPath), Options[profileTestConfig]{Profile: "testing"}, CodeProfileOutsideRoot) + requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: "testing"}, CodeOverlayOutsideRoot) }) } t.Run("byte source context", func(t *testing.T) { directory := t.TempDir() - writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: selected}\n") - result, err := testLoad[profileTestConfig](Bytes("s3://bucket/main.yml", []byte("{}\n")), Options[profileTestConfig]{ - Profile: "testing", Filesystem: &FilesystemContext{BaseDir: directory, Root: directory}, + writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: selected}\n") + result, err := testLoad[overlayTestConfig](Bytes("s3://bucket/main.yml", []byte("{}\n")), Options[overlayTestConfig]{ + Overlay: "testing", Filesystem: &FilesystemContext{BaseDir: directory, Root: directory}, }) if err != nil || result.Config.Settings.Changed != "selected" { - t.Fatalf("byte profile = %#v, %v", result, err) + t.Fatalf("byte overlay = %#v, %v", result, err) } - requireProfileLoadCode(t, Bytes("memory.yml", []byte("{}\n")), Options[profileTestConfig]{Profile: "testing"}, CodeOptionsInvalid) + requireOverlayLoadCode(t, Bytes("memory.yml", []byte("{}\n")), Options[overlayTestConfig]{Overlay: "testing"}, CodeOptionsInvalid) }) } -func TestLoadProfileCountsAgainstAggregateBudgets(t *testing.T) { +func TestLoadOverlayCountsAgainstAggregateBudgets(t *testing.T) { for _, test := range []struct { name string limits func(parsedSource, int) importLimits @@ -219,8 +219,8 @@ func TestLoadProfileCountsAgainstAggregateBudgets(t *testing.T) { {name: "source count", limits: func(_ parsedSource, _ int) importLimits { return importLimits{sources: 1, bytes: maxTotalBytes, nodes: maxDocumentNodes} }}, - {name: "aggregate bytes", limits: func(main parsedSource, profileBytes int) importLimits { - return importLimits{sources: 2, bytes: main.bytes + int64(profileBytes) - 1, nodes: maxDocumentNodes} + {name: "aggregate bytes", limits: func(main parsedSource, overlayBytes int) importLimits { + return importLimits{sources: 2, bytes: main.bytes + int64(overlayBytes) - 1, nodes: maxDocumentNodes} }}, {name: "aggregate nodes", limits: func(main parsedSource, _ int) importLimits { return importLimits{sources: 2, bytes: maxTotalBytes, nodes: countInputNodes(main.document) + 1} @@ -229,8 +229,8 @@ func TestLoadProfileCountsAgainstAggregateBudgets(t *testing.T) { t.Run(test.name, func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "{}\n") - profileData := "settings: {changed: selected}\n" - writeImportFile(t, directory, "profiles/testing.yml", profileData) + overlayData := "settings: {changed: selected}\n" + writeImportFile(t, directory, "overlays/testing.yml", overlayData) main, diagnostics := parseSource(File(mainPath), "source-1", defaultParseLimits()) if len(diagnostics) != 0 { t.Fatal(diagnostics) @@ -241,17 +241,17 @@ func TestLoadProfileCountsAgainstAggregateBudgets(t *testing.T) { } base.inputBytes = main.bytes base.inputNodes = countInputNodes(main.document) - _, _, diagnostics = loadProfileDocument(File(mainPath), nil, main, base, sourceInfos(main.source), "testing", nil, test.limits(main, len(profileData))) - if len(diagnostics) == 0 || diagnostics[0].Code != CodeProfileLimitExceeded { + _, _, diagnostics = loadSelectedOverlayDocument(File(mainPath), nil, main, base, sourceInfos(main.source), "testing", nil, test.limits(main, len(overlayData))) + if len(diagnostics) == 0 || diagnostics[0].Code != CodeOverlayLimitExceeded { t.Fatalf("diagnostics = %#v", diagnostics) } }) } } -func requireProfileLoadCode(t *testing.T, source Source, options Options[profileTestConfig], code Code) *LoadError { +func requireOverlayLoadCode(t *testing.T, source Source, options Options[overlayTestConfig], code Code) *LoadError { t.Helper() - result, err := testLoad[profileTestConfig](source, options) + result, err := testLoad[overlayTestConfig](source, options) if result != nil || err == nil { t.Fatalf("result, error = %#v, %v", result, err) } diff --git a/internal/engine/source_fifo_test.go b/internal/engine/source_fifo_test.go index a80d404..dd54d02 100644 --- a/internal/engine/source_fifo_test.go +++ b/internal/engine/source_fifo_test.go @@ -23,8 +23,8 @@ func TestNonregularSourcesDoNotBlock(t *testing.T) { source = File(filepath.Join(root, "pipe.yml")) case "import": opts.Imports = true - case "profile": - opts.Profile = "fifo" + case "overlay": + opts.Overlay = "fifo" } _, err := testLoad(source, opts) var loadErr *LoadError @@ -38,7 +38,7 @@ func TestNonregularSourcesDoNotBlock(t *testing.T) { } t.Fatalf("expected nonregular classification, got %v", loadErr.Diagnostics()) } - for _, mode := range []string{"file", "import", "profile"} { + for _, mode := range []string{"file", "import", "overlay"} { t.Run(mode, func(t *testing.T) { root := t.TempDir() input := "value: 1\n" @@ -48,12 +48,12 @@ func TestNonregularSourcesDoNotBlock(t *testing.T) { if err := os.WriteFile(filepath.Join(root, "config.yml"), []byte(input), 0600); err != nil { t.Fatal(err) } - if err := os.Mkdir(filepath.Join(root, "profiles"), 0700); err != nil { + if err := os.Mkdir(filepath.Join(root, "overlays"), 0700); err != nil { t.Fatal(err) } fifo := filepath.Join(root, "pipe.yml") - if mode == "profile" { - fifo = filepath.Join(root, "profiles", "fifo.yml") + if mode == "overlay" { + fifo = filepath.Join(root, "overlays", "fifo.yml") } if err := syscall.Mkfifo(fifo, 0600); err != nil { t.Fatal(err) diff --git a/lifecycle.go b/lifecycle.go index ae93c1a..c0ca1a3 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -18,17 +18,17 @@ type Definition[T any] struct { } // Options contains the application definition and per-load input selections. -// Imports enables explicit path and glob imports; Profile selects one fixed profile -// overlay; Overrides contains only values explicitly supplied by an application CLI. +// Imports enables explicit path and glob imports; Overlay selects one fixed overlay; +// Overrides contains only values explicitly supplied by an application CLI. type Options[T any] struct { Definition Definition[T] Imports bool - Profile string + Overlay string Filesystem *FilesystemContext Overrides []Override } -// FilesystemContext supplies filesystem roots for imports and profiles. +// FilesystemContext supplies filesystem roots for imports and overlays. type FilesystemContext struct { BaseDir string Root string @@ -75,7 +75,7 @@ type Result[T any] struct { } // Load parses and strictly decodes zero or one selected source, explicitly enabled -// imports, and one selected profile. It accepts at most one Options value and +// imports, and one selected overlay. It accepts at most one Options value and // returns no result when any error is reported. func Load[T any](source Source, supplied ...Options[T]) (*Result[T], error) { options := make([]engine.Options[T], len(supplied)) @@ -219,7 +219,7 @@ func toEngineOptions[T any](options Options[T]) engine.Options[T] { result := engine.Options[T]{ Definition: toEngineDefinition(options.Definition), Imports: options.Imports, - Profile: options.Profile, + Overlay: options.Overlay, } if options.Filesystem != nil { result.Filesystem = &engine.FilesystemContext{BaseDir: options.Filesystem.BaseDir, Root: options.Filesystem.Root} diff --git a/records.go b/records.go index a9738f3..fa7cec6 100644 --- a/records.go +++ b/records.go @@ -6,7 +6,7 @@ type Layer string const ( LayerDefault Layer = "default" LayerBase Layer = "base" - LayerProfile Layer = "profile" + LayerOverlay Layer = "overlay" LayerCLI Layer = "cli" LayerResolution Layer = "resolution" ) @@ -90,13 +90,13 @@ const ( CodeImportLimitExceeded Code = "import.limit_exceeded" CodeImportVersion Code = "import.version" - CodeProfileInvalid Code = "profile.invalid" - CodeProfileMissing Code = "profile.missing" - CodeProfileOutsideRoot Code = "profile.outside_root" - CodeProfileRepeated Code = "profile.repeated" - CodeProfileDirective Code = "profile.directive" - CodeProfileVersion Code = "profile.version" - CodeProfileLimitExceeded Code = "profile.limit_exceeded" + CodeOverlayInvalid Code = "overlay.invalid" + CodeOverlayMissing Code = "overlay.missing" + CodeOverlayOutsideRoot Code = "overlay.outside_root" + CodeOverlayRepeated Code = "overlay.repeated" + CodeOverlayDirective Code = "overlay.directive" + CodeOverlayVersion Code = "overlay.version" + CodeOverlayLimitExceeded Code = "overlay.limit_exceeded" CodeComposeInvalid Code = "compose.invalid" CodeComposeConflict Code = "compose.conflict"