Enable explicit configuration imports and overlays
This commit is contained in:
14
docs/cli.md
14
docs/cli.md
@@ -26,7 +26,11 @@ go run ./cmd/scriptorium render \
|
|||||||
- `scriptorium inspect prompt`: inspect one prompt definition without model execution.
|
- `scriptorium inspect prompt`: inspect one prompt definition without model execution.
|
||||||
- `scriptorium inspect profile`: inspect one effective profile without model execution.
|
- `scriptorium inspect profile`: inspect one effective profile without model execution.
|
||||||
|
|
||||||
All commands accept `--config <path>` and reject positional arguments. `run`,
|
All commands accept `--config <path>` and `--config-overlay <name>` and reject
|
||||||
|
positional arguments. `--config-overlay` selects one application configuration
|
||||||
|
overlay; it is separate from `--profile`, which selects a Promptkit execution
|
||||||
|
profile. See [configuration composition](config.md#explicit-imports-and-selected-overlays)
|
||||||
|
for name, root, and no-base behavior. `run`,
|
||||||
`render`, `serve`, and `inspect prompt` require an effective `prompt_dir`;
|
`render`, `serve`, and `inspect prompt` require an effective `prompt_dir`;
|
||||||
`inspect profile` does not. Supply the directory through the configuration
|
`inspect profile` does not. Supply the directory through the configuration
|
||||||
contract or a supported `--prompt-dir` flag. Explicit application flags,
|
contract or a supported `--prompt-dir` flag. Explicit application flags,
|
||||||
@@ -51,6 +55,7 @@ Optional flags:
|
|||||||
| Flag | Meaning |
|
| Flag | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--config <path>` | Application configuration file. |
|
| `--config <path>` | Application configuration file. |
|
||||||
|
| `--config-overlay <name>` | Selected application configuration overlay. |
|
||||||
| `--prompt-dir <dir>` | Prompt-definition directory override. |
|
| `--prompt-dir <dir>` | Prompt-definition directory override. |
|
||||||
| `--profile-dir <dir>` | Custom profile-directory override. |
|
| `--profile-dir <dir>` | Custom profile-directory override. |
|
||||||
| `--schema-dir <dir>` | Schema base-directory override. |
|
| `--schema-dir <dir>` | Schema base-directory override. |
|
||||||
@@ -148,7 +153,7 @@ scriptorium render [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
`--prompt <id>` is required. The following optional flags are supported:
|
`--prompt <id>` is required. The following optional flags are supported:
|
||||||
`--config`, `--prompt-dir`, `--profile-dir`, `--schema-dir`, `--prompt-version`, `--profile`,
|
`--config`, `--config-overlay`, `--prompt-dir`, `--profile-dir`, `--schema-dir`, `--prompt-version`, `--profile`,
|
||||||
`--input`, `--var`, `--out`, `--llm-base-url`,
|
`--input`, `--var`, `--out`, `--llm-base-url`,
|
||||||
`--model`, `--api-key-env`, `--temperature`, `--max-tokens`, `--top-p`,
|
`--model`, `--api-key-env`, `--temperature`, `--max-tokens`, `--top-p`,
|
||||||
`--reasoning-effort`, `--session-id`, `--timeout`, `--transient-retries`, and `--format text|json`. Their meanings match the corresponding
|
`--reasoning-effort`, `--session-id`, `--timeout`, `--transient-retries`, and `--format text|json`. Their meanings match the corresponding
|
||||||
@@ -174,6 +179,7 @@ Optional flags:
|
|||||||
| Flag | Meaning |
|
| Flag | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--config <path>` | Application configuration file. |
|
| `--config <path>` | Application configuration file. |
|
||||||
|
| `--config-overlay <name>` | Selected application configuration overlay. |
|
||||||
| `--addr <listen-address>` | HTTP listen-address override. |
|
| `--addr <listen-address>` | HTTP listen-address override. |
|
||||||
| `--prompt-dir <dir>` | Prompt-definition directory override. |
|
| `--prompt-dir <dir>` | Prompt-definition directory override. |
|
||||||
| `--profile-dir <dir>` | Custom profile-directory override. |
|
| `--profile-dir <dir>` | Custom profile-directory override. |
|
||||||
@@ -190,7 +196,7 @@ schemas, and error codes are defined in the [HTTP API reference](api.md).
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
scriptorium inspect prompt --prompt ID [--prompt-version VERSION]
|
scriptorium inspect prompt --prompt ID [--prompt-version VERSION]
|
||||||
[--config PATH] [--prompt-dir DIR] [--format text|json] [--out PATH]
|
[--config PATH] [--config-overlay NAME] [--prompt-dir DIR] [--format text|json] [--out PATH]
|
||||||
```
|
```
|
||||||
|
|
||||||
`--prompt` is required. Inspection uses normal configuration discovery and a
|
`--prompt` is required. Inspection uses normal configuration discovery and a
|
||||||
@@ -203,7 +209,7 @@ template, reserve backend capacity, or call a model.
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
scriptorium inspect profile --profile ID
|
scriptorium inspect profile --profile ID
|
||||||
[--config PATH] [--profile-dir DIR] [--format text|json] [--out PATH]
|
[--config PATH] [--config-overlay NAME] [--profile-dir DIR] [--format text|json] [--out PATH]
|
||||||
```
|
```
|
||||||
|
|
||||||
`--profile` is required; built-in profiles need no prompt directory. Inspection
|
`--profile` is required; built-in profiles need no prompt directory. Inspection
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ When `--config` is omitted, Scriptorium searches
|
|||||||
If neither exists, it uses built-in defaults. An explicit `--config` path must
|
If neither exists, it uses built-in defaults. An explicit `--config` path must
|
||||||
exist and decode successfully.
|
exist and decode successfully.
|
||||||
|
|
||||||
The maintained [minimal configuration](../examples/config.yml) and
|
The maintained [minimal configuration](../examples/config.yml),
|
||||||
[complete configuration](../examples/config.full.yml) are copyable examples.
|
[complete configuration](../examples/config.full.yml), and
|
||||||
|
[composed configuration](../examples/config.composed.yml) are copyable examples.
|
||||||
|
|
||||||
## Application Configuration File
|
## Application Configuration File
|
||||||
|
|
||||||
@@ -31,8 +32,7 @@ An explicit empty string is a value: it clears a file-provided optional path
|
|||||||
or, for required `prompt_dir`, fails the command's prerequisite. A scalar
|
or, for required `prompt_dir`, fails the command's prerequisite. A scalar
|
||||||
`null` is not an empty string and is rejected for scalar fields; nullable
|
`null` is not an empty string and is rejected for scalar fields; nullable
|
||||||
fields such as `transient_retries` retain their documented meaning. Raw
|
fields such as `transient_retries` retain their documented meaning. Raw
|
||||||
API-key fields are not accepted. Imports and selected overlays are not yet
|
API-key fields are not accepted.
|
||||||
enabled for application configuration.
|
|
||||||
|
|
||||||
ConfigKit applies defaults, the selected file, then only explicitly supplied
|
ConfigKit applies defaults, the selected file, then only explicitly supplied
|
||||||
application CLI flags before validating the result. Paths in the application
|
application CLI flags before validating the result. Paths in the application
|
||||||
@@ -40,6 +40,43 @@ document remain relative to the process working directory, not to the file's
|
|||||||
directory. Load diagnostics are printed safely on stderr; invalid application
|
directory. Load diagnostics are printed safely on stderr; invalid application
|
||||||
configuration exits with status 1.
|
configuration exits with status 1.
|
||||||
|
|
||||||
|
### Explicit imports and selected overlays
|
||||||
|
|
||||||
|
A file-backed configuration may declare `configkit.imports` with relative
|
||||||
|
`path` or `glob` entries. Each entry is explicit; Scriptorium does not scan
|
||||||
|
for fragments on its own. ConfigKit composes the selected file and imports by
|
||||||
|
conflict, so two fragments cannot define the same field merely to establish
|
||||||
|
precedence. A required missing import fails; an entry may use
|
||||||
|
`optional: true` to suppress target nonexistence. Imports are resolved from
|
||||||
|
the declaring file's canonical directory and remain within the selected
|
||||||
|
file's canonical directory by default. Scriptorium does not widen that root.
|
||||||
|
|
||||||
|
`--config-overlay NAME` selects one file at `overlays/NAME.yml` below that
|
||||||
|
root. The name must use letters, digits, `_`, or `-`; it is not a path. Empty
|
||||||
|
and exact lowercase `default` select no overlay. There is no fallback,
|
||||||
|
case folding, inherited stack, or automatic selection; a missing selected
|
||||||
|
overlay fails. ConfigKit merges mapping fields and replaces other values,
|
||||||
|
including sequences and explicit nulls, before applying explicit CLI flags.
|
||||||
|
An active overlay needs a selected base file. No-source defaults-only loads
|
||||||
|
cannot use imports or an active overlay.
|
||||||
|
|
||||||
|
The [composed example](../examples/config.composed.yml) imports two fragments
|
||||||
|
and has a selectable [local overlay](../examples/overlays/local.yml). From the
|
||||||
|
repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run ./cmd/scriptorium render \
|
||||||
|
--config ./examples/config.composed.yml --config-overlay local \
|
||||||
|
--prompt generic.markdown_summary \
|
||||||
|
--input transcript=./examples/fixtures/transcript.md \
|
||||||
|
--input glossary=./examples/fixtures/glossary.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Import and overlay file locations are constrained by ConfigKit;
|
||||||
|
*application path values* such as `prompt_dir` remain relative
|
||||||
|
to the process working directory even when authored in a fragment or overlay.
|
||||||
|
These source constraints are separate from HTTP artifact containment.
|
||||||
|
|
||||||
| Field | Default | Meaning |
|
| Field | Default | Meaning |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `prompt_dir` | unset | Promptkit prompt-definition source directory. `run`, `render`, `serve`, and `inspect prompt` require an effective value. |
|
| `prompt_dir` | unset | Promptkit prompt-definition source directory. `run`, `render`, `serve`, and `inspect prompt` require an effective value. |
|
||||||
|
|||||||
@@ -40,8 +40,10 @@ implicit discovery. One ConfigKit load applies explicit application overrides
|
|||||||
before validation and returns a result containing the resolved document and
|
before validation and returns a result containing the resolved document and
|
||||||
its frozen inspection records. `Settings` converts that document into the
|
its frozen inspection records. `Settings` converts that document into the
|
||||||
existing runtime settings with backend IDs sorted; it does not read files or
|
existing runtime settings with backend IDs sorted; it does not read files or
|
||||||
apply another merge. Imports and selected overlays remain disabled until their
|
apply another merge. File-backed loads enable explicit ConfigKit imports and
|
||||||
own user-facing integration is ready.
|
select one named overlay when requested. ConfigKit uses the selected file's
|
||||||
|
canonical directory as the source root; the application does not widen it.
|
||||||
|
Application path values retain process-working-directory-relative meaning.
|
||||||
|
|
||||||
Every configuration-loading command uses this boundary. The CLI maps only
|
Every configuration-loading command uses this boundary. The CLI maps only
|
||||||
explicitly visited application flags to ConfigKit overrides and prints
|
explicitly visited application flags to ConfigKit overrides and prints
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ schema sources that follow the tagged framework formats. For an HTTP deployment
|
|||||||
that accepts file artifacts, use a dedicated, narrow artifact directory rather
|
that accepts file artifacts, use a dedicated, narrow artifact directory rather
|
||||||
than a general-purpose or sensitive filesystem tree.
|
than a general-purpose or sensitive filesystem tree.
|
||||||
|
|
||||||
|
If application configuration uses explicit imports or a selected overlay,
|
||||||
|
deploy those files beneath the selected configuration file's canonical
|
||||||
|
directory. ConfigKit keeps imported and overlay files within that root;
|
||||||
|
Scriptorium does not widen it. Application path values inside those files still
|
||||||
|
resolve from the process working directory, so set that directory deliberately
|
||||||
|
in the service manager. This configuration-source boundary is distinct from
|
||||||
|
the HTTP artifact root and does not constrain prompt or schema directories.
|
||||||
|
|
||||||
When upgrading existing definitions, review selected profile IDs.
|
When upgrading existing definitions, review selected profile IDs.
|
||||||
`deepseek-3-2`, `deepseek-4-flash`, and `deepseek-4-pro` were removed from the
|
`deepseek-3-2`, `deepseek-4-flash`, and `deepseek-4-pro` were removed from the
|
||||||
selected catalog. Choose an available catalog profile, one of Promptkit's
|
selected catalog. Choose an available catalog profile, one of Promptkit's
|
||||||
|
|||||||
@@ -277,7 +277,11 @@ not yet advertised.
|
|||||||
|
|
||||||
## Stage 4: Enable Explicit Imports And Selected Configuration Overlays
|
## Stage 4: Enable Explicit Imports And Selected Configuration Overlays
|
||||||
|
|
||||||
**Status: Not started.**
|
**Status: Complete.** File-backed loads support explicit imports and one
|
||||||
|
selected overlay under the selected file's canonical directory. All five
|
||||||
|
existing command modes accept `--config-overlay`; real-file composition and a
|
||||||
|
render workflow are tested. The composed example and contracts are updated;
|
||||||
|
full test, race, vet, build, and diff checks passed.
|
||||||
|
|
||||||
Primary files: shared loader, CLI source-selection wiring, configuration and
|
Primary files: shared loader, CLI source-selection wiring, configuration and
|
||||||
CLI tests, maintained configuration examples.
|
CLI tests, maintained configuration examples.
|
||||||
|
|||||||
4
examples/config-fragments/backend.yml
Normal file
4
examples/config-fragments/backend.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
backends:
|
||||||
|
local-gpu:
|
||||||
|
endpoint: http://localhost:11434/v1
|
||||||
|
api_key_env: LOCAL_GPU_API_KEY
|
||||||
3
examples/config-fragments/sources.yml
Normal file
3
examples/config-fragments/sources.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
prompt_dir: ./examples/prompts
|
||||||
|
profile_dir: ./examples/profiles
|
||||||
|
schema_dir: ./examples/schemas
|
||||||
8
examples/config.composed.yml
Normal file
8
examples/config.composed.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Select from the repository root; application paths remain working-directory-relative.
|
||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: config-fragments/sources.yml
|
||||||
|
- path: config-fragments/backend.yml
|
||||||
|
|
||||||
|
server:
|
||||||
|
addr: :8080
|
||||||
4
examples/overlays/local.yml
Normal file
4
examples/overlays/local.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
defaults:
|
||||||
|
render_format: json
|
||||||
|
server:
|
||||||
|
addr: 127.0.0.1:8080
|
||||||
@@ -42,9 +42,9 @@ func applicationOverrides(fs *flag.FlagSet) []configkit.Override {
|
|||||||
return overrides
|
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{
|
result, err := appconfig.Load(appconfig.LoadOptions{
|
||||||
Path: configPath, Explicit: explicit, Overrides: overrides,
|
Path: configPath, Overlay: overlay, Explicit: explicit, Overrides: overrides,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return appconfig.AppSettings{}, nil, err
|
return appconfig.AppSettings{}, nil, err
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
type promptInspectionConfig struct {
|
type promptInspectionConfig struct {
|
||||||
configPath, promptDir, promptID, promptVersion, outputPath string
|
configPath, promptDir, promptID, promptVersion, outputPath string
|
||||||
|
configOverlay string
|
||||||
configExplicit bool
|
configExplicit bool
|
||||||
outputFormat appformat.OutputFormat
|
outputFormat appformat.OutputFormat
|
||||||
overrides []configkit.Override
|
overrides []configkit.Override
|
||||||
@@ -25,6 +26,7 @@ type promptInspectionConfig struct {
|
|||||||
|
|
||||||
type profileInspectionConfig struct {
|
type profileInspectionConfig struct {
|
||||||
configPath, profileDir, profileID, outputPath string
|
configPath, profileDir, profileID, outputPath string
|
||||||
|
configOverlay string
|
||||||
configExplicit bool
|
configExplicit bool
|
||||||
outputFormat appformat.OutputFormat
|
outputFormat appformat.OutputFormat
|
||||||
overrides []configkit.Override
|
overrides []configkit.Override
|
||||||
@@ -127,7 +129,7 @@ func parseProfileInspectionArgs(args []string) (*profileInspectionConfig, error)
|
|||||||
cfg := &profileInspectionConfig{outputFormat: appformat.DefaultOutputFormat}
|
cfg := &profileInspectionConfig{outputFormat: appformat.DefaultOutputFormat}
|
||||||
fs := flag.NewFlagSet("inspect profile", flag.ContinueOnError)
|
fs := flag.NewFlagSet("inspect profile", flag.ContinueOnError)
|
||||||
fs.SetOutput(io.Discard)
|
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.profileDir, "profile-dir", "", "directory containing execution profiles")
|
||||||
fs.StringVar(&cfg.profileID, "profile", "", "profile ID to inspect")
|
fs.StringVar(&cfg.profileID, "profile", "", "profile ID to inspect")
|
||||||
rawFormat := ""
|
rawFormat := ""
|
||||||
@@ -156,7 +158,7 @@ func parseProfileInspectionArgs(args []string) (*profileInspectionConfig, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveAppSettingsForProfileInspection(cfg *profileInspectionConfig) (engineSettings, 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 {
|
if err != nil {
|
||||||
return engineSettings{}, err
|
return engineSettings{}, err
|
||||||
}
|
}
|
||||||
@@ -168,7 +170,7 @@ func parsePromptInspectionArgs(args []string) (*promptInspectionConfig, error) {
|
|||||||
cfg := &promptInspectionConfig{outputFormat: appformat.DefaultOutputFormat}
|
cfg := &promptInspectionConfig{outputFormat: appformat.DefaultOutputFormat}
|
||||||
fs := flag.NewFlagSet("inspect prompt", flag.ContinueOnError)
|
fs := flag.NewFlagSet("inspect prompt", flag.ContinueOnError)
|
||||||
fs.SetOutput(io.Discard)
|
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.promptDir, "prompt-dir", "", "directory containing prompt definitions")
|
||||||
fs.StringVar(&cfg.promptID, "prompt", "", "prompt ID to inspect")
|
fs.StringVar(&cfg.promptID, "prompt", "", "prompt ID to inspect")
|
||||||
fs.StringVar(&cfg.promptVersion, "prompt-version", "", "optional prompt version")
|
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) {
|
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 {
|
if err != nil {
|
||||||
return engineSettings{}, err
|
return engineSettings{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ type runConfig struct {
|
|||||||
transientRetries int
|
transientRetries int
|
||||||
configuredTransientRetries *int
|
configuredTransientRetries *int
|
||||||
diagnostics []configkit.Diagnostic
|
diagnostics []configkit.Diagnostic
|
||||||
|
configOverlay string
|
||||||
|
|
||||||
defaultRenderFormat renderformat.PreparedRunOutputFormat
|
defaultRenderFormat renderformat.PreparedRunOutputFormat
|
||||||
|
|
||||||
@@ -92,6 +93,7 @@ type serveConfig struct {
|
|||||||
backends []appconfig.BackendSettings
|
backends []appconfig.BackendSettings
|
||||||
transientRetries *int
|
transientRetries *int
|
||||||
diagnostics []configkit.Diagnostic
|
diagnostics []configkit.Diagnostic
|
||||||
|
configOverlay string
|
||||||
}
|
}
|
||||||
|
|
||||||
type commonCommandSettings struct {
|
type commonCommandSettings struct {
|
||||||
@@ -306,7 +308,7 @@ func parseRunArgs(args []string) (*runConfig, error) {
|
|||||||
fs := flag.NewFlagSet("run", flag.ContinueOnError)
|
fs := flag.NewFlagSet("run", flag.ContinueOnError)
|
||||||
fs.SetOutput(io.Discard)
|
fs.SetOutput(io.Discard)
|
||||||
|
|
||||||
registerConfigPathFlag(fs, &cfg.configPath)
|
registerConfigSelectionFlags(fs, &cfg.configPath, &cfg.configOverlay)
|
||||||
registerExecutionRequestFlags(fs, cfg)
|
registerExecutionRequestFlags(fs, cfg)
|
||||||
fs.StringVar(&cfg.schemaDir, "schema-dir", "", "base directory for validation schemas")
|
fs.StringVar(&cfg.schemaDir, "schema-dir", "", "base directory for validation schemas")
|
||||||
fs.DurationVar(&cfg.runTimeout, "run-timeout", 0, "whole-run execution deadline")
|
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 := flag.NewFlagSet("render", flag.ContinueOnError)
|
||||||
fs.SetOutput(io.Discard)
|
fs.SetOutput(io.Discard)
|
||||||
|
|
||||||
registerConfigPathFlag(fs, &cfg.configPath)
|
registerConfigSelectionFlags(fs, &cfg.configPath, &cfg.configOverlay)
|
||||||
registerExecutionRequestFlags(fs, &cfg.runConfig)
|
registerExecutionRequestFlags(fs, &cfg.runConfig)
|
||||||
fs.StringVar(&cfg.schemaDir, "schema-dir", "", "base directory for validation schemas")
|
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 := flag.NewFlagSet("serve", flag.ContinueOnError)
|
||||||
fs.SetOutput(io.Discard)
|
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.addr, "addr", "", "HTTP listen address")
|
||||||
fs.StringVar(&cfg.promptDir, "prompt-dir", "", "directory containing prompt definition YAML files")
|
fs.StringVar(&cfg.promptDir, "prompt-dir", "", "directory containing prompt definition YAML files")
|
||||||
fs.StringVar(&cfg.profileDir, "profile-dir", "", "directory containing execution profile 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())
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -463,7 +465,7 @@ func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
|
|||||||
return fmt.Errorf("unexpected positional args: %v", fs.Args())
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -500,7 +502,7 @@ func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerConfigPathFlag(fs *flag.FlagSet, target *string) {
|
func registerConfigSelectionFlags(fs *flag.FlagSet, target, overlay *string) {
|
||||||
fs.StringVar(
|
fs.StringVar(
|
||||||
target,
|
target,
|
||||||
"config",
|
"config",
|
||||||
@@ -511,10 +513,11 @@ func registerConfigPathFlag(fs *flag.FlagSet, target *string) {
|
|||||||
appconfig.DefaultConfigPath,
|
appconfig.DefaultConfigPath,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
fs.StringVar(overlay, "config-overlay", "", "selected application configuration overlay")
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveCommonSettings(fs *flag.FlagSet, configPath string) (commonCommandSettings, error) {
|
func resolveCommonSettings(fs *flag.FlagSet, configPath, overlay string) (commonCommandSettings, error) {
|
||||||
settings, diagnostics, err := loadApplicationSettings(configPath, flagWasSet(fs, "config"), applicationOverrides(fs))
|
settings, diagnostics, err := loadApplicationSettings(configPath, overlay, flagWasSet(fs, "config"), applicationOverrides(fs))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commonCommandSettings{}, err
|
return commonCommandSettings{}, err
|
||||||
}
|
}
|
||||||
@@ -750,9 +753,9 @@ func runErrorMessage(err error) string {
|
|||||||
|
|
||||||
func printUsage(w io.Writer) {
|
func printUsage(w io.Writer) {
|
||||||
fmt.Fprintln(w, "usage: scriptorium <run|render|serve|inspect> ...")
|
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, " 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] [--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.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] [--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.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] [--prompt-dir DIR] [--format text|json] [--out path]")
|
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] [--profile-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]")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
func TestRenderCommandUsesConfiguredCustomBackend(t *testing.T) {
|
||||||
lib := newCLITestLibrary(t)
|
lib := newCLITestLibrary(t)
|
||||||
writePromptDefinition(t, lib.promptDir, "custom.yaml", `id: custom
|
writePromptDefinition(t, lib.promptDir, "custom.yaml", `id: custom
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ type LoadOptions struct {
|
|||||||
// Load resolves one ConfigKit document and retains its inspection snapshot.
|
// Load resolves one ConfigKit document and retains its inspection snapshot.
|
||||||
func Load(options LoadOptions) (*configkit.Result[Document], error) {
|
func Load(options LoadOptions) (*configkit.Result[Document], error) {
|
||||||
var source configkit.Source
|
var source configkit.Source
|
||||||
|
fileBacked := false
|
||||||
if options.Explicit {
|
if options.Explicit {
|
||||||
source = configkit.File(options.Path)
|
source = configkit.File(options.Path)
|
||||||
|
fileBacked = true
|
||||||
} else {
|
} else {
|
||||||
selected := ""
|
selected := ""
|
||||||
for _, candidate := range defaultConfigSearchPaths {
|
for _, candidate := range defaultConfigSearchPaths {
|
||||||
@@ -41,11 +43,14 @@ func Load(options LoadOptions) (*configkit.Result[Document], error) {
|
|||||||
source = configkit.NoSource()
|
source = configkit.NoSource()
|
||||||
} else {
|
} else {
|
||||||
source = configkit.File(selected)
|
source = configkit.File(selected)
|
||||||
|
fileBacked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := configkit.Load(source, configkit.Options[Document]{
|
result, err := configkit.Load(source, configkit.Options[Document]{
|
||||||
Definition: documentDefinition(),
|
Definition: documentDefinition(),
|
||||||
|
Imports: fileBacked,
|
||||||
|
Overlay: options.Overlay,
|
||||||
Overrides: options.Overrides,
|
Overrides: options.Overrides,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -174,3 +174,56 @@ func TestLoadDiscoveryErrorQuotesPath(t *testing.T) {
|
|||||||
t.Fatalf("discovery error did not quote path safely: %v", err)
|
t.Fatalf("discovery error did not quote path safely: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadComposesExplicitImportsAndSelectedOverlay(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := os.MkdirAll(filepath.Join(dir, "fragments"), 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Join(dir, "overlays"), 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
base := filepath.Join(dir, "config.yml")
|
||||||
|
writeApplicationFile(t, base, "configkit:\n imports:\n - glob: fragments/*.yml\nprompt_dir: ./base-prompts\nserver:\n addr: :8081\n")
|
||||||
|
writeApplicationFile(t, filepath.Join(dir, "fragments", "a.yml"), "backends:\n alpha:\n endpoint: http://alpha.invalid/v1\n")
|
||||||
|
writeApplicationFile(t, filepath.Join(dir, "fragments", "b.yml"), "backends:\n beta:\n endpoint: http://beta.invalid/v1\n")
|
||||||
|
writeApplicationFile(t, filepath.Join(dir, "overlays", "local.yml"), "prompt_dir: ./overlay-prompts\nserver:\n addr: :9090\n")
|
||||||
|
|
||||||
|
result, err := Load(LoadOptions{Path: base, Explicit: true})
|
||||||
|
if err != nil || result.Config.PromptDir != "base-prompts" || len(result.Config.Backends) != 2 || len(result.Sources()) != 3 {
|
||||||
|
t.Fatalf("disjoint imports did not compose: result=%+v err=%v", result, err)
|
||||||
|
}
|
||||||
|
result, err = Load(LoadOptions{Path: base, Explicit: true, Overlay: "local", Overrides: []configkit.Override{{
|
||||||
|
Path: configkit.Keys("server", "addr"), Value: ":7070", Flag: "--addr",
|
||||||
|
}}})
|
||||||
|
if err != nil || result.Config.PromptDir != "overlay-prompts" || result.Config.Server.Addr != ":7070" || len(result.Sources()) != 5 {
|
||||||
|
t.Fatalf("overlay or explicit override changed: result=%+v err=%v", result, err)
|
||||||
|
}
|
||||||
|
if _, err := Load(LoadOptions{Path: base, Explicit: true, Overlay: "missing"}); err == nil {
|
||||||
|
t.Fatal("missing selected overlay accepted")
|
||||||
|
}
|
||||||
|
writeApplicationFile(t, filepath.Join(dir, "fragments", "b.yml"), "backends:\n alpha:\n endpoint: http://conflict.invalid/v1\n")
|
||||||
|
if _, err := Load(LoadOptions{Path: base, Explicit: true}); err == nil {
|
||||||
|
t.Fatal("conflicting import accepted")
|
||||||
|
} else {
|
||||||
|
var loadError *configkit.LoadError
|
||||||
|
if !errors.As(err, &loadError) {
|
||||||
|
t.Fatalf("conflict lost structured diagnostic: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadRejectsOverlayWithoutBase(t *testing.T) {
|
||||||
|
useConfigSearchPaths(t, filepath.Join(t.TempDir(), "absent.yml"))
|
||||||
|
if _, err := Load(LoadOptions{Overlay: "local"}); err == nil {
|
||||||
|
t.Fatal("overlay without base accepted")
|
||||||
|
} else {
|
||||||
|
var loadError *configkit.LoadError
|
||||||
|
if !errors.As(err, &loadError) {
|
||||||
|
t.Fatalf("no-base overlay lost structured diagnostic: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := Load(LoadOptions{Overlay: "default"}); err != nil {
|
||||||
|
t.Fatalf("default must select no overlay: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user