From da120c2dd4b06c8ac0f7a9a6ad95ee1f6aa566e1 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 7 Nov 2026 04:32:24 +0000 Subject: [PATCH] Expose configuration metadata and findings --- docs/cli.md | 5 ++- docs/consumers/guide.md | 5 ++- docs/roadmap/implementation.md | 7 +++- integrations/ckconfig/configkit_adapter.go | 24 ++++++------ integrations/ckconfig/contracts.go | 25 ++++++++++++ .../ckconfig/contracts_adapter_test.go | 38 ++++++++++++++----- integrations/ckconfig/explain.go | 2 +- integrations/ckconfig/family.go | 26 ++++++++++++- integrations/ckconfig/family_reports_test.go | 27 +++++++++++++ integrations/ckconfig/operations.go | 9 ++++- integrations/ckconfig/show.go | 2 +- integrations/ckconfig/sources.go | 2 +- integrations/ckconfig/validate.go | 2 +- integrations/ckprompt/adapters.go | 15 +++++++- integrations/ckprompt/adapters_test.go | 25 ++++++++++++ 15 files changed, 179 insertions(+), 35 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index cb9c411..8b8ec3e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -297,8 +297,9 @@ example config scopes example config scopes --format yaml ``` -Text output uses one `scope: operation, operation` line per scope. YAML uses a -`scopes` sequence with explicit `name` and `operations` fields. Scope order is +Text output uses one `scope: operation, operation` line per scope and lists any +declared optional features. Structured output uses a `scopes` sequence with +`name`, `operations`, and any supported `features`. Scope order is `app`, then `promptkit`, then other names alphabetically. A metadata-only scope can be listed without an acquisition callback. Discovery does not construct a runtime context or acquire configuration. It reports the intersection between diff --git a/docs/consumers/guide.md b/docs/consumers/guide.md index f97910d..1b11b63 100644 --- a/docs/consumers/guide.md +++ b/docs/consumers/guide.md @@ -249,7 +249,10 @@ adapts a typed ConfigKit loader without calling it during construction. The `config` family provides scope discovery, validation, safe effective configuration display, source inventory, path explanation, and ConfigKit-backed comparison for the capabilities declared by its scopes. The [CLI reference](../cli.md#configuration-scopes) -owns operator syntax and report details. +owns operator syntax and report details. `Scope.Features` declares optional modes; acquired +`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. ## PromptKit configuration, definitions, and requirements diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index e54d53f..60dbb89 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–13 complete. +Status: implementation in progress; Stages 1–14 complete. ## Scope, authority, and execution @@ -491,6 +491,11 @@ conflicts. Metadata/help/completion remain independent of engine acquisition. ## Stage 14: Metadata and failed-acquisition evidence facets +**Complete.** Scope feature discovery and native metadata, findings, and +failed-load source snapshots are available to subsequent operation modes. +The root, ckconfig, and ckprompt workspace suites passed with Go 1.27.0, +including wrapped failures and independent diagnostic delivery. + Extend [ckconfig contracts](../../integrations/ckconfig/contracts.go) with `Scope.Features ScopeFeatures`: `PathDiscovery`, `PathShow`, and `Overrides` booleans. These describe optional modes within existing commands, not new diff --git a/integrations/ckconfig/configkit_adapter.go b/integrations/ckconfig/configkit_adapter.go index 2ed4281..c1ac696 100644 --- a/integrations/ckconfig/configkit_adapter.go +++ b/integrations/ckconfig/configkit_adapter.go @@ -26,6 +26,7 @@ func FromConfigKit[I, S, T any]( Name: name, Summary: summary, Operations: []Operation{Validate, Show, Sources, Explain, Diff}, + Features: ScopeFeatures{PathDiscovery: true, PathShow: true}, } if load == nil { return WithScopeError(scope, fmt.Errorf("ConfigKit scope %q loader must not be nil", name)) @@ -34,22 +35,25 @@ func FromConfigKit[I, S, T any]( scope.Acquire = func(ctx context.Context, request Request, input I, state S) (commandkit.Acquired[View], error) { result, err := load(ctx, request.Selection, input, state) acquired := commandkit.Acquired[View]{} - if result != nil { - acquired.Value = View{Effective: result, Sources: result, Explanation: result} - acquired.Diagnostics, acquired.Explanation = configKitDiagnostics(name, result.Sources(), result.Diagnostics()) - } if err != nil { var loadError *configkit.LoadError if errors.As(err, &loadError) && loadError != nil { - warnings, explanation := configKitDiagnostics(name, loadError.Sources(), loadError.Diagnostics()) - acquired.Diagnostics = append(acquired.Diagnostics, warnings...) - acquired.Explanation = appendConfigKitExplanation(acquired.Explanation, explanation) + records := loadError.Diagnostics() + messages, explanation := configKitDiagnostics(name, loadError.Sources(), records) + acquired.Value.Sources = loadError + acquired.Value.Findings = &Findings{Records: records, Messages: messages} + acquired.Explanation = explanation } return acquired, err } if result == nil { return commandkit.Acquired[View]{}, fmt.Errorf("ConfigKit scope %q returned a nil result", name) } + metadata := result.Metadata() + records := result.Diagnostics() + messages, explanation := configKitDiagnostics(name, result.Sources(), records) + acquired.Value = View{Effective: result, Sources: result, Explanation: result, Metadata: &metadata, Findings: &Findings{Records: records, Messages: messages}} + acquired.Explanation = explanation return acquired, nil } scope.Compare = func(ctx context.Context, request ComparisonRequest[I], state S) (commandkit.Acquired[[]configkit.Difference], error) { @@ -99,11 +103,6 @@ func loadComparisonSide[I, S, T any]( result, err := load(ctx, request.Selection, request.Input, state) var diagnostics []commandkit.Diagnostic var explanation commandkit.Explanation - if result != nil { - warnings, terminal := configKitComparisonDiagnostics(scope, side, result.Sources(), result.Diagnostics()) - diagnostics = append(diagnostics, warnings...) - explanation = appendConfigKitExplanation(explanation, terminal) - } if err != nil { var loadError *configkit.LoadError if errors.As(err, &loadError) && loadError != nil { @@ -116,6 +115,7 @@ func loadComparisonSide[I, S, T any]( if result == nil { return nil, diagnostics, explanation, fmt.Errorf("ConfigKit scope %q %s loader returned a nil result", scope, side) } + diagnostics, explanation = configKitComparisonDiagnostics(scope, side, result.Sources(), result.Diagnostics()) return result, diagnostics, explanation, nil } diff --git a/integrations/ckconfig/contracts.go b/integrations/ckconfig/contracts.go index 95dd0c7..79c0c68 100644 --- a/integrations/ckconfig/contracts.go +++ b/integrations/ckconfig/contracts.go @@ -83,6 +83,29 @@ type View struct { Effective EffectiveWriter Sources SourceReader Explanation ExplanationReader + // Metadata supplies native configuration path metadata when available. + Metadata *configkit.Metadata + // Findings supplies native diagnostics and their ordinary presentations. + // Nil means unavailable; a nonnil value may contain no records. + Findings *Findings +} + +// Findings associates native diagnostic records with their ordinary messages. +type Findings struct { + // Records contains the native diagnostic records, which may be empty. + Records []configkit.Diagnostic + // Messages contains ordinary presentations associated with Records. + Messages []commandkit.Diagnostic +} + +// ScopeFeatures describes optional modes within configuration operations. +type ScopeFeatures struct { + // PathDiscovery indicates support for metadata-backed path discovery. + PathDiscovery bool + // PathShow indicates support for displaying a selected effective path. + PathShow bool + // Overrides indicates that the loader accepts explicit configuration overrides. + Overrides bool } // Scope declares the operations and providers for one named configuration. @@ -97,6 +120,8 @@ type Scope[I, S any] struct { Summary string // Operations selects the standard handlers exposed for this scope. Operations []Operation + // Features describes optional modes supported by this scope. + Features ScopeFeatures // Acquire loads the selected configuration for one ordinary inspection // invocation. It returns independent diagnostics and terminal context // separately from the native value and optional operation error. diff --git a/integrations/ckconfig/contracts_adapter_test.go b/integrations/ckconfig/contracts_adapter_test.go index 55525a5..1851766 100644 --- a/integrations/ckconfig/contracts_adapter_test.go +++ b/integrations/ckconfig/contracts_adapter_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "reflect" "strings" "testing" @@ -94,7 +95,7 @@ func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) { if gotContext != ctx || gotSelection != selection || gotInput.Label != "typed" || gotState != stateValue || stateValue.Calls != 1 { t.Fatalf("loader arguments/context/state = %v, %#v, %#v, %p, %d", gotContext, gotSelection, gotInput, gotState, stateValue.Calls) } - if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result { + if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result || acquired.Value.Metadata == nil || acquired.Value.Findings == nil { t.Fatal("adapter did not expose the native result facets directly") } @@ -150,14 +151,14 @@ func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) { if !errors.As(gotErr, &typed) { t.Fatalf("load error type was not retained: %T", gotErr) } - if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning { - t.Fatalf("failure diagnostics = %#v", acquired.Diagnostics) + if acquired.Value.Effective != nil || acquired.Value.Metadata != nil || acquired.Value.Sources == nil || acquired.Value.Findings == nil || len(acquired.Value.Findings.Messages) != 1 || acquired.Value.Findings.Messages[0].Severity != commandkit.SeverityWarning { + t.Fatalf("failure evidence = %#v", acquired.Value) } if acquired.Explanation.Message != "" || len(acquired.Explanation.Details) != 1 { t.Fatalf("failure explanation = %#v", acquired.Explanation) } var warning, terminal bytes.Buffer - if err := acquired.Diagnostics[0].Write(&warning); err != nil { + if err := acquired.Value.Findings.Messages[0].Write(&warning); err != nil { t.Fatal(err) } if err := acquired.Explanation.Details[0](&terminal); err != nil { @@ -174,7 +175,7 @@ func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) { } } -func TestFromConfigKitPreservesValueAndDiagnosticsAlongsideError(t *testing.T) { +func TestFromConfigKitDoesNotExposeResultFromFailedLoad(t *testing.T) { result, err := adapterSettingsLoad(configkit.Mapping("partial source", map[string]any{"greeting": "Hello"})) if err != nil { t.Fatal(err) @@ -188,11 +189,8 @@ func TestFromConfigKitPreservesValueAndDiagnosticsAlongsideError(t *testing.T) { if gotErr != loadErr { t.Fatalf("load error identity = %v, want %v", gotErr, loadErr) } - if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result { - t.Fatalf("acquired value facets were lost: %#v", acquired.Value) - } - if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning { - t.Fatalf("independent diagnostics = %#v", acquired.Diagnostics) + if acquired.Value.Effective != nil || acquired.Value.Metadata != nil || acquired.Value.Sources != nil || acquired.Value.Explanation != nil || acquired.Value.Findings != nil { + t.Fatalf("failed load exposed result facets: %#v", acquired.Value) } if len(acquired.Explanation.Details) != 0 { t.Fatalf("warning-only result acquired terminal context: %#v", acquired.Explanation) @@ -274,3 +272,23 @@ func (shortWriter) Write(value []byte) (int, error) { return len(value) - 1, nil type failingWriter struct{ err error } func (w failingWriter) Write([]byte) (int, error) { return 0, w.err } + +func TestFromConfigKitUsesFailureSnapshotInsteadOfReturnedResult(t *testing.T) { + result, err := adapterSettingsLoad(configkit.Mapping("successful", map[string]any{"greeting": "Hello"})) + if err != nil { + t.Fatal(err) + } + _, failure := configkit.Load[map[string]any](configkit.Bytes("failed", []byte("broken: ["))) + var snapshot *configkit.LoadError + if !errors.As(failure, &snapshot) { + t.Fatalf("load failure: %v", failure) + } + wrapped := fmt.Errorf("loading: %w", failure) + scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[adapterSettings], error) { + return result, wrapped + }) + acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{}) + if !errors.Is(gotErr, failure) || acquired.Value.Effective != nil || acquired.Value.Metadata != nil || acquired.Value.Explanation != nil || acquired.Value.Sources == nil || acquired.Value.Findings == nil || !reflect.DeepEqual(acquired.Value.Sources.Sources(), snapshot.Sources()) || !reflect.DeepEqual(acquired.Value.Findings.Records, snapshot.Diagnostics()) || len(acquired.Diagnostics) != 0 { + t.Fatalf("failed acquisition = %#v, %v", acquired, gotErr) + } +} diff --git a/integrations/ckconfig/explain.go b/integrations/ckconfig/explain.go index 14bcf1c..4576b76 100644 --- a/integrations/ckconfig/explain.go +++ b/integrations/ckconfig/explain.go @@ -65,7 +65,7 @@ func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *conf Operation: Explain, Selection: plan.selection, }, selected.input, state) - queueConfigDiagnostics(reporter, acquired.Diagnostics) + queueConfigAcquisition(reporter, acquired) if acquireErr != nil { return configFailure(fmt.Sprintf("Unable to explain configuration scope %q.", name), acquired.Explanation, acquireErr) } diff --git a/integrations/ckconfig/family.go b/integrations/ckconfig/family.go index 75ebdea..8d07834 100644 --- a/integrations/ckconfig/family.go +++ b/integrations/ckconfig/family.go @@ -15,6 +15,7 @@ import ( type scopeMetadata struct { Name string `json:"name"` Operations []Operation `json:"operations"` + Features []string `json:"features,omitempty"` } type scopeInventory struct { @@ -188,6 +189,7 @@ func captureSuite[I, S any](suite Suite[I, S]) *preparedSuite[I, S] { prepared.inventory.Scopes = append(prepared.inventory.Scopes, scopeMetadata{ Name: scope.scope.Name, Operations: append([]Operation(nil), scope.scope.Operations...), + Features: featureNames(scope.scope.Features), }) } return prepared @@ -253,12 +255,32 @@ func joinNames(names []string) string { return strings.Join(ordered, ", ") } -var scopeInventoryTemplate = template.Must(commandkit.ParseTextTemplate("scopes", `{{range .Scopes}}{{.Name}}: {{range $index, $operation := .Operations}}{{if $index}}, {{end}}{{$operation}}{{end}} +func featureNames(features ScopeFeatures) []string { + var names []string + if features.PathDiscovery { + names = append(names, "path_discovery") + } + if features.PathShow { + names = append(names, "path_show") + } + if features.Overrides { + names = append(names, "overrides") + } + return names +} + +var scopeInventoryTemplate = template.Must(commandkit.ParseTextTemplate("scopes", `{{range .Scopes}}{{.Name}}: {{range $index, $operation := .Operations}}{{if $index}}, {{end}}{{$operation}}{{end}}{{if .Features}} (features: {{range $index, $feature := .Features}}{{if $index}}, {{end}}{{$feature}}{{end}}){{end}} {{end}}`, nil)) func newConfigScopesCommand[I, S any](prepared *preparedSuite[I, S]) commandkit.Command { + var description strings.Builder + for _, scope := range prepared.inventory.Scopes { + if len(scope.Features) > 0 { + fmt.Fprintf(&description, "%s: %s\n", scope.Name, strings.Join(scope.Features, ", ")) + } + } return commandkit.NewCommand(commandkit.CommandSpec[struct{}]{ - Name: "scopes", Summary: "List configuration scopes and supported operations.", + Name: "scopes", Summary: "List configuration scopes, operations, and supported features.", Description: description.String(), Run: func(invocation commandkit.Invocation, _ struct{}) commandkit.Outcome { _ = invocation.Reporter().Report(commandkit.Report{Value: prepared.inventory, TextTemplate: scopeInventoryTemplate}) return commandkit.Success() diff --git a/integrations/ckconfig/family_reports_test.go b/integrations/ckconfig/family_reports_test.go index e6b4fb1..c40d8da 100644 --- a/integrations/ckconfig/family_reports_test.go +++ b/integrations/ckconfig/family_reports_test.go @@ -593,3 +593,30 @@ func newConfigTestApp(t *testing.T, command commandkit.Command) *commandkit.App } return app } + +func TestScopeFeaturesAndAcquisitionMessages(t *testing.T) { + calls := 0 + message := func(text string) commandkit.Diagnostic { + return commandkit.Diagnostic{Severity: commandkit.SeverityWarning, Write: func(w io.Writer) error { _, err := io.WriteString(w, text); return err }} + } + scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Validate}, Features: ScopeFeatures{PathDiscovery: true, PathShow: true}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { + calls++ + return commandkit.Acquired[View]{Value: View{Findings: &Findings{Messages: []commandkit.Diagnostic{message("native warning\n")}}}, Diagnostics: []commandkit.Diagnostic{message("application warning\n")}}, nil + }} + app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}})) + var out, diagnostic bytes.Buffer + result := app.Execute(context.Background(), []string{"config", "scopes", "--format=json"}, commandkit.Streams{Out: &out, Err: &diagnostic}) + if result.Kind != commandkit.OutcomeSuccess || calls != 0 || !strings.Contains(out.String(), "path_discovery") || !strings.Contains(out.String(), "path_show") { + t.Fatalf("discovery = %#v %q", result, out.String()) + } + out.Reset() + result = app.Execute(context.Background(), []string{"config", "scopes", "--help"}, commandkit.Streams{Out: &out, Err: &diagnostic}) + if result.Kind != commandkit.OutcomeSuccess || calls != 0 || !strings.Contains(out.String(), "path_discovery") { + t.Fatalf("help = %#v %q", result, out.String()) + } + out.Reset() + result = app.Execute(context.Background(), []string{"config", "validate", "--verbose"}, commandkit.Streams{Out: &out, Err: &diagnostic}) + if result.Kind != commandkit.OutcomeSuccess || calls != 1 || strings.Count(diagnostic.String(), "native warning") != 1 || strings.Count(diagnostic.String(), "application warning") != 1 { + t.Fatalf("diagnostics = %#v %q", result, diagnostic.String()) + } +} diff --git a/integrations/ckconfig/operations.go b/integrations/ckconfig/operations.go index 8d20017..f4095a0 100644 --- a/integrations/ckconfig/operations.go +++ b/integrations/ckconfig/operations.go @@ -54,6 +54,13 @@ func queueConfigDiagnostics(reporter *commandkit.Reporter, diagnostics []command } } +func queueConfigAcquisition(reporter *commandkit.Reporter, acquired commandkit.Acquired[View]) { + queueConfigDiagnostics(reporter, acquired.Diagnostics) + if acquired.Value.Findings != nil { + queueConfigDiagnostics(reporter, acquired.Value.Findings.Messages) + } +} + func configFailure(message string, explanation commandkit.Explanation, cause error) commandkit.Outcome { if explanation.Message != "" || len(explanation.Details) > 0 { message = explanation.Message @@ -71,7 +78,7 @@ func configAcquisitionFailure( acquired commandkit.Acquired[View], cause error, ) commandkit.Outcome { - queueConfigDiagnostics(reporter, acquired.Diagnostics) + queueConfigAcquisition(reporter, acquired) message := fmt.Sprintf("Unable to %s configuration scope %q.", operation, scope) return configFailure(message, acquired.Explanation, cause) } diff --git a/integrations/ckconfig/show.go b/integrations/ckconfig/show.go index 1a83c60..c0317e6 100644 --- a/integrations/ckconfig/show.go +++ b/integrations/ckconfig/show.go @@ -27,7 +27,7 @@ func runConfigShow[I, S any](invocation commandkit.Invocation, plan *configOpera if acquireErr != nil { return configAcquisitionFailure(reporter, "show", selected.scope.scope.Name, acquired, acquireErr) } - queueConfigDiagnostics(reporter, acquired.Diagnostics) + queueConfigAcquisition(reporter, acquired) if err := invocation.Context.Err(); err != nil { return configFailure("Configuration show was canceled.", acquired.Explanation, err) } diff --git a/integrations/ckconfig/sources.go b/integrations/ckconfig/sources.go index c748da3..c0b95f4 100644 --- a/integrations/ckconfig/sources.go +++ b/integrations/ckconfig/sources.go @@ -68,7 +68,7 @@ func runConfigSources[I, S any](invocation commandkit.Invocation, plan *configOp Operation: Sources, Selection: plan.selection, }, selected.input, state) - queueConfigDiagnostics(reporter, acquired.Diagnostics) + queueConfigAcquisition(reporter, acquired) if acquireErr != nil { return configFailure(fmt.Sprintf("Unable to list sources for configuration scope %q.", name), acquired.Explanation, acquireErr) } diff --git a/integrations/ckconfig/validate.go b/integrations/ckconfig/validate.go index d510b7c..69a471e 100644 --- a/integrations/ckconfig/validate.go +++ b/integrations/ckconfig/validate.go @@ -43,7 +43,7 @@ func runConfigValidation[I, S any](invocation commandkit.Invocation, plan *confi outcome = configAcquisitionFailure(reporter, "validate", name, acquired, err) break } - queueConfigDiagnostics(reporter, acquired.Diagnostics) + queueConfigAcquisition(reporter, acquired) if err := invocation.Context.Err(); err != nil { report.Scopes = append(report.Scopes, validationScope{Scope: name, Outcome: "canceled"}) outcome = configFailure("Configuration validation was canceled.", acquired.Explanation, err) diff --git a/integrations/ckprompt/adapters.go b/integrations/ckprompt/adapters.go index d3bfd81..1a73085 100644 --- a/integrations/ckprompt/adapters.go +++ b/integrations/ckprompt/adapters.go @@ -267,6 +267,7 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S) Name: "promptkit", Summary: "PromptKit engine configuration.", Operations: []ckconfig.Operation{ckconfig.Validate, ckconfig.Show, ckconfig.Sources, ckconfig.Explain, ckconfig.Diff}, + Features: ckconfig.ScopeFeatures{PathDiscovery: true, PathShow: true}, } if load == nil { return ckconfig.WithScopeError(scope, errors.New("PromptKit configuration loader is nil")) @@ -275,15 +276,25 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S) scope.Acquire = func(ctx context.Context, request ckconfig.Request, input I, state S) (commandkit.Acquired[ckconfig.View], error) { _, inspection, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { - return commandkit.Acquired[ckconfig.View]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err + acquired := commandkit.Acquired[ckconfig.View]{Explanation: messages.explanation} + var loadError *configkit.LoadError + if errors.As(err, &loadError) && loadError != nil { + acquired.Value.Sources = loadError + acquired.Value.Findings = &ckconfig.Findings{Records: loadError.Diagnostics(), Messages: messages.diagnostics} + } else { + acquired.Diagnostics = messages.diagnostics + } + return acquired, err } + metadata := inspection.Metadata() return commandkit.Acquired[ckconfig.View]{ Value: ckconfig.View{ Effective: inspection, Sources: inspection, Explanation: inspection, + Metadata: &metadata, + Findings: &ckconfig.Findings{Records: inspection.Diagnostics(), Messages: messages.diagnostics}, }, - Diagnostics: messages.diagnostics, Explanation: messages.explanation, }, nil } diff --git a/integrations/ckprompt/adapters_test.go b/integrations/ckprompt/adapters_test.go index 58d6c1c..5231402 100644 --- a/integrations/ckprompt/adapters_test.go +++ b/integrations/ckprompt/adapters_test.go @@ -288,6 +288,9 @@ func TestConfigScopeUsesNativeInspectionFacetsAndInvocationValues(t *testing.T) if err != nil || loaderCalls != 1 { t.Fatalf("scope acquisition = (%#v, %v), calls=%d", acquired, err, loaderCalls) } + if acquired.Value.Metadata == nil || acquired.Value.Findings == nil || !scope.Features.PathDiscovery || !scope.Features.PathShow { + t.Fatalf("missing native metadata or findings: %#v", acquired.Value) + } var effective bytes.Buffer if err := acquired.Value.Effective.WriteEffective(&effective); err != nil || !strings.Contains(effective.String(), "id: briefing") { t.Fatalf("native effective report = %q, %v", effective.String(), err) @@ -931,3 +934,25 @@ type failingDiagnosticWriter struct{ err error } func (writer failingDiagnosticWriter) Write([]byte) (int, error) { return 0, writer.err } var _ io.Writer = failingDiagnosticWriter{} + +func TestConfigScopeRetainsFailedLoadSnapshot(t *testing.T) { + _, loadErr := configkit.Load[map[string]any](configkit.Bytes("broken", []byte("broken: ["))) + if loadErr == nil { + t.Fatal("fixture unexpectedly loaded") + } + var snapshot *configkit.LoadError + if !errors.As(loadErr, &snapshot) { + t.Fatalf("unexpected load error: %v", loadErr) + } + wrapped := errors.Join(errors.New("loader context"), loadErr) + scope := ConfigScope(func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) { + return nil, wrapped + }) + got, err := scope.Acquire(context.Background(), ckconfig.Request{}, struct{}{}, struct{}{}) + if !errors.Is(err, loadErr) || got.Value.Sources == nil || got.Value.Findings == nil || got.Value.Effective != nil || got.Value.Metadata != nil || got.Value.Explanation != nil || len(got.Diagnostics) != 0 { + t.Fatalf("failed acquisition = %#v, %v", got, err) + } + if !reflect.DeepEqual(got.Value.Sources.Sources(), snapshot.Sources()) || !reflect.DeepEqual(got.Value.Findings.Records, snapshot.Diagnostics()) { + t.Fatalf("failed snapshot = %#v", got.Value) + } +}