Add overlay migration regression coverage
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Configuration Overlay Rename Implementation Plan
|
# Configuration Overlay Rename Implementation Plan
|
||||||
|
|
||||||
Status: Stage 1 complete. Execute Stage 2.
|
Status: complete.
|
||||||
|
|
||||||
## Scope and operating instructions
|
## Scope and operating instructions
|
||||||
|
|
||||||
@@ -188,3 +188,23 @@ Report the stage commit, validation results, any skipped platform-specific cases
|
|||||||
and the API/filesystem migration summary. The implementation is complete only
|
and the API/filesystem migration summary. The implementation is complete only
|
||||||
when both stages pass and the roadmap's target surface and preserved behavior
|
when both stages pass and the roadmap's target surface and preserved behavior
|
||||||
are accounted for. There are no unresolved product questions in this plan.
|
are accounted for. There are no unresolved product questions in this plan.
|
||||||
|
|
||||||
|
### Completion
|
||||||
|
|
||||||
|
Completed 2026-09-23. Added regression coverage that accepts only
|
||||||
|
`overlays/<name>.yml` for selection, rejects a legacy-only selected file with
|
||||||
|
`CodeOverlayMissing`, and ignores a legacy sibling when the new selected file is
|
||||||
|
present. Explicit imports from a directory named `profiles` remain base-layer
|
||||||
|
imports. The maintained consumer confirms `ModelProfile` is unchanged while its
|
||||||
|
selected source reports `overlays/testing.yml` and `LayerOverlay`; public
|
||||||
|
layer/code strings and rendered explanation values are covered.
|
||||||
|
|
||||||
|
Validation passed with `./scripts/check.sh` and `GOPROXY=off go test -race
|
||||||
|
./internal/engine` on Go 1.26.8/linux. No additional platform-specific validation
|
||||||
|
was required; existing symlink coverage remains conditional on Linux or macOS.
|
||||||
|
The API audit found no legacy Go declarations, and the remaining `profile`
|
||||||
|
references are intentionally limited to application-domain fields, upstream or
|
||||||
|
historical records, migration guidance, and explicit legacy-path regressions. The
|
||||||
|
feature roadmap is now a completion summary; v0.4.0 release preparation must still
|
||||||
|
describe the Go API, selected-file directory, provenance layer, and diagnostic-code
|
||||||
|
breaks.
|
||||||
|
|||||||
@@ -1,139 +1,20 @@
|
|||||||
# Rename Configuration Profiles to Overlays
|
# Configuration Overlay Rename
|
||||||
|
|
||||||
Status: approved target, not yet implemented. Execution steps and stage status
|
Status: implementation complete; release preparation remains separate.
|
||||||
belong to the [implementation plan](implementation.md).
|
|
||||||
|
|
||||||
## Purpose and release scope
|
ConfigKit now calls the selected configuration feature an overlay. The current
|
||||||
|
behavior is owned by the [selected-overlay contract](../config.md#selected-overlays),
|
||||||
|
and upgrade instructions are in the [consumer migration guide](../consumers/README.md#selected-overlay-migration).
|
||||||
|
|
||||||
ConfigKit's name "profile" collides with application-owned model, pipeline, and
|
The completed migration intentionally changed four breaking surfaces:
|
||||||
execution profiles. The feature already applies a named configuration overlay;
|
|
||||||
use that name directly instead of requiring consumers to distinguish another
|
|
||||||
kind of profile.
|
|
||||||
|
|
||||||
The maintainer has approved a breaking rename for the next minor release,
|
- Go API identifiers, including `Options.Overlay`, `LayerOverlay`, and
|
||||||
intended as v0.4.0. There will be no backward-compatibility aliases, deprecated
|
`CodeOverlay*`;
|
||||||
fields, dual diagnostic names, or legacy-directory fallback. This roadmap does
|
- selected-file location to `overlays/<name>.yml` without a `profiles/` fallback;
|
||||||
not authorize publication or changes to downstream repositories.
|
- provenance layer value to `overlay`; and
|
||||||
|
- diagnostic-code strings to `overlay.*`.
|
||||||
|
|
||||||
The baseline is ConfigKit at `aead20f`, following v0.3.1 and the release-workflow
|
Application-owned profile concepts, including `ModelProfile` and `model_profile`,
|
||||||
simplification. Current behavior is owned by the
|
remain unchanged. A future v0.4.0 release-preparation task must describe all four
|
||||||
[selected-overlay contract](../config.md#selected-overlays). This roadmap owns
|
breaking surfaces. Versioning, release notes, tagging, publishing, dependency
|
||||||
the unimplemented rename; it is not a new current API reference.
|
updates, and downstream changes are not part of this completed implementation.
|
||||||
|
|
||||||
## Target terminology and public surface
|
|
||||||
|
|
||||||
Use **configuration overlay** in introductory prose and **selected overlay** or
|
|
||||||
**overlay** where context is clear. An overlay is the named configuration input;
|
|
||||||
overlaying is also the merge operation. CLI overrides may use that operation
|
|
||||||
without becoming named configuration overlays or changing their CLI provenance.
|
|
||||||
|
|
||||||
| Current surface | Target surface |
|
|
||||||
| --- | --- |
|
|
||||||
| `Options[T].Profile string` | `Options[T].Overlay string` |
|
|
||||||
| `<configuration-root>/profiles/<name>.yml` | `<configuration-root>/overlays/<name>.yml` |
|
|
||||||
| Source label `profiles/<name>.yml` | Source label `overlays/<name>.yml` |
|
|
||||||
| `LayerProfile`, value `"profile"` | `LayerOverlay`, value `"overlay"` |
|
|
||||||
| `CodeProfileInvalid`, `profile.invalid` | `CodeOverlayInvalid`, `overlay.invalid` |
|
|
||||||
| `CodeProfileMissing`, `profile.missing` | `CodeOverlayMissing`, `overlay.missing` |
|
|
||||||
| `CodeProfileOutsideRoot`, `profile.outside_root` | `CodeOverlayOutsideRoot`, `overlay.outside_root` |
|
|
||||||
| `CodeProfileRepeated`, `profile.repeated` | `CodeOverlayRepeated`, `overlay.repeated` |
|
|
||||||
| `CodeProfileDirective`, `profile.directive` | `CodeOverlayDirective`, `overlay.directive` |
|
|
||||||
| `CodeProfileVersion`, `profile.version` | `CodeOverlayVersion`, `overlay.version` |
|
|
||||||
| `CodeProfileLimitExceeded`, `profile.limit_exceeded` | `CodeOverlayLimitExceeded`, `overlay.limit_exceeded` |
|
|
||||||
| Maintained example `LoadNarratioProfile` | `LoadNarratioOverlay` |
|
|
||||||
|
|
||||||
Apply these changes to public declarations, engine counterparts, facade
|
|
||||||
forwarding/conversion, diagnostics, source records, origin/history records, and
|
|
||||||
rendered explanations. Messages describing this ConfigKit feature use "overlay".
|
|
||||||
Serialization and output that carry the layer or diagnostic-code strings use
|
|
||||||
the new values. No legacy values are emitted for compatibility.
|
|
||||||
|
|
||||||
Not every error during loading currently has a `profile.*` code: shared options,
|
|
||||||
source, import, and parser classifications remain unchanged. This is not an
|
|
||||||
opportunity to redesign error categories or add new diagnostic guarantees.
|
|
||||||
|
|
||||||
## Behavior preserved
|
|
||||||
|
|
||||||
This is a naming migration, not a new layering model. Preserve the existing
|
|
||||||
contract except for the explicitly renamed surfaces:
|
|
||||||
|
|
||||||
- Select at most one overlay by name, not a path or list. Preserve the current
|
|
||||||
ASCII name grammar, case sensitivity, and lack of extension fallback.
|
|
||||||
- Empty selection and exact lowercase `default` continue to mean no selection.
|
|
||||||
`overlays/default.yml` is not implicitly loaded. The sentinel is existing
|
|
||||||
behavior, not a compatibility alias for the old feature name.
|
|
||||||
- Load the selected mapping after strict base composition and schema-version
|
|
||||||
checks, before CLI overrides and defaults/typed decoding. Preserve existing
|
|
||||||
precedence, replacement rules, explicit null/empty behavior, and error ordering.
|
|
||||||
- Preserve filesystem-context requirements, canonical containment and duplicate
|
|
||||||
source checks, aggregate limits, directive/version-path rules, and `NoSource`
|
|
||||||
option validation. Do not strengthen or relax them as part of this rename.
|
|
||||||
- Preserve source-ID ordering, detached metadata, history, provenance queries,
|
|
||||||
callback lifetime, secret handling, and load independence. Only the named
|
|
||||||
layer, selected-file labels/paths, and corresponding diagnostic terminology
|
|
||||||
change where specified above.
|
|
||||||
- Keep CLI overrides and strict composition distinct. Do not add overlay stacks,
|
|
||||||
inheritance, discovery, arbitrary file selection, or new merge options.
|
|
||||||
|
|
||||||
## No backward compatibility
|
|
||||||
|
|
||||||
Old Go identifiers are removed rather than retained as aliases. Applications
|
|
||||||
using them must update before they compile against the new minor version.
|
|
||||||
|
|
||||||
Selection searches only `overlays/<name>.yml`. A file present solely in
|
|
||||||
`profiles/` does not satisfy selection and produces the corresponding new
|
|
||||||
overlay diagnostic. No automatic rename, migration, or search of both locations
|
|
||||||
is performed. If both directories exist, only the selected path under
|
|
||||||
`overlays/` participates in this feature.
|
|
||||||
|
|
||||||
Do not introduce a prohibition against a directory named `profiles`, literal
|
|
||||||
strings containing "profile", or application fields such as `model_profile`.
|
|
||||||
Ordinary explicit imports retain their existing rules regardless of a file's
|
|
||||||
directory name. Removing legacy selection support does not reserve or ban these
|
|
||||||
names elsewhere.
|
|
||||||
|
|
||||||
## Internal architecture and maintained examples
|
|
||||||
|
|
||||||
The public facade retains root-owned named types and inward-only dependencies.
|
|
||||||
Selected-file acquisition and the existing overlay merge mechanism remain
|
|
||||||
distinct responsibilities within the engine. Internal naming reflects the new
|
|
||||||
terminology without a pipeline or merge-algorithm redesign.
|
|
||||||
|
|
||||||
Maintained examples use the new API and selected-file directory. The Narratio
|
|
||||||
example still has an application-owned `ModelProfile` / `model_profile`; that
|
|
||||||
concept is deliberately independent of ConfigKit overlay selection.
|
|
||||||
|
|
||||||
## Documentation and migration contract
|
|
||||||
|
|
||||||
Current public comments, configuration documentation, consumer guidance,
|
|
||||||
architecture terminology, and implementation descriptions consistently call the
|
|
||||||
feature an overlay. Accepted ADR decisions and historical release notes retain
|
|
||||||
their historical wording. The extraction inventory preserves literal upstream
|
|
||||||
symbols and revision-specific evidence while distinguishing current ConfigKit
|
|
||||||
implementation names. Historical navigation links may change when their targets
|
|
||||||
move; historical decisions do not change.
|
|
||||||
|
|
||||||
Consumer migration consists of updating Go identifiers, moving selected files to
|
|
||||||
`overlays/`, and updating any handling of the renamed provenance-layer and
|
|
||||||
diagnostic-code strings. Applications own any corresponding CLI flag rename;
|
|
||||||
ConfigKit does not define or modify downstream CLIs.
|
|
||||||
|
|
||||||
The future minor-release note must explain those four breaking surfaces. Release
|
|
||||||
preparation and publication remain governed by the [release procedure](../release.md);
|
|
||||||
this roadmap does not create a release candidate or introduce additional gates.
|
|
||||||
|
|
||||||
## Completion criteria
|
|
||||||
|
|
||||||
The rename is complete when all maintained consumers compile against the new
|
|
||||||
API, selection and reporting consistently use the new terminology, the legacy
|
|
||||||
feature surface is absent, and regression coverage confirms unchanged semantics
|
|
||||||
and no directory fallback. Remaining "profile" references must be intentional
|
|
||||||
application data, migration explanation, or historical records, not active
|
|
||||||
ConfigKit feature names.
|
|
||||||
|
|
||||||
Tests establish the new selection directory, absence of legacy fallback, and
|
|
||||||
preservation of existing semantics. Existing coverage remains with its owning
|
|
||||||
layer instead of being duplicated across the facade and engine.
|
|
||||||
|
|
||||||
After implementation, current contracts own the renamed behavior and this
|
|
||||||
roadmap becomes a completion record for retirement during release preparation.
|
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ func TestMaintainedConsumerFixturesPreserveDomainOutcomes(t *testing.T) {
|
|||||||
overlaid.Config.ModelProfile != "concise" || len(overlaid.Sources()) != 3 {
|
overlaid.Config.ModelProfile != "concise" || len(overlaid.Sources()) != 3 {
|
||||||
t.Fatalf("configuration overlay and application model profile = %#v, %v", overlaid, err)
|
t.Fatalf("configuration overlay and application model profile = %#v, %v", overlaid, err)
|
||||||
}
|
}
|
||||||
|
if source := overlaid.Sources()[2]; source.Label != "overlays/testing.yml" || source.Layer != configkit.LayerOverlay {
|
||||||
|
t.Fatalf("Narratio overlay source = %#v", source)
|
||||||
|
}
|
||||||
|
|
||||||
notarius, err := consumers.LoadNotarius(fixture("notarius", "config.yml"))
|
notarius, err := consumers.LoadNotarius(fixture("notarius", "config.yml"))
|
||||||
window, converted := consumers.NotariusWindow(notarius.Config, 0)
|
window, converted := consumers.NotariusWindow(notarius.Config, 0)
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ func TestExplainShowsDefaultBaseAndEffectiveOverlayValue(t *testing.T) {
|
|||||||
explanation.Origin.Locations[0].Line != 2 {
|
explanation.Origin.Locations[0].Line != 2 {
|
||||||
t.Fatalf("effective explanation = %#v", explanation)
|
t.Fatalf("effective explanation = %#v", explanation)
|
||||||
}
|
}
|
||||||
|
var rendered bytes.Buffer
|
||||||
|
if err := result.WriteExplanation(&rendered, Keys("transcription", "concurrency")); err != nil ||
|
||||||
|
!strings.Contains(rendered.String(), `layer="overlay"`) ||
|
||||||
|
!strings.Contains(rendered.String(), `label="overlays/testing.yml"`) {
|
||||||
|
t.Fatalf("rendered explanation = %v\n%s", err, rendered.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type explanationConfig struct {
|
type explanationConfig struct {
|
||||||
|
|||||||
@@ -89,6 +89,18 @@ settings:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadImportsFromProfilesDirectoryAsBaseInput(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: profiles/shared.yml}]}\n")
|
||||||
|
writeImportFile(t, directory, "profiles/shared.yml", "settings: {root: imported}\n")
|
||||||
|
|
||||||
|
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||||
|
if err != nil || result.Config.Settings.Root != "imported" || len(result.Sources()) != 2 ||
|
||||||
|
result.Sources()[1].Layer != LayerBase {
|
||||||
|
t.Fatalf("profiles import = %#v, %v", result, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadRejectsImportConflictsAndFragmentVersions(t *testing.T) {
|
func TestLoadRejectsImportConflictsAndFragmentVersions(t *testing.T) {
|
||||||
t.Run("all duplicate claimants", func(t *testing.T) {
|
t.Run("all duplicate claimants", func(t *testing.T) {
|
||||||
directory := t.TempDir()
|
directory := t.TempDir()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func overlayDocuments(base *composedDocument, overlay *document.Node) (*composed
|
|||||||
if base == nil || base.root == nil || base.root.Kind != document.Mapping || overlay == nil || overlay.Kind != document.Mapping {
|
if base == nil || base.root == nil || base.root.Kind != document.Mapping || overlay == nil || overlay.Kind != document.Mapping {
|
||||||
return nil, []Diagnostic{{
|
return nil, []Diagnostic{{
|
||||||
Code: CodeOverlayInvalid, Severity: SeverityError,
|
Code: CodeOverlayInvalid, Severity: SeverityError,
|
||||||
Message: "overlay overlay requires mapping document roots",
|
Message: "overlay requires mapping document roots",
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
result := cloneComposedDocument(base)
|
result := cloneComposedDocument(base)
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ func TestLoadOverlaySelectionUsesOneFixedYAMLFile(t *testing.T) {
|
|||||||
directory := t.TempDir()
|
directory := t.TempDir()
|
||||||
mainPath := writeImportFile(t, directory, "main.yml", "settings: {kept: base}\n")
|
mainPath := writeImportFile(t, directory, "main.yml", "settings: {kept: base}\n")
|
||||||
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: selected}\n")
|
writeImportFile(t, directory, "overlays/testing.yml", "settings: {changed: selected}\n")
|
||||||
|
writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: legacy}\n")
|
||||||
writeImportFile(t, directory, "overlays/default.yml", "settings: {changed: forbidden}\n")
|
writeImportFile(t, directory, "overlays/default.yml", "settings: {changed: forbidden}\n")
|
||||||
|
|
||||||
for _, name := range []string{"", "default"} {
|
for _, name := range []string{"", "default"} {
|
||||||
@@ -107,10 +108,18 @@ func TestLoadOverlaySelectionUsesOneFixedYAMLFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
selected, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: "testing"})
|
selected, err := testLoad[overlayTestConfig](File(mainPath), Options[overlayTestConfig]{Overlay: "testing"})
|
||||||
if err != nil || selected.Config.Settings.Changed != "selected" || len(selected.Sources()) != 2 {
|
if err != nil || selected.Config.Settings.Changed != "selected" || len(selected.Sources()) != 2 ||
|
||||||
|
selected.Sources()[1].Label != "overlays/testing.yml" || selected.Sources()[1].Layer != LayerOverlay {
|
||||||
t.Fatalf("selected overlay = %#v, %v", selected, err)
|
t.Fatalf("selected overlay = %#v, %v", selected, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Run("legacy directory is not selected", func(t *testing.T) {
|
||||||
|
legacyDirectory := t.TempDir()
|
||||||
|
legacyMain := writeImportFile(t, legacyDirectory, "main.yml", "settings: {kept: base}\n")
|
||||||
|
writeImportFile(t, legacyDirectory, "profiles/testing.yml", "settings: {changed: legacy}\n")
|
||||||
|
requireOverlayLoadCode(t, File(legacyMain), Options[overlayTestConfig]{Overlay: "testing"}, CodeOverlayMissing)
|
||||||
|
})
|
||||||
|
|
||||||
for _, name := range []string{"../testing", "testing.yml", "one,two", "with space", "é", "bad\x00name"} {
|
for _, name := range []string{"../testing", "testing.yml", "one,two", "with space", "é", "bad\x00name"} {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: name}, CodeOptionsInvalid)
|
requireOverlayLoadCode(t, File(mainPath), Options[overlayTestConfig]{Overlay: name}, CodeOptionsInvalid)
|
||||||
|
|||||||
22
records_test.go
Normal file
22
records_test.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package configkit
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestOverlayPublicValues(t *testing.T) {
|
||||||
|
if LayerOverlay != "overlay" {
|
||||||
|
t.Fatalf("LayerOverlay = %q", LayerOverlay)
|
||||||
|
}
|
||||||
|
for code, value := range map[Code]string{
|
||||||
|
CodeOverlayInvalid: "overlay.invalid",
|
||||||
|
CodeOverlayMissing: "overlay.missing",
|
||||||
|
CodeOverlayOutsideRoot: "overlay.outside_root",
|
||||||
|
CodeOverlayRepeated: "overlay.repeated",
|
||||||
|
CodeOverlayDirective: "overlay.directive",
|
||||||
|
CodeOverlayVersion: "overlay.version",
|
||||||
|
CodeOverlayLimitExceeded: "overlay.limit_exceeded",
|
||||||
|
} {
|
||||||
|
if string(code) != value {
|
||||||
|
t.Errorf("code = %q, want %q", code, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user