Expose prompt requirement inspection

This commit is contained in:
2026-10-08 20:40:16 +00:00
parent 089e824a92
commit bc5a2da0f2
10 changed files with 515 additions and 13 deletions

17
doc.go
View File

@@ -4,7 +4,7 @@
// Applications construct an [Engine] with [NewEngine], select ConfigKit
// sources for prompts, profiles, and backends, and
// call [Engine.InspectConfiguration], [Engine.InspectPrompt],
// [Engine.InspectProfile], [Engine.Prepare],
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare],
// [Engine.PrepareExecution], [Engine.Run], or [Engine.RunPrepared]. Concrete
// registries, repositories, validators, and the built-in OpenAI-compatible
// client remain internal implementation details.
@@ -12,7 +12,7 @@
// # Concurrency and ownership
//
// An Engine supports concurrent InspectConfiguration, InspectPrompt,
// InspectProfile, Prepare,
// InspectRequirements, InspectProfile, Prepare,
// PrepareExecution, Run, and RunPrepared calls. Engine-local backend policies
// bound admitted Run and RunPrepared calls and model generations where
// configured, while different backend pools and unlimited backends continue
@@ -20,11 +20,11 @@
// still receive concurrent calls and must be safe for that use.
//
// ConfigKit captures the selected configuration when NewEngine constructs an
// Engine. Prepare,
// PrepareExecution, and Run copy request maps, slices, pointer values, and
// JSON-compatible extra parameters before using them. InspectPrompt and
// InspectProfile return copied inspection values. Returned values and values
// passed to extension interfaces are likewise isolated from engine state.
// Engine. Prepare, PrepareExecution, and Run copy request maps, slices, pointer
// values, and JSON-compatible extra parameters before using them. InspectPrompt,
// InspectRequirements, and InspectProfile return copied inspection values.
// Returned values and values passed to extension interfaces are likewise
// isolated from engine state.
// Callers own those copies and may mutate them after the call that supplied or
// returned them. [CapacityError] values are caller-owned and may be mutated
// without affecting engine state or another error. Immutable [GenerationError]
@@ -58,7 +58,8 @@
// [ConfigurationSources], [ConfigurationInspection], [BackendConfig],
// [ProfileConfig], [RunRequest],
// [ArtifactRef], [ExecutionTargetOverride], [ProfileInspection],
// [PromptInputDefinition], [PromptInspection], [PreparedExecution],
// [PromptInputDefinition], [PromptInspection], [RequirementInspectionRequest],
// [RequirementInspection], [PreparedExecution],
// [CapacityError], [GenerationError], [RetryExhaustedError], and
// [ExecutionEvent], do not have stable JSON
// representations. Direct API keys are nevertheless excluded from JSON for

View File

