diff --git a/docs/cli.md b/docs/cli.md index edd99a1..caab668 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -555,6 +555,25 @@ performs a network probe. See the [PromptKit resource inspection contracts](http A missing ID is a usage error; native lookup errors and cancellation retain supported cause identity. +### Resource namespace selection + +An application can enable resource namespace flags with `ckprompt.WithNamespaces` +and provide prompt, profile, and backend defaults. Omitted flags retain those +defaults; an explicitly empty value clears the corresponding default. Qualified +resource IDs pass through unchanged. Without the option, the application loader +controls the engine view and these flags are unavailable. + +| Operation | Available namespace flags | +| --- | --- | +| `show prompt` | `--llm-prompt-namespace` | +| `show profile` | `--llm-profile-namespace` | +| `show backend` | `--llm-backend-namespace` | +| `requirements show` | `--llm-prompt-namespace`, `--llm-profile-namespace` | +| `requirements validate`, `show prepared`, `test` | All three, including `--llm-backend-namespace` | + +Catalog namespace filters remain separate; configuration inspection has no +resource namespace selectors. + ## PromptKit prepared-request preview When `Suite.PrepareRequest` is present, `ckprompt.Command(suite)` installs @@ -601,9 +620,12 @@ The command accepts: The `promptkit` namespace identifies the operation, so there is no built-in `--scope` flag. An explicitly empty profile or session value retains PromptKit's -native empty-string meaning. Requirements inspection does not require a profile -to be execution-ready. See the -[PromptKit v0.18.0 consumer guide](https://forge.maximumdirect.net/base-layer/promptkit/src/commit/d9f6aad30509e538fc73ea16710df3a3ec084502/docs/consumers/pkg-promptkit.md) +native empty-string meaning. Profile selection establishes the reported profile +identity rather than changing the discovered requirements. An enabled profile +namespace qualifies an explicitly supplied short profile ID; it does not rewrite +the prompt's stored default reference. Requirements inspection does not require +the selected profile to be execution-ready. See the +[PromptKit v0.20.0 consumer guide](https://forge.maximumdirect.net/base-layer/promptkit/src/commit/8ff9aa3efdc9f16b9d2729e0a29ea083eb3c3eaf/docs/consumers/pkg-promptkit.md) for native configuration, selection, and requirements behavior. The built-in flags other than `--llm-var` are singleton bindings; when repeated, the last value is used. Suite selectors retain their declared binding behavior. diff --git a/docs/consumers/guide.md b/docs/consumers/guide.md index dff8645..b7e959b 100644 --- a/docs/consumers/guide.md +++ b/docs/consumers/guide.md @@ -339,6 +339,12 @@ same canonical request flags into their own input types. Each collection exposes individual handles and an `All` method, so a command can choose its applicable subset. The application decides how to apply the resulting values to its request. +`ckprompt.WithNamespaces` enables standard resource namespace selection from +application-supplied defaults. Loaders used with this option should leave view +selection to the adapter. Without it, a loader can supply its own engine view. +Direct callbacks receive the composed selection in their request. The +[CLI reference](../cli.md#resource-namespace-selection) lists applicable flags. + Catalog, named definition inspection, and requirements discovery do not render messages or load request artifacts. Concrete request validation follows native PromptKit preparation, which can resolve prompt/profile resources, load diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 0b677e9..4d3e877 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–11 complete. +Status: implementation in progress; Stages 1–12 complete. ## Scope, authority, and execution @@ -433,6 +433,11 @@ native result rather than asserting PromptKit's rendering algorithm. ## Stage 12: Opt-in namespace composition +**Complete.** Opt-in namespace defaults and applicable selectors reach custom +callbacks and native engine views. The root, ckconfig, and ckprompt workspace +suites passed with Go 1.27.0; focused tests cover default composition, clearing, +applicability, loader views, and native error identity. + Add `WithNamespaces(suite, promptkit.Namespaces) Suite` and store explicit application defaults in an optional suite field. Add `Namespaces *promptkit.Namespaces` to applicable ckprompt request envelopes, including diff --git a/integrations/ckprompt/adapters.go b/integrations/ckprompt/adapters.go index eef9876..d3bfd81 100644 --- a/integrations/ckprompt/adapters.go +++ b/integrations/ckprompt/adapters.go @@ -19,6 +19,8 @@ type PromptRequest struct { Selection ckconfig.Selection // ID is the selected named prompt. ID string + // Namespaces selects the native engine view; nil preserves the loader view. + Namespaces *promptkit.Namespaces } // ProfileRequest selects one named profile for inspection. @@ -27,6 +29,8 @@ type ProfileRequest struct { Selection ckconfig.Selection // ID is the selected named profile. ID string + // Namespaces selects the native engine view; nil preserves the loader view. + Namespaces *promptkit.Namespaces } // BackendRequest selects one named backend for inspection. @@ -35,6 +39,8 @@ type BackendRequest struct { Selection ckconfig.Selection // ID is the selected named backend. ID string + // Namespaces selects the native engine view; nil preserves the loader view. + Namespaces *promptkit.Namespaces } // CatalogRequest selects the configuration and native namespace filter for a @@ -68,6 +74,8 @@ type RequirementsRequest struct { // discovery context. Profile and session strings retain PromptKit's // empty-value semantics. Inspection promptkit.RequirementInspectionRequest + // Namespaces selects the native engine view; nil preserves the loader view. + Namespaces *promptkit.Namespaces } // Suite describes application-owned input, shared state, and optional catalog, @@ -150,7 +158,8 @@ type Suite[I, S any] struct { // omits only this catalog command. ListCollections func(context.Context, ckconfig.Selection, I, S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) - issues []error + namespaces *promptkit.Namespaces + issues []error } // WithRequestValidation attaches PromptKit-backed concrete request validation @@ -205,6 +214,12 @@ func WithRequestValidation[I, S any]( return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit request validation before native validation: %w", err) } + engine, err = namespacedEngine(engine, request.Namespaces) + if err != nil { + messages = messages.withOperationError(err) + return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err + } + if err := engine.ValidateRequest(ctx, concreteRequest); err != nil { messages = messages.withOperationError(err) return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err @@ -384,6 +399,11 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) ( if err != nil { return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } + engine, err = namespacedEngine(engine, request.Namespaces) + if err != nil { + messages = messages.withOperationError(err) + return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err + } inspection, err := engine.InspectPrompt(ctx, request.ID) messages = messages.withOperationError(err) return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err @@ -393,6 +413,11 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) ( if err != nil { return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } + engine, err = namespacedEngine(engine, request.Namespaces) + if err != nil { + messages = messages.withOperationError(err) + return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err + } inspection, err := engine.InspectProfile(ctx, request.ID) messages = messages.withOperationError(err) return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err @@ -402,6 +427,11 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) ( if err != nil { return commandkit.Acquired[*promptkit.BackendInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } + engine, err = namespacedEngine(engine, request.Namespaces) + if err != nil { + messages = messages.withOperationError(err) + return commandkit.Acquired[*promptkit.BackendInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err + } inspection, err := engine.InspectBackend(ctx, request.ID) messages = messages.withOperationError(err) return commandkit.Acquired[*promptkit.BackendInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err @@ -411,6 +441,11 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) ( if err != nil { return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err } + engine, err = namespacedEngine(engine, request.Namespaces) + if err != nil { + messages = messages.withOperationError(err) + return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err + } inspection, err := engine.InspectRequirements(ctx, request.Inspection) messages = messages.withOperationError(err) return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err diff --git a/integrations/ckprompt/command.go b/integrations/ckprompt/command.go index 90dddab..5d1692a 100644 --- a/integrations/ckprompt/command.go +++ b/integrations/ckprompt/command.go @@ -12,10 +12,11 @@ import ( ) type definitionInput[I any] struct { - selection ckconfig.Selection - id string - captured commandkit.FlagValues[I] - selected I + namespaces namespaceInput + selection ckconfig.Selection + id string + captured commandkit.FlagValues[I] + selected I } type catalogInput[I any] struct { @@ -29,6 +30,7 @@ type catalogInput[I any] struct { } type requirementsInput[I any] struct { + namespaces namespaceInput selection ckconfig.Selection inspection promptkit.RequirementInspectionRequest resources ResourceInput @@ -244,6 +246,10 @@ func newDefinitionCommand[I, S any](suite Suite[I, S], name string) commandkit.C (*input).selection.Overlay = value }), ) + if suite.namespaces != nil { + flags = append(flags, namespaceFlags(func(input **definitionInput[I]) *namespaceInput { return &(*input).namespaces }, name)...) + } + spec := commandkit.CommandSpec[*definitionInput[I]]{ Name: name, Summary: "Inspect a named PromptKit " + name + ".", @@ -292,9 +298,14 @@ func newRequirementsCommand[I, S any](suite Suite[I, S]) commandkit.Command { inputs := NewPromptInputFlags(func(input **requirementsInput[I]) *PromptInputs { return &(*input).inputs }) flags = append(flags, resources.Profile, resources.SessionID, inputs.Variable) + if suite.namespaces != nil { + flags = append(flags, namespaceFlags(func(input **requirementsInput[I]) *namespaceInput { return &(*input).namespaces }, "prompt", "profile")...) + } + spec := commandkit.CommandSpec[*requirementsInput[I]]{ - Name: "show", - Summary: "Inspect requirements for a named PromptKit prompt.", + Name: "show", + Summary: "Inspect requirements for a named PromptKit prompt.", + Description: "Profile selection establishes the reported profile identity, not a different set of requirements. A profile namespace qualifies an explicitly supplied short profile ID, not the prompt’s stored default reference.", NewInput: func() *requirementsInput[I] { return &requirementsInput[I]{} }, @@ -398,21 +409,21 @@ func runDefinitionInspection[I, S any](invocation commandkit.Invocation, suite S switch kind { case "prompt": var result commandkit.Acquired[*promptkit.PromptInspection] - result, err = suite.InspectPrompt(invocation.Context, PromptRequest{Selection: input.selection, ID: input.id}, input.selected, state) + result, err = suite.InspectPrompt(invocation.Context, PromptRequest{Selection: input.selection, ID: input.id, Namespaces: selectedNamespaces(suite.namespaces, input.namespaces)}, input.selected, state) if result.Value != nil { value = result.Value } diagnostics, explanation = result.Diagnostics, result.Explanation case "profile": var result commandkit.Acquired[*promptkit.ProfileInspection] - result, err = suite.InspectProfile(invocation.Context, ProfileRequest{Selection: input.selection, ID: input.id}, input.selected, state) + result, err = suite.InspectProfile(invocation.Context, ProfileRequest{Selection: input.selection, ID: input.id, Namespaces: selectedNamespaces(suite.namespaces, input.namespaces)}, input.selected, state) if result.Value != nil { value = result.Value } diagnostics, explanation = result.Diagnostics, result.Explanation case "backend": var result commandkit.Acquired[*promptkit.BackendInspection] - result, err = suite.InspectBackend(invocation.Context, BackendRequest{Selection: input.selection, ID: input.id}, input.selected, state) + result, err = suite.InspectBackend(invocation.Context, BackendRequest{Selection: input.selection, ID: input.id, Namespaces: selectedNamespaces(suite.namespaces, input.namespaces)}, input.selected, state) if result.Value != nil { value = result.Value } @@ -518,7 +529,7 @@ func runRequirementsInspection[I, S any](invocation commandkit.Invocation, suite } } - request := RequirementsRequest{Selection: input.selection, Inspection: input.inspection} + request := RequirementsRequest{Selection: input.selection, Inspection: input.inspection, Namespaces: selectedNamespaces(suite.namespaces, input.namespaces)} acquired, err := suite.InspectRequirements(invocation.Context, request, input.selected, state) queuePromptDiagnostics(invocation.Reporter(), acquired.Diagnostics) if err != nil { diff --git a/integrations/ckprompt/namespaces.go b/integrations/ckprompt/namespaces.go new file mode 100644 index 0000000..ca43dcc --- /dev/null +++ b/integrations/ckprompt/namespaces.go @@ -0,0 +1,69 @@ +package ckprompt + +import ( + commandkit "go.maximumdirect.net/commandkit" + promptkit "go.maximumdirect.net/promptkit" +) + +// WithNamespaces enables standard resource namespace selection with explicit +// defaults. Supplied CLI namespace flags replace only the corresponding default. +// Loaders used with this option should leave namespace selection to the adapter. +func WithNamespaces[I, S any](suite Suite[I, S], defaults promptkit.Namespaces) Suite[I, S] { + suite.namespaces = &defaults + return suite +} + +type namespaceInput struct { + prompt, profile, backend commandkit.Supplied[string] +} + +func namespaceFlags[I any](destination func(*I) *namespaceInput, kinds ...string) []commandkit.Flag[I] { + flags := make([]commandkit.Flag[I], 0, len(kinds)) + for _, kind := range kinds { + var meta commandkit.FlagMeta + var field func(*namespaceInput) *commandkit.Supplied[string] + switch kind { + case "prompt": + meta = namespaceFlagDefinitions.prompt.meta + field = func(input *namespaceInput) *commandkit.Supplied[string] { return &input.prompt } + case "profile": + meta = namespaceFlagDefinitions.profile.meta + field = func(input *namespaceInput) *commandkit.Supplied[string] { return &input.profile } + case "backend": + meta = namespaceFlagDefinitions.backend.meta + field = func(input *namespaceInput) *commandkit.Supplied[string] { return &input.backend } + } + flags = append(flags, commandkit.ValueFlag(meta, (*string)(nil), parseDefinitionString, func(input *I, value commandkit.Supplied[string]) { *field(destination(input)) = value })) + } + return flags +} + +var namespaceFlagDefinitions = struct{ prompt, profile, backend requestFlagDefinition }{ + prompt: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-prompt-namespace", Usage: "Prompt resource namespace."}, []string{"prompt-namespace"}}, + profile: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-profile-namespace", Usage: "Profile resource namespace."}, []string{"profile-namespace"}}, + backend: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-backend-namespace", Usage: "Backend resource namespace."}, []string{"backend-namespace"}}, +} + +func selectedNamespaces(defaults *promptkit.Namespaces, selected namespaceInput) *promptkit.Namespaces { + if defaults == nil { + return nil + } + value := *defaults + if selected.prompt.Set { + value.Prompt = selected.prompt.Value + } + if selected.profile.Set { + value.Profile = selected.profile.Value + } + if selected.backend.Set { + value.Backend = selected.backend.Value + } + return &value +} + +func namespacedEngine(engine *promptkit.Engine, namespaces *promptkit.Namespaces) (*promptkit.Engine, error) { + if namespaces == nil { + return engine, nil + } + return engine.WithNamespaces(*namespaces) +} diff --git a/integrations/ckprompt/namespaces_test.go b/integrations/ckprompt/namespaces_test.go new file mode 100644 index 0000000..54c70c6 --- /dev/null +++ b/integrations/ckprompt/namespaces_test.go @@ -0,0 +1,146 @@ +package ckprompt + +import ( + "context" + "errors" + "io" + "reflect" + "testing" + + "go.maximumdirect.net/ckconfig" + commandkit "go.maximumdirect.net/commandkit" + promptkit "go.maximumdirect.net/promptkit" +) + +func TestNamespaceSelectionAndApplicability(t *testing.T) { + var got *promptkit.Namespaces + var id string + suite := Suite[struct{}, struct{}]{ + InspectPrompt: func(_ context.Context, req PromptRequest, _, _ struct{}) (commandkit.Acquired[*promptkit.PromptInspection], error) { + got, id = req.Namespaces, req.ID + return commandkit.Acquired[*promptkit.PromptInspection]{Value: &promptkit.PromptInspection{}}, nil + }, + InspectProfile: func(_ context.Context, req ProfileRequest, _, _ struct{}) (commandkit.Acquired[*promptkit.ProfileInspection], error) { + got, id = req.Namespaces, req.ID + return commandkit.Acquired[*promptkit.ProfileInspection]{Value: &promptkit.ProfileInspection{}}, nil + }, + InspectBackend: func(_ context.Context, req BackendRequest, _, _ struct{}) (commandkit.Acquired[*promptkit.BackendInspection], error) { + got, id = req.Namespaces, req.ID + return commandkit.Acquired[*promptkit.BackendInspection]{Value: &promptkit.BackendInspection{}}, nil + }, + InspectRequirements: func(_ context.Context, req RequirementsRequest, _, _ struct{}) (commandkit.Acquired[*promptkit.RequirementInspection], error) { + got, id = req.Namespaces, req.Inspection.PromptID + return commandkit.Acquired[*promptkit.RequirementInspection]{Value: &promptkit.RequirementInspection{}}, nil + }, + ValidateRequest: func(_ context.Context, req ValidationRequest, _, _ struct{}) (commandkit.Acquired[struct{}], error) { + got, id = req.Namespaces, req.Request.PromptID + return commandkit.Acquired[struct{}]{}, nil + }, + PrepareRequest: func(_ context.Context, req PreparationRequest, _, _ struct{}) (commandkit.Acquired[*promptkit.PreparedRun], error) { + got, id = req.Namespaces, req.Request.PromptID + return commandkit.Acquired[*promptkit.PreparedRun]{Value: &promptkit.PreparedRun{}}, nil + }, + SmokeTest: func(_ context.Context, req SmokeRequest, _, _ struct{}) (commandkit.Acquired[*promptkit.SmokeTestReport], error) { + got = req.Namespaces + return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: &promptkit.SmokeTestReport{}}, nil + }, + ListPrompts: func(context.Context, CatalogRequest, struct{}, struct{}) (commandkit.Acquired[[]promptkit.PromptIdentity], error) { + return commandkit.Acquired[[]promptkit.PromptIdentity]{}, nil + }, + } + defaults := promptkit.Namespaces{Prompt: "prompt-default", Profile: "profile-default", Backend: "backend-default"} + app := newPromptTestApp(t, Command(WithNamespaces(suite, defaults))) + tests := []struct { + args []string + allowed, forbidden string + expected promptkit.Namespaces + }{ + {[]string{"show", "prompt", "qualified/name"}, "--llm-prompt-namespace=", "--llm-profile-namespace=x", promptkit.Namespaces{Profile: defaults.Profile, Backend: defaults.Backend}}, + {[]string{"show", "profile", "qualified/name"}, "--llm-profile-namespace=other", "--llm-prompt-namespace=x", promptkit.Namespaces{Prompt: defaults.Prompt, Profile: "other", Backend: defaults.Backend}}, + {[]string{"show", "backend", "qualified/name"}, "--llm-backend-namespace=other", "--llm-profile-namespace=x", promptkit.Namespaces{Prompt: defaults.Prompt, Profile: defaults.Profile, Backend: "other"}}, + {[]string{"requirements", "show", "qualified/name"}, "--llm-profile-namespace=other", "--llm-backend-namespace=x", promptkit.Namespaces{Prompt: defaults.Prompt, Profile: "other", Backend: defaults.Backend}}, + {[]string{"requirements", "validate", "qualified/name"}, "--llm-backend-namespace=other", "--llm-prompt=invalid", promptkit.Namespaces{Prompt: defaults.Prompt, Profile: defaults.Profile, Backend: "other"}}, + {[]string{"show", "prepared", "qualified/name"}, "--llm-prompt-namespace=", "--llm-backend=invalid", promptkit.Namespaces{Profile: defaults.Profile, Backend: defaults.Backend}}, + {[]string{"test", "authentication"}, "--llm-backend-namespace=other", "--llm-session-id=invalid", promptkit.Namespaces{Prompt: defaults.Prompt, Profile: defaults.Profile, Backend: "other"}}, + } + for _, test := range tests { + got = nil + id = "" + args := append(append([]string{"promptkit"}, test.args...), test.allowed) + result := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard}) + if result.Kind != commandkit.OutcomeSuccess || got == nil || *got != test.expected || (test.args[0] != "test" && id != "qualified/name") { + t.Fatalf("%v: result=%#v got=%#v id=%q", args, result, got, id) + } + args = append(append([]string{"promptkit"}, test.args...), test.forbidden) + if result := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard}); result.Kind != commandkit.OutcomeUsage { + t.Fatalf("%v: %#v", args, result) + } + } + unscoped := newPromptTestApp(t, Command(suite)) + result := unscoped.Execute(context.Background(), []string{"promptkit", "show", "prompt", "name"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) + if result.Kind != commandkit.OutcomeSuccess || got != nil { + t.Fatalf("without option: %#v %#v", result, got) + } + result = unscoped.Execute(context.Background(), []string{"promptkit", "show", "prompt", "name", "--llm-prompt-namespace=x"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) + if result.Kind != commandkit.OutcomeUsage { + t.Fatalf("unexpected flag without option: %#v", result) + } + result = app.Execute(context.Background(), []string{"promptkit", "list", "prompts", "--llm-prompt-namespace=x"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}) + if result.Kind != commandkit.OutcomeUsage { + t.Fatalf("namespace selection on catalog: %#v", result) + } +} + +func TestNativeNamespaceViewAndErrorForwarding(t *testing.T) { + engine := adapterEngine(t) + load := func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) { + return engine, nil + } + namespaces := promptkit.Namespaces{Prompt: "team"} + view, err := engine.WithNamespaces(namespaces) + if err != nil { + t.Fatal(err) + } + want, err := view.InspectProfile(context.Background(), "default") + if err != nil { + t.Fatal(err) + } + suite := FromEngine(load) + got, err := suite.InspectProfile(context.Background(), ProfileRequest{ID: "default", Namespaces: &namespaces}, struct{}{}, struct{}{}) + if err != nil || !reflect.DeepEqual(got.Value, want) { + t.Fatalf("native view: %#v %v", got, err) + } + invalid := promptkit.Namespaces{Prompt: "invalid/name"} + _, nativeErr := engine.WithNamespaces(invalid) + _, err = suite.InspectProfile(context.Background(), ProfileRequest{ID: "default", Namespaces: &invalid}, struct{}{}, struct{}{}) + if nativeErr == nil || !errors.Is(err, promptkit.ErrInvalidConfig) { + t.Fatalf("namespace error: native=%v forwarded=%v", nativeErr, err) + } + validation := WithRequestValidation(Suite[struct{}, struct{}]{}, load, nil) + _, err = validation.ValidateRequest(context.Background(), ValidationRequest{Namespaces: &invalid}, struct{}{}, struct{}{}) + if !errors.Is(err, promptkit.ErrInvalidConfig) { + t.Fatalf("validation namespace error: %v", err) + } + preparation := WithRequestPreparation(Suite[struct{}, struct{}]{}, load, nil) + _, err = preparation.PrepareRequest(context.Background(), PreparationRequest{Namespaces: &invalid}, struct{}{}, struct{}{}) + if !errors.Is(err, promptkit.ErrInvalidConfig) { + t.Fatalf("preparation namespace error: %v", err) + } + smoke := WithSmokeTesting(Suite[struct{}, struct{}]{}, load, nil) + _, err = smoke.SmokeTest(context.Background(), SmokeRequest{Namespaces: &invalid}, struct{}{}, struct{}{}) + if !errors.Is(err, promptkit.ErrInvalidConfig) { + t.Fatalf("smoke namespace error: %v", err) + } + preexisting, err := engine.WithNamespaces(promptkit.Namespaces{Profile: "other"}) + if err != nil { + t.Fatal(err) + } + _, nativeErr = preexisting.InspectProfile(context.Background(), "default") + custom := FromEngine(func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) { + return preexisting, nil + }) + _, err = custom.InspectProfile(context.Background(), ProfileRequest{ID: "default"}, struct{}{}, struct{}{}) + if !errors.Is(nativeErr, promptkit.ErrProfileNotFound) || !errors.Is(err, promptkit.ErrProfileNotFound) { + t.Fatalf("loader view: native=%v forwarded=%v", nativeErr, err) + } +} diff --git a/integrations/ckprompt/preparation.go b/integrations/ckprompt/preparation.go index 1c7e455..a5294f8 100644 --- a/integrations/ckprompt/preparation.go +++ b/integrations/ckprompt/preparation.go @@ -17,6 +17,8 @@ type PreparationRequest struct { Selection ckconfig.Selection // Request contains the native seed request populated by command inputs. Request promptkit.RunRequest + // Namespaces selects the native engine view; nil preserves the loader view. + Namespaces *promptkit.Namespaces } // WithRequestPreparation attaches native prepared-request inspection. The @@ -55,6 +57,11 @@ func WithRequestPreparation[I, S any]( if err := promptKitContextError(ctx); err != nil { return commandkit.Acquired[*promptkit.PreparedRun]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit request preparation before native preparation: %w", err) } + engine, err = namespacedEngine(engine, request.Namespaces) + if err != nil { + messages = messages.withOperationError(err) + return commandkit.Acquired[*promptkit.PreparedRun]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err + } prepared, err := engine.Prepare(ctx, concrete) messages = messages.withOperationError(err) return commandkit.Acquired[*promptkit.PreparedRun]{Value: prepared, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err @@ -84,7 +91,7 @@ func runRequestPreparation[I, S any](invocation commandkit.Invocation, suite Sui if err := promptKitContextError(invocation.Context); err != nil { return definitionFailure("PromptKit request preparation was canceled.", commandkit.Explanation{}, err) } - request := PreparationRequest{Selection: input.request.Selection, Request: input.request.Request} + request := PreparationRequest{Selection: input.request.Selection, Request: input.request.Request, Namespaces: input.request.Namespaces} acquired, err := suite.PrepareRequest(invocation.Context, request, input.selected, state) queuePromptDiagnostics(invocation.Reporter(), acquired.Diagnostics) if err != nil { diff --git a/integrations/ckprompt/smoke.go b/integrations/ckprompt/smoke.go index 690a0f3..b36467c 100644 --- a/integrations/ckprompt/smoke.go +++ b/integrations/ckprompt/smoke.go @@ -18,6 +18,8 @@ type SmokeRequest struct { Selection ckconfig.Selection // Request contains the native smoke-test operation and its selected inputs. Request promptkit.SmokeTestRequest + // Namespaces selects the native engine view; nil preserves the loader view. + Namespaces *promptkit.Namespaces } // WithSmokeTesting attaches PromptKit-backed smoke testing to suite while @@ -65,6 +67,11 @@ func WithSmokeTesting[I, S any]( return commandkit.Acquired[*promptkit.SmokeTestReport]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit smoke test before native operation: %w", err) } + engine, err = namespacedEngine(engine, request.Namespaces) + if err != nil { + messages = messages.withOperationError(err) + return commandkit.Acquired[*promptkit.SmokeTestReport]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err + } report, err := engine.SmokeTest(ctx, concreteRequest) messages = messages.withOperationError(err) return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: report, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err diff --git a/integrations/ckprompt/smoke_command.go b/integrations/ckprompt/smoke_command.go index 6f3edd4..2e588cf 100644 --- a/integrations/ckprompt/smoke_command.go +++ b/integrations/ckprompt/smoke_command.go @@ -11,13 +11,14 @@ import ( ) type smokeCommandInput[I any] struct { - selection ckconfig.Selection - kind string - request promptkit.SmokeTestRequest - execution ExecutionInput - resources ResourceInput - captured commandkit.FlagValues[I] - selected I + namespaces namespaceInput + selection ckconfig.Selection + kind string + request promptkit.SmokeTestRequest + execution ExecutionInput + resources ResourceInput + captured commandkit.FlagValues[I] + selected I } func newSmokeTestCommand[I, S any](suite Suite[I, S]) commandkit.Command { @@ -43,6 +44,10 @@ func newSmokeTestCommand[I, S any](suite Suite[I, S]) commandkit.Command { flags = append(flags, resources.Backend, resources.Prompt, resources.Profile) flags = append(flags, execution.All()...) + if suite.namespaces != nil { + flags = append(flags, namespaceFlags(func(input **smokeCommandInput[I]) *namespaceInput { return &(*input).namespaces }, "prompt", "profile", "backend")...) + } + return commandkit.NewCommand(commandkit.CommandSpec[*smokeCommandInput[I]]{ Name: "test", Summary: "Run one PromptKit smoke test.", @@ -94,7 +99,7 @@ func runSmokeTest[I, S any](invocation commandkit.Invocation, suite Suite[I, S], return runSmokeAggregate(invocation, suite, input, state) } - request := SmokeRequest{Selection: input.selection, Request: input.request} + request := SmokeRequest{Selection: input.selection, Request: input.request, Namespaces: selectedNamespaces(suite.namespaces, input.namespaces)} acquired, operationErr := suite.SmokeTest(invocation.Context, request, input.selected, state) queuePromptDiagnostics(invocation.Reporter(), acquired.Diagnostics) if acquired.Value == nil && operationErr == nil { @@ -143,7 +148,7 @@ func runSmokeAggregate[I, S any](invocation commandkit.Invocation, suite Suite[I } seed := smokeRequestForKind(input.request, kind) - acquired, operationErr := suite.SmokeTest(invocation.Context, SmokeRequest{Selection: input.selection, Request: seed}, input.selected, state) + acquired, operationErr := suite.SmokeTest(invocation.Context, SmokeRequest{Selection: input.selection, Request: seed, Namespaces: selectedNamespaces(suite.namespaces, input.namespaces)}, input.selected, state) if acquired.Value == nil && operationErr == nil { operationErr = errors.New("PromptKit smoke test returned neither a report nor an error") } diff --git a/integrations/ckprompt/validation.go b/integrations/ckprompt/validation.go index 544fea7..35edfa0 100644 --- a/integrations/ckprompt/validation.go +++ b/integrations/ckprompt/validation.go @@ -13,4 +13,6 @@ type ValidationRequest struct { // Request contains the prompt identity, scalar context, variables, and // artifact references supplied to the command. Request promptkit.RunRequest + // Namespaces selects the native engine view; nil preserves the loader view. + Namespaces *promptkit.Namespaces } diff --git a/integrations/ckprompt/validation_command.go b/integrations/ckprompt/validation_command.go index 0ad74b5..984a764 100644 --- a/integrations/ckprompt/validation_command.go +++ b/integrations/ckprompt/validation_command.go @@ -8,13 +8,14 @@ import ( ) type concreteRequestInput[I any] struct { - promptID string - request ValidationRequest - execution ExecutionInput - resources ResourceInput - inputs PromptInputs - captured commandkit.FlagValues[I] - selected I + namespaces namespaceInput + promptID string + request ValidationRequest + execution ExecutionInput + resources ResourceInput + inputs PromptInputs + captured commandkit.FlagValues[I] + selected I } type requestValidationReport struct { @@ -46,6 +47,10 @@ func newConcreteRequestSpec[I, S any](suite Suite[I, S]) commandkit.CommandSpec[ flags = append(flags, resources.Profile, resources.SessionID, inputs.Variable, inputs.Text, inputs.File) flags = append(flags, execution.All()...) + if suite.namespaces != nil { + flags = append(flags, namespaceFlags(func(input **concreteRequestInput[I]) *namespaceInput { return &(*input).namespaces }, "prompt", "profile", "backend")...) + } + return commandkit.CommandSpec[*concreteRequestInput[I]]{ NewInput: func() *concreteRequestInput[I] { return &concreteRequestInput[I]{} @@ -62,6 +67,7 @@ func newConcreteRequestSpec[I, S any](suite Suite[I, S]) commandkit.CommandSpec[ if input == nil { return errors.New("PromptKit request input must not be nil") } + input.request.Namespaces = selectedNamespaces(suite.namespaces, input.namespaces) input.request.Request.ProfileID = input.resources.Profile.Value input.request.Request.SessionID = input.resources.SessionID.Value input.request.Request.Vars = input.inputs.Vars