diff --git a/docs/cli.md b/docs/cli.md index 3c0c41ab..92c591cd 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -12,8 +12,8 @@ defined in [Operations](operations.md). notarius help notarius --version notarius run --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 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 pipelines list [--config path/to/config.yml] [--config-overlay name] [--json] ~~~ Running Notarius without arguments, or with **help**, **--help**, or **-h**, @@ -42,7 +42,7 @@ pipeline ID and **--input** are required. | Flag | Meaning | | --- | --- | | **--config path** | Use this configuration file. When omitted, configuration discovery applies; see [Configuration](config.md). | -| **--config-profile name** | Select a ConfigKit configuration overlay. Empty and **default** select the composed base configuration. | +| **--config-overlay name** | Select a ConfigKit configuration overlay. Empty and **default** select the composed base configuration. | | **--input path** | Source input file to process. Required. | | **--output-dir path** | Override the configured output root for this run. | | **--json** | Write the successful run-result receipt as JSON to standard output. | @@ -155,7 +155,7 @@ OPENROUTER_API_KEY=your-api-key \ ## config validate ~~~ -notarius config validate [--config path/to/config.yml] [--config-profile name] [--pipeline pipeline-id] [--only lane-a,lane-b] +notarius config validate [--config path/to/config.yml] [--config-overlay name] [--pipeline pipeline-id] [--only lane-a,lane-b] ~~~ This command loads and validates the selected composed configuration. With **--pipeline**, it also @@ -184,7 +184,7 @@ validation; it cannot run a provider-backed pipeline. ## config effective ~~~ -notarius config effective [--config path/to/config.yml] [--config-profile name] +notarius config effective [--config path/to/config.yml] [--config-overlay name] ~~~ This command writes the selected composed configuration as deterministic, @@ -194,19 +194,19 @@ pipeline, prepare modules, materialize references, or contact a provider. ## config explain ~~~ -notarius config explain [--config path/to/config.yml] [--config-profile name] +notarius config explain [--config path/to/config.yml] [--config-overlay name] ~~~ This command writes ConfigKit's redacted, human-readable explanation for one canonical JSON Pointer. Supply `""` explicitly to explain the root. A malformed pointer is a syntax error; a well-formed unknown path is a command failure. -The explanation can describe a value displaced by a selected profile. Standard +The explanation can describe a value displaced by a selected overlay. Standard output contains only the explanation. ## pipelines list ~~~ -notarius pipelines list [--config path/to/config.yml] [--config-profile name] [--json] +notarius pipelines list [--config path/to/config.yml] [--config-overlay name] [--json] ~~~ This command lists configured pipeline IDs in sorted order. By default, it @@ -225,7 +225,7 @@ errors are written to standard error. Configuration load errors include ConfigKit's field diagnostics and source coordinates, followed by a source-ID legend identifying the acquired files or -source labels. This includes imported files and the selected configuration profile. +source labels. This includes imported files and the selected configuration overlay. For **run --json**, actionable process warnings remain on standard error and standard output is a machine-readable success result only. Syntax and runtime diagnostics remain on diff --git a/docs/config.md b/docs/config.md index c7df43cb..f0ab8403 100644 --- a/docs/config.md +++ b/docs/config.md @@ -20,12 +20,13 @@ For configuration values, precedence is: 1. built-in defaults; 2. composed YAML sources, including imports; -3. the selected ConfigKit profile; and +3. the selected ConfigKit overlay; and 4. explicitly supplied configuration-valued run flags. -`--config-profile` selects a ConfigKit overlay from `profiles/.yml` +`--config-overlay` selects a ConfigKit overlay from `overlays/.yml` beneath the selected configuration root. Empty and `default` select no overlay. -Configuration profiles are distinct from PromptKit LLM profiles selected by +`profiles/.yml` is not a fallback location, and `--config-profile` is no +longer accepted. Configuration overlays are distinct from PromptKit LLM profiles selected by `llm_profile` or `--llm-profile`. The retired operational environment variables have no effect; `NOTARIUS_CONFIG` remains a source-discovery input. @@ -79,14 +80,14 @@ umask, setgid inheritance, and default ACLs; see [Operations](operations.md#cach Every nonempty relative configured destination or reference path resolves from the canonical directory of the file that supplies its surviving declaration. -This includes imported fragments and selected configuration profiles. Built-in +This includes imported fragments and selected configuration overlays. Built-in relative destinations use the selected main configuration file's directory; explicit `--output-dir` and `--debug-dir` values use the invocation working directory. Absolute values remain unchanged. The root file can use ConfigKit imports to compose disjoint definitions. Imports -and selected profiles remain under the configuration root; they do not merge or -append a pipeline's `steps` sequence. A profile replaces sequences as defined +and selected overlays remain under the configuration root; they do not merge or +append a pipeline's `steps` sequence. An overlay replaces sequences as defined by ConfigKit, so changing an individual ordered binding requires supplying the whole replacement sequence. @@ -201,7 +202,7 @@ debug: **concurrency.total_llm** must be greater than zero. The only supported **concurrency.stage_workers** key is **extract**; its value must be from 1 through **total_llm**. When omitted, it is recalculated from the effective -**total_llm** after imports, profile selection, and explicit ConfigKit +**total_llm** after imports, overlay selection, and explicit ConfigKit overrides. **cache.chunk_plans.mode** accepts **auto**, **bypass**, or **refresh**. @@ -440,7 +441,7 @@ pipeline, lane, and binding scopes and precedence are defined by the replace a configured generated artifact handoff. Required slots must be bound after configuration and CLI reference actions are applied. A YAML reference path resolves from the file that supplies its surviving declaration, including -an imported fragment or selected profile. CLI reference paths resolve relative +an imported fragment or selected overlay. CLI reference paths resolve relative to the process working directory. ### D&D Reference Slots @@ -601,5 +602,5 @@ Use [config effective](cli.md#config-effective) to write the selected composed configuration as deterministic, redacted YAML. Use [config explain](cli.md#config-explain) with a canonical JSON Pointer to inspect the effective or historical provenance for one path, including a value displaced -by a selected profile. Neither command resolves a pipeline, prepares modules, +by a selected overlay. Neither command resolves a pipeline, prepares modules, materializes references, or requires provider credentials. diff --git a/docs/consumers/subprocess.md b/docs/consumers/subprocess.md index 702971a5..4bc984fa 100644 --- a/docs/consumers/subprocess.md +++ b/docs/consumers/subprocess.md @@ -15,10 +15,10 @@ configured lane inventory, and downstream acceptance checklist, see Optionally preflight a selected configuration and pipeline before work starts: ```sh -notarius config validate --config /path/to/notarius.yml --config-profile production --pipeline pipeline-id +notarius config validate --config /path/to/notarius.yml --config-overlay production --pipeline pipeline-id ``` -Omit `--config-profile` when the composed base configuration is the intended +Omit `--config-overlay` when the composed base configuration is the intended selection; it is independent from any `--llm-profile` run override. Invoke the run with explicit paths and machine-readable output. Capture @@ -28,7 +28,7 @@ processing the result. ```sh notarius run pipeline-id \ --config /path/to/notarius.yml \ - --config-profile production \ + --config-overlay production \ --input /path/to/source.json \ --output-dir /path/to/output-root \ --json diff --git a/docs/integrations/pkg-configkit.md b/docs/integrations/pkg-configkit.md index f0994e93..6528daf9 100644 --- a/docs/integrations/pkg-configkit.md +++ b/docs/integrations/pkg-configkit.md @@ -2,7 +2,7 @@ Notarius uses the public root package [`gitea.maximumdirect.net/eric/ConfigKit`](https://gitea.maximumdirect.net/eric/ConfigKit) -at v0.3.0 for version 5 configuration acquisition, imports, profiles, explicit +at v0.4.0 for version 5 configuration acquisition, imports, overlays, explicit overrides, diagnostics, and safe inspection rendering. Notarius owns the typed document, domain defaults and validation, runtime diff --git a/docs/internal/cli.md b/docs/internal/cli.md index 2d3d6fa9..a6a0e084 100644 --- a/docs/internal/cli.md +++ b/docs/internal/cli.md @@ -35,7 +35,7 @@ options before dispatch so that a missing production dependency fails as a command error rather than reaching execution. Commands that need configuration use one shared ConfigKit-backed loader. The -CLI discovers the file, passes the requested profile and only explicit +CLI discovers the file, passes the requested overlay and only explicit configuration-valued run overrides, and receives a **LoadedConfig**. The configured discovery and precedence contract is in [Configuration](../config.md), while the loading and resolution mechanics are diff --git a/docs/internal/configuration.md b/docs/internal/configuration.md index fdde4087..3448ed51 100644 --- a/docs/internal/configuration.md +++ b/docs/internal/configuration.md @@ -28,7 +28,7 @@ at their respective [CLI](cli.md), [pipeline](pipeline.md), and ## Loading And Validation -ConfigKit acquires the selected root, imports, optional selected profile, and +ConfigKit acquires the selected root, imports, optional selected overlay, and explicit CLI overrides in precedence order. Its document definition supplies typed defaults, source-aware path normalization, and structural validation before conversion to the independently owned runtime **Config**. Only @@ -112,7 +112,7 @@ recording arbitrary environment values or provider requests. ## Invariants To Preserve -- ConfigKit composition applies defaults, imports, an optional profile, and +- ConfigKit composition applies defaults, imports, an optional overlay, and explicit overrides in one direction with retained origin metadata. - A configuration is structurally valid before it is resolved, and a resolved pipeline is compatible with the supplied catalog before preparation begins. @@ -126,7 +126,7 @@ recording arbitrary environment values or provider requests. - **internal/core/config/document_test.go** and **operational_document_test.go** cover ConfigKit document loading, - normalization, imports, profiles, and overrides. + normalization, imports, overlays, and overrides. - **internal/core/config/validation_contract_test.go** covers configuration invariants and contextual failures. - **internal/core/config/effective_config_contract_test.go** covers defaults, diff --git a/docs/internal/pipeline.md b/docs/internal/pipeline.md index 40f91c57..b6c6dc17 100644 --- a/docs/internal/pipeline.md +++ b/docs/internal/pipeline.md @@ -52,7 +52,7 @@ consumers do not repeat profile inheritance or configuration resolution. Each LLM request receives its own copy of that resolved value. PromptKit spends it only for structural correction inside one completion; the runner's binding retry policy remains the separate outer budget for complete stage attempts. -Configuration resolution supplies the selected profile and catalog; see +Configuration resolution supplies the selected overlay and catalog; see [Configuration Internals](configuration.md). External reference materialization happens before preparation. The materializer diff --git a/docs/operations.md b/docs/operations.md index 404bc98a..be6b381e 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -319,7 +319,7 @@ content. Neither surface is a cache input. Do not treat a debug bundle as safe to share merely because its configuration summary is redacted. Invocation metadata omits reasoning effort when it is inherited, records the replacement value when one is supplied, and records an empty value when inherited reasoning -was explicitly cleared. It records `config_profile` only when a non-base +was explicitly cleared. It records `config_overlay` only when a non-base configuration overlay was selected. Notarius never creates debug state without an explicit request and never diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 8044d55f..f130551d 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -50,6 +50,8 @@ Do not repeat passed checks for unchanged content merely to make a commit. ## Stage 1 — Cut Over Configuration Selection To Overlays +**Status:** Completed 2026-09-25. + **Goal:** Adopt ConfigKit v0.4.0 and finish its narrow operator-facing rename in one coherent change. This is one prompt: it is a naming migration, not a new configuration engine. diff --git a/examples/profiles/development.yml b/examples/overlays/development.yml similarity index 100% rename from examples/profiles/development.yml rename to examples/overlays/development.yml diff --git a/go.mod b/go.mod index b83f898d..d890ec96 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module gitea.maximumdirect.net/eric/notarius go 1.25.5 require ( - gitea.maximumdirect.net/eric/ConfigKit v0.3.0 + gitea.maximumdirect.net/eric/ConfigKit v0.4.0 gitea.maximumdirect.net/eric/promptkit v0.10.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6068ff26..2472a3e5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -gitea.maximumdirect.net/eric/ConfigKit v0.3.0 h1:LgXEUjR0KsgwGBznOWa8FJTQ+iElctHtI1via5vKdu4= -gitea.maximumdirect.net/eric/ConfigKit v0.3.0/go.mod h1:tYWpsrbLyu05r8+1MapWp5fN6ZrX9I8hU2xEPedYmYk= +gitea.maximumdirect.net/eric/ConfigKit v0.4.0 h1:fo+vKff3zTGeWqJIA38NtT84b6OUP//9ZmBH3lFuKgk= +gitea.maximumdirect.net/eric/ConfigKit v0.4.0/go.mod h1:tYWpsrbLyu05r8+1MapWp5fN6ZrX9I8hU2xEPedYmYk= gitea.maximumdirect.net/eric/promptkit v0.10.0 h1:Nyi+YbqvO7tkRjen+Opc79wuzuKIIwcSfkriixPPwrE= gitea.maximumdirect.net/eric/promptkit v0.10.0/go.mod h1:0zEBVlJJC+ldDdXazAgybYBXNSe74vwFx5dyOuSos/w= gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2 v2.20260922.1 h1:S2/Q/JDE+m8Ca3pDhAjVbfYrdlEhs9blj0STgNopHo0= diff --git a/internal/cli/configkit_commands.go b/internal/cli/configkit_commands.go index 93ad053d..fb6c90db 100644 --- a/internal/cli/configkit_commands.go +++ b/internal/cli/configkit_commands.go @@ -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 diff --git a/internal/cli/configkit_commands_test.go b/internal/cli/configkit_commands_test.go index 027171f5..8f6e5115 100644 --- a/internal/cli/configkit_commands_test.go +++ b/internal/cli/configkit_commands_test.go @@ -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()) } } diff --git a/internal/cli/configkit_load.go b/internal/cli/configkit_load.go index 4e17779f..0d49c69a 100644 --- a/internal/cli/configkit_load.go +++ b/internal/cli/configkit_load.go @@ -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 diff --git a/internal/cli/configkit_load_test.go b/internal/cli/configkit_load_test.go index e38c9e28..73af837a 100644 --- a/internal/cli/configkit_load_test.go +++ b/internal/cli/configkit_load_test.go @@ -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) } } diff --git a/internal/cli/example_contract_test.go b/internal/cli/example_contract_test.go index eeeaa0f3..cc9c09c7 100644 --- a/internal/cli/example_contract_test.go +++ b/internal/cli/example_contract_test.go @@ -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 { diff --git a/internal/cli/run.go b/internal/cli/run.go index a3b738a7..dbf634e3 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -34,10 +34,10 @@ const usage = `Usage: notarius help notarius --version notarius run --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] - 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] + 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 { diff --git a/internal/cli/run_contract_test.go b/internal/cli/run_contract_test.go index 4c28fb30..15e2496f 100644 --- a/internal/cli/run_contract_test.go +++ b/internal/cli/run_contract_test.go @@ -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) } } diff --git a/internal/core/config/document_paths_test.go b/internal/core/config/document_paths_test.go index 10a5f7cc..c6867076 100644 --- a/internal/core/config/document_paths_test.go +++ b/internal/core/config/document_paths_test.go @@ -77,27 +77,48 @@ func TestDocumentPathsUseInvocationDirectoryForOverrides(t *testing.T) { } } -func TestDocumentPathsUseProfileDeclarationDirectory(t *testing.T) { +func TestDocumentPathsUseOverlayDeclarationDirectory(t *testing.T) { t.Parallel() root := t.TempDir() - profiles := filepath.Join(root, "profiles") - if err := os.MkdirAll(profiles, 0o700); err != nil { + overlays := filepath.Join(root, "overlays") + if err := os.MkdirAll(overlays, 0o700); err != nil { t.Fatal(err) } main := filepath.Join(root, "notarius.yml") if err := os.WriteFile(main, []byte("version: 5\n"), 0o600); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(profiles, "development.yml"), []byte("output: {directory: ./profile-output}\n"), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(overlays, "development.yml"), []byte("output: {directory: ./overlay-output}\n"), 0o600); err != nil { t.Fatal(err) } - loaded, err := loadConfig(main, LoadOptions{Profile: "development"}) + loaded, err := loadConfig(main, LoadOptions{Overlay: "development"}) if err != nil { t.Fatalf("loadConfig() error = %v", err) } - if got := loaded.Config.Output.Directory; got != filepath.Join(profiles, "profile-output") { - t.Fatalf("profile directory = %q", got) + if got := loaded.Config.Output.Directory; got != filepath.Join(overlays, "overlay-output") { + t.Fatalf("overlay directory = %q", got) + } +} + +func TestDocumentLoadDoesNotFallBackToLegacyProfileDirectory(t *testing.T) { + t.Parallel() + + root := t.TempDir() + main := filepath.Join(root, "notarius.yml") + if err := os.WriteFile(main, []byte("version: 5\n"), 0o600); err != nil { + t.Fatal(err) + } + legacy := filepath.Join(root, "profiles", "development.yml") + if err := os.MkdirAll(filepath.Dir(legacy), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(legacy, []byte("output: {directory: ./legacy-output}\n"), 0o600); err != nil { + t.Fatal(err) + } + + if _, err := loadConfig(main, LoadOptions{Overlay: "development"}); err == nil { + t.Fatal("loadConfig() error = nil, want missing overlay") } } diff --git a/internal/core/config/load.go b/internal/core/config/load.go index e6088ed7..a83ac5a9 100644 --- a/internal/core/config/load.go +++ b/internal/core/config/load.go @@ -10,7 +10,7 @@ import ( // boundary. It is intentionally narrow: configuration discovery and CLI flag // parsing remain outside this package. type LoadOptions struct { - Profile string + Overlay string Overrides []configkit.Override InvocationDirectory string } @@ -30,7 +30,7 @@ func loadDocumentWithSource(source configkit.Source, options LoadOptions, import return configkit.Load(source, configkit.Options[Document]{ Definition: documentDefinition(state), Imports: imports, - Profile: options.Profile, + Overlay: options.Overlay, Overrides: options.Overrides, }) } diff --git a/internal/core/config/operational_document_test.go b/internal/core/config/operational_document_test.go index d5fe2b2c..3657f261 100644 --- a/internal/core/config/operational_document_test.go +++ b/internal/core/config/operational_document_test.go @@ -105,28 +105,28 @@ debug: } } -func TestDocumentLoadDerivesWorkersAfterProfileAndCLIOverrides(t *testing.T) { +func TestDocumentLoadDerivesWorkersAfterOverlayAndCLIOverrides(t *testing.T) { t.Parallel() directory := t.TempDir() main := filepath.Join(directory, "config.yml") - profiles := filepath.Join(directory, "profiles") - if err := os.Mkdir(profiles, 0o755); err != nil { + overlays := filepath.Join(directory, "overlays") + if err := os.Mkdir(overlays, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(main, []byte("version: 5\nconcurrency:\n total_llm: 4\n"), 0o600); err != nil { t.Fatal(err) } - if err := os.WriteFile(filepath.Join(profiles, "development.yml"), []byte("concurrency:\n total_llm: 3\n"), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(overlays, "development.yml"), []byte("concurrency:\n total_llm: 3\n"), 0o600); err != nil { t.Fatal(err) } - profiled, err := loadDocument(main, LoadOptions{Profile: "development"}) + overlaid, err := loadDocument(main, LoadOptions{Overlay: "development"}) if err != nil { t.Fatal(err) } - if profiled.Config.Concurrency.TotalLLM == nil || *profiled.Config.Concurrency.TotalLLM != 3 || profiled.Config.Concurrency.StageWorkers["extract"] != 3 { - t.Fatalf("profiled concurrency = %#v", profiled.Config.Concurrency) + if overlaid.Config.Concurrency.TotalLLM == nil || *overlaid.Config.Concurrency.TotalLLM != 3 || overlaid.Config.Concurrency.StageWorkers["extract"] != 3 { + t.Fatalf("overlaid concurrency = %#v", overlaid.Config.Concurrency) } overridden, err := loadDocument(main, LoadOptions{Overrides: []configkit.Override{{ @@ -138,6 +138,16 @@ func TestDocumentLoadDerivesWorkersAfterProfileAndCLIOverrides(t *testing.T) { if overridden.Config.Concurrency.TotalLLM == nil || *overridden.Config.Concurrency.TotalLLM != 2 || overridden.Config.Concurrency.StageWorkers["extract"] != 2 { t.Fatalf("overridden concurrency = %#v", overridden.Config.Concurrency) } + + overlaidAndOverridden, err := loadDocument(main, LoadOptions{Overlay: "development", Overrides: []configkit.Override{{ + Path: configkit.Keys("concurrency", "total_llm"), Value: 2, Flag: "--total-llm", + }}}) + if err != nil { + t.Fatal(err) + } + if overlaidAndOverridden.Config.Concurrency.TotalLLM == nil || *overlaidAndOverridden.Config.Concurrency.TotalLLM != 2 || overlaidAndOverridden.Config.Concurrency.StageWorkers["extract"] != 2 { + t.Fatalf("overlaid and overridden concurrency = %#v", overlaidAndOverridden.Config.Concurrency) + } } func TestDocumentLoadRetainsExplicitWorkerAndRejectsInvalidOperationalValues(t *testing.T) { diff --git a/internal/core/debugbundle/summary.go b/internal/core/debugbundle/summary.go index 78d95424..2b2834b6 100644 --- a/internal/core/debugbundle/summary.go +++ b/internal/core/debugbundle/summary.go @@ -39,7 +39,7 @@ type Invocation struct { InputPath string `json:"input_path,omitempty"` ConfigPath string `json:"config_path,omitempty"` ConfigSource string `json:"config_source,omitempty"` - ConfigProfile string `json:"config_profile,omitempty"` + ConfigOverlay string `json:"config_overlay,omitempty"` OnlyLanes []string `json:"only_lanes,omitempty"` ChunkCacheOverride string `json:"chunk_cache_override,omitempty"` ReasoningEffortOverride *string `json:"reasoning_effort_override,omitempty"`