Add PromptKit catalog providers
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Implement PromptKit comparison and catalogs
|
# Implement PromptKit comparison and catalogs
|
||||||
|
|
||||||
Status: Stages 1–4 complete; remaining stages are planned.
|
Status: Stages 1–5 complete; remaining stages are planned.
|
||||||
|
|
||||||
## Scope and authority
|
## Scope and authority
|
||||||
|
|
||||||
@@ -324,6 +324,22 @@ selection/input/state forwarding, load diagnostics/errors, and cancellation.
|
|||||||
Retain named-inspection tests. Custom empty catalogs and presentation belong to
|
Retain named-inspection tests. Custom empty catalogs and presentation belong to
|
||||||
Stage 8; do not retest PromptKit's complete membership semantics.
|
Stage 8; do not retest PromptKit's complete membership semantics.
|
||||||
|
|
||||||
|
**Completion evidence (2026-10-12)**
|
||||||
|
|
||||||
|
- Added the selected optional `ListPrompts` and `ListProfiles` callbacks to
|
||||||
|
`ckprompt.Suite` with the accepted signatures, and updated its declaration and
|
||||||
|
`FromEngine` documentation to cover catalogs alongside named inspection.
|
||||||
|
- `FromEngine` now loads an engine only when a catalog callback is reached,
|
||||||
|
delegates to PromptKit's native `ListPrompts` and `ListProfiles`, and returns
|
||||||
|
the native ordered results with acquisition diagnostics.
|
||||||
|
- Adapter tests compare both results with the same engine's public native
|
||||||
|
methods and cover context/selection/input/state forwarding, ConfigKit load
|
||||||
|
diagnostic and error identity retention, cancellation after loading, and a
|
||||||
|
nil-loader issue after all four callbacks are cleared. Existing named
|
||||||
|
inspection coverage remains in place.
|
||||||
|
- `GOTOOLCHAIN=local go test -count=1 .` passed in `integrations/ckprompt`.
|
||||||
|
Family assembly and catalog presentation remain assigned to Stages 7 and 8.
|
||||||
|
|
||||||
## Stage 6: Implement the catalog YAML representation
|
## Stage 6: Implement the catalog YAML representation
|
||||||
|
|
||||||
**Outcome:** small private helpers encode the explicitly selected CLI schema.
|
**Outcome:** small private helpers encode the explicitly selected CLI schema.
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ type ProfileRequest struct {
|
|||||||
ID string
|
ID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suite describes application-owned input, shared state, and named inspection
|
// Suite describes application-owned input, shared state, and optional catalog
|
||||||
// capabilities. Its private issues retain adapter construction failures.
|
// and named inspection capabilities. Its private issues retain adapter
|
||||||
|
// construction failures.
|
||||||
// Factory results and callback effects remain caller-owned: Command does not
|
// Factory results and callback effects remain caller-owned: Command does not
|
||||||
// deep-copy arbitrary objects or repair aliases returned by factories.
|
// deep-copy arbitrary objects or repair aliases returned by factories.
|
||||||
// Factories are not invoked during construction, installation, help, or version,
|
// Factories are not invoked during construction, installation, help, or version,
|
||||||
@@ -45,14 +46,22 @@ type Suite[I, S any] struct {
|
|||||||
// NewState is called once when inspection is reached, after input checks
|
// NewState is called once when inspection is reached, after input checks
|
||||||
// pass. If nil, state is the zero value of S, including nil for pointer types.
|
// pass. If nil, state is the zero value of S, including nil for pointer types.
|
||||||
NewState func() S
|
NewState func() S
|
||||||
// Selectors declares the original typed flags accepted by definition commands.
|
// Selectors declares the original typed flags accepted by definition and
|
||||||
|
// catalog commands.
|
||||||
Selectors []commandkit.Flag[I]
|
Selectors []commandkit.Flag[I]
|
||||||
// CheckInput validates the selected definition inputs before state creation.
|
// CheckInput validates selected definition or catalog input before state
|
||||||
|
// creation.
|
||||||
CheckInput func(ckconfig.Selection, I) error
|
CheckInput func(ckconfig.Selection, I) error
|
||||||
// InspectPrompt inspects the selected prompt after input and state preparation.
|
// InspectPrompt inspects the selected prompt after input and state preparation.
|
||||||
InspectPrompt func(context.Context, PromptRequest, I, S) (commandkit.Acquired[*promptkit.PromptInspection], error)
|
InspectPrompt func(context.Context, PromptRequest, I, S) (commandkit.Acquired[*promptkit.PromptInspection], error)
|
||||||
// InspectProfile inspects the selected profile after input and state preparation.
|
// InspectProfile inspects the selected profile after input and state preparation.
|
||||||
InspectProfile func(context.Context, ProfileRequest, I, S) (commandkit.Acquired[*promptkit.ProfileInspection], error)
|
InspectProfile func(context.Context, ProfileRequest, I, S) (commandkit.Acquired[*promptkit.ProfileInspection], error)
|
||||||
|
// ListPrompts lists prompt identities after input and state preparation.
|
||||||
|
// It is optional; a nil callback does not install a prompt catalog command.
|
||||||
|
ListPrompts func(context.Context, ckconfig.Selection, I, S) (commandkit.Acquired[[]promptkit.PromptIdentity], error)
|
||||||
|
// ListProfiles lists profile IDs after input and state preparation.
|
||||||
|
// It is optional; a nil callback does not install a profile catalog command.
|
||||||
|
ListProfiles func(context.Context, ckconfig.Selection, I, S) (commandkit.Acquired[[]string], error)
|
||||||
|
|
||||||
issues []error
|
issues []error
|
||||||
}
|
}
|
||||||
@@ -130,17 +139,34 @@ func promptKitContextError(ctx context.Context) error {
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromEngine creates named prompt and profile inspection callbacks backed by a
|
// FromEngine creates prompt and profile catalog and inspection callbacks backed
|
||||||
// PromptKit engine loader. It does not call the loader during construction. Each
|
// by a PromptKit engine loader. It does not call the loader during construction.
|
||||||
// selected inspection passes its context, supplied selection, initialized input,
|
// Each selected operation passes its context, supplied selection, initialized
|
||||||
// and shared invocation state to the loader before calling PromptKit's public
|
// input, and shared invocation state to the loader before calling PromptKit's
|
||||||
// inspection method. A nil loader is retained as an adapter issue.
|
// public listing or inspection method. A nil loader is retained as an adapter
|
||||||
|
// issue.
|
||||||
func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) Suite[I, S] {
|
func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) Suite[I, S] {
|
||||||
if load == nil {
|
if load == nil {
|
||||||
return Suite[I, S]{issues: []error{errors.New("PromptKit engine loader is nil")}}
|
return Suite[I, S]{issues: []error{errors.New("PromptKit engine loader is nil")}}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Suite[I, S]{
|
return Suite[I, S]{
|
||||||
|
ListPrompts: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]promptkit.PromptIdentity], error) {
|
||||||
|
engine, _, diagnostics, err := loadPromptKitEngine(ctx, selection, input, state, "", load)
|
||||||
|
if err != nil {
|
||||||
|
return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: diagnostics}, err
|
||||||
|
}
|
||||||
|
prompts, err := engine.ListPrompts(ctx)
|
||||||
|
return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: diagnostics}, err
|
||||||
|
},
|
||||||
|
ListProfiles: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]string], error) {
|
||||||
|
engine, _, diagnostics, err := loadPromptKitEngine(ctx, selection, input, state, "", load)
|
||||||
|
if err != nil {
|
||||||
|
return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err
|
||||||
|
}
|
||||||
|
profiles, err := engine.ListProfiles(ctx)
|
||||||
|
return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: diagnostics}, err
|
||||||
|
},
|
||||||
InspectPrompt: func(ctx context.Context, request PromptRequest, input I, state S) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
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 {
|
if err != nil {
|
||||||
|
|||||||
@@ -311,6 +311,8 @@ func TestFromEnginePassesRequestsAndRetainsNilLoaderIssue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
empty := FromEngine[int, int](nil)
|
empty := FromEngine[int, int](nil)
|
||||||
|
empty.ListPrompts = nil
|
||||||
|
empty.ListProfiles = nil
|
||||||
empty.InspectPrompt = nil
|
empty.InspectPrompt = nil
|
||||||
empty.InspectProfile = nil
|
empty.InspectProfile = nil
|
||||||
if len(empty.issues) != 1 || !strings.Contains(empty.issues[0].Error(), "PromptKit engine loader is nil") {
|
if len(empty.issues) != 1 || !strings.Contains(empty.issues[0].Error(), "PromptKit engine loader is nil") {
|
||||||
@@ -318,6 +320,116 @@ func TestFromEnginePassesRequestsAndRetainsNilLoaderIssue(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFromEngineListsNativeCatalogsWithInvocationValues(t *testing.T) {
|
||||||
|
type input struct{ label string }
|
||||||
|
type state struct{ label string }
|
||||||
|
type contextKey struct{}
|
||||||
|
|
||||||
|
engine := adapterEngine(t)
|
||||||
|
wantContext := context.WithValue(context.Background(), contextKey{}, "catalog invocation")
|
||||||
|
wantSelection := ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "selected.yml", Set: true}}
|
||||||
|
wantInput := input{label: "typed input"}
|
||||||
|
wantState := state{label: "shared state"}
|
||||||
|
loaderCalls := 0
|
||||||
|
suite := FromEngine(func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||||
|
loaderCalls++
|
||||||
|
if ctx.Value(contextKey{}) != "catalog invocation" || selection != wantSelection || gotInput != wantInput || gotState != wantState {
|
||||||
|
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
||||||
|
}
|
||||||
|
return engine, nil
|
||||||
|
})
|
||||||
|
if suite.ListPrompts == nil || suite.ListProfiles == nil {
|
||||||
|
t.Fatalf("FromEngine did not supply both catalog callbacks: %#v", suite)
|
||||||
|
}
|
||||||
|
wantPrompts, err := engine.ListPrompts(wantContext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantProfiles, err := engine.ListProfiles(wantContext)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
prompts, err := suite.ListPrompts(wantContext, wantSelection, wantInput, wantState)
|
||||||
|
if err != nil || !reflect.DeepEqual(prompts.Value, wantPrompts) || len(prompts.Diagnostics) != 0 || loaderCalls != 1 {
|
||||||
|
t.Fatalf("prompt catalog = (%#v, %v), calls=%d, want %#v", prompts, err, loaderCalls, wantPrompts)
|
||||||
|
}
|
||||||
|
profiles, err := suite.ListProfiles(wantContext, wantSelection, wantInput, wantState)
|
||||||
|
if err != nil || !reflect.DeepEqual(profiles.Value, wantProfiles) || len(profiles.Diagnostics) != 0 || loaderCalls != 2 {
|
||||||
|
t.Fatalf("profile catalog = (%#v, %v), calls=%d, want %#v", profiles, err, loaderCalls, wantProfiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFromEngineCatalogsRetainLoadDiagnosticsAndErrorIdentity(t *testing.T) {
|
||||||
|
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid catalog configuration", []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
|
||||||
|
})
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
call func() (int, error)
|
||||||
|
}{
|
||||||
|
{name: "prompts", call: func() (int, error) {
|
||||||
|
acquired, err := suite.ListPrompts(context.Background(), ckconfig.Selection{}, 0, 0)
|
||||||
|
var retained *configkit.LoadError
|
||||||
|
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 {
|
||||||
|
t.Errorf("prompt load failure = (%#v, %v)", acquired, err)
|
||||||
|
}
|
||||||
|
return len(acquired.Diagnostics), err
|
||||||
|
}},
|
||||||
|
{name: "profiles", call: func() (int, error) {
|
||||||
|
acquired, err := suite.ListProfiles(context.Background(), ckconfig.Selection{}, 0, 0)
|
||||||
|
var retained *configkit.LoadError
|
||||||
|
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 {
|
||||||
|
t.Errorf("profile load failure = (%#v, %v)", acquired, err)
|
||||||
|
}
|
||||||
|
return len(acquired.Diagnostics), err
|
||||||
|
}},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
diagnosticCount, err := test.call()
|
||||||
|
if !errors.Is(err, loadErr) || diagnosticCount == 0 {
|
||||||
|
t.Fatalf("catalog failure = (%d diagnostics, %v)", diagnosticCount, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFromEngineCatalogsHonorCancellationAfterLoad(t *testing.T) {
|
||||||
|
engine := adapterEngine(t)
|
||||||
|
loaderCalls := 0
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
call func(Suite[int, int], context.Context) error
|
||||||
|
}{
|
||||||
|
{name: "prompts", call: func(suite Suite[int, int], ctx context.Context) error {
|
||||||
|
_, err := suite.ListPrompts(ctx, ckconfig.Selection{}, 0, 0)
|
||||||
|
return err
|
||||||
|
}},
|
||||||
|
{name: "profiles", call: func(suite Suite[int, int], ctx context.Context) error {
|
||||||
|
_, err := suite.ListProfiles(ctx, ckconfig.Selection{}, 0, 0)
|
||||||
|
return err
|
||||||
|
}},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancelSuite := FromEngine(func(_ context.Context, _ ckconfig.Selection, _, _ int) (*promptkit.Engine, error) {
|
||||||
|
loaderCalls++
|
||||||
|
cancel()
|
||||||
|
return engine, nil
|
||||||
|
})
|
||||||
|
if err := test.call(cancelSuite, ctx); !errors.Is(err, context.Canceled) {
|
||||||
|
t.Fatalf("catalog cancellation = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if loaderCalls != 2 {
|
||||||
|
t.Fatalf("catalog loaders called = %d, want one per provider", loaderCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAdapterAcquisitionPreservesLoadAndContextErrors(t *testing.T) {
|
func TestAdapterAcquisitionPreservesLoadAndContextErrors(t *testing.T) {
|
||||||
loadErr := errors.New("loader failure")
|
loadErr := errors.New("loader failure")
|
||||||
suite := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
suite := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user