From 65b454b2abf6f29d1510132923c6f4c0cae26d73 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 7 Nov 2026 04:47:23 +0000 Subject: [PATCH] Add focused configuration reports --- docs/cli.md | 12 +- docs/roadmap/implementation.md | 7 +- .../ckconfig/comparison_input_test.go | 2 +- integrations/ckconfig/focused_reports_test.go | 126 ++++++++++++++++++ integrations/ckconfig/input.go | 19 ++- integrations/ckconfig/show.go | 26 ++++ integrations/ckconfig/sources.go | 38 +++++- 7 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 integrations/ckconfig/focused_reports_test.go diff --git a/docs/cli.md b/docs/cli.md index 090cd95..a203906 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -358,6 +358,7 @@ YAML for structured values; `--format` selects text, YAML, or JSON. example config show example config show --scope promptkit example config show --scope all +example config show --path /server/addr --format json ``` `--scope all` acquires every selected scope before producing one mapping keyed @@ -365,12 +366,21 @@ by scope name, including when only one scope is registered. A single selected scope is unwrapped. A provider failure prevents report output. Conversion or output failures are recorded in `Outcome.Presentation.Output`. +For a scope that supports path-selected show, `--path POINTER` displays only +that safe effective value. An explicitly empty pointer selects the root, and a +present null remains a value. Missing and historical-only paths fail; path +selection cannot be combined with `--scope all`. + ### List configuration sources `config sources` reports the source inventory captured during the selected scope's acquisition. It reads the source facet once and does not reload configuration. The default scope is `app`; `--scope all` is unsupported. Text is -the automatic format, and `--format yaml` or `--format json` returns a structured report: +the automatic format, and `--format yaml` or `--format json` returns a structured report. +`--source-id ID` selects one entry by the ID from that acquisition; an unknown +ID fails. If acquisition fails but returns a captured source inventory, the +command reports that evidence and preserves the acquisition error. Unavailable +inventory is reported as such. ```yaml scope: app diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 0afad92..e6ee44a 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Shared reporting and PromptKit v0.20.0 implementation plan -Status: implementation in progress; Stages 1–17 complete. +Status: implementation in progress; Stages 1–18 complete. ## Scope, authority, and execution @@ -648,6 +648,11 @@ capabilities, historical-only evidence, and result delivery. ## Stage 18: Focused effective values and source evidence +**Complete.** Focused show and source selection use native safe evidence, +including source inventories returned with acquisition failures. Root, +ckconfig, and ckprompt workspace suites passed with Go 1.27.0, including null, +historical-only paths, missing evidence, and delivery failures. + Add `config show --path POINTER` for one scope with PathShow support. Parse the pointer before loading, call the acquired native `Explain`, and select its safe effective snapshot. An absent effective path is operational failure; explicitly diff --git a/integrations/ckconfig/comparison_input_test.go b/integrations/ckconfig/comparison_input_test.go index eb6c35e..20ba794 100644 --- a/integrations/ckconfig/comparison_input_test.go +++ b/integrations/ckconfig/comparison_input_test.go @@ -38,7 +38,7 @@ func TestComparisonHelpLimitsFlagsAndDefaultsFormatToYAML(t *testing.T) { if strings.Contains(operationHelp, "--against-config") || strings.Contains(operationHelp, "--against-overlay") { t.Errorf("%s exposes comparison-only flags:\n%s", operation, operationHelp) } - if operation != "explain" && strings.Contains(operationHelp, "--path") { + if operation != "explain" && operation != "show" && strings.Contains(operationHelp, "--path") { t.Errorf("%s exposes the explain-only path flag:\n%s", operation, operationHelp) } } diff --git a/integrations/ckconfig/focused_reports_test.go b/integrations/ckconfig/focused_reports_test.go new file mode 100644 index 0000000..436af6a --- /dev/null +++ b/integrations/ckconfig/focused_reports_test.go @@ -0,0 +1,126 @@ +package ckconfig + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "reflect" + "strings" + "testing" + + commandkit "go.maximumdirect.net/commandkit" + configkit "go.maximumdirect.net/configkit" +) + +func TestFocusedShowUsesSafeNativeValue(t *testing.T) { + loads := 0 + scope := FromConfigKit("app", "", func(_ context.Context, selection Selection, _, _ struct{}) (*configkit.Result[map[string]any], error) { + loads++ + return configkit.Load(configkit.Mapping("fixture", map[string]any{"object": map[string]any{"x": "y"}, "text": "hello", "false": false, "zero": 0, "null": nil, "gone": map[string]any{"x": 1}}), configkit.Options[map[string]any]{Overrides: selection.Overrides}) + }) + scope.Features.Overrides = true + app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}})) + for _, test := range []struct { + path string + want any + }{{"/object", map[string]any{"x": "y"}}, {"/text", "hello"}, {"/false", false}, {"/zero", float64(0)}, {"/null", nil}} { + var out bytes.Buffer + result := app.Execute(context.Background(), []string{"config", "show", "--path=" + test.path, "--format=json"}, commandkit.Streams{Out: &out, Err: io.Discard}) + var got any + if result.Kind != commandkit.OutcomeSuccess || json.Unmarshal(out.Bytes(), &got) != nil || !reflect.DeepEqual(got, test.want) { + t.Fatalf("%s: %#v %q", test.path, result, out.String()) + } + } + var out bytes.Buffer + result := app.Execute(context.Background(), []string{"config", "show", "--path=/text"}, commandkit.Streams{Out: &out, Err: io.Discard}) + if result.Kind != commandkit.OutcomeSuccess || out.String() != "hello\n" { + t.Fatalf("automatic text: %#v %q", result, out.String()) + } + out.Reset() + result = app.Execute(context.Background(), []string{"config", "show", "--path=/false", "--format=yaml"}, commandkit.Streams{Out: &out, Err: io.Discard}) + if result.Kind != commandkit.OutcomeSuccess || strings.TrimSpace(out.String()) != "false" { + t.Fatalf("yaml false: %#v %q", result, out.String()) + } + for _, args := range [][]string{{"--path=/missing"}, {"--path=/gone/x", "--set=/gone=null"}} { + result = app.Execute(context.Background(), append([]string{"config", "show"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard}) + if result.Kind != commandkit.OutcomeOperational { + t.Fatalf("missing %v: %#v", args, result) + } + } + before := loads + for _, args := range [][]string{{"--path=bad"}, {"--scope=all", "--path=/text"}} { + result = app.Execute(context.Background(), append([]string{"config", "show"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard}) + if result.Kind != commandkit.OutcomeUsage || loads != before { + t.Fatalf("preflight %v: %#v", args, result) + } + } +} + +func TestFocusedShowChecksCapabilityAndFacet(t *testing.T) { + calls := 0 + scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Show}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { + calls++ + return commandkit.Acquired[View]{}, nil + }} + app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}})) + result := app.Execute(context.Background(), []string{"config", "show", "--path=/x"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) + if result.Kind != commandkit.OutcomeUsage || calls != 0 { + t.Fatalf("unsupported: %#v", result) + } + scope.Features.PathShow = true + app = newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}})) + result = app.Execute(context.Background(), []string{"config", "show", "--path=/x"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) + if result.Kind != commandkit.OutcomeOperational || calls != 1 { + t.Fatalf("missing facet: %#v", result) + } +} + +func TestSourcesFilterAndFailureEvidence(t *testing.T) { + cause := errors.New("load failed") + reader := &sourceInventory{values: []configkit.SourceInfo{{ID: "one", Label: "first"}, {ID: "two", Label: "second"}}} + var returnedErr error + var source SourceReader = reader + scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Sources}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { + return commandkit.Acquired[View]{Value: View{Sources: source}}, returnedErr + }} + app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}})) + run := func(args ...string) (commandkit.Outcome, string, string) { + var out, diag bytes.Buffer + result := app.Execute(context.Background(), append([]string{"config", "sources"}, args...), commandkit.Streams{Out: &out, Err: &diag}) + return result, out.String(), diag.String() + } + result, out, _ := run("--source-id=two", "--format=json") + if result.Kind != commandkit.OutcomeSuccess || !strings.Contains(out, `"id": "two"`) || strings.Contains(out, `"id": "one"`) { + t.Fatalf("selected source: %#v %q", result, out) + } + result, _, _ = run("--source-id=missing") + if result.Kind != commandkit.OutcomeOperational { + t.Fatalf("unknown source: %#v", result) + } + returnedErr = cause + result, out, _ = run("--source-id=one", "--format=json") + if !errors.Is(result.Cause, cause) || !strings.Contains(out, `"id": "one"`) || strings.Contains(out, `"id": "two"`) { + t.Fatalf("failed selection: %#v %q", result, out) + } + result, _, diag := run("--source-id=missing") + if !errors.Is(result.Cause, cause) || !strings.Contains(diag, "not found") { + t.Fatalf("failed missing source: %#v %q", result, diag) + } + source = &sourceInventory{} + result, out, _ = run("--format=json") + if !errors.Is(result.Cause, cause) || !strings.Contains(out, `"sources": []`) { + t.Fatalf("empty evidence: %#v %q", result, out) + } + source = nil + result, out, diag = run() + if !errors.Is(result.Cause, cause) || out != "" || !strings.Contains(diag, "unavailable") { + t.Fatalf("unavailable evidence: %#v %q %q", result, out, diag) + } + source = reader + result = app.Execute(context.Background(), []string{"config", "sources"}, commandkit.Streams{Out: shortWriter{}, Err: io.Discard}) + if !errors.Is(result.Cause, cause) || result.Presentation.Output.State != commandkit.DeliveryIncomplete { + t.Fatalf("delivery failure: %#v", result) + } +} diff --git a/integrations/ckconfig/input.go b/integrations/ckconfig/input.go index c66c4c9..b4037da 100644 --- a/integrations/ckconfig/input.go +++ b/integrations/ckconfig/input.go @@ -17,6 +17,7 @@ type configCommandInput[I, S any] struct { historical commandkit.Supplied[bool] under commandkit.Supplied[string] explicit commandkit.Supplied[bool] + sourceID commandkit.Supplied[string] values commandkit.FlagValues[I] againstValues commandkit.FlagValues[I] plan *configOperationPlan[I, S] @@ -38,6 +39,8 @@ type configOperationPlan[I, S any] struct { operation Operation scopeName string path configkit.Path + pathSet bool + sourceID commandkit.Supplied[string] list bool help bool historical bool @@ -106,6 +109,12 @@ func newConfigOperationCommand[I, S any](prepared *preparedSuite[I, S], operatio commandkit.BoolFlag(commandkit.FlagMeta{Name: "explicit", Usage: "Filter listed current paths by explicitness."}, false, func(input **configCommandInput[I, S], value commandkit.Supplied[bool]) { (*input).explicit = value }), ) } + if operation == Show { + flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "path", Usage: "JSON Pointer to one effective value."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).path = value })) + } + if operation == Sources { + flags = append(flags, commandkit.ValueFlag(commandkit.FlagMeta{Name: "source-id", Usage: "Select one source ID from this acquisition."}, (*string)(nil), parseConfigString, func(input **configCommandInput[I, S], value commandkit.Supplied[string]) { (*input).sourceID = value })) + } flags = append(flags, commandkit.CaptureFlags(prepared.bindingsByOperation[operation], func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] { if input == nil || *input == nil { return nil @@ -187,6 +196,9 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O if scopeName == "all" && operation != Validate && operation != Show { return fmt.Errorf("scope all is supported only for validate and show") } + if scopeName == "all" && operation == Show && input.path.Set { + return fmt.Errorf("path-selected show requires one scope") + } var selected []*preparedScope[I, S] if scopeName == "all" { @@ -207,10 +219,13 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O if operation == Explain && input.list.Value && !scope.scope.Features.PathDiscovery { return fmt.Errorf("configuration scope %q does not support path discovery", scope.scope.Name) } + if operation == Show && input.path.Set && !scope.scope.Features.PathShow { + return fmt.Errorf("configuration scope %q does not support path-selected show", scope.scope.Name) + } } var path configkit.Path - if operation == Explain && input.path.Set { + if (operation == Explain || operation == Show) && input.path.Set { parsed, err := configkit.ParsePath(input.path.Value) if err != nil { return fmt.Errorf("invalid configuration path: %w", err) @@ -231,6 +246,8 @@ func prepareConfigOperation[I, S any](prepared *preparedSuite[I, S], operation O operation: operation, scopeName: scopeName, path: path, + pathSet: input.path.Set, + sourceID: input.sourceID, list: input.list.Value, historical: input.historical.Value, under: under, diff --git a/integrations/ckconfig/show.go b/integrations/ckconfig/show.go index cfa84ca..68d864e 100644 --- a/integrations/ckconfig/show.go +++ b/integrations/ckconfig/show.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "strings" commandkit "go.maximumdirect.net/commandkit" ) @@ -31,6 +32,31 @@ func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOpera if err := invocation.Context.Err(); err != nil { return configFailure("Configuration show was canceled.", acquired.Explanation, err) } + if plan.pathSet { + if acquired.Value.Explanation == nil { + return configFailure("Configuration scope did not provide explanation data.", acquired.Explanation, fmt.Errorf("configuration scope %q declared path show without an explanation reader", selected.scope.scope.Name)) + } + explanation, found := acquired.Value.Explanation.Explain(plan.path) + if !found || !explanation.Effective.Present { + return configFailure("The requested effective configuration path is not available.", acquired.Explanation, fmt.Errorf("configuration path %q is not effective in scope %q", plan.path.String(), selected.scope.scope.Name)) + } + if err := invocation.Context.Err(); err != nil { + return configFailure("Configuration show was canceled.", acquired.Explanation, err) + } + var value any + err := reporter.RenderOutput(func(io.Writer) error { + var err error + value, err = commandkit.DecodeYAMLReport(strings.NewReader(explanation.Effective.YAML)) + return err + }) + if err == nil { + if canceled := invocation.Context.Err(); canceled != nil { + return configFailure("Configuration show was canceled.", acquired.Explanation, canceled) + } + _ = reporter.Report(commandkit.Report{Value: value}) + } + return commandkit.Success() + } if acquired.Value.Effective == nil { return commandkit.Failure(fmt.Sprintf("Configuration scope %q did not provide an effective configuration writer.", selected.scope.scope.Name)) } diff --git a/integrations/ckconfig/sources.go b/integrations/ckconfig/sources.go index 468bc1d..bcc4a08 100644 --- a/integrations/ckconfig/sources.go +++ b/integrations/ckconfig/sources.go @@ -2,6 +2,7 @@ package ckconfig import ( "fmt" + "io" "text/template" commandkit "go.maximumdirect.net/commandkit" @@ -69,13 +70,15 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp Selection: selected.selection, }, selected.input, state) queueConfigAcquisition(reporter, acquired) - if acquireErr != nil { - return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr) - } - if err := invocation.Context.Err(); err != nil { + if acquireErr == nil && invocation.Context.Err() != nil { + err := invocation.Context.Err() return configFailure("Configuration source listing was canceled.", acquired.Explanation, err) } if acquired.Value.Sources == nil { + if acquireErr != nil { + explanation := sourceSelectionExplanation(acquired.Explanation, "Source inventory is unavailable.") + return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), explanation, acquireErr) + } cause := fmt.Errorf("configuration scope %q did not provide a source reader", name) return configFailure("Configuration scope did not provide source information.", acquired.Explanation, cause) } @@ -83,6 +86,9 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp sources := acquired.Value.Sources.Sources() document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))} for _, source := range sources { + if plan.sourceID.Set && source.ID != plan.sourceID.Value { + continue + } placement := "mapping" if source.PlacementMode == configkit.PlacementModeSequenceItem { placement = "sequence_item" @@ -121,14 +127,36 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp item.EmptyImportSelections = append(item.EmptyImportSelections, emptyImportSelectionDTO{Path: selection.Path.String(), Location: locationDTO{SourceID: selection.Location.SourceID, Line: selection.Location.Line, Column: selection.Location.Column}, Selector: string(selection.Selector)}) } document.Sources = append(document.Sources, item) + if plan.sourceID.Set { + break + } } - if err := invocation.Context.Err(); err != nil { + if plan.sourceID.Set && len(document.Sources) == 0 { + message := fmt.Sprintf("Source ID %q was not found in configuration scope %q.", plan.sourceID.Value, name) + if acquireErr != nil { + return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), sourceSelectionExplanation(acquired.Explanation, message), acquireErr) + } + return configFailure(message, acquired.Explanation, fmt.Errorf("source ID %q was not found", plan.sourceID.Value)) + } + if acquireErr == nil && invocation.Context.Err() != nil { + err := invocation.Context.Err() return configFailure("Configuration source listing was canceled.", acquired.Explanation, err) } _ = reporter.Report(commandkit.Report{Value: document, TextTemplate: sourceTextTemplate}) + if acquireErr != nil { + return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr) + } return commandkit.Success() } +func sourceSelectionExplanation(explanation commandkit.Explanation, message string) commandkit.Explanation { + explanation.Details = append(append([]commandkit.Detail(nil), explanation.Details...), func(writer io.Writer) error { + _, err := fmt.Fprintln(writer, message) + return err + }) + return explanation +} + var sourceTextTemplate = template.Must(commandkit.ParseTextTemplate("sources", `Scope: {{printf "%q" .Scope}} {{if .Sources}}Sources: {{range .Sources}} - id: {{printf "%q" .ID}}