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

@@ -12,8 +12,8 @@ defined in [Operations](operations.md).
notarius help notarius help
notarius --version notarius --version
notarius run <pipeline-id> --input path/to/source.json [--json] [flags] 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 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-profile name] [--json] notarius pipelines list [--config path/to/config.yml] [--config-overlay name] [--json]
~~~ ~~~
Running Notarius without arguments, or with **help**, **--help**, or **-h**, Running Notarius without arguments, or with **help**, **--help**, or **-h**,
@@ -42,7 +42,7 @@ pipeline ID and **--input** are required.
| Flag | Meaning | | Flag | Meaning |
| --- | --- | | --- | --- |
| **--config path** | Use this configuration file. When omitted, configuration discovery applies; see [Configuration](config.md). | | **--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. | | **--input path** | Source input file to process. Required. |
| **--output-dir path** | Override the configured output root for this run. | | **--output-dir path** | Override the configured output root for this run. |
| **--json** | Write the successful run-result receipt as JSON to standard output. | | **--json** | Write the successful run-result receipt as JSON to standard output. |
@@ -155,7 +155,7 @@ OPENROUTER_API_KEY=your-api-key \
## config validate ## 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 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 ## 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, This command writes the selected composed configuration as deterministic,
@@ -194,19 +194,19 @@ pipeline, prepare modules, materialize references, or contact a provider.
## config explain ## config explain
~~~ ~~~
notarius config explain [--config path/to/config.yml] [--config-profile name] <json-pointer> notarius config explain [--config path/to/config.yml] [--config-overlay name] <json-pointer>
~~~ ~~~
This command writes ConfigKit's redacted, human-readable explanation for one This command writes ConfigKit's redacted, human-readable explanation for one
canonical JSON Pointer. Supply `""` explicitly to explain the root. A malformed 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. 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. output contains only the explanation.
## pipelines list ## 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 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 Configuration load errors include ConfigKit's field diagnostics and source
coordinates, followed by a source-ID legend identifying the acquired files or 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 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 machine-readable success result only. Syntax and runtime diagnostics remain on

View File

@@ -20,12 +20,13 @@ For configuration values, precedence is:
1. built-in defaults; 1. built-in defaults;
2. composed YAML sources, including imports; 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. 4. explicitly supplied configuration-valued run flags.
`--config-profile` selects a ConfigKit overlay from `profiles/<name>.yml` `--config-overlay` selects a ConfigKit overlay from `overlays/<name>.yml`
beneath the selected configuration root. Empty and `default` select no overlay. beneath the selected configuration root. Empty and `default` select no overlay.
Configuration profiles are distinct from PromptKit LLM profiles selected by `profiles/<name>.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 `llm_profile` or `--llm-profile`. The retired operational environment variables
have no effect; `NOTARIUS_CONFIG` remains a source-discovery input. 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 Every nonempty relative configured destination or reference path resolves from
the canonical directory of the file that supplies its surviving declaration. 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; relative destinations use the selected main configuration file's directory;
explicit `--output-dir` and `--debug-dir` values use the invocation working explicit `--output-dir` and `--debug-dir` values use the invocation working
directory. Absolute values remain unchanged. directory. Absolute values remain unchanged.
The root file can use ConfigKit imports to compose disjoint definitions. Imports 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 and selected overlays remain under the configuration root; they do not merge or
append a pipeline's `steps` sequence. A profile replaces sequences as defined append a pipeline's `steps` sequence. An overlay replaces sequences as defined
by ConfigKit, so changing an individual ordered binding requires supplying the by ConfigKit, so changing an individual ordered binding requires supplying the
whole replacement sequence. whole replacement sequence.
@@ -201,7 +202,7 @@ debug:
**concurrency.total_llm** must be greater than zero. The only supported **concurrency.total_llm** must be greater than zero. The only supported
**concurrency.stage_workers** key is **extract**; its value must be from 1 **concurrency.stage_workers** key is **extract**; its value must be from 1
through **total_llm**. When omitted, it is recalculated from the effective 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. overrides.
**cache.chunk_plans.mode** accepts **auto**, **bypass**, or **refresh**. **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 replace a configured generated artifact handoff. Required slots must be bound
after configuration and CLI reference actions are applied. A YAML reference after configuration and CLI reference actions are applied. A YAML reference
path resolves from the file that supplies its surviving declaration, including 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. to the process working directory.
### D&D Reference Slots ### 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 configuration as deterministic, redacted YAML. Use
[config explain](cli.md#config-explain) with a canonical JSON Pointer to inspect [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 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. materializes references, or requires provider credentials.

View File

@@ -15,10 +15,10 @@ configured lane inventory, and downstream acceptance checklist, see
Optionally preflight a selected configuration and pipeline before work starts: Optionally preflight a selected configuration and pipeline before work starts:
```sh ```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. selection; it is independent from any `--llm-profile` run override.
Invoke the run with explicit paths and machine-readable output. Capture Invoke the run with explicit paths and machine-readable output. Capture
@@ -28,7 +28,7 @@ processing the result.
```sh ```sh
notarius run pipeline-id \ notarius run pipeline-id \
--config /path/to/notarius.yml \ --config /path/to/notarius.yml \
--config-profile production \ --config-overlay production \
--input /path/to/source.json \ --input /path/to/source.json \
--output-dir /path/to/output-root \ --output-dir /path/to/output-root \
--json --json

View File

@@ -2,7 +2,7 @@
Notarius uses the public root package Notarius uses the public root package
[`gitea.maximumdirect.net/eric/ConfigKit`](https://gitea.maximumdirect.net/eric/ConfigKit) [`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. overrides, diagnostics, and safe inspection rendering.
Notarius owns the typed document, domain defaults and validation, runtime Notarius owns the typed document, domain defaults and validation, runtime