@@ -11,7 +11,7 @@ contributor workflow and validation.
| Component | Implemented responsibility | References |
| --- | --- | --- |
| Root `promptkit` package | Provides the supported engine facade, ConfigKit source helpers, profile and backend mapping conveniences, injection options, public requests and results, explicit inspection, observation values, opaque prepared-execution handles, value conversion, redacted formatting, and typed capacity and generation error mapping. | [Package GoDoc](../../doc.go), [observations](../../observations.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine facade](../../engine.go) |
| Root `promptkit` package | Provides the supported engine facade, ConfigKit source helpers, profile and backend mapping conveniences, injection options, public requests and results, prompt/profile/requirement inspection, observation values, opaque prepared-execution handles, value conversion, redacted formatting, and typed capacity and generation error mapping. | [Package GoDoc](../../doc.go), [observations](../../observations.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine facade](../../engine.go), [requirement facade](../../requirements.go) |
| `examples/go-library/prepare` | Demonstrates offline preparation and explicit configuration inspection with a prompt file, programmatic profile, and inline input. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go) |
| `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, programmatic profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) |
| `examples/go-library/observe` | Demonstrates offline built-in HTTP retry and synchronous metadata observation across structured proposal and assessment runs using embedded sources, two profiles on one finite backend, and an application decision. It is not a public library package. | [Example program](../../examples/go-library/observe/main.go) |
@@ -32,7 +32,7 @@ contributor workflow and validation.
| `internal/validate` | Validates basic, JSON, and JSON Schema output using operating-system filesystem or `fs.FS` schema sources and creates operation-local validation plans with canonical contained schema resources. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) |
| `internal/llm` | Defines the internal generation boundary and implements outbound OpenAI-compatible chat requests from resolved execution targets, including bounded structured non-success response decoding, successful-response decoding, authentication, deadline handling, and ownership of the OpenAI-compatible reserved request-field policy. | [Internal model client](llm.md) |
| `internal/observation` | Owns invocation-local run IDs, presence-aware usage and phase accounting, and synchronous metadata event delivery for ordinary and claimed prepared runs and generation attempts. | [Internal execution observation](observation.md) |
| `internal/usecase` | Selects captured prompts and profiles, applies request overrides and completion checks, and coordinates ordinary and single-claim prepared execution across rendering, artifact loading, operation-local validation plans, generation, capacity, and bounded repair. | [Internal runner](runner.md), [prepared-execution implementation](../../internal/usecase/prepared_execution.go) |
| `internal/usecase` | Selects captured prompts and profiles, enumerates requirements from captured prompt syntax, applies request overrides and completion checks, and coordinates ordinary and single-claim prepared execution across rendering, artifact loading, operation-local validation plans, generation, capacity, and bounded repair. | [Internal runner](runner.md), [requirement inspection](../../internal/usecase/requirements_inspection.go), [prepared-execution implementation](../../internal/usecase/prepared_execution.go) |
The internal engine assembles these internal components without exposing their
representations. Consumers depend only on the root facade.

View File

@@ -43,6 +43,15 @@ touch profile, artifact, schema, renderer, validator, admission, or model
collaborators. The root [`Engine.InspectPrompt`](../../engine.go) GoDoc owns
the public operation's exact contract.
`InspectRequirements` uses the same prompt selection boundary, then selects the
request profile or prompt default for identity and obtains the exact captured
requirement snapshot. It analyzes that snapshot with the supplied caller
variables and normalized direct-session bypass; it does not resolve target
readiness or enter the preparation pipeline. Missing snapshot capability is a
prompt-load failure rather than a source fallback. The root
[`Engine.InspectRequirements`](../../requirements.go) GoDoc owns the public
operation's exact contract.
## Shared Profile Selection
The runner uses one profile-selection and target-resolution boundary for

View File

@@ -340,6 +340,10 @@ in results/explanations. Run `go test ./internal/prompt`; commit.
## Stage 5 — Wire Selection And The Public Facade
**Status: Complete (2026-10-08).** The root API now selects captured prompt and
profile identities, analyzes the exact captured snapshot, and returns
caller-owned requirement values without entering preparation or execution.
**Outcome:** Consumers can inspect a selected captured prompt before artifacts
exist, without execution collaborators or readiness checks.

View File

@@ -82,12 +82,12 @@ var (
ErrValidation = errors.New("failed to validate output")
)
// Engine inspects prompts and profiles and prepares and runs Promptkit prompt
// requests.
// Engine inspects prompts and profiles, enumerates prompt requirements, and
// prepares and runs Promptkit prompt requests.
//
// An Engine is safe for concurrent calls to [Engine.InspectConfiguration],
// [Engine.InspectPrompt],
// [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
// [Engine.Run], and [Engine.RunPrepared]. Each Engine owns independent
// backend-capacity pools that coordinate Run and RunPrepared admission and
// model generation. Injected collaborators may still be invoked concurrently

View File

@@ -178,6 +178,12 @@ func fileSource(name string) (fs.FS, string, string, error) {
func (e *Engine) InspectPrompt(ctx context.Context, id, version string) (*domain.PromptInspection, error) {
return e.runner.InspectPrompt(ctx, id, version)
}
func (e *Engine) InspectRequirements(
ctx context.Context,
request usecase.RequirementInspectionRequest,
) (*domain.RequirementInspection, error) {
return e.runner.InspectRequirements(ctx, request)
}
func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.ProfileInspection, error) {
return e.runner.InspectProfile(ctx, id)
}

View File

@@ -0,0 +1,79 @@
package usecase
import (
"context"
"fmt"
"strings"
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
"gitea.maximumdirect.net/eric/promptkit/internal/prompt"
)
// RequirementInspectionRequest selects a captured prompt and supplies the
// context used for dependency classification.
type RequirementInspectionRequest struct {
PromptID string
PromptVersion string
ProfileID string
Vars map[string]string
SessionID string
}
type requirementSnapshotRepository interface {
GetRequirementSnapshot(id, version string) (prompt.RequirementSnapshot, bool)
}
// InspectRequirements discovers prompt inputs and caller variables without
// loading artifacts or performing execution preparation.
func (r *Runner) InspectRequirements(
ctx context.Context,
req RequirementInspectionRequest,
) (*domain.RequirementInspection, error) {
if strings.TrimSpace(req.PromptID) == "" {
return nil, fmt.Errorf("%w: prompt id is required", ErrInvalidRequest)
}
directSessionID, err := domain.NormalizeSessionID(req.SessionID)
if err != nil {
return nil, fmt.Errorf("%w: session_id: %v", ErrInvalidRequest, err)
}
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
}
selection, err := r.resolvePromptDefinition(ctx, req.PromptID, req.PromptVersion)
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, err)
}
selectedProfileID := req.ProfileID
if strings.TrimSpace(selectedProfileID) == "" {
selectedProfileID = selection.definition.DefaultProfile
}
if strings.TrimSpace(selectedProfileID) == "" {
return nil, fmt.Errorf("%w: %w: profile id is required either in request or prompt default_profile", ErrInvalidRequest, ErrProfileRequired)
}
profileSelection, err := r.resolveProfileSelection(ctx, selectedProfileID)
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
}
repository, ok := r.promptDefs.(requirementSnapshotRepository)
if !ok {
return nil, fmt.Errorf("%w: prompt repository does not provide captured requirement snapshots", ErrPromptLoad)
}
snapshot, ok := repository.GetRequirementSnapshot(selection.definition.ID, selection.definition.Version)
if !ok {
return nil, fmt.Errorf("%w: captured requirement snapshot is unavailable", ErrPromptLoad)
}
inspection, err := prompt.AnalyzeRequirements(ctx, selection.definition, snapshot, req.Vars, directSessionID != "")
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
}
inspection.ProfileID = profileSelection.id
return &inspection, nil
}

View File

@@ -0,0 +1,230 @@
package usecase
import (
"context"
"errors"
"testing"
"text/template"
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
"gitea.maximumdirect.net/eric/promptkit/internal/prompt"
"gitea.maximumdirect.net/eric/promptkit/internal/promptdef"
)
type requirementInspectionPromptRepository struct {
definition *domain.PromptDefinition
snapshot prompt.RequirementSnapshot
lookupErr error
snapshotFound bool
lookupCalls int
snapshotCalls int
lookupID string
lookupVersion string
snapshotID string
snapshotVersion string
}
func (repository *requirementInspectionPromptRepository) GetPromptDefinition(
context.Context,
string,
string,
) (*domain.PromptDefinition, error) {
return repository.definition, repository.lookupErr
}
func (repository *requirementInspectionPromptRepository) GetPromptSnapshot(
_ context.Context,
id string,
version string,
) (*domain.PromptDefinition, string, error) {
repository.lookupCalls++
repository.lookupID = id
repository.lookupVersion = version
return repository.definition, "captured-hash", repository.lookupErr
}
func (repository *requirementInspectionPromptRepository) GetRequirementSnapshot(
id string,
version string,
) (prompt.RequirementSnapshot, bool) {
repository.snapshotCalls++
repository.snapshotID = id
repository.snapshotVersion = version
return repository.snapshot, repository.snapshotFound
}
func TestRunnerInspectRequirementsUsesCapturedSelectionWithoutExecutionWork(t *testing.T) {
message, err := prompt.CompileTemplate("message", `{{input "message-input"}} {{.caller}}`)
if err != nil {
t.Fatal(err)
}
session, err := prompt.CompileTemplate("session", `{{input "session-only"}}`)
if err != nil {
t.Fatal(err)
}
definition := &domain.PromptDefinition{ID: "selected", Version: "v2", DefaultProfile: "default-profile"}
repository := &requirementInspectionPromptRepository{
definition: definition,
snapshotFound: true,
snapshot: prompt.RequirementSnapshot{Templates: prompt.CompiledTemplates{
Messages: []*template.Template{message}, Session: session,
}},
}
profiles := &inspectionProfileRepository{profile: &domain.ExecutionProfile{
ID: "partial-profile", ResolvedTarget: &domain.ExecutionTarget{Endpoint: ""},
}}
runner := &Runner{promptDefs: repository, profiles: profiles}
inspection, err := runner.InspectRequirements(context.Background(), RequirementInspectionRequest{
PromptID: " requested ", PromptVersion: " exact-version ", ProfileID: "partial-profile",
Vars: map[string]string{"caller": ""}, SessionID: " direct-session ",
})
if err != nil {
t.Fatalf("inspect requirements: %v", err)
}
if repository.lookupCalls != 1 || repository.lookupID != " requested " || repository.lookupVersion != " exact-version " ||
repository.snapshotCalls != 1 || repository.snapshotID != definition.ID || repository.snapshotVersion != definition.Version {
t.Fatalf("selection lookup=(%d,%q,%q), snapshot=(%d,%q,%q)", repository.lookupCalls, repository.lookupID,
repository.lookupVersion, repository.snapshotCalls, repository.snapshotID, repository.snapshotVersion)
}
if profiles.calls != 1 || profiles.id != "partial-profile" || inspection.PromptID != definition.ID ||
inspection.PromptVersion != definition.Version || inspection.ProfileID != "partial-profile" || !inspection.Complete ||
len(inspection.Inputs) != 1 || inspection.Inputs[0].Name != "message-input" || len(inspection.Variables) != 1 ||
inspection.Variables[0].Name != "caller" || !inspection.Variables[0].Supplied {
t.Fatalf("unexpected requirement inspection: %+v", inspection)
}
}
func TestRunnerInspectRequirementsUsesPromptDefaultAndIncludesSessionWhenNotBypassed(t *testing.T) {
message, err := prompt.CompileTemplate("message", `{{input "message-input"}}`)
if err != nil {
t.Fatal(err)
}
session, err := prompt.CompileTemplate("session", `{{input "session-input"}}`)
if err != nil {
t.Fatal(err)
}
definition := &domain.PromptDefinition{ID: "selected", Version: "v1", DefaultProfile: "default-profile"}
repository := &requirementInspectionPromptRepository{
definition: definition, snapshotFound: true,
snapshot: prompt.RequirementSnapshot{Templates: prompt.CompiledTemplates{Messages: []*template.Template{message}, Session: session}},
}
profiles := &inspectionProfileRepository{profile: &domain.ExecutionProfile{
ID: "default-profile", ResolvedTarget: &domain.ExecutionTarget{},
}}
inspection, err := (&Runner{promptDefs: repository, profiles: profiles}).InspectRequirements(
context.Background(), RequirementInspectionRequest{PromptID: "selected"},
)
if err != nil {
t.Fatalf("inspect requirements: %v", err)
}
if profiles.id != "default-profile" || inspection.ProfileID != "default-profile" || len(inspection.Inputs) != 2 ||
inspection.Inputs[0].Name != "message-input" || inspection.Inputs[1].Name != "session-input" {
t.Fatalf("prompt-default profile or session template was not selected: %+v", inspection)
}
}
func TestRunnerInspectRequirementsReturnsIncompleteDiscoveryAsSuccess(t *testing.T) {
compiled, err := prompt.CompileTemplate("message", `{{if .missing}}{{input "left"}}{{else}}{{input "right"}}{{end}}`)
if err != nil {
t.Fatal(err)
}
repository := &requirementInspectionPromptRepository{
definition: &domain.PromptDefinition{ID: "selected", Version: "v1", DefaultProfile: "profile"},
snapshotFound: true,
snapshot: prompt.RequirementSnapshot{Templates: prompt.CompiledTemplates{Messages: []*template.Template{compiled}}},
}
profiles := &inspectionProfileRepository{profile: &domain.ExecutionProfile{ID: "profile", ResolvedTarget: &domain.ExecutionTarget{}}}
inspection, err := (&Runner{promptDefs: repository, profiles: profiles}).InspectRequirements(
context.Background(), RequirementInspectionRequest{PromptID: "selected"},
)
if err != nil || inspection == nil || inspection.Complete || len(inspection.Issues) != 1 ||
inspection.Issues[0].Code != domain.RequirementIssueMissingVariable {
t.Fatalf("incomplete discovery returned (%+v, %v), want successful partial result", inspection, err)
}
}
func TestRunnerInspectRequirementsClassifiesSelectionAndSnapshotFailures(t *testing.T) {
t.Run("blank prompt", func(t *testing.T) {
repository := &requirementInspectionPromptRepository{}
_, err := (&Runner{promptDefs: repository}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: " \t "})
if !errors.Is(err, ErrInvalidRequest) || repository.lookupCalls != 0 {
t.Fatalf("blank prompt error=%v lookups=%d", err, repository.lookupCalls)
}
})
t.Run("invalid session", func(t *testing.T) {
repository := &requirementInspectionPromptRepository{}
_, err := (&Runner{promptDefs: repository}).InspectRequirements(context.Background(), RequirementInspectionRequest{
PromptID: "selected", SessionID: string([]byte{0xff}),
})
if !errors.Is(err, ErrInvalidRequest) || repository.lookupCalls != 0 {
t.Fatalf("invalid session error=%v lookups=%d", err, repository.lookupCalls)
}
})
t.Run("cancellation before lookup", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
repository := &requirementInspectionPromptRepository{}
_, err := (&Runner{promptDefs: repository}).InspectRequirements(ctx, RequirementInspectionRequest{PromptID: "selected"})
if !errors.Is(err, ErrPromptLoad) || !errors.Is(err, context.Canceled) || repository.lookupCalls != 0 {
t.Fatalf("canceled inspection=%v lookups=%d", err, repository.lookupCalls)
}
})
t.Run("no profile selection", func(t *testing.T) {
repository := &requirementInspectionPromptRepository{definition: &domain.PromptDefinition{ID: "selected"}}
_, err := (&Runner{promptDefs: repository}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"})
if !errors.Is(err, ErrInvalidRequest) || !errors.Is(err, ErrProfileRequired) {
t.Fatalf("missing profile error=%v", err)
}
})
t.Run("ambiguous prompt selection", func(t *testing.T) {
ambiguous := errors.New("ambiguous omitted version")
repository := &requirementInspectionPromptRepository{lookupErr: ambiguous}
_, err := (&Runner{promptDefs: repository}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"})
if !errors.Is(err, ErrPromptLoad) || !errors.Is(err, ambiguous) || repository.lookupVersion != "" {
t.Fatalf("ambiguous selection error=%v version=%q", err, repository.lookupVersion)
}
})
t.Run("captured snapshot unavailable", func(t *testing.T) {
repository := &requirementInspectionPromptRepository{definition: &domain.PromptDefinition{ID: "selected", DefaultProfile: "profile"}}
profiles := &inspectionProfileRepository{profile: &domain.ExecutionProfile{ID: "profile", ResolvedTarget: &domain.ExecutionTarget{}}}
_, err := (&Runner{promptDefs: repository, profiles: profiles}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"})
if !errors.Is(err, ErrPromptLoad) || repository.snapshotCalls != 1 {
t.Fatalf("missing snapshot error=%v calls=%d", err, repository.snapshotCalls)
}
})
t.Run("repository lacks snapshot capability", func(t *testing.T) {
_, err := (&Runner{promptDefs: &inspectionPromptRepository{definition: &domain.PromptDefinition{ID: "selected", DefaultProfile: "profile"}},
profiles: &inspectionProfileRepository{profile: &domain.ExecutionProfile{ID: "profile", ResolvedTarget: &domain.ExecutionTarget{}}},
}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"})
if !errors.Is(err, ErrPromptLoad) {
t.Fatalf("missing capability error=%v", err)
}
})
if _, err := (*Runner)(nil).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"}); !errors.Is(err, ErrPromptLoad) {
t.Fatalf("nil runner error=%v", err)
}
if _, err := (&Runner{}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"}); !errors.Is(err, ErrPromptLoad) {
t.Fatalf("nil repository error=%v", err)
}
if _, err := (&Runner{promptDefs: &requirementInspectionPromptRepository{definition: &domain.PromptDefinition{ID: "selected"}}}).InspectRequirements(
context.Background(), RequirementInspectionRequest{PromptID: "selected", ProfileID: "explicit"},
); !errors.Is(err, ErrProfileLoad) {
t.Fatalf("missing profile error=%v", err)
}
if _, err := (&Runner{promptDefs: &requirementInspectionPromptRepository{definition: &domain.PromptDefinition{ID: "selected", DefaultProfile: "profile"}},
profiles: &inspectionProfileRepository{profile: &domain.ExecutionProfile{ID: "profile"}},
}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"}); !errors.Is(err, ErrProfileLoad) {
t.Fatalf("unresolved profile error=%v", err)
}
if _, err := (&Runner{promptDefs: &requirementInspectionPromptRepository{lookupErr: promptdef.ErrPromptDefinitionNotFound}}).InspectRequirements(
context.Background(), RequirementInspectionRequest{PromptID: "missing"},
); !errors.Is(err, ErrPromptLoad) || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) {
t.Fatalf("missing prompt error=%v", err)
}
}

158
requirements.go Normal file
View File

@@ -0,0 +1,158 @@
package promptkit
import (
"context"
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
"gitea.maximumdirect.net/eric/promptkit/internal/usecase"
)
// RequirementInspectionRequest selects a prompt and supplies caller context
// for requirement discovery. It requires no artifact bindings or credentials.
type RequirementInspectionRequest struct {
PromptID string
PromptVersion string
ProfileID string
Vars map[string]string
SessionID string
}
// RequirementKind describes how a selected prompt may depend on an input or
// caller variable for the supplied context.
type RequirementKind string
const (
// RequirementRequired identifies a declaration or definite access.
RequirementRequired RequirementKind = "required"
// RequirementConditional identifies a dependency whose reachability is
// unresolved for the supplied context.
RequirementConditional RequirementKind = "conditional"
// RequirementOptional identifies a dependency known not to be required,
// including a reference used only on a known skipped path.
RequirementOptional RequirementKind = "optional"
)
// InputRequirement describes a declared or template-referenced prompt input.
type InputRequirement struct {
Name string
Kind RequirementKind
Declared bool
ContentType *string
Description *string
Explanation string
}
// VariableRequirement describes a caller-map key referenced by a prompt.
type VariableRequirement struct {
Name string
Kind RequirementKind
Supplied bool
Explanation string
}
// RequirementIssueCode identifies a limitation that can affect discovery.
type RequirementIssueCode string
const (
RequirementIssueMissingVariable RequirementIssueCode = "missing_variable"
RequirementIssueInputContent RequirementIssueCode = "input_content"
RequirementIssueDynamicInputName RequirementIssueCode = "dynamic_input_name"
RequirementIssueUnsupportedConstruct RequirementIssueCode = "unsupported_construct"
)
// RequirementIssue describes one discovery limitation using safe summary
// text. Descriptions are not intended as machine-readable details.
type RequirementIssue struct {
Code RequirementIssueCode
InputName string
VariableName string
Description string
}
// RequirementInspection contains discovered dependencies for one selected
// prompt context. Complete describes discovery, not renderability or readiness.
type RequirementInspection struct {
PromptID string
PromptVersion string
ProfileID string
Inputs []InputRequirement
Variables []VariableRequirement
Complete bool
Issues []RequirementIssue
}
// InspectRequirements discovers inputs and caller variables for one selected
// prompt before its input artifacts exist. It uses exact, case-sensitive
// prompt selection and requires a profile ID either in the request or the
// selected prompt's default. The selected profile establishes identity only;
// it need not be execution-ready. A normalized nonblank SessionID bypasses the
// captured session template. Vars are copied before analysis, and returned
// slices and metadata pointers belong to the caller.
//
// This method does not perform artifact, schema, credential, rendering,
// validation, capacity, observation, provider, or execution work. Complete
// describes dependency discovery, not whether every dependency is supplied or
// the prompt is ready to execute. Discovery limitations return a partial
// inspection and no error; selection and operational failures return no
// inspection. Blank prompt IDs, invalid session IDs, and absent profile
// selection match ErrInvalidRequest; an absent profile selection also matches
// ErrProfileRequired. Missing prompt/profile IDs match ErrPromptNotFound or
// ErrProfileNotFound. Other selection/snapshot failures match ErrPromptLoad or
// ErrProfileLoad. A nil Engine matches ErrInvalidConfig.
func (e *Engine) InspectRequirements(
ctx context.Context,
req RequirementInspectionRequest,
) (*RequirementInspection, error) {
if e == nil || e.internal == nil {
return nil, ErrInvalidConfig
}
inspection, err := e.internal.InspectRequirements(ctx, usecase.RequirementInspectionRequest{
PromptID: req.PromptID, PromptVersion: req.PromptVersion, ProfileID: req.ProfileID,
Vars: copyStringMap(req.Vars), SessionID: req.SessionID,
})
if err != nil {
return nil, mapPublicError(err)
}
return fromDomainRequirementInspection(inspection), nil
}
func fromDomainRequirementInspection(inspection *domain.RequirementInspection) *RequirementInspection {
if inspection == nil {
return nil
}
result := &RequirementInspection{
PromptID: inspection.PromptID, PromptVersion: inspection.PromptVersion,
ProfileID: inspection.ProfileID, Complete: inspection.Complete,
Inputs: make([]InputRequirement, len(inspection.Inputs)),
Variables: make([]VariableRequirement, len(inspection.Variables)),
Issues: make([]RequirementIssue, len(inspection.Issues)),
}
for index, input := range inspection.Inputs {
result.Inputs[index] = InputRequirement{
Name: input.Name, Kind: RequirementKind(input.Kind), Declared: input.Declared,
ContentType: copyRequirementString(input.ContentType), Description: copyRequirementString(input.Description),
Explanation: input.Explanation,
}
}
for index, variable := range inspection.Variables {
result.Variables[index] = VariableRequirement{
Name: variable.Name, Kind: RequirementKind(variable.Kind), Supplied: variable.Supplied,
Explanation: variable.Explanation,
}
}
for index, issue := range inspection.Issues {
result.Issues[index] = RequirementIssue{
Code: RequirementIssueCode(issue.Code), InputName: issue.InputName,
VariableName: issue.VariableName, Description: issue.Description,
}
}
return result
}
func copyRequirementString(value *string) *string {
if value == nil {
return nil
}
copy := *value
return &copy
}

15
requirements_test.go Normal file
View File

@@ -0,0 +1,15 @@
package promptkit
import (
"context"
"errors"
"testing"
)
func TestInspectRequirementsRejectsNilEngine(t *testing.T) {
var engine *Engine
inspection, err := engine.InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "prompt"})
if inspection != nil || !errors.Is(err, ErrInvalidConfig) {
t.Fatalf("nil engine returned (%+v, %v), want ErrInvalidConfig", inspection, err)
}
}