Add PromptKit requirements adapter

This commit is contained in:
2026-10-12 16:54:00 +00:00
parent 3935dcdbda
commit ebd29598db
3 changed files with 184 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
# Implement PromptKit requirements inspection # Implement PromptKit requirements inspection
Status: ready for implementation; no stages have been completed. Status: in progress; Stage 1 is complete.
## Authority and working rules ## Authority and working rules
@@ -116,6 +116,17 @@ adds the callback; its CLI availability arrives in Stage 3. Update existing
absence tests when the command assembly starts counting the fifth callback in absence tests when the command assembly starts counting the fifth callback in
that stage, rather than treating their old four-callback assumption as policy. that stage, rather than treating their old four-callback assumption as policy.
**Completed:** Added `RequirementsRequest`, the optional suite callback, and
the `FromEngine` native adapter in `integrations/ckprompt/adapters.go`; added
adapter coverage in `adapters_test.go`. The ignored workspace already included
core, ckconfig, and ckprompt, and the baseline ckprompt suite passed. After the
change, `GOTOOLCHAIN=local go test ./integrations/ckprompt -run
'TestFromEngine(InspectsRequirementsWithNativeRequest|RequirementsRetainLoadDiagnosticsAndSelectionErrors|RequirementsHonorCancellationAfterLoad|PassesRequestsAndRetainsNilLoaderIssue)$'
-count=1` and `GOTOOLCHAIN=local go test ./...` from `integrations/ckprompt`
passed. The tests verify native result equivalence, loader arguments, retained
ConfigKit diagnostics, nil-engine handling, native selection error identity, and
cancellation after loading. No material limitations were encountered.
## Stage 2: Implement the requirements YAML report ## Stage 2: Implement the requirements YAML report
**Outcome:** native inspections map to the accepted YAML shape without losing **Outcome:** native inspections map to the accepted YAML shape without losing

View File

@@ -31,9 +31,18 @@ type ProfileRequest struct {
ID string ID string
} }
// Suite describes application-owned input, shared state, and optional catalog // RequirementsRequest selects a prompt context for native requirements
// and named inspection capabilities. Its private issues retain adapter // inspection.
// construction failures. type RequirementsRequest struct {
// Selection carries the configuration and overlay values supplied to the command.
Selection ckconfig.Selection
// Inspection carries PromptKit's discovery context for the selected prompt.
Inspection promptkit.RequirementInspectionRequest
}
// Suite describes application-owned input, shared state, and optional catalog,
// named inspection, and requirements 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.
// A reached operation prepares one input, applies its captured selectors, and // A reached operation prepares one input, applies its captured selectors, and
@@ -41,24 +50,28 @@ type ProfileRequest struct {
// Factories are not invoked during construction, installation, help, or version, // Factories are not invoked during construction, installation, help, or version,
// and need not run after failure. // and need not run after failure.
type Suite[I, S any] struct { type Suite[I, S any] struct {
// NewInput is called once when definition or catalog input preparation is // NewInput is called once when definition, requirements, or catalog input
// reached, before applying selectors and calling CheckInput. // preparation is reached, before applying selectors and calling CheckInput.
// If nil, input is the zero value of I, including nil for pointer types. // If nil, input is the zero value of I, including nil for pointer types.
NewInput func() I NewInput func() I
// NewState is called once when an inspection or catalog callback is reached, // NewState is called once when an inspection or catalog callback is reached,
// after input checks pass. If nil, state is the zero value of S, including // after input checks pass. If nil, state is the zero value of S, including
// nil for pointer types. // nil for pointer types.
NewState func() S NewState func() S
// Selectors declares the original typed flags accepted by definition and // Selectors declares the original typed flags accepted by definition,
// catalog commands. // requirements, and catalog commands.
Selectors []commandkit.Flag[I] Selectors []commandkit.Flag[I]
// CheckInput validates selected definition or catalog input before state // CheckInput validates selected definition, requirements, or catalog input
// creation. // 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)
// InspectRequirements inspects native PromptKit requirements after input and
// state preparation. It is optional; a nil callback omits the requirements
// command without disabling other capabilities.
InspectRequirements func(context.Context, RequirementsRequest, I, S) (commandkit.Acquired[*promptkit.RequirementInspection], error)
// ListPrompts lists prompt identities after input and state preparation. // ListPrompts lists prompt identities after input and state preparation.
// It is optional; a nil callback does not install a prompt catalog command. // 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) ListPrompts func(context.Context, ckconfig.Selection, I, S) (commandkit.Acquired[[]promptkit.PromptIdentity], error)
@@ -143,13 +156,13 @@ func promptKitContextError(ctx context.Context) error {
return ctx.Err() return ctx.Err()
} }
// FromEngine supplies callbacks for prompt and profile catalogs and named // FromEngine supplies callbacks for prompt and profile catalogs, named
// inspections backed by a PromptKit engine loader. It does not call the loader // inspections, and requirements inspection backed by a PromptKit engine loader.
// during construction. Each selected operation passes its context, supplied // It does not call the loader during construction. Each selected operation
// selection, initialized input, and shared invocation state to the loader before // passes its context, supplied selection, initialized input, and shared
// calling PromptKit's public listing or inspection method. The callbacks can be // invocation state to the loader before calling PromptKit's public listing or
// narrowed before Command captures them. A nil loader is retained as an adapter // inspection method. Applications can narrow these callbacks before Command
// issue. // captures them. 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")}}
@@ -188,6 +201,14 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
inspection, err := engine.InspectProfile(ctx, request.ID) inspection, err := engine.InspectProfile(ctx, request.ID)
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: diagnostics}, err return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: diagnostics}, err
}, },
InspectRequirements: func(ctx context.Context, request RequirementsRequest, input I, state S) (commandkit.Acquired[*promptkit.RequirementInspection], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: diagnostics}, err
}
inspection, err := engine.InspectRequirements(ctx, request.Inspection)
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: diagnostics}, err
},
} }
} }

