diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 64f5113..74233f6 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Implement PromptKit comparison and catalogs -Status: Stage 1 complete; remaining stages are planned. +Status: Stages 1–2 complete; remaining stages are planned. ## Scope and authority @@ -154,6 +154,25 @@ warnings. Check ordinary headings and deferred-writer error propagation. Create native diagnostic fixtures through public APIs; do not import upstream internals or assert exact cancellation polling counts. +**Completion evidence (2026-10-12)** + +- Refactored the private engine loader to collect inspection diagnostics before + checking cancellation observed after a successful load. Diagnostic headings + retain the ordinary scope form and can identify a comparison side. ConfigKit + load diagnostics and the original load error remain available. +- Added adapter coverage for post-load cancellation, ConfigKit load-error identity + and diagnostics, ordinary and comparison-side headings, and deferred-writer + error propagation. `GOTOOLCHAIN=local go test ./...` passed in + `integrations/ckprompt`. +- PromptKit v0.14.0 provides no public warning callback in engine configuration, + and its native configuration path emits no warnings. Therefore a successful + engine with inspection warnings cannot be constructed through the public API; + native warning retention across cancellation remains unexercised. The ordering + change directly retains any diagnostics if a later PromptKit version supplies + them. The selected integration evidence is otherwise complete. +- Full repository and final assembled-state validation remain assigned to + Stage 12. + ## Stage 3: Add the bundled PromptKit configuration pair provider **Outcome:** `ConfigScope` advertises Diff and supplies a native provider usable diff --git a/integrations/ckprompt/adapters.go b/integrations/ckprompt/adapters.go index 9447694..0acf4af 100644 --- a/integrations/ckprompt/adapters.go +++ b/integrations/ckprompt/adapters.go @@ -76,7 +76,7 @@ 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, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, load) + _, inspection, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[ckconfig.View]{Diagnostics: diagnostics}, err } @@ -104,7 +104,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) ( return Suite[I, S]{ InspectPrompt: func(ctx context.Context, request PromptRequest, input I, state S) (commandkit.Acquired[*promptkit.PromptInspection], error) { - engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, load) + engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: diagnostics}, err } @@ -112,7 +112,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) ( return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: diagnostics}, err }, InspectProfile: func(ctx context.Context, request ProfileRequest, input I, state S) (commandkit.Acquired[*promptkit.ProfileInspection], error) { - engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, load) + engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load) if err != nil { return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: diagnostics}, err } @@ -127,33 +127,34 @@ func loadPromptKitEngine[I, S any]( selection ckconfig.Selection, input I, state S, + side string, load engineLoader[I, S], ) (*promptkit.Engine, promptkit.ConfigurationInspection, []commandkit.Diagnostic, error) { engine, err := load(ctx, selection, input, state) if err != nil { var loadErr *configkit.LoadError if errors.As(err, &loadErr) { - return nil, promptkit.ConfigurationInspection{}, promptKitDiagnostics("promptkit", loadErr.Sources(), loadErr.Diagnostics()), err + return nil, promptkit.ConfigurationInspection{}, promptKitDiagnostics("promptkit", side, loadErr.Sources(), loadErr.Diagnostics()), err } return nil, promptkit.ConfigurationInspection{}, nil, err } if engine == nil { return nil, promptkit.ConfigurationInspection{}, nil, errors.New("PromptKit configuration loader returned a nil engine") } - if ctx != nil { - if err := ctx.Err(); err != nil { - return nil, promptkit.ConfigurationInspection{}, nil, err - } - } inspection, err := engine.InspectConfiguration() if err != nil { return nil, promptkit.ConfigurationInspection{}, nil, err } - diagnostics := promptKitDiagnostics("promptkit", inspection.Sources(), inspection.Diagnostics()) + diagnostics := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics()) + if ctx != nil { + if err := ctx.Err(); err != nil { + return nil, promptkit.ConfigurationInspection{}, diagnostics, err + } + } return engine, inspection, diagnostics, nil } -func promptKitDiagnostics(scope string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic { +func promptKitDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic { if len(records) == 0 { return nil } @@ -177,7 +178,11 @@ func promptKitDiagnostics(scope string, sources []configkit.SourceInfo, records return []commandkit.Diagnostic{{ Severity: severity, Write: func(writer io.Writer) error { - if _, err := fmt.Fprintf(writer, "Configuration scope %s:\n", strconv.Quote(scope)); err != nil { + heading := "Configuration scope " + strconv.Quote(scope) + if side != "" { + heading += " (" + side + ")" + } + if _, err := fmt.Fprintf(writer, "%s:\n", heading); err != nil { return err } if len(sources) > 0 { diff --git a/integrations/ckprompt/adapters_test.go b/integrations/ckprompt/adapters_test.go index 5d2fb64..3fe1e82 100644 --- a/integrations/ckprompt/adapters_test.go +++ b/integrations/ckprompt/adapters_test.go @@ -203,12 +203,48 @@ func TestAdapterAcquisitionPreservesLoadAndContextErrors(t *testing.T) { } } +func TestFromEngineRetainsConfigKitLoadDiagnostics(t *testing.T) { + _, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid PromptKit config", []byte("broken: ["))) + if loadErr == nil { + t.Fatal("invalid configuration unexpectedly loaded") + } + suite := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) { + return nil, loadErr + }) + + acquired, err := suite.InspectProfile(context.Background(), ProfileRequest{ID: "default"}, 0, 0) + var retained *configkit.LoadError + if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 { + t.Fatalf("load failure = (%#v, %v), want original ConfigKit error and diagnostics", acquired, err) + } + if acquired.Diagnostics[0].Severity != commandkit.SeverityError { + t.Fatalf("load diagnostics = %#v, want error severity", acquired.Diagnostics) + } +} + +func TestConfigScopeHonorsCancellationAfterLoad(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + engine := adapterEngine(t) + scope := ConfigScope(func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) { + cancel() + return engine, nil + }) + + acquired, err := scope.Acquire(ctx, ckconfig.Request{Operation: ckconfig.Show}, 0, 0) + if !errors.Is(err, context.Canceled) || acquired.Value.Effective != nil { + t.Fatalf("cancelled scope acquisition = (%#v, %v), want cancellation and no value", acquired, err) + } +} + func TestPromptKitDiagnosticsRemainDeferredAndPropagateWriterErrors(t *testing.T) { writeErr := errors.New("diagnostic writer failed") - diagnostics := promptKitDiagnostics("promptkit", []configkit.SourceInfo{{ID: "source"}}, []configkit.Diagnostic{{ + sources := []configkit.SourceInfo{{ID: "source"}} + records := []configkit.Diagnostic{{ Severity: configkit.SeverityWarning, Message: "safe warning", - }}) + }} + diagnostics := promptKitDiagnostics("promptkit", "", sources, records) if len(diagnostics) != 1 || diagnostics[0].Severity != commandkit.SeverityWarning { t.Fatalf("diagnostics = %#v", diagnostics) } @@ -216,9 +252,15 @@ func TestPromptKitDiagnosticsRemainDeferredAndPropagateWriterErrors(t *testing.T t.Fatalf("diagnostic write error = %v", err) } var output bytes.Buffer - if err := diagnostics[0].Write(&output); err != nil || !strings.Contains(output.String(), "safe warning") || !strings.Contains(output.String(), `"source"`) { + if err := diagnostics[0].Write(&output); err != nil || !strings.Contains(output.String(), `Configuration scope "promptkit":`) || !strings.Contains(output.String(), "safe warning") || !strings.Contains(output.String(), `"source"`) { t.Fatalf("diagnostic text = %q, %v", output.String(), err) } + + sideDiagnostics := promptKitDiagnostics("promptkit", "left", sources, records) + output.Reset() + if err := sideDiagnostics[0].Write(&output); err != nil || !strings.Contains(output.String(), `Configuration scope "promptkit" (left):`) { + t.Fatalf("comparison-side diagnostic text = %q, %v", output.String(), err) + } } type failingDiagnosticWriter struct{ err error }