Separate PromptKit messages from terminal context

This commit is contained in:
2026-10-26 03:26:14 +00:00
parent c0e0010683
commit 37dfea5b25
5 changed files with 218 additions and 145 deletions

View File

@@ -88,26 +88,33 @@ type Suite[I, S any] struct {
// CheckInput validates selected inspection, requirements, validation, smoke,
// or catalog input before state creation.
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. Its acquisition carries independent loader diagnostics and
// terminal context alongside any native inspection result.
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. Its acquisition carries independent loader diagnostics and
// terminal context alongside any native inspection result.
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
// `promptkit requirements show` without disabling validation or other
// capabilities.
// state preparation. Its acquisition carries independent loader diagnostics
// and terminal context alongside any native inspection result. It is
// optional; a nil callback omits `promptkit requirements show` without
// disabling validation or other capabilities.
InspectRequirements func(context.Context, RequirementsRequest, I, S) (commandkit.Acquired[*promptkit.RequirementInspection], error)
// ValidateRequest validates a concrete PromptKit request after input and
// state preparation. It is optional; a nil callback omits
// `promptkit requirements validate` without disabling discovery or other
// capabilities. The acquisition carries diagnostics and an empty value on
// success; PromptKit errors are returned unchanged where supported.
// capabilities. The acquisition carries independent diagnostics, terminal
// context, and an empty value on success; PromptKit errors are returned
// unchanged where supported.
ValidateRequest func(context.Context, ValidationRequest, I, S) (commandkit.Acquired[struct{}], error)
// SmokeTest runs one concrete native smoke-test operation after input and
// state preparation. It is optional; nil omits only the smoke-test command.
// The acquisition retains a native report even when the callback also
// returns an error. For `test all`, CommandKit calls the captured callback
// sequentially for each selected kind after preparing input and state once.
// The acquisition retains a native report, independent diagnostics, and
// terminal context even when the callback also returns an error. For
// `test all`, CommandKit calls the captured callback sequentially for each
// selected kind after preparing input and state once.
SmokeTest func(context.Context, SmokeRequest, I, S) (commandkit.Acquired[*promptkit.SmokeTestReport], error)
// ListPrompts lists prompt identities after input and state preparation.
// It is optional independently of the other catalogs; nil omits only this
@@ -143,9 +150,10 @@ type Suite[I, S any] struct {
// selection. A nil builder forwards the seed request unchanged. A builder
// failure prevents engine loading and preserves its supported error identity.
// Native validation errors are returned with supported identity, and
// configuration diagnostics from engine loading remain in the acquisition. A
// nil loader is retained as a suite wiring issue and does not install a
// validation callback. Callback references and objects remain caller-owned.
// configuration diagnostics and terminal context from engine loading remain
// separate in the acquisition. A nil loader is retained as a suite wiring
// issue and does not install a validation callback. Callback references and
// objects remain caller-owned.
func WithRequestValidation[I, S any](
suite Suite[I, S],
load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error),
@@ -175,34 +183,39 @@ func WithRequestValidation[I, S any](
return commandkit.Acquired[struct{}]{}, fmt.Errorf("PromptKit request validation after building: %w", err)
}
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, err
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
if err := promptKitContextError(ctx); err != nil {
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, 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)
}
if err := engine.ValidateRequest(ctx, concreteRequest); err != nil {
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, err
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
if err := promptKitContextError(ctx); err != nil {
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit request validation after native validation: %w", err)
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit request validation after native validation: %w", err)
}
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, nil
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, nil
}
return suite
}
type engineLoader[I, S any] func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)
type promptKitAcquisition struct {
diagnostics []commandkit.Diagnostic
explanation commandkit.Explanation
}
// ConfigScope adapts PromptKit's captured configuration inspection to the
// standard configuration commands, including native comparison when the scope
// declares Diff. The loader is not called during declaration construction or
// installation; it is called only when a config operation selects the
// promptkit scope, with that operation's input and shared state. PromptKit
// diagnostics and supported error identity are returned through ckconfig's
// acquisition and comparison contracts.
// independent diagnostics, terminal context, and supported error identity are
// returned through ckconfig's acquisition and comparison contracts.
func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) ckconfig.Scope[I, S] {
scope := ckconfig.Scope[I, S]{
Name: "promptkit",
@@ -214,9 +227,9 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S)
}
scope.Acquire = func(ctx context.Context, request ckconfig.Request, input I, state S) (commandkit.Acquired[ckconfig.View], error) {
_, inspection, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
_, inspection, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[ckconfig.View]{Diagnostics: diagnostics}, err
return commandkit.Acquired[ckconfig.View]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
return commandkit.Acquired[ckconfig.View]{
Value: ckconfig.View{
@@ -224,7 +237,8 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S)
Sources: inspection,
Explanation: inspection,
},
Diagnostics: diagnostics,
Diagnostics: messages.diagnostics,
Explanation: messages.explanation,
}, nil
}
scope.Compare = func(ctx context.Context, request ckconfig.ComparisonRequest[I], state S) (commandkit.Acquired[[]configkit.Difference], error) {
@@ -232,31 +246,30 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S)
return commandkit.Acquired[[]configkit.Difference]{}, fmt.Errorf("PromptKit scope %q comparison before left load: %w", scope.Name, err)
}
_, leftInspection, leftDiagnostics, err := loadPromptKitEngine(ctx, request.Left.Selection, request.Left.Input, state, "left", load)
diagnostics := append([]commandkit.Diagnostic(nil), leftDiagnostics...)
_, leftInspection, messages, err := loadPromptKitEngine(ctx, request.Left.Selection, request.Left.Input, state, "left", load)
if err != nil {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q left comparison load: %w", scope.Name, err)
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q left comparison load: %w", scope.Name, err)
}
if err := promptKitContextError(ctx); err != nil {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q comparison after left load: %w", scope.Name, err)
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q comparison after left load: %w", scope.Name, err)
}
_, rightInspection, rightDiagnostics, err := loadPromptKitEngine(ctx, request.Right.Selection, request.Right.Input, state, "right", load)
diagnostics = append(diagnostics, rightDiagnostics...)
_, rightInspection, rightMessages, err := loadPromptKitEngine(ctx, request.Right.Selection, request.Right.Input, state, "right", load)
messages = appendPromptKitAcquisition(messages, rightMessages)
if err != nil {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q right comparison load: %w", scope.Name, err)
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q right comparison load: %w", scope.Name, err)
}
if err := promptKitContextError(ctx); err != nil {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q comparison before native diff: %w", scope.Name, err)
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q comparison before native diff: %w", scope.Name, err)
}
differences, err := leftInspection.Diff(rightInspection)
if err != nil {
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit scope %q native configuration diff: %w", scope.Name, err)
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q native configuration diff: %w", scope.Name, err)
}
return commandkit.Acquired[[]configkit.Difference]{Value: differences, Diagnostics: diagnostics}, nil
return commandkit.Acquired[[]configkit.Difference]{Value: differences, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, nil
}
return scope
}
@@ -280,8 +293,8 @@ func promptKitContextError(ctx context.Context) error {
// shared invocation state to the loader before calling PromptKit's public
// listing or inspection method. Applications can narrow, wrap, or replace these
// callbacks before Command captures them. A nil loader is retained as an
// adapter issue. Each callback returns loader diagnostics with the native
// operation's value and error.
// adapter issue. Each callback returns independent loader diagnostics and
// terminal context alongside the native operation's value and error.
func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) Suite[I, S] {
if load == nil {
return Suite[I, S]{issues: []error{errors.New("PromptKit engine loader is nil")}}
@@ -289,68 +302,68 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
return Suite[I, S]{
ListPrompts: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]promptkit.PromptIdentity], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: diagnostics}, err
return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
prompts, err := engine.ListPrompts(ctx, request.Filter)
return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: diagnostics}, err
return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
ListProfiles: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
profiles, err := engine.ListProfiles(ctx, request.Filter)
return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
ListBackends: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
backends, err := engine.ListBackends(ctx, request.Filter)
return commandkit.Acquired[[]string]{Value: backends, Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Value: backends, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
ListNamespaces: func(ctx context.Context, request NamespaceRequest, input I, state S) (commandkit.Acquired[[]string], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[[]string]{Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
namespaces, err := engine.ListNamespaces(ctx, request.Kinds...)
return commandkit.Acquired[[]string]{Value: namespaces, Diagnostics: diagnostics}, err
return commandkit.Acquired[[]string]{Value: namespaces, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
ListCollections: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[[]promptkit.CollectionInfo]{Diagnostics: diagnostics}, err
return commandkit.Acquired[[]promptkit.CollectionInfo]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
collections, err := engine.ListCollections(ctx)
return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: collections, Diagnostics: diagnostics}, err
return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: collections, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
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, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
inspection, err := engine.InspectPrompt(ctx, request.ID)
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
InspectProfile: func(ctx context.Context, request ProfileRequest, input I, state S) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
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: messages.diagnostics, Explanation: messages.explanation}, 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)
engine, _, messages, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
if err != nil {
return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
}
inspection, err := engine.InspectRequirements(ctx, request.Inspection)
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: diagnostics}, err
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
},
}
}
@@ -362,41 +375,44 @@ func loadPromptKitEngine[I, S any](
state S,
side string,
load engineLoader[I, S],
) (*promptkit.Engine, promptkit.ConfigurationInspection, []commandkit.Diagnostic, error) {
) (*promptkit.Engine, promptkit.ConfigurationInspection, promptKitAcquisition, error) {
engine, err := load(ctx, selection, input, state)
if err != nil {
var loadErr *configkit.LoadError
if errors.As(err, &loadErr) {
return nil, promptkit.ConfigurationInspection{}, promptKitDiagnostics("promptkit", side, loadErr.Sources(), loadErr.Diagnostics()), err
messages := promptKitDiagnostics("promptkit", side, loadErr.Sources(), loadErr.Diagnostics())
return nil, promptkit.ConfigurationInspection{}, messages, err
}
return nil, promptkit.ConfigurationInspection{}, nil, err
return nil, promptkit.ConfigurationInspection{}, promptKitAcquisition{}, err
}
if engine == nil {
return nil, promptkit.ConfigurationInspection{}, nil, errors.New("PromptKit configuration loader returned a nil engine")
return nil, promptkit.ConfigurationInspection{}, promptKitAcquisition{}, errors.New("PromptKit configuration loader returned a nil engine")
}
inspection, err := engine.InspectConfiguration()
messages := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics())
if err != nil {
return nil, promptkit.ConfigurationInspection{}, nil, err
return nil, inspection, messages, err
}
diagnostics := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics())
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, promptkit.ConfigurationInspection{}, diagnostics, err
return nil, promptkit.ConfigurationInspection{}, messages, err
}
}
return engine, inspection, diagnostics, nil
return engine, inspection, messages, nil
}
func promptKitDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic {
if len(records) == 0 {
return nil
func appendPromptKitAcquisition(left, right promptKitAcquisition) promptKitAcquisition {
left.diagnostics = append(left.diagnostics, right.diagnostics...)
left.explanation.Details = append(left.explanation.Details, right.explanation.Details...)
if left.explanation.Message == "" {
left.explanation.Message = right.explanation.Message
}
severity := commandkit.SeverityWarning
for _, record := range records {
if record.Severity == configkit.SeverityError {
severity = commandkit.SeverityError
break
}
return left
}
func promptKitDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) promptKitAcquisition {
if len(records) == 0 {
return promptKitAcquisition{}
}
// Snapshot provider-owned slices before deferring their report to invocation.
sources = append([]configkit.SourceInfo(nil), sources...)
@@ -408,27 +424,48 @@ func promptKitDiagnostics(scope, side string, sources []configkit.SourceInfo, re
}
records[index].Related = append([]configkit.Location(nil), records[index].Related...)
}
return []commandkit.Diagnostic{{
Severity: severity,
Write: func(writer io.Writer) error {
heading := "Configuration scope " + strconv.Quote(scope)
if side != "" {
heading += " (" + side + ")"
}
if _, err := fmt.Fprintf(writer, "%s:\n", heading); err != nil {
var warnings, failures []configkit.Diagnostic
for _, record := range records {
if record.Severity == configkit.SeverityError {
failures = append(failures, record)
} else {
warnings = append(warnings, record)
}
}
messages := promptKitAcquisition{}
if len(warnings) > 0 {
messages.diagnostics = []commandkit.Diagnostic{{
Severity: commandkit.SeverityWarning,
Write: func(writer io.Writer) error {
return writePromptKitDiagnostics(writer, scope, side, sources, warnings)
},
}}
}
if len(failures) > 0 {
messages.explanation = commandkit.Explanation{Details: []commandkit.Detail{func(writer io.Writer) error {
return writePromptKitDiagnostics(writer, scope, side, sources, failures)
}}}
}
return messages
}
func writePromptKitDiagnostics(writer io.Writer, scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) error {
heading := "Configuration scope " + strconv.Quote(scope)
if side != "" {
heading += " (" + side + ")"
}
if _, err := fmt.Fprintf(writer, "%s:\n", heading); err != nil {
return err
}
if len(sources) > 0 {
if _, err := io.WriteString(writer, "Source IDs:\n"); err != nil {
return err
}
for _, source := range sources {
if _, err := fmt.Fprintf(writer, " %s: label=%s, path=%s\n", strconv.Quote(source.ID), strconv.Quote(source.Label), strconv.Quote(source.Path)); err != nil {
return err
}
if len(sources) > 0 {
if _, err := io.WriteString(writer, "Source IDs:\n"); err != nil {
return err
}
for _, source := range sources {
if _, err := fmt.Fprintf(writer, " %s: label=%s, path=%s\n", strconv.Quote(source.ID), strconv.Quote(source.Label), strconv.Quote(source.Path)); err != nil {
return err
}
}
}
return configkit.WriteDiagnostics(writer, records)
},
}}
}
}
return configkit.WriteDiagnostics(writer, records)
}