Add configuration inspection commands
This commit is contained in:
24
docs/cli.md
24
docs/cli.md
@@ -181,6 +181,28 @@ OPENROUTER_API_KEY=validation-placeholder \
|
|||||||
The placeholder in the second command is sufficient only for offline
|
The placeholder in the second command is sufficient only for offline
|
||||||
validation; it cannot run a provider-backed pipeline.
|
validation; it cannot run a provider-backed pipeline.
|
||||||
|
|
||||||
|
## config effective
|
||||||
|
|
||||||
|
~~~
|
||||||
|
notarius config effective [--config path/to/config.yml] [--config-profile name]
|
||||||
|
~~~
|
||||||
|
|
||||||
|
This command writes the selected composed configuration as deterministic,
|
||||||
|
redacted YAML. Standard output contains only that YAML: it does not validate a
|
||||||
|
pipeline, prepare modules, materialize references, or contact a provider.
|
||||||
|
|
||||||
|
## config explain
|
||||||
|
|
||||||
|
~~~
|
||||||
|
notarius config explain [--config path/to/config.yml] [--config-profile name] <json-pointer>
|
||||||
|
~~~
|
||||||
|
|
||||||
|
This command writes ConfigKit's redacted, human-readable explanation for one
|
||||||
|
canonical JSON Pointer. Supply `""` explicitly to explain the root. A malformed
|
||||||
|
pointer is a syntax error; a well-formed unknown path is a command failure.
|
||||||
|
The explanation can describe a value displaced by a selected profile. Standard
|
||||||
|
output contains only the explanation.
|
||||||
|
|
||||||
## pipelines list
|
## pipelines list
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
@@ -212,6 +234,6 @@ standard error. Parse the result only after the process exits with status 0.
|
|||||||
| 2 | The command or flag syntax was invalid, including unknown commands, missing required arguments, invalid flag values, or invalid flag combinations. |
|
| 2 | The command or flag syntax was invalid, including unknown commands, missing required arguments, invalid flag values, or invalid flag combinations. |
|
||||||
|
|
||||||
The root help spellings are the supported help path. Invoking **--help** on
|
The root help spellings are the supported help path. Invoking **--help** on
|
||||||
**run**, **config validate**, or **pipelines list** is handled by the flag
|
**run**, a **config** subcommand, or **pipelines list** is handled by the flag
|
||||||
parser as a usage error: it writes an error to standard error and exits with
|
parser as a usage error: it writes an error to standard error and exits with
|
||||||
status 2.
|
status 2.
|
||||||
|
|||||||
@@ -593,3 +593,12 @@ defaults or explicit CLI overrides, incompatible module keys, unknown options,
|
|||||||
invalid reference bindings, missing required reference slots, invalid validator
|
invalid reference bindings, missing required reference slots, invalid validator
|
||||||
overrides, and incompatible generated artifact handoffs. Use
|
overrides, and incompatible generated artifact handoffs. Use
|
||||||
[pipelines list](cli.md#pipelines-list) to inspect configured IDs.
|
[pipelines list](cli.md#pipelines-list) to inspect configured IDs.
|
||||||
|
|
||||||
|
## Inspection
|
||||||
|
|
||||||
|
Use [config effective](cli.md#config-effective) to write the selected composed
|
||||||
|
configuration as deterministic, redacted YAML. Use
|
||||||
|
[config explain](cli.md#config-explain) with a canonical JSON Pointer to inspect
|
||||||
|
the effective or historical provenance for one path, including a value displaced
|
||||||
|
by a selected profile. Neither command resolves a pipeline, prepares modules,
|
||||||
|
materializes references, or requires provider credentials.
|
||||||
|
|||||||
@@ -532,6 +532,8 @@ helpers, and debug writer must already be complete in earlier stages.
|
|||||||
|
|
||||||
## Stage 11 — Add Effective And Explanation Commands
|
## Stage 11 — Add Effective And Explanation Commands
|
||||||
|
|
||||||
|
**Status:** Completed 2026-09-13.
|
||||||
|
|
||||||
**Goal:** Expose the accepted operator inspection workflow.
|
**Goal:** Expose the accepted operator inspection workflow.
|
||||||
|
|
||||||
**Work:**
|
**Work:**
|
||||||
|
|||||||
@@ -8,9 +8,66 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func runConfigEffective(args []string, stdout, stderr io.Writer, opts Options) int {
|
||||||
|
fs := flag.NewFlagSet("config effective", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(io.Discard)
|
||||||
|
configPath := fs.String("config", "", "config file path")
|
||||||
|
profile := fs.String("config-profile", "", "configuration profile")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if fs.NArg() != 0 {
|
||||||
|
fmt.Fprintf(stderr, "notarius: unexpected argument %q\n", fs.Arg(0))
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
loaded, _, err := loadConfigKit(*configPath, *profile, nil, opts)
|
||||||
|
if err != nil {
|
||||||
|
writeConfigLoadError(stderr, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := loaded.WriteEffective(stdout); err != nil {
|
||||||
|
fmt.Fprintf(stderr, "notarius: write effective configuration: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConfigExplain(args []string, stdout, stderr io.Writer, opts Options) int {
|
||||||
|
fs := flag.NewFlagSet("config explain", flag.ContinueOnError)
|
||||||
|
fs.SetOutput(io.Discard)
|
||||||
|
configPath := fs.String("config", "", "config file path")
|
||||||
|
profile := fs.String("config-profile", "", "configuration profile")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if fs.NArg() != 1 {
|
||||||
|
fmt.Fprintln(stderr, "notarius: config explain requires exactly one JSON Pointer")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
pointer := fs.Arg(0)
|
||||||
|
parsed, err := configkit.ParsePath(pointer)
|
||||||
|
if err != nil || parsed.String() != pointer {
|
||||||
|
fmt.Fprintf(stderr, "notarius: %q is not a canonical JSON Pointer\n", pointer)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
loaded, _, err := loadConfigKit(*configPath, *profile, nil, opts)
|
||||||
|
if err != nil {
|
||||||
|
writeConfigLoadError(stderr, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := loaded.WriteExplanation(stdout, pointer); err != nil {
|
||||||
|
fmt.Fprintf(stderr, "notarius: write configuration explanation: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func runConfigValidate(args []string, stdout, stderr io.Writer, opts Options) int {
|
func runConfigValidate(args []string, stdout, stderr io.Writer, opts Options) int {
|
||||||
fs := flag.NewFlagSet("config validate", flag.ContinueOnError)
|
fs := flag.NewFlagSet("config validate", flag.ContinueOnError)
|
||||||
fs.SetOutput(io.Discard)
|
fs.SetOutput(io.Discard)
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ package cli
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/core/artifacts"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigKitReadOnlyCommandsLoadImportsAndProfiles(t *testing.T) {
|
func TestConfigKitReadOnlyCommandsLoadImportsAndProfiles(t *testing.T) {
|
||||||
@@ -34,3 +41,92 @@ func TestConfigKitReadOnlyCommandsRejectVersionFour(t *testing.T) {
|
|||||||
t.Fatalf("list = %d, %q", code, stderr.String())
|
t.Fatalf("list = %d, %q", code, stderr.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigurationInspectionCommandsRenderSafeConfigKitViews(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
root := t.TempDir()
|
||||||
|
path := filepath.Join(root, "notarius.yml")
|
||||||
|
writeConfigKitFile(t, path, `version: 5
|
||||||
|
concurrency:
|
||||||
|
total_llm: 4
|
||||||
|
pipelines:
|
||||||
|
"0":
|
||||||
|
input: {module: input}
|
||||||
|
`)
|
||||||
|
writeConfigKitFile(t, filepath.Join(root, "profiles", "development.yml"), "concurrency:\n total_llm: 3\n")
|
||||||
|
writeConfigKitFile(t, filepath.Join(root, "profiles", "retired.yml"), "pipelines: null\n")
|
||||||
|
|
||||||
|
providerCalled := false
|
||||||
|
opts := commandContractOptions(t)
|
||||||
|
opts.LLMClientFactory = func(context.Context, config.Config, string, LLMRuntimeOverrides) (contracts.StructuredLLMClient, []artifacts.LLMProfileManifest, error) {
|
||||||
|
providerCalled = true
|
||||||
|
return nil, nil, errors.New("LLM client must not be constructed")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
t.Fatalf("effective = %d, %q, %q", code, stdout.String(), stderr.String())
|
||||||
|
}
|
||||||
|
if providerCalled {
|
||||||
|
t.Fatal("config effective constructed an LLM client")
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.Reset()
|
||||||
|
stderr.Reset()
|
||||||
|
if code := RunWithOptions([]string{"config", "explain", "--config", path, "--config-profile", "development", "/concurrency/total_llm"}, &stdout, &stderr, opts); code != 0 || !strings.Contains(stdout.String(), `path: "/concurrency/total_llm"`) || stderr.Len() != 0 {
|
||||||
|
t.Fatalf("explain effective = %d, %q, %q", code, stdout.String(), stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.Reset()
|
||||||
|
stderr.Reset()
|
||||||
|
if code := RunWithOptions([]string{"config", "explain", "--config", path, ""}, &stdout, &stderr, opts); code != 0 || !strings.Contains(stdout.String(), `path: ""`) || stderr.Len() != 0 {
|
||||||
|
t.Fatalf("explain root = %d, %q, %q", code, stdout.String(), stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout.Reset()
|
||||||
|
stderr.Reset()
|
||||||
|
if code := RunWithOptions([]string{"config", "explain", "--config", path, "--config-profile", "retired", "/pipelines/0"}, &stdout, &stderr, opts); code != 0 || !strings.Contains(stdout.String(), `path: "/pipelines/0"`) || stderr.Len() != 0 {
|
||||||
|
t.Fatalf("explain removed mapping key = %d, %q, %q", code, stdout.String(), stderr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigExplainRejectsInvalidAndUnknownPointers(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
path := writeCommandConfig(t, "session", "other")
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
pointer string
|
||||||
|
wantCode int
|
||||||
|
}{
|
||||||
|
{name: "missing", wantCode: 2},
|
||||||
|
{name: "malformed escape", pointer: "/pipelines~2session", wantCode: 2},
|
||||||
|
{name: "unknown", pointer: "/unknown", wantCode: 1},
|
||||||
|
{name: "invalid sequence index", pointer: "/pipelines/session/steps/+0", wantCode: 1},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
args := []string{"config", "explain", "--config", path}
|
||||||
|
if test.name != "missing" {
|
||||||
|
args = append(args, test.pointer)
|
||||||
|
}
|
||||||
|
if code := RunWithOptions(args, &stdout, &stderr, commandContractOptions(t)); code != test.wantCode || stdout.Len() != 0 || stderr.Len() == 0 {
|
||||||
|
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigInspectionCommandsReportWriterFailures(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
path := writeCommandConfig(t, "session", "other")
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
if code := RunWithOptions([]string{"config", "effective", "--config", path}, failingInspectionWriter{}, &stderr, commandContractOptions(t)); code != 1 || !strings.Contains(stderr.String(), "write effective configuration") {
|
||||||
|
t.Fatalf("effective writer failure = %d, %q", code, stderr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type failingInspectionWriter struct{}
|
||||||
|
|
||||||
|
func (failingInspectionWriter) Write([]byte) (int, error) {
|
||||||
|
return 0, io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ const usage = `Usage:
|
|||||||
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-profile name] [--pipeline pipeline-id] [--only lane-a,lane-b]
|
||||||
|
notarius config effective --config path/to/config.yml [--config-profile name]
|
||||||
|
notarius config explain --config path/to/config.yml [--config-profile name] <json-pointer>
|
||||||
notarius pipelines list --config path/to/config.yml [--config-profile name] [--json]
|
notarius pipelines list --config path/to/config.yml [--config-profile name] [--json]
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -1053,6 +1055,10 @@ func runConfig(args []string, stdout, stderr io.Writer, opts Options) int {
|
|||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "validate":
|
case "validate":
|
||||||
return runConfigValidate(args[1:], stdout, stderr, opts)
|
return runConfigValidate(args[1:], stdout, stderr, opts)
|
||||||
|
case "effective":
|
||||||
|
return runConfigEffective(args[1:], stdout, stderr, opts)
|
||||||
|
case "explain":
|
||||||
|
return runConfigExplain(args[1:], stdout, stderr, opts)
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(stderr, "notarius: unknown config subcommand %q\n", args[0])
|
fmt.Fprintf(stderr, "notarius: unknown config subcommand %q\n", args[0])
|
||||||
writeUsage(stderr)
|
writeUsage(stderr)
|
||||||
|
|||||||
Reference in New Issue
Block a user