From 52f1d9999451462183274dcce401ce2543603d7b Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 7 Nov 2026 15:47:17 +0000 Subject: [PATCH] Clarify configuration evidence providers --- docs/consumers/guide.md | 3 ++ docs/roadmap/audit/catalog.md | 9 ++++- docs/roadmap/implementation.md | 8 +++- integrations/ckconfig/contracts.go | 10 ++--- .../ckconfig/contracts_adapter_test.go | 7 ---- integrations/ckconfig/family_reports_test.go | 37 +++++-------------- report_value.go | 7 ++-- 7 files changed, 36 insertions(+), 45 deletions(-) diff --git a/docs/consumers/guide.md b/docs/consumers/guide.md index 8961010..17ce0dd 100644 --- a/docs/consumers/guide.md +++ b/docs/consumers/guide.md @@ -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 records when available. `Findings.Messages` contains presentations associated 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 `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 diff --git a/docs/roadmap/audit/catalog.md b/docs/roadmap/audit/catalog.md index 677cb43..04fbf99 100644 --- a/docs/roadmap/audit/catalog.md +++ b/docs/roadmap/audit/catalog.md @@ -1,6 +1,6 @@ # 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: `db732e3a1de55c844f77870e1c6db683c2685d5d`. @@ -191,6 +191,9 @@ consumer without changing core's optional-binding semantics. ## 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 show` converts one supported YAML document through `DecodeYAMLReport`. Custom 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 +**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 show/explain handlers acquire evidence with `Explain` and core renders it. This burdens custom providers and suggests a presentation responsibility that diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 8c53bc3..361b9aa 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Audit remediation implementation plan -Status: in progress; Stages 1–8 complete. +Status: in progress; Stages 1–9 complete. Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint, 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 +**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`, `contracts_adapter_test.go`; core `report_value.go` conversion GoDoc. diff --git a/integrations/ckconfig/contracts.go b/integrations/ckconfig/contracts.go index 4e615f1..1902330 100644 --- a/integrations/ckconfig/contracts.go +++ b/integrations/ckconfig/contracts.go @@ -63,7 +63,9 @@ type ComparisonRequest[I any] struct { 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 { WriteEffective(io.Writer) error } @@ -73,14 +75,12 @@ type SourceReader interface { Sources() []configkit.SourceInfo } -// ExplanationReader reads and writes a configuration's native path explanation. +// ExplanationReader acquires a configuration's native path explanation. type ExplanationReader interface { Explain(configkit.Path) (configkit.Explanation, bool) - WriteExplanation(io.Writer, configkit.Path) error } -// View exposes native configuration facets without reconstructing their -// reports. +// View exposes native configuration evidence used to build reports. type View struct { Effective EffectiveWriter Sources SourceReader diff --git a/integrations/ckconfig/contracts_adapter_test.go b/integrations/ckconfig/contracts_adapter_test.go index da5b88b..ff0f735 100644 --- a/integrations/ckconfig/contracts_adapter_test.go +++ b/integrations/ckconfig/contracts_adapter_test.go @@ -117,13 +117,6 @@ func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) { if _, ok := acquired.Value.Explanation.Explain(path); !ok { 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) { diff --git a/integrations/ckconfig/family_reports_test.go b/integrations/ckconfig/family_reports_test.go index 2b41c0a..bbe2e0d 100644 --- a/integrations/ckconfig/family_reports_test.go +++ b/integrations/ckconfig/family_reports_test.go @@ -491,16 +491,21 @@ func TestExplainUsesAcquiredEvidenceAcrossFormats(t *testing.T) { t.Fatal("missing fixture explanation") } metadata := result.Metadata() - reader := &failingNativeExplanation{value: explanation, err: errors.New("provider writer must not run")} - 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) { + reader := &nativeExplanation{value: explanation, found: true} + 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 }}}})) + 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"} { t.Run(format, func(t *testing.T) { 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}) - if outcome.Status != 0 || reader.calls != 0 { - t.Fatalf("outcome=%#v writer calls=%d output=%s", outcome, reader.calls, output.String()) + if outcome.Status != 0 { + t.Fatalf("outcome=%#v output=%s", outcome, output.String()) } for _, evidence := range []string{"tenant", "/token", "before", "after", "original source", "--fixture-set"} { if !strings.Contains(output.String(), evidence) { @@ -586,35 +591,11 @@ type nativeExplanation struct { 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) { reader.path = path 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 } func (writer failingOutput) Write(value []byte) (int, error) { diff --git a/report_value.go b/report_value.go index 475cb31..7543a8d 100644 --- a/report_value.go +++ b/report_value.go @@ -32,9 +32,10 @@ func normalizeReport(value any) (any, error) { } // DecodeYAMLReport decodes one YAML document from safe inspection output into -// JSON-compatible values. Unsupported YAML meanings return an error; the result -// is not a lossless representation of the input document. Scalar types follow -// the resolved YAML tags; numeric-looking strings remain strings. +// JSON-compatible values. Multiple documents, unsupported tags and non-string +// mapping keys return an error. The result does not preserve formatting, comments +// or framing. Scalar types follow resolved YAML tags; numeric-looking strings +// remain strings. func DecodeYAMLReport(reader io.Reader) (any, error) { decoder := yaml.NewDecoder(reader) var doc yaml.Node