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
example config show --scope promptkit example config show --scope promptkit
example config show --scope all example config show --scope all
example config show --path /server/addr --format json
``` ```
`--scope all` acquires every selected scope before producing one mapping keyed `--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 scope is unwrapped. A provider failure prevents report output. Conversion or
output failures are recorded in `Outcome.Presentation.Output`. 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 ### List configuration sources
`config sources` reports the source inventory captured during the selected `config sources` reports the source inventory captured during the selected
scope's acquisition. It reads the source facet once and does not reload 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 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 ```yaml
scope: app scope: app

View File

@@ -1,6 +1,6 @@
# Shared reporting and PromptKit v0.20.0 implementation plan # 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 ## Scope, authority, and execution
@@ -648,6 +648,11 @@ capabilities, historical-only evidence, and result delivery.
## Stage 18: Focused effective values and source evidence ## 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 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 pointer before loading, call the acquired native `Explain`, and select its safe
effective snapshot. An absent effective path is operational failure; explicitly 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") { if strings.Contains(operationHelp, "--against-config") || strings.Contains(operationHelp, "--against-overlay") {
t.Errorf("%s exposes comparison-only flags:\n%s", operation, operationHelp) 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) 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] historical commandkit.Supplied[bool]
under commandkit.Supplied[string] under commandkit.Supplied[string]
explicit commandkit.Supplied[bool] explicit commandkit.Supplied[bool]
sourceID commandkit.Supplied[string]
values commandkit.FlagValues[I] values commandkit.FlagValues[I]
againstValues commandkit.FlagValues[I] againstValues commandkit.FlagValues[I]
plan *configOperationPlan[I, S] plan *configOperationPlan[I, S]
@@ -38,6 +39,8 @@ type configOperationPlan[I, S any] struct {
operation Operation operation Operation
scopeName string scopeName string
path configkit.Path path configkit.Path
pathSet bool
sourceID commandkit.Supplied[string]
list bool list bool
help bool help bool
historical 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 }), 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] { flags = append(flags, commandkit.CaptureFlags(prepared.bindingsByOperation[operation], func(input **configCommandInput[I, S]) *commandkit.FlagValues[I] {
if input == nil || *input == nil { if input == nil || *input == nil {
return 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 { if scopeName == "all" && operation != Validate && operation != Show {
return fmt.Errorf("scope all is supported only for validate and 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] var selected []*preparedScope[I, S]
if scopeName == "all" { 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 { if operation == Explain && input.list.Value && !scope.scope.Features.PathDiscovery {
return fmt.Errorf("configuration scope %q does not support path discovery", scope.scope.Name) 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 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) parsed, err := configkit.ParsePath(input.path.Value)
if err != nil { if err != nil {
return fmt.Errorf("invalid configuration path: %w", err) 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, operation: operation,
scopeName: scopeName, scopeName: scopeName,
path: path, path: path,
pathSet: input.path.Set,
sourceID: input.sourceID,
list: input.list.Value, list: input.list.Value,
historical: input.historical.Value, historical: input.historical.Value,
under: under, under: under,

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"strings"
commandkit "go.maximumdirect.net/commandkit" 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 { if err := invocation.Context.Err(); err != nil {
return configFailure("Configuration show was canceled.", acquired.Explanation, err) 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 { if acquired.Value.Effective == nil {
return commandkit.Failure(fmt.Sprintf("Configuration scope %q did not provide an effective configuration writer.", selected.scope.scope.Name)) 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 ( import (
"fmt" "fmt"
"io"
"text/template" "text/template"
commandkit "go.maximumdirect.net/commandkit" commandkit "go.maximumdirect.net/commandkit"
@@ -69,13 +70,15 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp
Selection: selected.selection, Selection: selected.selection,
}, selected.input, state) }, selected.input, state)
queueConfigAcquisition(reporter, acquired) queueConfigAcquisition(reporter, acquired)
if acquireErr != nil { if acquireErr == nil && invocation.Context.Err() != nil {
return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr) err := invocation.Context.Err()
}
if err := invocation.Context.Err(); err != nil {
return configFailure("Configuration source listing was canceled.", acquired.Explanation, err) return configFailure("Configuration source listing was canceled.", acquired.Explanation, err)
} }
if acquired.Value.Sources == nil { 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) 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) 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() sources := acquired.Value.Sources.Sources()
document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))} document := sourceReport{Scope: name, Sources: make([]sourceDTO, 0, len(sources))}
for _, source := range sources { for _, source := range sources {
if plan.sourceID.Set && source.ID != plan.sourceID.Value {
continue
}
placement := "mapping" placement := "mapping"
if source.PlacementMode == configkit.PlacementModeSequenceItem { if source.PlacementMode == configkit.PlacementModeSequenceItem {
placement = "sequence_item" 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)}) 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) 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) return configFailure("Configuration source listing was canceled.", acquired.Explanation, err)
} }
_ = reporter.Report(commandkit.Report{Value: document, TextTemplate: sourceTextTemplate}) _ = 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() 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}} var sourceTextTemplate = template.Must(commandkit.ParseTextTemplate("sources", `Scope: {{printf "%q" .Scope}}
{{if .Sources}}Sources: {{if .Sources}}Sources:
{{range .Sources}} - id: {{printf "%q" .ID}} {{range .Sources}} - id: {{printf "%q" .ID}}