Add focused configuration reports

This commit is contained in:
2026-11-07 04:47:23 +00:00
parent b38130548e
commit 65b454b2ab
7 changed files with 221 additions and 9 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -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))
}

View File

@@ -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}}