From bae96e5dd5ca043c317b72233c0d82f76c3a5a56 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 10 Nov 2026 20:22:23 +0000 Subject: [PATCH] Load prompt configuration comparison sides independently --- docs/roadmap/remediation.md | 2 ++ internal/cli/app_wiring.go | 36 ++++++++++++++++++++------------- internal/cli/app_wiring_test.go | 36 +++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index dec01dd..1c63b68 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -190,6 +190,8 @@ the actionable configuration problem through the same path. ## R5 — P3: Keep PromptKit Configuration Comparison Sides Independent +**Status:** Completed. The native configuration scope acquires each comparison side independently. A comparison with equal selections detects a resource edit between loads. Request commands retain invocation-local capture. + **Category** Native behavior overridden in Stage 5. diff --git a/internal/cli/app_wiring.go b/internal/cli/app_wiring.go index c77e042..ebfe522 100644 --- a/internal/cli/app_wiring.go +++ b/internal/cli/app_wiring.go @@ -44,13 +44,6 @@ func loadApplication(ctx context.Context, selection ckconfig.Selection, _ struct } func loadPromptEngine(ctx context.Context, selection ckconfig.Selection, input struct{}, state *invocationState) (*pk.Engine, error) { - if state != nil { - for _, entry := range state.engines { - if reflect.DeepEqual(entry.selection, selection) { - return entry.engine, nil - } - } - } loaded, err := loadApplication(ctx, selection, input, state) if err != nil { return nil, err @@ -63,12 +56,27 @@ func loadPromptEngine(ctx context.Context, selection ckconfig.Selection, input s if err != nil { return nil, err } - if state != nil { - state.engines = append(state.engines, engineEntry{selection: selection, engine: runtime.Engine}) - } return runtime.Engine, nil } +func capturePromptEngine(ctx context.Context, selection ckconfig.Selection, input struct{}, state *invocationState) (*pk.Engine, error) { + if state != nil { + for _, entry := range state.engines { + if reflect.DeepEqual(entry.selection, selection) { + return entry.engine, nil + } + } + } + engine, err := loadPromptEngine(ctx, selection, input, state) + if err != nil { + return nil, err + } + if state != nil { + state.engines = append(state.engines, engineEntry{selection: selection, engine: engine}) + } + return engine, nil +} + func buildApplication(extra ...commandkit.Declaration) (*commandkit.App, error) { return buildApplicationWithClient(nil, extra...) } @@ -78,15 +86,15 @@ func buildApplicationWithClient(client *http.Client, extra ...commandkit.Declara appScope.Features.Overrides = true promptScope := ckprompt.ConfigScope(loadPromptEngine) configCommand := ckconfig.Command(ckconfig.Suite[struct{}, *invocationState]{NewState: func() *invocationState { return &invocationState{client: client} }, Scopes: []ckconfig.Scope[struct{}, *invocationState]{appScope, promptScope}}) - definitions := ckprompt.WithNamespaces(ckprompt.FromEngine(loadPromptEngine), pk.Namespaces{}) + definitions := ckprompt.WithNamespaces(ckprompt.FromEngine(capturePromptEngine), pk.Namespaces{}) definitions.NewState = func() *invocationState { return &invocationState{client: client} } - definitions = ckprompt.WithRequestValidation(definitions, loadPromptEngine, func(_ context.Context, seed ckprompt.ValidationRequest, _ struct{}, _ *invocationState) (pk.RunRequest, error) { + definitions = ckprompt.WithRequestValidation(definitions, capturePromptEngine, func(_ context.Context, seed ckprompt.ValidationRequest, _ struct{}, _ *invocationState) (pk.RunRequest, error) { return applicationPromptRequest(seed.Request, seed.Namespaces), nil }) - definitions = ckprompt.WithRequestPreparation(definitions, loadPromptEngine, func(_ context.Context, seed ckprompt.PreparationRequest, _ struct{}, _ *invocationState) (pk.RunRequest, error) { + definitions = ckprompt.WithRequestPreparation(definitions, capturePromptEngine, func(_ context.Context, seed ckprompt.PreparationRequest, _ struct{}, _ *invocationState) (pk.RunRequest, error) { return applicationPromptRequest(seed.Request, seed.Namespaces), nil }) - definitions = ckprompt.WithSmokeTesting(definitions, loadPromptEngine, nil) + definitions = ckprompt.WithSmokeTesting(definitions, capturePromptEngine, nil) declarations := []commandkit.Declaration{configCommand, commandkit.Completion("audita"), processCommand()} declarations = append(declarations, ckprompt.Declarations(definitions)...) declarations = append(declarations, extra...) diff --git a/internal/cli/app_wiring_test.go b/internal/cli/app_wiring_test.go index 12f0875..def6a67 100644 --- a/internal/cli/app_wiring_test.go +++ b/internal/cli/app_wiring_test.go @@ -11,6 +11,8 @@ import ( "strings" "testing" + "go.maximumdirect.net/ckconfig" + "go.maximumdirect.net/ckprompt" commandkit "go.maximumdirect.net/commandkit" pk "go.maximumdirect.net/promptkit" ) @@ -80,6 +82,40 @@ func TestExplicitEmptyConfigurationPathFails(t *testing.T) { } } +func TestPromptConfigurationComparisonAcquiresEqualSelectionsIndependently(t *testing.T) { + dir := t.TempDir() + main := filepath.Join(dir, "config.yml") + if err := os.WriteFile(main, []byte("version: 3\n"), 0600); err != nil { + t.Fatal(err) + } + resource := filepath.Join(dir, "prompts.yml") + writeProfile := func(model string) { + t.Helper() + if err := os.WriteFile(resource, []byte("profiles: [{id: local, model: "+model+"}]\n"), 0600); err != nil { + t.Fatal(err) + } + } + writeProfile("before") + scope := ckprompt.ConfigScope(func(ctx context.Context, selection ckconfig.Selection, input struct{}, state *invocationState) (*pk.Engine, error) { + engine, err := loadPromptEngine(ctx, selection, input, state) + writeProfile("after") + return engine, err + }) + side := ckconfig.ComparisonSide[struct{}]{Selection: ckconfig.Selection{ + Config: commandkit.Supplied[string]{Value: main, Set: true}, + }} + result, err := scope.Compare(context.Background(), ckconfig.ComparisonRequest[struct{}]{Left: side, Right: side}, &invocationState{}) + if err != nil { + t.Fatal(err) + } + for _, difference := range result.Value { + if strings.Contains(difference.Left.YAML, "before") && strings.Contains(difference.Right.YAML, "after") { + return + } + } + t.Fatal("comparison did not capture the model before and after the resource changed") +} + func TestRequestInspectionAndExplicitSmokeUseSameSources(t *testing.T) { dir := t.TempDir() file := filepath.Join(dir, "config.yml")