Add opt-in PromptKit namespaces

This commit is contained in:
2026-11-07 04:23:39 +00:00
parent 8e5e64c6e9
commit 644e6c365b
12 changed files with 353 additions and 32 deletions

View File

@@ -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 A missing ID is a usage error; native lookup errors and cancellation retain
supported cause identity. 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 ## PromptKit prepared-request preview
When `Suite.PrepareRequest` is present, `ckprompt.Command(suite)` installs 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 The `promptkit` namespace identifies the operation, so there is no built-in
`--scope` flag. An explicitly empty profile or session value retains PromptKit's `--scope` flag. An explicitly empty profile or session value retains PromptKit's
native empty-string meaning. Requirements inspection does not require a profile native empty-string meaning. Profile selection establishes the reported profile
to be execution-ready. See the identity rather than changing the discovered requirements. An enabled profile
[PromptKit v0.18.0 consumer guide](https://forge.maximumdirect.net/base-layer/promptkit/src/commit/d9f6aad30509e538fc73ea16710df3a3ec084502/docs/consumers/pkg-promptkit.md) 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 for native configuration, selection, and requirements behavior. The built-in
flags other than `--llm-var` are singleton bindings; when repeated, the last value flags other than `--llm-var` are singleton bindings; when repeated, the last value
is used. Suite selectors retain their declared binding behavior. is used. Suite selectors retain their declared binding behavior.

View File

@@ -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 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. 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 Catalog, named definition inspection, and requirements discovery do not render
messages or load request artifacts. Concrete request validation follows native messages or load request artifacts. Concrete request validation follows native
PromptKit preparation, which can resolve prompt/profile resources, load PromptKit preparation, which can resolve prompt/profile resources, load

View File

@@ -1,6 +1,6 @@
# Shared reporting and PromptKit v0.20.0 implementation plan # 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 ## Scope, authority, and execution
@@ -433,6 +433,11 @@ native result rather than asserting PromptKit's rendering algorithm.
## Stage 12: Opt-in namespace composition ## 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 Add `WithNamespaces(suite, promptkit.Namespaces) Suite` and store explicit
application defaults in an optional suite field. Add `Namespaces application defaults in an optional suite field. Add `Namespaces
*promptkit.Namespaces` to applicable ckprompt request envelopes, including *promptkit.Namespaces` to applicable ckprompt request envelopes, including

View File

@@ -19,6 +19,8 @@ type PromptRequest struct {
Selection ckconfig.Selection Selection ckconfig.Selection
// ID is the selected named prompt. // ID is the selected named prompt.
ID string ID string
// Namespaces selects the native engine view; nil preserves the loader view.
Namespaces *promptkit.Namespaces
} }
// ProfileRequest selects one named profile for inspection. // ProfileRequest selects one named profile for inspection.
@@ -27,6 +29,8 @@ type ProfileRequest struct {
Selection ckconfig.Selection Selection ckconfig.Selection
// ID is the selected named profile. // ID is the selected named profile.
ID string ID string
// Namespaces selects the native engine view; nil preserves the loader view.
Namespaces *promptkit.Namespaces
} }
// BackendRequest selects one named backend for inspection. // BackendRequest selects one named backend for inspection.
@@ -35,6 +39,8 @@ type BackendRequest struct {
Selection ckconfig.Selection Selection ckconfig.Selection
// ID is the selected named backend. // ID is the selected named backend.
ID string 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 // 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 // discovery context. Profile and session strings retain PromptKit's
// empty-value semantics. // empty-value semantics.
Inspection promptkit.RequirementInspectionRequest 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, // 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. // omits only this catalog command.
ListCollections func(context.Context, ckconfig.Selection, I, S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) 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 // 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) 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 { if err := engine.ValidateRequest(ctx, concreteRequest); err != nil {
messages = messages.withOperationError(err) messages = messages.withOperationError(err)
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, 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 { if err != nil {
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err 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) inspection, err := engine.InspectPrompt(ctx, request.ID)
messages = messages.withOperationError(err) messages = messages.withOperationError(err)
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, 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 { if err != nil {
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err 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) inspection, err := engine.InspectProfile(ctx, request.ID)
messages = messages.withOperationError(err) messages = messages.withOperationError(err)
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, 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 { if err != nil {
return commandkit.Acquired[*promptkit.BackendInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err 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) inspection, err := engine.InspectBackend(ctx, request.ID)
messages = messages.withOperationError(err) messages = messages.withOperationError(err)
return commandkit.Acquired[*promptkit.BackendInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, 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 { if err != nil {
return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err 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) inspection, err := engine.InspectRequirements(ctx, request.Inspection)
messages = messages.withOperationError(err) messages = messages.withOperationError(err)
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err

View File

@@ -12,10 +12,11 @@ import (
) )
type definitionInput[I any] struct { type definitionInput[I any] struct {
selection ckconfig.Selection namespaces namespaceInput
id string selection ckconfig.Selection
captured commandkit.FlagValues[I] id string
selected I captured commandkit.FlagValues[I]
selected I
} }
type catalogInput[I any] struct { type catalogInput[I any] struct {
@@ -29,6 +30,7 @@ type catalogInput[I any] struct {
} }
type requirementsInput[I any] struct { type requirementsInput[I any] struct {
namespaces namespaceInput
selection ckconfig.Selection selection ckconfig.Selection
inspection promptkit.RequirementInspectionRequest inspection promptkit.RequirementInspectionRequest
resources ResourceInput resources ResourceInput
@@ -244,6 +246,10 @@ func newDefinitionCommand[I, S any](suite Suite[I, S], name string) commandkit.C
(*input).selection.Overlay = value (*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]]{ spec := commandkit.CommandSpec[*definitionInput[I]]{
Name: name, Name: name,
Summary: "Inspect a named PromptKit " + 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 }) inputs := NewPromptInputFlags(func(input **requirementsInput[I]) *PromptInputs { return &(*input).inputs })
flags = append(flags, resources.Profile, resources.SessionID, inputs.Variable) 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]]{ spec := commandkit.CommandSpec[*requirementsInput[I]]{
Name: "show", Name: "show",
Summary: "Inspect requirements for a named PromptKit prompt.", 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] { NewInput: func() *requirementsInput[I] {
return &requirementsInput[I]{} return &requirementsInput[I]{}
}, },
@@ -398,21 +409,21 @@ func runDefinitionInspection[I, S any](invocation commandkit.Invocation, suite S
switch kind { switch kind {
case "prompt": case "prompt":
var result commandkit.Acquired[*promptkit.PromptInspection] 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 { if result.Value != nil {
value = result.Value value = result.Value
} }
diagnostics, explanation = result.Diagnostics, result.Explanation diagnostics, explanation = result.Diagnostics, result.Explanation
case "profile": case "profile":
var result commandkit.Acquired[*promptkit.ProfileInspection] 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 { if result.Value != nil {
value = result.Value value = result.Value
} }
diagnostics, explanation = result.Diagnostics, result.Explanation diagnostics, explanation = result.Diagnostics, result.Explanation
case "backend": case "backend":
var result commandkit.Acquired[*promptkit.BackendInspection] 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 { if result.Value != nil {
value = result.Value 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) acquired, err := suite.InspectRequirements(invocation.Context, request, input.selected, state)
queuePromptDiagnostics(invocation.Reporter(), acquired.Diagnostics) queuePromptDiagnostics(invocation.Reporter(), acquired.Diagnostics)
if err != nil { if err != nil {

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -17,6 +17,8 @@ type PreparationRequest struct {
Selection ckconfig.Selection Selection ckconfig.Selection
// Request contains the native seed request populated by command inputs. // Request contains the native seed request populated by command inputs.
Request promptkit.RunRequest Request promptkit.RunRequest
// Namespaces selects the native engine view; nil preserves the loader view.
Namespaces *promptkit.Namespaces
} }
// WithRequestPreparation attaches native prepared-request inspection. The // WithRequestPreparation attaches native prepared-request inspection. The
@@ -55,6 +57,11 @@ func WithRequestPreparation[I, S any](
if err := promptKitContextError(ctx); err != nil { 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) 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) prepared, err := engine.Prepare(ctx, concrete)
messages = messages.withOperationError(err) messages = messages.withOperationError(err)
return commandkit.Acquired[*promptkit.PreparedRun]{Value: prepared, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, 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 { if err := promptKitContextError(invocation.Context); err != nil {
return definitionFailure("PromptKit request preparation was canceled.", commandkit.Explanation{}, err) 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) acquired, err := suite.PrepareRequest(invocation.Context, request, input.selected, state)
queuePromptDiagnostics(invocation.Reporter(), acquired.Diagnostics) queuePromptDiagnostics(invocation.Reporter(), acquired.Diagnostics)
if err != nil { if err != nil {

View File

@@ -18,6 +18,8 @@ type SmokeRequest struct {
Selection ckconfig.Selection Selection ckconfig.Selection
// Request contains the native smoke-test operation and its selected inputs. // Request contains the native smoke-test operation and its selected inputs.
Request promptkit.SmokeTestRequest 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 // 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) 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) report, err := engine.SmokeTest(ctx, concreteRequest)
messages = messages.withOperationError(err) messages = messages.withOperationError(err)
return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: report, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: report, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err

View File

@@ -11,13 +11,14 @@ import (
) )
type smokeCommandInput[I any] struct { type smokeCommandInput[I any] struct {
selection ckconfig.Selection namespaces namespaceInput
kind string selection ckconfig.Selection
request promptkit.SmokeTestRequest kind string
execution ExecutionInput request promptkit.SmokeTestRequest
resources ResourceInput execution ExecutionInput
captured commandkit.FlagValues[I] resources ResourceInput
selected I captured commandkit.FlagValues[I]
selected I
} }
func newSmokeTestCommand[I, S any](suite Suite[I, S]) commandkit.Command { 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, resources.Backend, resources.Prompt, resources.Profile)
flags = append(flags, execution.All()...) 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]]{ return commandkit.NewCommand(commandkit.CommandSpec[*smokeCommandInput[I]]{
Name: "test", Name: "test",
Summary: "Run one PromptKit smoke 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) 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) acquired, operationErr := suite.SmokeTest(invocation.Context, request, input.selected, state)
queuePromptDiagnostics(invocation.Reporter(), acquired.Diagnostics) queuePromptDiagnostics(invocation.Reporter(), acquired.Diagnostics)
if acquired.Value == nil && operationErr == nil { 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) 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 { if acquired.Value == nil && operationErr == nil {
operationErr = errors.New("PromptKit smoke test returned neither a report nor an error") operationErr = errors.New("PromptKit smoke test returned neither a report nor an error")
} }

View File

@@ -13,4 +13,6 @@ type ValidationRequest struct {
// Request contains the prompt identity, scalar context, variables, and // Request contains the prompt identity, scalar context, variables, and
// artifact references supplied to the command. // artifact references supplied to the command.
Request promptkit.RunRequest Request promptkit.RunRequest
// Namespaces selects the native engine view; nil preserves the loader view.
Namespaces *promptkit.Namespaces
} }

View File

@@ -8,13 +8,14 @@ import (
) )
type concreteRequestInput[I any] struct { type concreteRequestInput[I any] struct {
promptID string namespaces namespaceInput
request ValidationRequest promptID string
execution ExecutionInput request ValidationRequest
resources ResourceInput execution ExecutionInput
inputs PromptInputs resources ResourceInput
captured commandkit.FlagValues[I] inputs PromptInputs
selected I captured commandkit.FlagValues[I]
selected I
} }
type requestValidationReport struct { 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, resources.Profile, resources.SessionID, inputs.Variable, inputs.Text, inputs.File)
flags = append(flags, execution.All()...) 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]]{ return commandkit.CommandSpec[*concreteRequestInput[I]]{
NewInput: func() *concreteRequestInput[I] { NewInput: func() *concreteRequestInput[I] {
return &concreteRequestInput[I]{} return &concreteRequestInput[I]{}
@@ -62,6 +67,7 @@ func newConcreteRequestSpec[I, S any](suite Suite[I, S]) commandkit.CommandSpec[
if input == nil { if input == nil {
return errors.New("PromptKit request input must not be 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.ProfileID = input.resources.Profile.Value
input.request.Request.SessionID = input.resources.SessionID.Value input.request.Request.SessionID = input.resources.SessionID.Value
input.request.Request.Vars = input.inputs.Vars input.request.Request.Vars = input.inputs.Vars