View File

@@ -66,6 +66,26 @@ profiles:
api_key_required: true api_key_required: true
` `
const adapterRequirementsPrompt = `prompts:
briefing:
"1":
default_profile: default
session_id: '{{input "session-only"}} {{.session_variable}}'
messages:
- role: user
content: '{{if eq .audience "internal"}}{{input "internal-notes"}}{{else}}{{input "public-notes"}}{{end}} {{.audience}}'
output:
format: text
validation_mode: none
backends:
local:
endpoint: "https://example.invalid/v1"
profiles:
default:
backend: local
model: example
`
func adapterEngine(t *testing.T) *promptkit.Engine { func adapterEngine(t *testing.T) *promptkit.Engine {
return adapterEngineFrom(t, "adapter fixture", adapterPrompt) return adapterEngineFrom(t, "adapter fixture", adapterPrompt)
} }
@@ -321,11 +341,126 @@ func TestFromEnginePassesRequestsAndRetainsNilLoaderIssue(t *testing.T) {
empty.ListProfiles = nil empty.ListProfiles = nil
empty.InspectPrompt = nil empty.InspectPrompt = nil
empty.InspectProfile = nil empty.InspectProfile = nil
empty.InspectRequirements = 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") {
t.Fatalf("nil-loader issue was lost when callbacks were cleared: %#v", empty.issues) t.Fatalf("nil-loader issue was lost when callbacks were cleared: %#v", empty.issues)
} }
} }
func TestFromEngineInspectsRequirementsWithNativeRequest(t *testing.T) {
type input struct{ value string }
type state struct{ value string }
type contextKey struct{}
engine := adapterEngineFrom(t, "requirements adapter fixture", adapterRequirementsPrompt)
wantContext := context.WithValue(context.Background(), contextKey{}, "requirements invocation")
wantInput := input{value: "typed input"}
wantState := state{value: "shared state"}
selection := ckconfig.Selection{
Config: commandkit.Supplied[string]{Value: "prompt.yml", Set: true},
Overlay: commandkit.Supplied[string]{Value: "overlay.yml", Set: true},
}
request := RequirementsRequest{
Selection: selection,
Inspection: promptkit.RequirementInspectionRequest{
PromptID: "briefing",
PromptVersion: "1",
ProfileID: "default",
Vars: map[string]string{"audience": "internal"},
SessionID: "session-42",
},
}
want, err := engine.InspectRequirements(wantContext, request.Inspection)
if err != nil {
t.Fatalf("direct native requirements inspection: %v", err)
}
loaderCalls := 0
suite := FromEngine(func(ctx context.Context, gotSelection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
loaderCalls++
if ctx.Value(contextKey{}) != "requirements invocation" || !reflect.DeepEqual(gotSelection, selection) || gotInput != wantInput || gotState != wantState {
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), gotSelection, gotInput, gotState)
}
return engine, nil
})
acquired, err := suite.InspectRequirements(wantContext, request, wantInput, wantState)
if err != nil || acquired.Value == nil || !reflect.DeepEqual(acquired.Value, want) || loaderCalls != 1 {
t.Fatalf("requirements inspection = (%#v, %v), calls=%d; want native result %#v", acquired, err, loaderCalls, want)
}
if acquired.Value.PromptID != "briefing" || acquired.Value.PromptVersion != "1" || acquired.Value.ProfileID != "default" {
t.Fatalf("requirements selection = %#v", acquired.Value)
}
inputs := make(map[string]promptkit.InputRequirement, len(acquired.Value.Inputs))
for _, input := range acquired.Value.Inputs {
inputs[input.Name] = input
}
if inputs["internal-notes"].Kind != promptkit.RequirementRequired || inputs["public-notes"].Kind != promptkit.RequirementOptional {
t.Fatalf("caller variable did not reach native discovery: %#v", acquired.Value.Inputs)
}
if _, found := inputs["session-only"]; found {
t.Fatalf("session template was not bypassed by the supplied session ID: %#v", acquired.Value.Inputs)
}
variables := make(map[string]promptkit.VariableRequirement, len(acquired.Value.Variables))
for _, variable := range acquired.Value.Variables {
variables[variable.Name] = variable
}
if !variables["audience"].Supplied {
t.Fatalf("caller variable presence was not forwarded: %#v", acquired.Value.Variables)
}
}
func TestFromEngineRequirementsRetainLoadDiagnosticsAndSelectionErrors(t *testing.T) {
_, loadErr := configkit.Load[map[string]any](configkit.Bytes("invalid requirements configuration", []byte("broken: [")))
if loadErr == nil {
t.Fatal("invalid configuration unexpectedly loaded")
}
loadFailure := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
return nil, loadErr
})
acquired, err := loadFailure.InspectRequirements(context.Background(), RequirementsRequest{}, 0, 0)
var retained *configkit.LoadError
if !errors.As(err, &retained) || retained != loadErr || len(acquired.Diagnostics) == 0 || acquired.Value != nil {
t.Fatalf("requirements load failure = (%#v, %v), want original ConfigKit error and diagnostics", acquired, err)
}
if acquired.Diagnostics[0].Severity != commandkit.SeverityError {
t.Fatalf("load diagnostics = %#v, want error severity", acquired.Diagnostics)
}
nilEngine := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
return nil, nil
})
acquired, err = nilEngine.InspectRequirements(context.Background(), RequirementsRequest{}, 0, 0)
if err == nil || acquired.Value != nil || !strings.Contains(err.Error(), "nil engine") {
t.Fatalf("nil engine result = (%#v, %v)", acquired, err)
}
engine := adapterEngine(t)
selectionFailure := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
return engine, nil
})
acquired, err = selectionFailure.InspectRequirements(context.Background(), RequirementsRequest{
Inspection: promptkit.RequirementInspectionRequest{PromptID: "missing", ProfileID: "default"},
}, 0, 0)
if !errors.Is(err, promptkit.ErrPromptNotFound) || acquired.Value != nil {
t.Fatalf("native selection failure = (%#v, %v), want ErrPromptNotFound", acquired, err)
}
}
func TestFromEngineRequirementsHonorCancellationAfterLoad(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
engine := adapterEngine(t)
suite := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
cancel()
return engine, nil
})
acquired, err := suite.InspectRequirements(ctx, RequirementsRequest{
Inspection: promptkit.RequirementInspectionRequest{PromptID: "briefing", ProfileID: "default"},
}, 0, 0)
if !errors.Is(err, context.Canceled) || acquired.Value != nil {
t.Fatalf("cancelled requirements inspection = (%#v, %v)", acquired, err)
}
}
func TestFromEngineListsNativeCatalogsWithInvocationValues(t *testing.T) { func TestFromEngineListsNativeCatalogsWithInvocationValues(t *testing.T) {
type input struct{ label string } type input struct{ label string }
type state struct{ label string } type state struct{ label string }