Rename configuration selection to overlays

This commit is contained in:
2026-09-25 21:46:35 +00:00
parent 8c0e93143f
commit 31b48d2d5e
23 changed files with 171 additions and 110 deletions

View File

@@ -16,7 +16,7 @@ func runConfigEffective(args []string, stdout, stderr io.Writer, opts Options) i
fs := flag.NewFlagSet("config effective", flag.ContinueOnError)
fs.SetOutput(io.Discard)
configPath := fs.String("config", "", "config file path")
profile := fs.String("config-profile", "", "configuration profile")
overlay := fs.String("config-overlay", "", "configuration overlay")
if err := fs.Parse(args); err != nil {
fmt.Fprintf(stderr, "notarius: %v\n", err)
return 2
@@ -25,7 +25,7 @@ func runConfigEffective(args []string, stdout, stderr io.Writer, opts Options) i
fmt.Fprintf(stderr, "notarius: unexpected argument %q\n", fs.Arg(0))
return 2
}
loaded, _, err := loadConfigKit(*configPath, *profile, nil, opts)
loaded, _, err := loadConfigKit(*configPath, *overlay, nil, opts)
if err != nil {
writeConfigLoadError(stderr, err)
return 1
@@ -41,7 +41,7 @@ func runConfigExplain(args []string, stdout, stderr io.Writer, opts Options) int
fs := flag.NewFlagSet("config explain", flag.ContinueOnError)
fs.SetOutput(io.Discard)
configPath := fs.String("config", "", "config file path")
profile := fs.String("config-profile", "", "configuration profile")
overlay := fs.String("config-overlay", "", "configuration overlay")
if err := fs.Parse(args); err != nil {
fmt.Fprintf(stderr, "notarius: %v\n", err)
return 2
@@ -56,7 +56,7 @@ func runConfigExplain(args []string, stdout, stderr io.Writer, opts Options) int
fmt.Fprintf(stderr, "notarius: %q is not a canonical JSON Pointer\n", pointer)
return 2
}
loaded, _, err := loadConfigKit(*configPath, *profile, nil, opts)
loaded, _, err := loadConfigKit(*configPath, *overlay, nil, opts)
if err != nil {
writeConfigLoadError(stderr, err)
return 1
@@ -72,7 +72,7 @@ func runConfigValidate(args []string, stdout, stderr io.Writer, opts Options) in
fs := flag.NewFlagSet("config validate", flag.ContinueOnError)
fs.SetOutput(io.Discard)
configPath := fs.String("config", "", "config file path")
profile := fs.String("config-profile", "", "configuration profile")
overlay := fs.String("config-overlay", "", "configuration overlay")
pipelineID := fs.String("pipeline", "", "pipeline ID")
onlyRaw := fs.String("only", "", "comma-separated artifact lanes")
if err := fs.Parse(args); err != nil {
@@ -92,7 +92,7 @@ func runConfigValidate(args []string, stdout, stderr io.Writer, opts Options) in
fmt.Fprintf(stderr, "notarius: %v\n", err)
return 2
}
loaded, path, err := loadConfigKit(*configPath, *profile, nil, opts)
loaded, path, err := loadConfigKit(*configPath, *overlay, nil, opts)
if err != nil {
writeConfigLoadError(stderr, err)
return 1
@@ -123,7 +123,7 @@ func runPipelinesList(args []string, stdout, stderr io.Writer, opts Options) int
fs := flag.NewFlagSet("pipelines list", flag.ContinueOnError)
fs.SetOutput(io.Discard)
configPath := fs.String("config", "", "config file path")
profile := fs.String("config-profile", "", "configuration profile")
overlay := fs.String("config-overlay", "", "configuration overlay")
jsonOutput := fs.Bool("json", false, "write JSON output")
if err := fs.Parse(args); err != nil {
fmt.Fprintf(stderr, "notarius: %v\n", err)
@@ -133,7 +133,7 @@ func runPipelinesList(args []string, stdout, stderr io.Writer, opts Options) int
fmt.Fprintf(stderr, "notarius: unexpected argument %q\n", fs.Arg(0))
return 2
}
loaded, _, err := loadConfigKit(*configPath, *profile, nil, opts)
loaded, _, err := loadConfigKit(*configPath, *overlay, nil, opts)
if err != nil {
writeConfigLoadError(stderr, err)
return 1

View File

@@ -14,22 +14,49 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
)
func TestConfigKitReadOnlyCommandsLoadImportsAndProfiles(t *testing.T) {
func TestConfigKitReadOnlyCommandsLoadImportsAndOverlays(t *testing.T) {
t.Parallel()
root := t.TempDir()
path := filepath.Join(root, "notarius.yml")
writeConfigKitFile(t, filepath.Join(root, "fragment.yml"), "pipelines:\n imported:\n input: {module: input}\n")
writeConfigKitFile(t, filepath.Join(root, "profiles", "development.yml"), "pipelines:\n profiled:\n input: {module: input}\n")
writeConfigKitFile(t, filepath.Join(root, "overlays", "development.yml"), "pipelines:\n overlaid:\n input: {module: input}\n")
writeConfigKitFile(t, path, "version: 5\nconfigkit:\n imports:\n - path: fragment.yml\npipelines:\n base:\n input: {module: input}\n")
var stdout, stderr bytes.Buffer
if code := RunWithOptions([]string{"pipelines", "list", "--config", path, "--config-profile", "development"}, &stdout, &stderr, commandContractOptions(t)); code != 0 || stdout.String() != "base\nimported\nprofiled\n" {
if code := RunWithOptions([]string{"pipelines", "list", "--config", path, "--config-overlay", "development"}, &stdout, &stderr, commandContractOptions(t)); code != 0 || stdout.String() != "base\nimported\noverlaid\n" {
t.Fatalf("list = %d, %q, %q", code, stdout.String(), stderr.String())
}
stdout.Reset()
stderr.Reset()
if code := RunWithOptions([]string{"config", "validate", "--config", path, "--config-profile", "development"}, &stdout, &stderr, commandContractOptions(t)); code != 0 || !strings.Contains(stdout.String(), "is valid") {
if code := RunWithOptions([]string{"config", "validate", "--config", path, "--config-overlay", "development"}, &stdout, &stderr, commandContractOptions(t)); code != 0 || !strings.Contains(stdout.String(), "is valid") {
t.Fatalf("validate = %d, %q, %q", code, stdout.String(), stderr.String())
}
for _, selection := range [][]string{nil, {"--config-overlay", ""}, {"--config-overlay", "default"}} {
stdout.Reset()
stderr.Reset()
args := append([]string{"pipelines", "list", "--config", path}, selection...)
if code := RunWithOptions(args, &stdout, &stderr, commandContractOptions(t)); code != 0 || stdout.String() != "base\nimported\n" || stderr.Len() != 0 {
t.Fatalf("base list %q = %d, %q, %q", selection, code, stdout.String(), stderr.String())
}
}
}
func TestRetiredConfigProfileFlagUsesNormalUnknownFlagBehavior(t *testing.T) {
t.Parallel()
for _, args := range [][]string{
{"run", "sample", "--config-profile=development"},
{"config", "validate", "--config-profile=development"},
{"config", "effective", "--config-profile=development"},
{"config", "explain", "--config-profile=development", ""},
{"pipelines", "list", "--config-profile=development"},
} {
t.Run(strings.Join(args[:2], " "), func(t *testing.T) {
var stdout, stderr bytes.Buffer
if code := RunWithOptions(args, &stdout, &stderr, commandContractOptions(t)); code != 2 || stdout.Len() != 0 || !strings.Contains(stderr.String(), "not defined") {
t.Fatalf("args=%q code=%d stdout=%q stderr=%q", args, code, stdout.String(), stderr.String())
}
})
}
}
func TestConfigKitReadOnlyCommandsRejectVersionFour(t *testing.T) {
@@ -53,8 +80,8 @@ pipelines:
"0":
input: {module: input}
`)
writeConfigKitFile(t, filepath.Join(root, "profiles", "development.yml"), "concurrency:\n total_llm: 3\n")
writeConfigKitFile(t, filepath.Join(root, "profiles", "retired.yml"), "pipelines: null\n")
writeConfigKitFile(t, filepath.Join(root, "overlays", "development.yml"), "concurrency:\n total_llm: 3\n")
writeConfigKitFile(t, filepath.Join(root, "overlays", "retired.yml"), "pipelines: null\n")
providerCalled := false
opts := commandContractOptions(t)
@@ -64,7 +91,7 @@ pipelines:
}
var stdout, stderr bytes.Buffer
if code := RunWithOptions([]string{"config", "effective", "--config", path, "--config-profile", "development"}, &stdout, &stderr, opts); code != 0 || !strings.Contains(stdout.String(), "total_llm: 3") || stderr.Len() != 0 {
if code := RunWithOptions([]string{"config", "effective", "--config", path, "--config-overlay", "development"}, &stdout, &stderr, opts); code != 0 || !strings.Contains(stdout.String(), "total_llm: 3") || stderr.Len() != 0 {
t.Fatalf("effective = %d, %q, %q", code, stdout.String(), stderr.String())
}
if providerCalled {
@@ -73,7 +100,7 @@ pipelines:
stdout.Reset()
stderr.Reset()
if code := RunWithOptions([]string{"config", "explain", "--config", path, "--config-profile", "development", "/concurrency/total_llm"}, &stdout, &stderr, opts); code != 0 || !strings.Contains(stdout.String(), `path: "/concurrency/total_llm"`) || stderr.Len() != 0 {
if code := RunWithOptions([]string{"config", "explain", "--config", path, "--config-overlay", "development", "/concurrency/total_llm"}, &stdout, &stderr, opts); code != 0 || !strings.Contains(stdout.String(), `path: "/concurrency/total_llm"`) || stderr.Len() != 0 {
t.Fatalf("explain effective = %d, %q, %q", code, stdout.String(), stderr.String())
}
@@ -85,7 +112,7 @@ pipelines:
stdout.Reset()
stderr.Reset()
if code := RunWithOptions([]string{"config", "explain", "--config", path, "--config-profile", "retired", "/pipelines/0"}, &stdout, &stderr, opts); code != 0 || !strings.Contains(stdout.String(), `path: "/pipelines/0"`) || stderr.Len() != 0 {
if code := RunWithOptions([]string{"config", "explain", "--config", path, "--config-overlay", "retired", "/pipelines/0"}, &stdout, &stderr, opts); code != 0 || !strings.Contains(stdout.String(), `path: "/pipelines/0"`) || stderr.Len() != 0 {
t.Fatalf("explain removed mapping key = %d, %q, %q", code, stdout.String(), stderr.String())
}
}

View File

@@ -10,7 +10,7 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
)
func loadConfigKit(configPath, profile string, overrides []configkit.Override, opts Options) (*config.LoadedConfig, string, error) {
func loadConfigKit(configPath, overlay string, overrides []configkit.Override, opts Options) (*config.LoadedConfig, string, error) {
path, err := discoverConfigPath(configPath, opts)
if err != nil {
return nil, "", err
@@ -19,7 +19,7 @@ func loadConfigKit(configPath, profile string, overrides []configkit.Override, o
if err != nil {
return nil, "", fmt.Errorf("determine invocation directory: %w", err)
}
loaded, err := config.Load(path, config.LoadOptions{Profile: profile, Overrides: overrides, InvocationDirectory: workingDirectory})
loaded, err := config.Load(path, config.LoadOptions{Overlay: overlay, Overrides: overrides, InvocationDirectory: workingDirectory})
if err != nil {
return nil, "", err
}
@@ -31,7 +31,7 @@ func writeConfigLoadError(output io.Writer, err error) {
if errors.As(err, &loadError) {
if writeErr := configkit.WriteDiagnostics(output, loadError.Diagnostics()); writeErr == nil {
// ConfigKit's locations contain source IDs. Keep its renderer and
// supply the acquired-source legend so imported/profile errors can
// supply the acquired-source legend so imported/overlay errors can
// be traced to a file. Quoting also escapes terminal controls.
for _, source := range loadError.Sources() {
label := source.Path

View File

@@ -13,16 +13,16 @@ import (
func TestConfigLoadDiagnosticsIdentifyComposedSources(t *testing.T) {
t.Parallel()
for _, profile := range []bool{false, true} {
t.Run(strconv.FormatBool(profile), func(t *testing.T) {
for _, overlay := range []bool{false, true} {
t.Run(strconv.FormatBool(overlay), func(t *testing.T) {
root := t.TempDir()
main := filepath.Join(root, "config.yml")
fragment := filepath.Join(root, "pipeline\n\x1b.yml")
args := []string{"config", "validate", "--config", main}
if profile {
fragment = filepath.Join(root, "profiles", "development.yml")
if overlay {
fragment = filepath.Join(root, "overlays", "development.yml")
writeConfigKitFile(t, main, "version: 5\n")
args = append(args, "--config-profile", "development")
args = append(args, "--config-overlay", "development")
} else {
writeConfigKitFile(t, main, "version: 5\nconfigkit:\n imports:\n - path: "+strconv.Quote(filepath.Base(fragment))+"\n")
}
@@ -43,18 +43,18 @@ func TestConfigLoadDiagnosticsIdentifyComposedSources(t *testing.T) {
}
}
func TestLoadConfigKitUsesDiscoveryProfileAndOverrides(t *testing.T) {
func TestLoadConfigKitUsesDiscoveryOverlayAndOverrides(t *testing.T) {
t.Parallel()
root := t.TempDir()
path := filepath.Join(root, "notarius.yml")
writeConfigKitFile(t, path, "version: 5\npipelines:\n base:\n input: {module: input}\n")
profiles := filepath.Join(root, "profiles")
writeConfigKitFile(t, filepath.Join(profiles, "development.yml"), "pipelines:\n profiled:\n input: {module: input}\n")
overlays := filepath.Join(root, "overlays")
writeConfigKitFile(t, filepath.Join(overlays, "development.yml"), "pipelines:\n overlaid:\n input: {module: input}\n")
loaded, gotPath, err := loadConfigKit(path, "development", []configkit.Override{{Path: configkit.Keys("output", "directory"), Value: "generated", Flag: "--output"}}, commandContractOptions(t))
if err != nil {
t.Fatalf("loadConfigKit() error = %v", err)
}
if gotPath != path || loaded.Config.Pipelines["profiled"].Input.Module != "input" || loaded.Config.Output.Directory == "generated" {
if gotPath != path || loaded.Config.Pipelines["overlaid"].Input.Module != "input" || loaded.Config.Output.Directory == "generated" {
t.Fatalf("loaded = %#v, path=%q", loaded.Config, gotPath)
}
}

View File

@@ -136,19 +136,19 @@ func TestMaintainedConfigurationExampleSet(t *testing.T) {
t.Fatalf("maintained configuration examples = %q, want only the minimal and complete D&D examples", got)
}
profileEntries, err := os.ReadDir(repositoryPath("examples", "profiles"))
overlayEntries, err := os.ReadDir(repositoryPath("examples", "overlays"))
if err != nil {
t.Fatal(err)
}
names = names[:0]
for _, entry := range profileEntries {
for _, entry := range overlayEntries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yml") {
names = append(names, entry.Name())
}
}
sort.Strings(names)
if got := strings.Join(names, ","); got != "development.yml" {
t.Fatalf("maintained configuration profiles = %q, want development.yml", got)
t.Fatalf("maintained configuration overlays = %q, want development.yml", got)
}
llmProfiles, err := os.ReadDir(repositoryPath("examples", "llm-profiles"))
if err != nil {

View File

@@ -34,10 +34,10 @@ const usage = `Usage:
notarius help
notarius --version
notarius run <pipeline-id> --input path/to/source.json [--json] [flags]
notarius config validate --config path/to/config.yml [--config-profile name] [--pipeline pipeline-id] [--only lane-a,lane-b]
notarius config effective --config path/to/config.yml [--config-profile name]
notarius config explain --config path/to/config.yml [--config-profile name] <json-pointer>
notarius pipelines list --config path/to/config.yml [--config-profile name] [--json]
notarius config validate --config path/to/config.yml [--config-overlay name] [--pipeline pipeline-id] [--only lane-a,lane-b]
notarius config effective --config path/to/config.yml [--config-overlay name]
notarius config explain --config path/to/config.yml [--config-overlay name] <json-pointer>
notarius pipelines list --config path/to/config.yml [--config-overlay name] [--json]
`
type Options struct {
@@ -160,7 +160,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
fs := flag.NewFlagSet("run", flag.ContinueOnError)
fs.SetOutput(io.Discard)
configPath := fs.String("config", "", "config file path")
configProfile := fs.String("config-profile", "", "configuration profile")
configOverlay := fs.String("config-overlay", "", "configuration overlay")
inputPath := fs.String("input", "", "source input file path")
onlyRaw := fs.String("only", "", "comma-separated artifact lanes")
outputDir := fs.String("output-dir", "", "output directory")
@@ -268,7 +268,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
return 2
}
loaded, loadedConfigPath, err := loadConfigKit(*configPath, *configProfile, runConfigurationOverrides(chunkCache, *outputDir, *debugDir), opts)
loaded, loadedConfigPath, err := loadConfigKit(*configPath, *configOverlay, runConfigurationOverrides(chunkCache, *outputDir, *debugDir), opts)
if err != nil {
writeConfigLoadError(stderr, err)
return 1
@@ -331,7 +331,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
InputPath: strings.TrimSpace(*inputPath),
ConfigPath: loadedConfigPath,
ConfigSource: configSource(*configPath),
ConfigProfile: effectiveConfigProfile(*configProfile),
ConfigOverlay: effectiveConfigOverlay(*configOverlay),
OnlyLanes: append([]string(nil), only...),
ChunkCacheOverride: chunkCache.explicitValue(),
ReasoningEffortOverride: runtimeOverrides.ReasoningEffort,
@@ -877,7 +877,7 @@ func reorderRunArgs(args []string) []string {
func runFlagTakesValue(arg string) bool {
switch arg {
case "--config", "--config-profile", "--input", "--only", "--output-dir", "--debug-dir", "--llm-profile", "--session-id", "--reasoning-effort", "--chunk_cache", "--reference", "--without-reference", "--recompute-step":
case "--config", "--config-overlay", "--input", "--only", "--output-dir", "--debug-dir", "--llm-profile", "--session-id", "--reasoning-effort", "--chunk_cache", "--reference", "--without-reference", "--recompute-step":
return true
default:
return false
@@ -963,7 +963,7 @@ func chunkPlanStoreForRun(cfg config.ChunkPlanCacheConfig, opts Options) (pipeli
func validateRunFlagValues(args []string) error {
for i, arg := range args {
if arg != "--config-profile" && arg != "--session-id" && arg != "--reasoning-effort" {
if arg != "--config-overlay" && arg != "--session-id" && arg != "--reasoning-effort" {
continue
}
if i+1 >= len(args) || strings.HasPrefix(args[i+1], "-") {
@@ -973,11 +973,11 @@ func validateRunFlagValues(args []string) error {
return nil
}
func effectiveConfigProfile(profile string) string {
if profile == "" || profile == "default" {
func effectiveConfigOverlay(overlay string) string {
if overlay == "" || overlay == "default" {
return ""
}
return profile
return overlay
}
func flagWasProvided(args []string, name string) bool {

View File

@@ -38,8 +38,8 @@ func TestRunControlsRejectSyntaxWithoutAllocatingState(t *testing.T) {
{name: "blank debug directory", args: func(roots stateTestRoots) []string {
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--debug-dir", ""}
}},
{name: "missing configuration profile", args: func(roots stateTestRoots) []string {
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--config-profile"}
{name: "missing configuration overlay", args: func(roots stateTestRoots) []string {
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--config-overlay"}
}},
{name: "debug directory without debug", args: func(roots stateTestRoots) []string {
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--debug-dir", filepath.Join(filepath.Dir(roots.debug), "requested-debug")}
@@ -247,38 +247,38 @@ func TestRunStateRootsHonorEnvironmentFlagsAndDefaults(t *testing.T) {
})
}
func TestRunAppliesConfigurationProfileAndRecordsSelection(t *testing.T) {
func TestRunAppliesConfigurationOverlayAndRecordsSelection(t *testing.T) {
roots := newStateTestRoots(t)
profileDirectory := filepath.Join(filepath.Dir(roots.config), "profiles")
profileOutput := filepath.Join(profileDirectory, "profile-output")
profileDebug := filepath.Join(profileDirectory, "profile-debug")
writeConfigKitFile(t, filepath.Join(profileDirectory, "development.yml"), "output:\n directory: ./profile-output\ndebug:\n directory: ./profile-debug\n")
overlayDirectory := filepath.Join(filepath.Dir(roots.config), "overlays")
overlayOutput := filepath.Join(overlayDirectory, "overlay-output")
overlayDebug := filepath.Join(overlayDirectory, "overlay-debug")
writeConfigKitFile(t, filepath.Join(overlayDirectory, "development.yml"), "output:\n directory: ./overlay-output\ndebug:\n directory: ./overlay-debug\n")
var stdout, stderr bytes.Buffer
code := RunWithOptions([]string{
"run", "sample", "--config", roots.config, "--config-profile", "development",
"run", "sample", "--config", roots.config, "--config-overlay", "development",
"--input", roots.input, "--chunk_cache", "bypass", "--debug",
}, &stdout, &stderr, newStateTestHarness().options())
if code != 0 || stderr.Len() != 0 {
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
assertFile(t, filepath.Join(profileOutput, filepath.Base(onlyChildDir(t, profileOutput)), "result.json"))
bundle := onlyChildDir(t, profileDebug)
assertFile(t, filepath.Join(overlayOutput, filepath.Base(onlyChildDir(t, overlayOutput)), "result.json"))
bundle := onlyChildDir(t, overlayDebug)
var invocation debugbundle.Invocation
readStateTestSummaryJSON(t, bundle, "invocation.json", &invocation)
if invocation.ConfigProfile != "development" {
t.Fatalf("config profile = %q, want development", invocation.ConfigProfile)
if invocation.ConfigOverlay != "development" {
t.Fatalf("config overlay = %q, want development", invocation.ConfigOverlay)
}
}
func TestEffectiveConfigProfileOmitsBaseSelection(t *testing.T) {
for _, profile := range []string{"", "default"} {
if got := effectiveConfigProfile(profile); got != "" {
t.Fatalf("effectiveConfigProfile(%q) = %q, want empty", profile, got)
func TestEffectiveConfigOverlayOmitsBaseSelection(t *testing.T) {
for _, overlay := range []string{"", "default"} {
if got := effectiveConfigOverlay(overlay); got != "" {
t.Fatalf("effectiveConfigOverlay(%q) = %q, want empty", overlay, got)
}
}
if got := effectiveConfigProfile("development"); got != "development" {
t.Fatalf("effectiveConfigProfile(development) = %q", got)
if got := effectiveConfigOverlay("development"); got != "development" {
t.Fatalf("effectiveConfigOverlay(development) = %q", got)
}
}