Enable explicit configuration imports and overlays

This commit is contained in:
2026-09-26 18:12:27 +00:00
parent 36e7e857f5
commit 9ac7006a79
15 changed files with 204 additions and 30 deletions

View File

@@ -42,9 +42,9 @@ func applicationOverrides(fs *flag.FlagSet) []configkit.Override {
return overrides
}
func loadApplicationSettings(configPath string, explicit bool, overrides []configkit.Override) (appconfig.AppSettings, []configkit.Diagnostic, error) {
func loadApplicationSettings(configPath, overlay string, explicit bool, overrides []configkit.Override) (appconfig.AppSettings, []configkit.Diagnostic, error) {
result, err := appconfig.Load(appconfig.LoadOptions{
Path: configPath, Explicit: explicit, Overrides: overrides,
Path: configPath, Overlay: overlay, Explicit: explicit, Overrides: overrides,
})
if err != nil {
return appconfig.AppSettings{}, nil, err

View File

@@ -17,6 +17,7 @@ import (
type promptInspectionConfig struct {
configPath, promptDir, promptID, promptVersion, outputPath string
configOverlay string
configExplicit bool
outputFormat appformat.OutputFormat
overrides []configkit.Override
@@ -25,6 +26,7 @@ type promptInspectionConfig struct {
type profileInspectionConfig struct {
configPath, profileDir, profileID, outputPath string
configOverlay string
configExplicit bool
outputFormat appformat.OutputFormat
overrides []configkit.Override
@@ -127,7 +129,7 @@ func parseProfileInspectionArgs(args []string) (*profileInspectionConfig, error)
cfg := &profileInspectionConfig{outputFormat: appformat.DefaultOutputFormat}
fs := flag.NewFlagSet("inspect profile", flag.ContinueOnError)
fs.SetOutput(io.Discard)
registerConfigPathFlag(fs, &cfg.configPath)
registerConfigSelectionFlags(fs, &cfg.configPath, &cfg.configOverlay)
fs.StringVar(&cfg.profileDir, "profile-dir", "", "directory containing execution profiles")
fs.StringVar(&cfg.profileID, "profile", "", "profile ID to inspect")
rawFormat := ""
@@ -156,7 +158,7 @@ func parseProfileInspectionArgs(args []string) (*profileInspectionConfig, error)
}
func resolveAppSettingsForProfileInspection(cfg *profileInspectionConfig) (engineSettings, error) {
settings, diagnostics, err := loadApplicationSettings(cfg.configPath, cfg.configExplicit, cfg.overrides)
settings, diagnostics, err := loadApplicationSettings(cfg.configPath, cfg.configOverlay, cfg.configExplicit, cfg.overrides)
if err != nil {
return engineSettings{}, err
}
@@ -168,7 +170,7 @@ func parsePromptInspectionArgs(args []string) (*promptInspectionConfig, error) {
cfg := &promptInspectionConfig{outputFormat: appformat.DefaultOutputFormat}
fs := flag.NewFlagSet("inspect prompt", flag.ContinueOnError)
fs.SetOutput(io.Discard)
registerConfigPathFlag(fs, &cfg.configPath)
registerConfigSelectionFlags(fs, &cfg.configPath, &cfg.configOverlay)
fs.StringVar(&cfg.promptDir, "prompt-dir", "", "directory containing prompt definitions")
fs.StringVar(&cfg.promptID, "prompt", "", "prompt ID to inspect")
fs.StringVar(&cfg.promptVersion, "prompt-version", "", "optional prompt version")
@@ -198,7 +200,7 @@ func parsePromptInspectionArgs(args []string) (*promptInspectionConfig, error) {
}
func resolveAppSettingsForPromptInspection(cfg *promptInspectionConfig) (engineSettings, error) {
settings, diagnostics, err := loadApplicationSettings(cfg.configPath, cfg.configExplicit, cfg.overrides)
settings, diagnostics, err := loadApplicationSettings(cfg.configPath, cfg.configOverlay, cfg.configExplicit, cfg.overrides)
if err != nil {
return engineSettings{}, err
}

View File

@@ -59,6 +59,7 @@ type runConfig struct {
transientRetries int
configuredTransientRetries *int
diagnostics []configkit.Diagnostic
configOverlay string
defaultRenderFormat renderformat.PreparedRunOutputFormat
@@ -92,6 +93,7 @@ type serveConfig struct {
backends []appconfig.BackendSettings
transientRetries *int
diagnostics []configkit.Diagnostic
configOverlay string
}
type commonCommandSettings struct {
@@ -306,7 +308,7 @@ func parseRunArgs(args []string) (*runConfig, error) {
fs := flag.NewFlagSet("run", flag.ContinueOnError)
fs.SetOutput(io.Discard)
registerConfigPathFlag(fs, &cfg.configPath)
registerConfigSelectionFlags(fs, &cfg.configPath, &cfg.configOverlay)
registerExecutionRequestFlags(fs, cfg)
fs.StringVar(&cfg.schemaDir, "schema-dir", "", "base directory for validation schemas")
fs.DurationVar(&cfg.runTimeout, "run-timeout", 0, "whole-run execution deadline")
@@ -363,7 +365,7 @@ func parseRenderArgs(args []string) (*renderConfig, error) {
fs := flag.NewFlagSet("render", flag.ContinueOnError)
fs.SetOutput(io.Discard)
registerConfigPathFlag(fs, &cfg.configPath)
registerConfigSelectionFlags(fs, &cfg.configPath, &cfg.configOverlay)
registerExecutionRequestFlags(fs, &cfg.runConfig)
fs.StringVar(&cfg.schemaDir, "schema-dir", "", "base directory for validation schemas")
@@ -394,7 +396,7 @@ func parseServeArgs(args []string) (*serveConfig, error) {
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
fs.SetOutput(io.Discard)
registerConfigPathFlag(fs, &cfg.configPath)
registerConfigSelectionFlags(fs, &cfg.configPath, &cfg.configOverlay)
fs.StringVar(&cfg.addr, "addr", "", "HTTP listen address")
fs.StringVar(&cfg.promptDir, "prompt-dir", "", "directory containing prompt definition YAML files")
fs.StringVar(&cfg.profileDir, "profile-dir", "", "directory containing execution profile YAML files")
@@ -411,7 +413,7 @@ func parseServeArgs(args []string) (*serveConfig, error) {
return nil, fmt.Errorf("unexpected positional args: %v", fs.Args())
}
settings, err := resolveCommonSettings(fs, cfg.configPath)
settings, err := resolveCommonSettings(fs, cfg.configPath, cfg.configOverlay)
if err != nil {
return nil, err
}
@@ -463,7 +465,7 @@ func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
return fmt.Errorf("unexpected positional args: %v", fs.Args())
}
settings, err := resolveCommonSettings(fs, cfg.configPath)
settings, err := resolveCommonSettings(fs, cfg.configPath, cfg.configOverlay)
if err != nil {
return err
}
@@ -500,7 +502,7 @@ func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
return nil
}
func registerConfigPathFlag(fs *flag.FlagSet, target *string) {
func registerConfigSelectionFlags(fs *flag.FlagSet, target, overlay *string) {
fs.StringVar(
target,
"config",
@@ -511,10 +513,11 @@ func registerConfigPathFlag(fs *flag.FlagSet, target *string) {
appconfig.DefaultConfigPath,
),
)
fs.StringVar(overlay, "config-overlay", "", "selected application configuration overlay")
}
func resolveCommonSettings(fs *flag.FlagSet, configPath string) (commonCommandSettings, error) {
settings, diagnostics, err := loadApplicationSettings(configPath, flagWasSet(fs, "config"), applicationOverrides(fs))
func resolveCommonSettings(fs *flag.FlagSet, configPath, overlay string) (commonCommandSettings, error) {
settings, diagnostics, err := loadApplicationSettings(configPath, overlay, flagWasSet(fs, "config"), applicationOverrides(fs))
if err != nil {
return commonCommandSettings{}, err
}
@@ -750,9 +753,9 @@ func runErrorMessage(err error) string {
func printUsage(w io.Writer) {
fmt.Fprintln(w, "usage: scriptorium <run|render|serve|inspect> ...")
fmt.Fprintln(w, " run: scriptorium run [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID [--prompt-version VERSION] [--input name=path] [--profile ID] [--session-id ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--reasoning-effort VALUE] [--var k=v] [--out path] [--execution-report path] [--timeout 10m] [--run-timeout 10m] [--transient-retries N]")
fmt.Fprintln(w, " render: scriptorium render [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID [--prompt-version VERSION] [--input name=path] [--profile ID] [--session-id ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--reasoning-effort VALUE] [--var k=v] [--format text|json] [--out path] [--timeout 10m] [--transient-retries N]")
fmt.Fprintf(w, " serve: scriptorium serve [--config PATH] [--addr %s] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR] [--artifact-root DIR] [--max-request-bytes N] [--max-artifact-bytes N] [--max-response-bytes N]\n", defaults.HTTPAddrDefault)
fmt.Fprintln(w, " inspect prompt: scriptorium inspect prompt --prompt ID [--prompt-version VERSION] [--config PATH] [--prompt-dir DIR] [--format text|json] [--out path]")
fmt.Fprintln(w, " inspect profile: scriptorium inspect profile --profile ID [--config PATH] [--profile-dir DIR] [--format text|json] [--out path]")
fmt.Fprintln(w, " run: scriptorium run [--config PATH] [--config-overlay NAME] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR] --prompt ID [--prompt-version VERSION] [--input name=path] [--profile ID] [--session-id ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--reasoning-effort VALUE] [--var k=v] [--out path] [--execution-report path] [--timeout 10m] [--run-timeout 10m] [--transient-retries N]")
fmt.Fprintln(w, " render: scriptorium render [--config PATH] [--config-overlay NAME] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR] --prompt ID [--prompt-version VERSION] [--input name=path] [--profile ID] [--session-id ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--reasoning-effort VALUE] [--var k=v] [--format text|json] [--out path] [--timeout 10m] [--transient-retries N]")
fmt.Fprintf(w, " serve: scriptorium serve [--config PATH] [--config-overlay NAME] [--addr %s] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR] [--artifact-root DIR] [--max-request-bytes N] [--max-artifact-bytes N] [--max-response-bytes N]\n", defaults.HTTPAddrDefault)
fmt.Fprintln(w, " inspect prompt: scriptorium inspect prompt --prompt ID [--prompt-version VERSION] [--config PATH] [--config-overlay NAME] [--prompt-dir DIR] [--format text|json] [--out path]")
fmt.Fprintln(w, " inspect profile: scriptorium inspect profile --profile ID [--config PATH] [--config-overlay NAME] [--profile-dir DIR] [--format text|json] [--out path]")
}

View File

@@ -1071,6 +1071,41 @@ profile_dir: %s
}
}
func TestRenderCommandLoadsImportsAndOverlayWithoutChangingExecutionProfile(t *testing.T) {
lib := newCLITestLibrary(t)
inputPath := lib.writeInputFile(t, "transcript.md", "hello transcript")
writePromptFile(t, lib.promptDir, "prompt.render", "local-default")
writeProfileFile(t, lib.profileDir, "local-default", "http://127.0.0.1:1/v1", "profile-model")
if err := os.MkdirAll(filepath.Join(lib.rootDir, "fragments"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(lib.rootDir, "overlays"), 0o755); err != nil {
t.Fatal(err)
}
base := filepath.Join(lib.rootDir, "config.yml")
for path, body := range map[string]string{
base: "configkit:\n imports:\n - path: fragments/sources.yml\n",
filepath.Join(lib.rootDir, "fragments", "sources.yml"): fmt.Sprintf("prompt_dir: %q\nprofile_dir: %q\n", lib.promptDir, lib.profileDir),
filepath.Join(lib.rootDir, "overlays", "local.yml"): "defaults:\n render_format: json\n",
} {
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
args := []string{"--config", base, "--config-overlay", "local", "--profile", "local-default", "--prompt", "prompt.render", "--input", "transcript=" + inputPath}
cfg, err := parseRenderArgs(args)
if err != nil {
t.Fatal(err)
}
if cfg.profileID != "local-default" || cfg.outputFormat != renderformat.PreparedRunFormatJSON {
t.Fatalf("overlay changed execution profile or output selection: %+v", cfg)
}
code, stdout, stderr := runCLICommand(t, renderCommand, args)
if code != ExitOK || !strings.Contains(stdout, `"selected_profile_id": "local-default"`) {
t.Fatalf("render did not use composed config and independent execution profile: code=%d out=%q err=%q", code, stdout, stderr)
}
}
func TestRenderCommandUsesConfiguredCustomBackend(t *testing.T) {
lib := newCLITestLibrary(t)
writePromptDefinition(t, lib.promptDir, "custom.yaml", `id: custom