Clarify configuration evidence providers

This commit is contained in:
2026-11-07 15:47:17 +00:00
parent ac113291fc
commit 52f1d99994
7 changed files with 36 additions and 45 deletions

View File

@@ -253,6 +253,9 @@ owns operator syntax and report details. `Scope.Features` declares optional mode
`View.Metadata` and `View.Findings` carry native path metadata and diagnostic `View.Metadata` and `View.Findings` carry native path metadata and diagnostic
records when available. `Findings.Messages` contains presentations associated records when available. `Findings.Messages` contains presentations associated
with those records, while `Acquired.Diagnostics` carries independent messages. with those records, while `Acquired.Diagnostics` carries independent messages.
Custom explanation providers implement `Explain`; CommandKit renders the
acquired evidence. See `EffectiveWriter` and `ExplanationReader` in the
[ckconfig GoDoc](../../integrations/ckconfig/contracts.go) for the provider boundary.
Set `Scope.Features.Overrides` only when the loader passes Set `Scope.Features.Overrides` only when the loader passes
`Selection.Overrides` to ConfigKit as part of its normal load. The integration `Selection.Overrides` to ConfigKit as part of its normal load. The integration
parses CLI records but cannot add them to an already-loaded result. The parses CLI records but cannot add them to an already-loaded result. The

View File

@@ -1,6 +1,6 @@
# Consolidated audit catalog # Consolidated audit catalog
Status: **18 distinct open findings: 18 P3**, consolidated from 30 Status: **16 distinct open findings: 16 P3**, consolidated from 30
numbered entries in seven audit documents. Reviewed audit state: numbered entries in seven audit documents. Reviewed audit state:
`db732e3a1de55c844f77870e1c6db683c2685d5d`. `db732e3a1de55c844f77870e1c6db683c2685d5d`.
@@ -191,6 +191,9 @@ consumer without changing core's optional-binding semantics.
## F09 — P3: The configuration provider contract omits its required YAML representation ## F09 — P3: The configuration provider contract omits its required YAML representation
**Complete.** EffectiveWriter, View and DecodeYAMLReport GoDoc describe the
actual single-document conversion boundary; focused conversion and show tests pass.
`EffectiveWriter` GoDoc promises a native representation but omits that `config `EffectiveWriter` GoDoc promises a native representation but omits that `config
show` converts one supported YAML document through `DecodeYAMLReport`. Custom show` converts one supported YAML document through `DecodeYAMLReport`. Custom
providers can supply output that appears compatible with the description yet providers can supply output that appears compatible with the description yet
@@ -227,6 +230,10 @@ of the documentation correction remains pending.
## F11 — P3: Explanation providers must implement an unused text writer ## F11 — P3: Explanation providers must implement an unused text writer
**Complete.** The interface now requires only Explain; focused tests exercise an
Explain-only provider for focused show and explanations across formats, including
delivery failures. Obsolete writer stubs and the interface-writer assertion were removed.
`ExplanationReader` still requires `WriteExplanation` even though production `ExplanationReader` still requires `WriteExplanation` even though production
show/explain handlers acquire evidence with `Explain` and core renders it. This show/explain handlers acquire evidence with `Explain` and core renders it. This
burdens custom providers and suggests a presentation responsibility that burdens custom providers and suggests a presentation responsibility that

View File

@@ -1,6 +1,6 @@
# Audit remediation implementation plan # Audit remediation implementation plan
Status: in progress; Stages 1–8 complete. Status: in progress; Stages 1–9 complete.
Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint, Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint,
intervening changes recorded decisions and retired the completed older intervening changes recorded decisions and retired the completed older
@@ -327,6 +327,12 @@ Complete when both paths use one implementation of the delicate detail policy.
## Stage 9 — Clarify configuration evidence and narrow explanation providers ## Stage 9 — Clarify configuration evidence and narrow explanation providers
**Complete.** Provider and YAML conversion GoDoc now describe the current
handoff; `ExplanationReader` only acquires evidence. Focused core YAML conversion
and ckconfig show, explanation, adapter and delivery tests passed in the offline
aligned Go 1.27.0 workspace with `-mod=readonly`, including a custom Explain-only
provider for path show and all explanation formats.
Findings: F09 and F11. Sources: ckconfig `contracts.go`, `show.go`, `explain.go`, Findings: F09 and F11. Sources: ckconfig `contracts.go`, `show.go`, `explain.go`,
`contracts_adapter_test.go`; core `report_value.go` conversion GoDoc. `contracts_adapter_test.go`; core `report_value.go` conversion GoDoc.

View File