View File

@@ -35,7 +35,7 @@ options before dispatch so that a missing production dependency fails as a
command error rather than reaching execution. command error rather than reaching execution.
Commands that need configuration use one shared ConfigKit-backed loader. The 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 configuration-valued run overrides, and receives a **LoadedConfig**. The
configured discovery and precedence contract is in configured discovery and precedence contract is in
[Configuration](../config.md), while the loading and resolution mechanics are [Configuration](../config.md), while the loading and resolution mechanics are

View File

@@ -28,7 +28,7 @@ at their respective [CLI](cli.md), [pipeline](pipeline.md), and
## Loading And Validation ## 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 explicit CLI overrides in precedence order. Its document definition supplies
typed defaults, source-aware path normalization, and structural validation typed defaults, source-aware path normalization, and structural validation
before conversion to the independently owned runtime **Config**. Only before conversion to the independently owned runtime **Config**. Only
@@ -112,7 +112,7 @@ recording arbitrary environment values or provider requests.
## Invariants To Preserve ## 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. explicit overrides in one direction with retained origin metadata.
- A configuration is structurally valid before it is resolved, and a resolved - A configuration is structurally valid before it is resolved, and a resolved
pipeline is compatible with the supplied catalog before preparation begins. 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 - **internal/core/config/document_test.go** and
**operational_document_test.go** cover ConfigKit document loading, **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 - **internal/core/config/validation_contract_test.go** covers configuration
invariants and contextual failures. invariants and contextual failures.
- **internal/core/config/effective_config_contract_test.go** covers defaults, - **internal/core/config/effective_config_contract_test.go** covers defaults,

View File

@@ -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 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 it only for structural correction inside one completion; the runner's binding
retry policy remains the separate outer budget for complete stage attempts. 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). [Configuration Internals](configuration.md).
External reference materialization happens before preparation. The materializer External reference materialization happens before preparation. The materializer

View File

@@ -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 to share merely because its configuration summary is redacted. Invocation
metadata omits reasoning effort when it is inherited, records the replacement metadata omits reasoning effort when it is inherited, records the replacement
value when one is supplied, and records an empty value when inherited reasoning 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. configuration overlay was selected.
Notarius never creates debug state without an explicit request and never Notarius never creates debug state without an explicit request and never

View File

@@ -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 ## 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 **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 one coherent change. This is one prompt: it is a naming migration, not a new
configuration engine. configuration engine.

2
go.mod
View File