@@ -63,7 +63,9 @@ type ComparisonRequest[I any] struct {
Right ComparisonSide[I] Right ComparisonSide[I]
} }
// EffectiveWriter writes a configuration's native effective representation. // EffectiveWriter writes one safe YAML document for conversion by
// commandkit.DecodeYAMLReport. Its formatting, comments, and framing are not
// passed through to the resulting report.
type EffectiveWriter interface { type EffectiveWriter interface {
WriteEffective(io.Writer) error WriteEffective(io.Writer) error
} }
@@ -73,14 +75,12 @@ type SourceReader interface {
Sources() []configkit.SourceInfo Sources() []configkit.SourceInfo
} }
// ExplanationReader reads and writes a configuration's native path explanation. // ExplanationReader acquires a configuration's native path explanation.
type ExplanationReader interface { type ExplanationReader interface {
Explain(configkit.Path) (configkit.Explanation, bool) Explain(configkit.Path) (configkit.Explanation, bool)
WriteExplanation(io.Writer, configkit.Path) error
} }
// View exposes native configuration facets without reconstructing their // View exposes native configuration evidence used to build reports.
// reports.
type View struct { type View struct {
Effective EffectiveWriter Effective EffectiveWriter
Sources SourceReader Sources SourceReader

View File

@@ -117,13 +117,6 @@ func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) {
if _, ok := acquired.Value.Explanation.Explain(path); !ok { if _, ok := acquired.Value.Explanation.Explain(path); !ok {
t.Fatal("native explanation facet did not resolve /name") t.Fatal("native explanation facet did not resolve /name")
} }
var explanation bytes.Buffer
if err := acquired.Value.Explanation.WriteExplanation(&explanation, path); err != nil {
t.Fatal(err)
}
if explanation.Len() == 0 {
t.Fatal("native explanation writer produced no output")
}
} }
func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) { func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {

View File

@@ -491,16 +491,21 @@ func TestExplainUsesAcquiredEvidenceAcrossFormats(t *testing.T) {
t.Fatal("missing fixture explanation") t.Fatal("missing fixture explanation")
} }
metadata := result.Metadata() metadata := result.Metadata()
reader := &failingNativeExplanation{value: explanation, err: errors.New("provider writer must not run")} reader := &nativeExplanation{value: explanation, found: true}
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{Name: "tenant", Operations: []Operation{Explain}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{{Name: "tenant", Operations: []Operation{Explain, Show}, Features: ScopeFeatures{PathShow: true}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) {
return commandkit.Acquired[View]{Value: View{Explanation: reader, Metadata: &metadata, Sources: result}}, nil return commandkit.Acquired[View]{Value: View{Explanation: reader, Metadata: &metadata, Sources: result}}, nil
}}}})) }}}}))
var focused bytes.Buffer
show := app.Execute(context.Background(), []string{"config", "show", "--scope=tenant", "--path=/token", "--format=json"}, commandkit.Streams{Out: &focused, Err: io.Discard})
if show.Kind != commandkit.OutcomeSuccess || strings.TrimSpace(focused.String()) != `"after"` {
t.Fatalf("focused show = %#v %q", show, focused.String())
}
for _, format := range []string{"text", "yaml", "json"} { for _, format := range []string{"text", "yaml", "json"} {
t.Run(format, func(t *testing.T) { t.Run(format, func(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
outcome := app.Execute(context.Background(), []string{"config", "explain", "--scope=tenant", "--path=/token", "--format=" + format}, commandkit.Streams{Out: &output, Err: io.Discard}) outcome := app.Execute(context.Background(), []string{"config", "explain", "--scope=tenant", "--path=/token", "--format=" + format}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Status != 0 || reader.calls != 0 { if outcome.Status != 0 {
t.Fatalf("outcome=%#v writer calls=%d output=%s", outcome, reader.calls, output.String()) t.Fatalf("outcome=%#v output=%s", outcome, output.String())
} }
for _, evidence := range []string{"tenant", "/token", "before", "after", "original source", "--fixture-set"} { for _, evidence := range []string{"tenant", "/token", "before", "after", "original source", "--fixture-set"} {
if !strings.Contains(output.String(), evidence) { if !strings.Contains(output.String(), evidence) {
@@ -586,35 +591,11 @@ type nativeExplanation struct {
found bool found bool
} }
type failingNativeExplanation struct {
err error
value configkit.Explanation
calls int
}
func (reader *failingNativeExplanation) Explain(configkit.Path) (configkit.Explanation, bool) {
return reader.value, true
}
func (reader *failingNativeExplanation) WriteExplanation(writer io.Writer, _ configkit.Path) error {
reader.calls++
if _, err := io.WriteString(writer, "partial native explanation\n"); err != nil {
return err
}
return reader.err
}
func (reader *nativeExplanation) Explain(path configkit.Path) (configkit.Explanation, bool) { func (reader *nativeExplanation) Explain(path configkit.Path) (configkit.Explanation, bool) {
reader.path = path reader.path = path
return reader.value, reader.found return reader.value, reader.found
} }
func (reader *nativeExplanation) WriteExplanation(writer io.Writer, path configkit.Path) error {
reader.path = path
_, err := fmt.Fprintf(writer, "Explanation for %q\n", path.String())
return err
}
type failingOutput struct{ err error } type failingOutput struct{ err error }
func (writer failingOutput) Write(value []byte) (int, error) { func (writer failingOutput) Write(value []byte) (int, error) {

View File

@@ -32,9 +32,10 @@ func normalizeReport(value any) (any, error) {
} }
// DecodeYAMLReport decodes one YAML document from safe inspection output into // DecodeYAMLReport decodes one YAML document from safe inspection output into
// JSON-compatible values. Unsupported YAML meanings return an error; the result // JSON-compatible values. Multiple documents, unsupported tags and non-string
// is not a lossless representation of the input document. Scalar types follow // mapping keys return an error. The result does not preserve formatting, comments
// the resolved YAML tags; numeric-looking strings remain strings. // or framing. Scalar types follow resolved YAML tags; numeric-looking strings
// remain strings.
func DecodeYAMLReport(reader io.Reader) (any, error) { func DecodeYAMLReport(reader io.Reader) (any, error) {
decoder := yaml.NewDecoder(reader) decoder := yaml.NewDecoder(reader)
var doc yaml.Node var doc yaml.Node