@@ -3,7 +3,7 @@ module gitea.maximumdirect.net/eric/notarius
go 1.25.5 go 1.25.5
require ( 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 gitea.maximumdirect.net/eric/promptkit v0.10.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

4
go.sum
View File

@@ -1,5 +1,5 @@
gitea.maximumdirect.net/eric/ConfigKit v0.3.0 h1:LgXEUjR0KsgwGBznOWa8FJTQ+iElctHtI1via5vKdu4= gitea.maximumdirect.net/eric/ConfigKit v0.4.0 h1:fo+vKff3zTGeWqJIA38NtT84b6OUP//9ZmBH3lFuKgk=
gitea.maximumdirect.net/eric/ConfigKit v0.3.0/go.mod h1:tYWpsrbLyu05r8+1MapWp5fN6ZrX9I8hU2xEPedYmYk= 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 h1:Nyi+YbqvO7tkRjen+Opc79wuzuKIIwcSfkriixPPwrE=
gitea.maximumdirect.net/eric/promptkit v0.10.0/go.mod h1:0zEBVlJJC+ldDdXazAgybYBXNSe74vwFx5dyOuSos/w= 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= gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2 v2.20260922.1 h1:S2/Q/JDE+m8Ca3pDhAjVbfYrdlEhs9blj0STgNopHo0=

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 := flag.NewFlagSet("config effective", flag.ContinueOnError)
fs.SetOutput(io.Discard) fs.SetOutput(io.Discard)
configPath := fs.String("config", "", "config file path") 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 { if err := fs.Parse(args); err != nil {
fmt.Fprintf(stderr, "notarius: %v\n", err) fmt.Fprintf(stderr, "notarius: %v\n", err)
return 2 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)) fmt.Fprintf(stderr, "notarius: unexpected argument %q\n", fs.Arg(0))
return 2 return 2
} }
loaded, _, err := loadConfigKit(*configPath, *profile, nil, opts) loaded, _, err := loadConfigKit(*configPath, *overlay, nil, opts)
if err != nil { if err != nil {
writeConfigLoadError(stderr, err) writeConfigLoadError(stderr, err)
return 1 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 := flag.NewFlagSet("config explain", flag.ContinueOnError)
fs.SetOutput(io.Discard) fs.SetOutput(io.Discard)
configPath := fs.String("config", "", "config file path") 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 { if err := fs.Parse(args); err != nil {
fmt.Fprintf(stderr, "notarius: %v\n", err) fmt.Fprintf(stderr, "notarius: %v\n", err)
return 2 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) fmt.Fprintf(stderr, "notarius: %q is not a canonical JSON Pointer\n", pointer)
return 2 return 2
} }
loaded, _, err := loadConfigKit(*configPath, *profile, nil, opts) loaded, _, err := loadConfigKit(*configPath, *overlay, nil, opts)
if err != nil { if err != nil {
writeConfigLoadError(stderr, err) writeConfigLoadError(stderr, err)
return 1 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 := flag.NewFlagSet("config validate", flag.ContinueOnError)
fs.SetOutput(io.Discard) fs.SetOutput(io.Discard)
configPath := fs.String("config", "", "config file path") 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") pipelineID := fs.String("pipeline", "", "pipeline ID")
onlyRaw := fs.String("only", "", "comma-separated artifact lanes") onlyRaw := fs.String("only", "", "comma-separated artifact lanes")
if err := fs.Parse(args); err != nil { 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) fmt.Fprintf(stderr, "notarius: %v\n", err)
return 2 return 2
} }
loaded, path, err := loadConfigKit(*configPath, *profile, nil, opts) loaded, path, err := loadConfigKit(*configPath, *overlay, nil, opts)
if err != nil { if err != nil {
writeConfigLoadError(stderr, err) writeConfigLoadError(stderr, err)
return 1 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 := flag.NewFlagSet("pipelines list", flag.ContinueOnError)
fs.SetOutput(io.Discard) fs.SetOutput(io.Discard)
configPath := fs.String("config", "", "config file path") 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") jsonOutput := fs.Bool("json", false, "write JSON output")
if err := fs.Parse(args); err != nil { if err := fs.Parse(args); err != nil {
fmt.Fprintf(stderr, "notarius: %v\n", err) 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)) fmt.Fprintf(stderr, "notarius: unexpected argument %q\n", fs.Arg(0))
return 2 return 2
} }
loaded, _, err := loadConfigKit(*configPath, *profile, nil, opts) loaded, _, err := loadConfigKit(*configPath, *overlay, nil, opts)
if err != nil { if err != nil {
writeConfigLoadError(stderr, err) writeConfigLoadError(stderr, err)
return 1 return 1

View File

@@ -14,22 +14,49 @@ import (
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
) )
func TestConfigKitReadOnlyCommandsLoadImportsAndProfiles(t *testing.T) { func TestConfigKitReadOnlyCommandsLoadImportsAndOverlays(t *testing.T) {
t.Parallel() t.Parallel()
root := t.TempDir() root := t.TempDir()
path := filepath.Join(root, "notarius.yml") 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, "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") 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 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()) t.Fatalf("list = %d, %q, %q", code, stdout.String(), stderr.String())
} }
stdout.Reset() stdout.Reset()
stderr.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()) 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) { func TestConfigKitReadOnlyCommandsRejectVersionFour(t *testing.T) {
@@ -53,8 +80,8 @@ pipelines:
"0": "0":
input: {module: input} input: {module: input}
`) `)
writeConfigKitFile(t, filepath.Join(root, "profiles", "development.yml"), "concurrency:\n total_llm: 3\n") writeConfigKitFile(t, filepath.Join(root, "overlays", "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", "retired.yml"), "pipelines: null\n")
providerCalled := false providerCalled := false
opts := commandContractOptions(t) opts := commandContractOptions(t)
@@ -64,7 +91,7 @@ pipelines:
} }
var stdout, stderr bytes.Buffer 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()) t.Fatalf("effective = %d, %q, %q", code, stdout.String(), stderr.String())
} }
if providerCalled { if providerCalled {
@@ -73,7 +100,7 @@ pipelines:
stdout.Reset() stdout.Reset()
stderr.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()) t.Fatalf("explain effective = %d, %q, %q", code, stdout.String(), stderr.String())
} }
@@ -85,7 +112,7 @@ pipelines:
stdout.Reset() stdout.Reset()
stderr.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()) 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" "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) path, err := discoverConfigPath(configPath, opts)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@@ -19,7 +19,7 @@ func loadConfigKit(configPath, profile string, overrides []configkit.Override, o
if err != nil { if err != nil {
return nil, "", fmt.Errorf("determine invocation directory: %w", err) 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 { if err != nil {
return nil, "", err return nil, "", err
} }
@@ -31,7 +31,7 @@ func writeConfigLoadError(output io.Writer, err error) {
if errors.As(err, &loadError) { if errors.As(err, &loadError) {
if writeErr := configkit.WriteDiagnostics(output, loadError.Diagnostics()); writeErr == nil { if writeErr := configkit.WriteDiagnostics(output, loadError.Diagnostics()); writeErr == nil {
// ConfigKit's locations contain source IDs. Keep its renderer and // 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. // be traced to a file. Quoting also escapes terminal controls.
for _, source := range loadError.Sources() { for _, source := range loadError.Sources() {
label := source.Path label := source.Path

View File

@@ -13,16 +13,16 @@ import (
func TestConfigLoadDiagnosticsIdentifyComposedSources(t *testing.T) { func TestConfigLoadDiagnosticsIdentifyComposedSources(t *testing.T) {
t.Parallel() t.Parallel()
for _, profile := range []bool{false, true} { for _, overlay := range []bool{false, true} {
t.Run(strconv.FormatBool(profile), func(t *testing.T) { t.Run(strconv.FormatBool(overlay), func(t *testing.T) {
root := t.TempDir() root := t.TempDir()
main := filepath.Join(root, "config.yml") main := filepath.Join(root, "config.yml")
fragment := filepath.Join(root, "pipeline\n\x1b.yml") fragment := filepath.Join(root, "pipeline\n\x1b.yml")
args := []string{"config", "validate", "--config", main} args := []string{"config", "validate", "--config", main}
if profile { if overlay {
fragment = filepath.Join(root, "profiles", "development.yml") fragment = filepath.Join(root, "overlays", "development.yml")
writeConfigKitFile(t, main, "version: 5\n") writeConfigKitFile(t, main, "version: 5\n")
args = append(args, "--config-profile", "development") args = append(args, "--config-overlay", "development")
} else { } else {
writeConfigKitFile(t, main, "version: 5\nconfigkit:\n imports:\n - path: "+strconv.Quote(filepath.Base(fragment))+"\n") 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() t.Parallel()
root := t.TempDir() root := t.TempDir()
path := filepath.Join(root, "notarius.yml") path := filepath.Join(root, "notarius.yml")
writeConfigKitFile(t, path, "version: 5\npipelines:\n base:\n input: {module: input}\n") writeConfigKitFile(t, path, "version: 5\npipelines:\n base:\n input: {module: input}\n")
profiles := filepath.Join(root, "profiles") overlays := filepath.Join(root, "overlays")
writeConfigKitFile(t, filepath.Join(profiles, "development.yml"), "pipelines:\n profiled:\n input: {module: input}\n") 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)) loaded, gotPath, err := loadConfigKit(path, "development", []configkit.Override{{Path: configkit.Keys("output", "directory"), Value: "generated", Flag: "--output"}}, commandContractOptions(t))
if err != nil { if err != nil {
t.Fatalf("loadConfigKit() error = %v", err) 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) 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) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
names = names[:0] names = names[:0]
for _, entry := range profileEntries { for _, entry := range overlayEntries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yml") { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yml") {
names = append(names, entry.Name()) names = append(names, entry.Name())
} }
} }
sort.Strings(names) sort.Strings(names)
if got := strings.Join(names, ","); got != "development.yml" { 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")) llmProfiles, err := os.ReadDir(repositoryPath("examples", "llm-profiles"))
if err != nil { if err != nil {

View File

@@ -34,10 +34,10 @@ const usage = `Usage:
notarius help notarius help
notarius --version notarius --version
notarius run <pipeline-id> --input path/to/source.json [--json] [flags] 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 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-profile name] notarius config effective --config path/to/config.yml [--config-overlay name]
notarius config explain --config path/to/config.yml [--config-profile name] <json-pointer> notarius config explain --config path/to/config.yml [--config-overlay name] <json-pointer>
notarius pipelines list --config path/to/config.yml [--config-profile name] [--json] notarius pipelines list --config path/to/config.yml [--config-overlay name] [--json]
` `
type Options struct { 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 := flag.NewFlagSet("run", flag.ContinueOnError)
fs.SetOutput(io.Discard) fs.SetOutput(io.Discard)
configPath := fs.String("config", "", "config file path") 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") inputPath := fs.String("input", "", "source input file path")
onlyRaw := fs.String("only", "", "comma-separated artifact lanes") onlyRaw := fs.String("only", "", "comma-separated artifact lanes")
outputDir := fs.String("output-dir", "", "output directory") outputDir := fs.String("output-dir", "", "output directory")
@@ -268,7 +268,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
return 2 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 { if err != nil {
writeConfigLoadError(stderr, err) writeConfigLoadError(stderr, err)
return 1 return 1
@@ -331,7 +331,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
InputPath: strings.TrimSpace(*inputPath), InputPath: strings.TrimSpace(*inputPath),
ConfigPath: loadedConfigPath, ConfigPath: loadedConfigPath,
ConfigSource: configSource(*configPath), ConfigSource: configSource(*configPath),
ConfigProfile: effectiveConfigProfile(*configProfile), ConfigOverlay: effectiveConfigOverlay(*configOverlay),
OnlyLanes: append([]string(nil), only...), OnlyLanes: append([]string(nil), only...),
ChunkCacheOverride: chunkCache.explicitValue(), ChunkCacheOverride: chunkCache.explicitValue(),
ReasoningEffortOverride: runtimeOverrides.ReasoningEffort, ReasoningEffortOverride: runtimeOverrides.ReasoningEffort,
@@ -877,7 +877,7 @@ func reorderRunArgs(args []string) []string {
func runFlagTakesValue(arg string) bool { func runFlagTakesValue(arg string) bool {
switch arg { 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 return true
default: default:
return false return false
@@ -963,7 +963,7 @@ func chunkPlanStoreForRun(cfg config.ChunkPlanCacheConfig, opts Options) (pipeli
func validateRunFlagValues(args []string) error { func validateRunFlagValues(args []string) error {
for i, arg := range args { 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 continue
} }
if i+1 >= len(args) || strings.HasPrefix(args[i+1], "-") { if i+1 >= len(args) || strings.HasPrefix(args[i+1], "-") {
@@ -973,11 +973,11 @@ func validateRunFlagValues(args []string) error {
return nil return nil
} }
func effectiveConfigProfile(profile string) string { func effectiveConfigOverlay(overlay string) string {
if profile == "" || profile == "default" { if overlay == "" || overlay == "default" {
return "" return ""
} }
return profile return overlay
} }
func flagWasProvided(args []string, name string) bool { 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 { {name: "blank debug directory", args: func(roots stateTestRoots) []string {
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--debug-dir", ""} return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--debug-dir", ""}
}}, }},
{name: "missing configuration profile", args: func(roots stateTestRoots) []string { {name: "missing configuration overlay", args: func(roots stateTestRoots) []string {
return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--config-profile"} return []string{"run", "sample", "--config", roots.config, "--input", roots.input, "--config-overlay"}
}}, }},
{name: "debug directory without debug", args: func(roots stateTestRoots) []string { {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")} 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) roots := newStateTestRoots(t)
profileDirectory := filepath.Join(filepath.Dir(roots.config), "profiles") overlayDirectory := filepath.Join(filepath.Dir(roots.config), "overlays")
profileOutput := filepath.Join(profileDirectory, "profile-output") overlayOutput := filepath.Join(overlayDirectory, "overlay-output")
profileDebug := filepath.Join(profileDirectory, "profile-debug") overlayDebug := filepath.Join(overlayDirectory, "overlay-debug")
writeConfigKitFile(t, filepath.Join(profileDirectory, "development.yml"), "output:\n directory: ./profile-output\ndebug:\n directory: ./profile-debug\n") writeConfigKitFile(t, filepath.Join(overlayDirectory, "development.yml"), "output:\n directory: ./overlay-output\ndebug:\n directory: ./overlay-debug\n")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
code := RunWithOptions([]string{ 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", "--input", roots.input, "--chunk_cache", "bypass", "--debug",
}, &stdout, &stderr, newStateTestHarness().options()) }, &stdout, &stderr, newStateTestHarness().options())
if code != 0 || stderr.Len() != 0 { if code != 0 || stderr.Len() != 0 {
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String()) 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")) assertFile(t, filepath.Join(overlayOutput, filepath.Base(onlyChildDir(t, overlayOutput)), "result.json"))
bundle := onlyChildDir(t, profileDebug) bundle := onlyChildDir(t, overlayDebug)
var invocation debugbundle.Invocation var invocation debugbundle.Invocation
readStateTestSummaryJSON(t, bundle, "invocation.json", &invocation) readStateTestSummaryJSON(t, bundle, "invocation.json", &invocation)
if invocation.ConfigProfile != "development" { if invocation.ConfigOverlay != "development" {
t.Fatalf("config profile = %q, want development", invocation.ConfigProfile) t.Fatalf("config overlay = %q, want development", invocation.ConfigOverlay)
} }
} }
func TestEffectiveConfigProfileOmitsBaseSelection(t *testing.T) { func TestEffectiveConfigOverlayOmitsBaseSelection(t *testing.T) {
for _, profile := range []string{"", "default"} { for _, overlay := range []string{"", "default"} {
if got := effectiveConfigProfile(profile); got != "" { if got := effectiveConfigOverlay(overlay); got != "" {
t.Fatalf("effectiveConfigProfile(%q) = %q, want empty", profile, got) t.Fatalf("effectiveConfigOverlay(%q) = %q, want empty", overlay, got)
} }
} }
if got := effectiveConfigProfile("development"); got != "development" { if got := effectiveConfigOverlay("development"); got != "development" {
t.Fatalf("effectiveConfigProfile(development) = %q", got) t.Fatalf("effectiveConfigOverlay(development) = %q", got)
} }
} }

View File

@@ -77,27 +77,48 @@ func TestDocumentPathsUseInvocationDirectoryForOverrides(t *testing.T) {
} }
} }
func TestDocumentPathsUseProfileDeclarationDirectory(t *testing.T) { func TestDocumentPathsUseOverlayDeclarationDirectory(t *testing.T) {
t.Parallel() t.Parallel()
root := t.TempDir() root := t.TempDir()
profiles := filepath.Join(root, "profiles") overlays := filepath.Join(root, "overlays")
if err := os.MkdirAll(profiles, 0o700); err != nil { if err := os.MkdirAll(overlays, 0o700); err != nil {
t.Fatal(err) t.Fatal(err)
} }
main := filepath.Join(root, "notarius.yml") main := filepath.Join(root, "notarius.yml")
if err := os.WriteFile(main, []byte("version: 5\n"), 0o600); err != nil { if err := os.WriteFile(main, []byte("version: 5\n"), 0o600); err != nil {
t.Fatal(err) 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) t.Fatal(err)
} }
loaded, err := loadConfig(main, LoadOptions{Profile: "development"}) loaded, err := loadConfig(main, LoadOptions{Overlay: "development"})
if err != nil { if err != nil {
t.Fatalf("loadConfig() error = %v", err) t.Fatalf("loadConfig() error = %v", err)
} }
if got := loaded.Config.Output.Directory; got != filepath.Join(profiles, "profile-output") { if got := loaded.Config.Output.Directory; got != filepath.Join(overlays, "overlay-output") {
t.Fatalf("profile directory = %q", got) 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")
} }
} }

View File

@@ -10,7 +10,7 @@ import (
// boundary. It is intentionally narrow: configuration discovery and CLI flag // boundary. It is intentionally narrow: configuration discovery and CLI flag
// parsing remain outside this package. // parsing remain outside this package.
type LoadOptions struct { type LoadOptions struct {
Profile string Overlay string
Overrides []configkit.Override Overrides []configkit.Override
InvocationDirectory string InvocationDirectory string
} }
@@ -30,7 +30,7 @@ func loadDocumentWithSource(source configkit.Source, options LoadOptions, import
return configkit.Load(source, configkit.Options[Document]{ return configkit.Load(source, configkit.Options[Document]{
Definition: documentDefinition(state), Definition: documentDefinition(state),
Imports: imports, Imports: imports,
Profile: options.Profile, Overlay: options.Overlay,
Overrides: options.Overrides, Overrides: options.Overrides,
}) })
} }

View File

@@ -105,28 +105,28 @@ debug:
} }
} }
func TestDocumentLoadDerivesWorkersAfterProfileAndCLIOverrides(t *testing.T) { func TestDocumentLoadDerivesWorkersAfterOverlayAndCLIOverrides(t *testing.T) {
t.Parallel() t.Parallel()
directory := t.TempDir() directory := t.TempDir()
main := filepath.Join(directory, "config.yml") main := filepath.Join(directory, "config.yml")
profiles := filepath.Join(directory, "profiles") overlays := filepath.Join(directory, "overlays")
if err := os.Mkdir(profiles, 0o755); err != nil { if err := os.Mkdir(overlays, 0o755); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := os.WriteFile(main, []byte("version: 5\nconcurrency:\n total_llm: 4\n"), 0o600); err != nil { if err := os.WriteFile(main, []byte("version: 5\nconcurrency:\n total_llm: 4\n"), 0o600); err != nil {
t.Fatal(err) 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) t.Fatal(err)
} }
profiled, err := loadDocument(main, LoadOptions{Profile: "development"}) overlaid, err := loadDocument(main, LoadOptions{Overlay: "development"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if profiled.Config.Concurrency.TotalLLM == nil || *profiled.Config.Concurrency.TotalLLM != 3 || profiled.Config.Concurrency.StageWorkers["extract"] != 3 { if overlaid.Config.Concurrency.TotalLLM == nil || *overlaid.Config.Concurrency.TotalLLM != 3 || overlaid.Config.Concurrency.StageWorkers["extract"] != 3 {
t.Fatalf("profiled concurrency = %#v", profiled.Config.Concurrency) t.Fatalf("overlaid concurrency = %#v", overlaid.Config.Concurrency)
} }
overridden, err := loadDocument(main, LoadOptions{Overrides: []configkit.Override{{ 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 { 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) 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) { func TestDocumentLoadRetainsExplicitWorkerAndRejectsInvalidOperationalValues(t *testing.T) {

View File

@@ -39,7 +39,7 @@ type Invocation struct {
InputPath string `json:"input_path,omitempty"` InputPath string `json:"input_path,omitempty"`
ConfigPath string `json:"config_path,omitempty"` ConfigPath string `json:"config_path,omitempty"`
ConfigSource string `json:"config_source,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"` OnlyLanes []string `json:"only_lanes,omitempty"`
ChunkCacheOverride string `json:"chunk_cache_override,omitempty"` ChunkCacheOverride string `json:"chunk_cache_override,omitempty"`
ReasoningEffortOverride *string `json:"reasoning_effort_override,omitempty"` ReasoningEffortOverride *string `json:"reasoning_effort_override,omitempty"`