Move option source construction into the internal engine

This commit is contained in:
2026-09-22 04:44:41 +00:00
parent bd2b7b1862
commit 47bc9a6655
9 changed files with 190 additions and 157 deletions

View File

@@ -93,7 +93,7 @@ func WithBackend(backend Backend) Option {
queueCapacity = *backend.QueueCapacity queueCapacity = *backend.QueueCapacity
} }
return optionFunc(func(options *engineOptions) error { return optionFunc(func(options *engineOptions) error {
options.backends = append(options.backends, domain.Backend{ options.internal.AddBackend(domain.Backend{
ID: backend.ID, ID: backend.ID,
Endpoint: backend.Endpoint, Endpoint: backend.Endpoint,
APIKeyEnv: backend.APIKeyEnv, APIKeyEnv: backend.APIKeyEnv,

View File

@@ -14,7 +14,7 @@ stable JSON values.
## Construction And Pool Lifecycle ## Construction And Pool Lifecycle
Each root `NewEngine` call obtains a normalized capacity-policy snapshot from Each internal engine construction obtains a normalized capacity-policy snapshot from
its immutable backend registry and constructs a new `Manager`. The manager its immutable backend registry and constructs a new `Manager`. The manager
creates one pool for each limited backend ID. It has no package-global mutable creates one pool for each limited backend ID. It has no package-global mutable
state, background workers, shutdown protocol, or persistence, so engines with state, background workers, shutdown protocol, or persistence, so engines with

View File

@@ -10,7 +10,7 @@ owns the observable outbound HTTP contract. The
[framework format reference](../formats.md) owns the profile and prompt [framework format reference](../formats.md) owns the profile and prompt
settings consumed by the client. settings consumed by the client.
The concrete client remains under `internal/llm`. The root engine assembles it The concrete client remains under `internal/llm`. The internal engine assembles it
as the default implementation behind Promptkit's public client boundary. as the default implementation behind Promptkit's public client boundary.
## Components And Flow ## Components And Flow

View File

@@ -30,7 +30,7 @@ contributor workflow and validation.
| `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/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/usecase` | Resolves prompt definitions and hashes, profiles, backends, and targets for exact inspection and request settings for preparation, and coordinates ordinary execution and one-attempt prepared execution across internal sources, 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` | Resolves prompt definitions and hashes, profiles, backends, and targets for exact inspection and request settings for preparation, and coordinates ordinary execution and one-attempt prepared execution across internal sources, rendering, artifact loading, operation-local validation plans, generation, capacity, and bounded repair. | [Internal runner](runner.md), [prepared-execution implementation](../../internal/usecase/prepared_execution.go) |
The root package assembles these internal components without exposing their The internal engine assembles these internal components without exposing their
representations. Consumers depend only on the root facade. representations. Consumers depend only on the root facade.
## Maintenance ## Maintenance

View File

@@ -69,7 +69,7 @@ repositories described above.
`internal/catalog` validates the imported immutable OpenRouter and `internal/catalog` validates the imported immutable OpenRouter and
Rakestrawhome asset modules as one private adapter boundary. It enforces their Rakestrawhome asset modules as one private adapter boundary. It enforces their
manifest, layout, profile ownership, inheritance, and secret-safety rules manifest, layout, profile ownership, inheritance, and secret-safety rules
before returning raw catalog sources. Root assembly uses those validated before returning raw catalog sources. Internal engine assembly uses those validated
catalogs as the maintained lowest-precedence profile source. catalogs as the maintained lowest-precedence profile source.
The overlay repository consults the next repository only when the The overlay repository consults the next repository only when the

View File

@@ -55,15 +55,15 @@ packages or participate in internal assembly.
The internal engine assembles one immutable backend registry, one capacity manager, The internal engine assembles one immutable backend registry, one capacity manager,
the internal repositories, renderer, validator, outbound client, and use-case the internal repositories, renderer, validator, outbound client, and use-case
runner while translating public values and errors at the library boundary. The runner. The registry contains validated maintained definitions plus engine-scoped
registry contains validated maintained definitions plus engine-scoped consumer consumer additions. The root facade translates public values and errors at the
additions. library boundary.
The facade constructs the capacity manager from the registry's immutable The internal engine constructs the capacity manager from the registry's immutable
policy snapshot, wraps the selected built-in or injected model client, and policy snapshot, wraps the selected built-in or injected model client, and
supplies bounded admission to the runner. The defaults and renderer depend on supplies bounded admission to the runner. The defaults and renderer depend on
the domain model. Prompt-definition and profile repositories use the domain the domain model. Prompt-definition and profile repositories use the domain
model, file catalog, and YAML decoder. The catalog adapter validates imported model, file catalog, and YAML decoder. The catalog adapter validates imported
immutable maintained data before root assembly supplies it to the profile immutable maintained data before internal engine assembly supplies it to the profile
package. Artifact reading uses the domain package. Artifact reading uses the domain
model and application-neutral defaults. Validation uses the domain model, file model and application-neutral defaults. Validation uses the domain model, file
catalog, and JSON Schema implementation. The model client uses the domain catalog, and JSON Schema implementation. The model client uses the domain

View File

@@ -229,6 +229,17 @@ capacity managers, catalogs, or runners. Public options still have their existin
names, signatures, ordering, capture, and error behavior. No transitional state names, signatures, ordering, capture, and error behavior. No transitional state
mapping or duplicate source-assembly path remains. mapping or duplicate source-assembly path remains.
### Completion
- [x] Wrapped the root option state around internal engine options with private
construction state.
- [x] Moved filesystem and file source setup, source replacement, profile
repository construction, validator construction, and backend accumulation
into internal option methods.
- [x] Kept ordered public option application, boundary conversion, collaborator
adaptation, and error translation in the root facade.
- [x] Updated current architecture and internal ownership documentation.
## Stage 5: Guard The Public Boundary And Dependency Direction ## Stage 5: Guard The Public Boundary And Dependency Direction
Purpose: protect the intended architecture without freezing private layout. Purpose: protect the intended architecture without freezing private layout.

129
engine.go
View File

@@ -6,19 +6,11 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"net/http" "net/http"
"os"
"path/filepath"
"strings"
"time" "time"
artifactadapter "gitea.maximumdirect.net/eric/promptkit/internal/artifact"
"gitea.maximumdirect.net/eric/promptkit/internal/domain" "gitea.maximumdirect.net/eric/promptkit/internal/domain"
internalengine "gitea.maximumdirect.net/eric/promptkit/internal/engine" internalengine "gitea.maximumdirect.net/eric/promptkit/internal/engine"
"gitea.maximumdirect.net/eric/promptkit/internal/llm"
"gitea.maximumdirect.net/eric/promptkit/internal/profile"
"gitea.maximumdirect.net/eric/promptkit/internal/promptdef"
"gitea.maximumdirect.net/eric/promptkit/internal/usecase" "gitea.maximumdirect.net/eric/promptkit/internal/usecase"
"gitea.maximumdirect.net/eric/promptkit/internal/validate"
) )
// ErrInvalidConfig identifies invalid engine construction, including missing // ErrInvalidConfig identifies invalid engine construction, including missing
@@ -134,20 +126,7 @@ func (f optionFunc) apply(options *engineOptions) error {
} }
type engineOptions struct { type engineOptions struct {
llmClient llm.Client internal internalengine.Options
artifactReader artifactadapter.Reader
promptDefs promptdef.Repository
profiles profile.Repository
fallbackProfiles profile.Repository
memoryProfiles profile.Repository
backends []domain.Backend
validator validate.Validator
promptSource bool
profileSource bool
fallbackProfileSource bool
memorySource bool
validatorSource bool
artifactSource bool
} }
// WithLLMClient replaces the built-in model client used by [Engine.Run] and // WithLLMClient replaces the built-in model client used by [Engine.Run] and
@@ -163,7 +142,7 @@ func WithLLMClient(client LLMClient) Option {
if client == nil { if client == nil {
return ErrInvalidConfig return ErrInvalidConfig
} }
options.llmClient = publicLLMClientAdapter{client: client} options.internal.SetLLMClient(publicLLMClientAdapter{client: client})
return nil return nil
}) })
} }
@@ -178,8 +157,7 @@ func WithArtifactReader(reader ArtifactReader) Option {
if reader == nil { if reader == nil {
return ErrInvalidConfig return ErrInvalidConfig
} }
options.artifactReader = publicArtifactReaderAdapter{reader: reader} options.internal.SetArtifactReader(publicArtifactReaderAdapter{reader: reader})
options.artifactSource = true
return nil return nil
}) })
} }
@@ -193,15 +171,7 @@ func WithArtifactReader(reader ArtifactReader) Option {
// prompt-source options. // prompt-source options.
func WithPromptFS(fsys fs.FS, root string) Option { func WithPromptFS(fsys fs.FS, root string) Option {
return optionFunc(func(options *engineOptions) error { return optionFunc(func(options *engineOptions) error {
if fsys == nil { return options.internal.SetPromptFS(fsys, root)
return ErrInvalidConfig
}
if strings.TrimSpace(root) == "" {
return ErrInvalidConfig
}
options.promptDefs = promptdef.NewFSRepository(fsys, root)
options.promptSource = true
return nil
}) })
} }
@@ -212,13 +182,7 @@ func WithPromptFS(fsys fs.FS, root string) Option {
// This option replaces Config.PromptDir and earlier prompt-source options. // This option replaces Config.PromptDir and earlier prompt-source options.
func WithPromptFile(path string) Option { func WithPromptFile(path string) Option {
return optionFunc(func(options *engineOptions) error { return optionFunc(func(options *engineOptions) error {
fsys, root, err := fileSource(path) return options.internal.SetPromptFile(path)
if err != nil {
return err
}
options.promptDefs = promptdef.NewFileRepository(fsys, root, filepath.Dir(path))
options.promptSource = true
return nil
}) })
} }
@@ -232,15 +196,7 @@ func WithPromptFile(path string) Option {
// FS profile-source options, but remains below WithProfiles in precedence. // FS profile-source options, but remains below WithProfiles in precedence.
func WithProfileFS(fsys fs.FS, root string) Option { func WithProfileFS(fsys fs.FS, root string) Option {
return optionFunc(func(options *engineOptions) error { return optionFunc(func(options *engineOptions) error {
if fsys == nil { return options.internal.SetProfileFS(fsys, root)
return ErrInvalidConfig
}
if strings.TrimSpace(root) == "" {
return ErrInvalidConfig
}
options.profiles = profile.NewFSRepository(fsys, root)
options.profileSource = true
return nil
}) })
} }
@@ -254,13 +210,7 @@ func WithProfileFS(fsys fs.FS, root string) Option {
// precedence. // precedence.
func WithProfileFile(path string) Option { func WithProfileFile(path string) Option {
return optionFunc(func(options *engineOptions) error { return optionFunc(func(options *engineOptions) error {
fsys, root, err := fileSource(path) return options.internal.SetProfileFile(path)
if err != nil {
return err
}
options.profiles = profile.NewFSRepository(fsys, root)
options.profileSource = true
return nil
}) })
} }
@@ -285,15 +235,7 @@ func WithProfileFile(path string) Option {
// failover. // failover.
func WithFallbackProfileFS(fsys fs.FS, root string) Option { func WithFallbackProfileFS(fsys fs.FS, root string) Option {
return optionFunc(func(options *engineOptions) error { return optionFunc(func(options *engineOptions) error {
if fsys == nil { return options.internal.SetFallbackProfileFS(fsys, root)
return ErrInvalidConfig
}
if strings.TrimSpace(root) == "" {
return ErrInvalidConfig
}
options.fallbackProfiles = profile.NewFSRepository(fsys, root)
options.fallbackProfileSource = true
return nil
}) })
} }
@@ -312,13 +254,7 @@ func WithProfiles(profiles ...Profile) Option {
for index, publicProfile := range profiles { for index, publicProfile := range profiles {
definitions[index] = toDomainProfile(publicProfile) definitions[index] = toDomainProfile(publicProfile)
} }
repo, err := profile.NewMemoryRepository(definitions) return options.internal.SetMemoryProfiles(definitions)
if err != nil {
return err
}
options.memoryProfiles = repo
options.memorySource = true
return nil
}) })
} }
@@ -330,15 +266,7 @@ func WithProfiles(profiles ...Profile) Option {
// replaces Config.SchemaDir and earlier schema-source options. // replaces Config.SchemaDir and earlier schema-source options.
func WithSchemaFS(fsys fs.FS, root string) Option { func WithSchemaFS(fsys fs.FS, root string) Option {
return optionFunc(func(options *engineOptions) error { return optionFunc(func(options *engineOptions) error {
if fsys == nil { return options.internal.SetSchemaFS(fsys, root)
return ErrInvalidConfig
}
if strings.TrimSpace(root) == "" {
return ErrInvalidConfig
}
options.validator = validate.NewFSValidator(fsys, root)
options.validatorSource = true
return nil
}) })
} }
@@ -349,13 +277,7 @@ func WithSchemaFS(fsys fs.FS, root string) Option {
// replaces Config.SchemaDir and earlier schema-source options. // replaces Config.SchemaDir and earlier schema-source options.
func WithSchemaFile(path string) Option { func WithSchemaFile(path string) Option {
return optionFunc(func(options *engineOptions) error { return optionFunc(func(options *engineOptions) error {
fsys, root, err := fileSource(path) return options.internal.SetSchemaFile(path)
if err != nil {
return err
}
options.validator = validate.NewFSValidator(fsys, root)
options.validatorSource = true
return nil
}) })
} }
@@ -384,15 +306,7 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
assembled, err := internalengine.New( assembled, err := internalengine.New(
internalengine.Config{PromptDir: cfg.PromptDir, ProfileDir: cfg.ProfileDir, SchemaDir: cfg.SchemaDir, Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient}, internalengine.Config{PromptDir: cfg.PromptDir, ProfileDir: cfg.ProfileDir, SchemaDir: cfg.SchemaDir, Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient},
internalengine.Options{ options.internal,
LLMClient: options.llmClient, ArtifactReader: options.artifactReader,
PromptDefinitions: options.promptDefs, Profiles: options.profiles,
FallbackProfiles: options.fallbackProfiles, MemoryProfiles: options.memoryProfiles,
Backends: options.backends, Validator: options.validator,
PromptSource: options.promptSource, ProfileSource: options.profileSource,
FallbackProfileSource: options.fallbackProfileSource, MemorySource: options.memorySource,
ValidatorSource: options.validatorSource, ArtifactSource: options.artifactSource,
},
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err) return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err)
@@ -400,25 +314,6 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
return &Engine{internal: assembled}, nil return &Engine{internal: assembled}, nil
} }
func fileSource(name string) (fs.FS, string, error) {
if strings.TrimSpace(name) == "" {
return nil, "", ErrInvalidConfig
}
dir := filepath.Dir(name)
base := filepath.Base(name)
if base == "." || base == string(filepath.Separator) {
return nil, "", ErrInvalidConfig
}
info, err := os.Stat(name)
if err != nil {
return nil, "", fmt.Errorf("%w: failed to access source file %q: %v", ErrInvalidConfig, name, err)
}
if info.IsDir() {
return nil, "", fmt.Errorf("%w: source path %q must be a file", ErrInvalidConfig, name)
}
return os.DirFS(dir), filepath.ToSlash(base), nil
}
// InspectPrompt resolves one explicit prompt definition without selecting a // InspectPrompt resolves one explicit prompt definition without selecting a
// profile or starting execution work. // profile or starting execution work.
// //

View File

@@ -4,7 +4,10 @@ package engine
import ( import (
"context" "context"
"fmt" "fmt"
"io/fs"
"net/http" "net/http"
"os"
"path/filepath"
"strings" "strings"
"time" "time"
@@ -33,23 +36,118 @@ type Config struct {
HTTPClient *http.Client HTTPClient *http.Client
} }
// Options carries collaborators prepared by the public facade during the // Options collects construction choices from the public facade.
// transitional composition split.
type Options struct { type Options struct {
LLMClient llm.Client llmClient llm.Client
ArtifactReader artifact.Reader artifactReader artifact.Reader
PromptDefinitions promptdef.Repository promptDefinitions promptdef.Repository
Profiles profile.Repository profiles profile.Repository
FallbackProfiles profile.Repository fallbackProfiles profile.Repository
MemoryProfiles profile.Repository memoryProfiles profile.Repository
Backends []domain.Backend backends []domain.Backend
Validator validate.Validator validator validate.Validator
PromptSource bool promptSource bool
ProfileSource bool profileSource bool
FallbackProfileSource bool fallbackProfileSource bool
MemorySource bool memorySource bool
ValidatorSource bool validatorSource bool
ArtifactSource bool artifactSource bool
}
// SetLLMClient selects the client collaborator for one engine.
func (o *Options) SetLLMClient(client llm.Client) { o.llmClient = client }
// SetArtifactReader selects the artifact collaborator for one engine.
func (o *Options) SetArtifactReader(reader artifact.Reader) {
o.artifactReader = reader
o.artifactSource = true
}
// SetPromptFS selects a prompt source rooted in fsys.
func (o *Options) SetPromptFS(fsys fs.FS, root string) error {
if err := validateSourceFS(fsys, root); err != nil {
return err
}
o.promptDefinitions = promptdef.NewFSRepository(fsys, root)
o.promptSource = true
return nil
}
// SetPromptFile selects a single prompt file.
func (o *Options) SetPromptFile(path string) error {
fsys, root, directory, err := fileSource(path)
if err != nil {
return err
}
o.promptDefinitions = promptdef.NewFileRepository(fsys, root, directory)
o.promptSource = true
return nil
}
// SetProfileFS selects an ordinary profile source rooted in fsys.
func (o *Options) SetProfileFS(fsys fs.FS, root string) error {
if err := validateSourceFS(fsys, root); err != nil {
return err
}
o.profiles = profile.NewFSRepository(fsys, root)
o.profileSource = true
return nil
}
// SetProfileFile selects a single ordinary profile file.
func (o *Options) SetProfileFile(path string) error {
fsys, root, _, err := fileSource(path)
if err != nil {
return err
}
o.profiles = profile.NewFSRepository(fsys, root)
o.profileSource = true
return nil
}
// SetFallbackProfileFS selects a fallback profile source rooted in fsys.
func (o *Options) SetFallbackProfileFS(fsys fs.FS, root string) error {
if err := validateSourceFS(fsys, root); err != nil {
return err
}
o.fallbackProfiles = profile.NewFSRepository(fsys, root)
o.fallbackProfileSource = true
return nil
}
// SetMemoryProfiles validates and stores application-owned profile definitions.
func (o *Options) SetMemoryProfiles(definitions []domain.ExecutionProfile) error {
repository, err := profile.NewMemoryRepository(definitions)
if err != nil {
return err
}
o.memoryProfiles = repository
o.memorySource = true
return nil
}
// AddBackend appends one backend registration.
func (o *Options) AddBackend(backend domain.Backend) { o.backends = append(o.backends, backend) }
// SetSchemaFS selects a schema source rooted in fsys.
func (o *Options) SetSchemaFS(fsys fs.FS, root string) error {
if err := validateSourceFS(fsys, root); err != nil {
return err
}
o.validator = validate.NewFSValidator(fsys, root)
o.validatorSource = true
return nil
}
// SetSchemaFile selects a single schema file.
func (o *Options) SetSchemaFile(path string) error {
fsys, root, _, err := fileSource(path)
if err != nil {
return err
}
o.validator = validate.NewFSValidator(fsys, root)
o.validatorSource = true
return nil
} }
// Engine forwards domain operations to the single assembled use-case runner. // Engine forwards domain operations to the single assembled use-case runner.
@@ -59,8 +157,8 @@ type Engine struct {
// New assembles an engine from facade-selected configuration and collaborators. // New assembles an engine from facade-selected configuration and collaborators.
func New(cfg Config, options Options) (*Engine, error) { func New(cfg Config, options Options) (*Engine, error) {
promptDefinitions := options.PromptDefinitions promptDefinitions := options.promptDefinitions
if !options.PromptSource { if !options.promptSource {
if strings.TrimSpace(cfg.PromptDir) == "" { if strings.TrimSpace(cfg.PromptDir) == "" {
return nil, fmt.Errorf("prompt directory is required") return nil, fmt.Errorf("prompt directory is required")
} }
@@ -76,7 +174,7 @@ func New(cfg Config, options Options) (*Engine, error) {
} }
profiles := newProfileRepository(cfg.ProfileDir, options, maintained.Profiles) profiles := newProfileRepository(cfg.ProfileDir, options, maintained.Profiles)
registry, err := backend.NewRegistry(maintained.Backends, options.Backends) registry, err := backend.NewRegistry(maintained.Backends, options.backends)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to construct backend registry: %w", err) return nil, fmt.Errorf("failed to construct backend registry: %w", err)
} }
@@ -85,15 +183,15 @@ func New(cfg Config, options Options) (*Engine, error) {
return nil, fmt.Errorf("failed to construct backend capacity manager: %w", err) return nil, fmt.Errorf("failed to construct backend capacity manager: %w", err)
} }
validator := options.Validator validator := options.validator
if !options.ValidatorSource { if !options.validatorSource {
schemaDir := cfg.SchemaDir schemaDir := cfg.SchemaDir
if strings.TrimSpace(schemaDir) == "" { if strings.TrimSpace(schemaDir) == "" {
schemaDir = defaults.SchemaDirDefault schemaDir = defaults.SchemaDirDefault
} }
validator = validate.NewStandardValidator(schemaDir) validator = validate.NewStandardValidator(schemaDir)
} }
client := options.LLMClient client := options.llmClient
if client == nil { if client == nil {
client, err = llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient}) client, err = llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient})
if err != nil { if err != nil {
@@ -101,8 +199,8 @@ func New(cfg Config, options Options) (*Engine, error) {
} }
} }
client = capacity.NewClient(manager, client) client = capacity.NewClient(manager, client)
reader := options.ArtifactReader reader := options.artifactReader
if !options.ArtifactSource { if !options.artifactSource {
reader = artifact.NewCompositeReader() reader = artifact.NewCompositeReader()
} }
@@ -111,20 +209,49 @@ func New(cfg Config, options Options) (*Engine, error) {
func newProfileRepository(profileDir string, options Options, maintained profile.Repository) profile.Repository { func newProfileRepository(profileDir string, options Options, maintained profile.Repository) profile.Repository {
repository := maintained repository := maintained
if options.FallbackProfileSource { if options.fallbackProfileSource {
repository = profile.NewOverlayRepository(options.FallbackProfiles, repository) repository = profile.NewOverlayRepository(options.fallbackProfiles, repository)
} }
if options.ProfileSource { if options.profileSource {
repository = profile.NewOverlayRepository(options.Profiles, repository) repository = profile.NewOverlayRepository(options.profiles, repository)
} else if strings.TrimSpace(profileDir) != "" { } else if strings.TrimSpace(profileDir) != "" {
repository = profile.NewOverlayRepository(profile.NewFilesystemRepository(profileDir), repository) repository = profile.NewOverlayRepository(profile.NewFilesystemRepository(profileDir), repository)
} }
if options.MemorySource { if options.memorySource {
repository = profile.NewOverlayRepository(options.MemoryProfiles, repository) repository = profile.NewOverlayRepository(options.memoryProfiles, repository)
} }
return profile.NewResolvingRepository(repository) return profile.NewResolvingRepository(repository)
} }
func validateSourceFS(fsys fs.FS, root string) error {
if fsys == nil {
return fmt.Errorf("source filesystem is nil")
}
if strings.TrimSpace(root) == "" {
return fmt.Errorf("source root is required")
}
return nil
}
func fileSource(name string) (fs.FS, string, string, error) {
if strings.TrimSpace(name) == "" {
return nil, "", "", fmt.Errorf("source path is required")
}
directory := filepath.Dir(name)
base := filepath.Base(name)
if base == "." || base == string(filepath.Separator) {
return nil, "", "", fmt.Errorf("source path must name a file")
}
info, err := os.Stat(name)
if err != nil {
return nil, "", "", fmt.Errorf("failed to access source file %q: %w", name, err)
}
if info.IsDir() {
return nil, "", "", fmt.Errorf("source path %q must be a file", name)
}
return os.DirFS(directory), filepath.ToSlash(base), directory, nil
}
func (e *Engine) InspectPrompt(ctx context.Context, id, version string) (*domain.PromptInspection, error) { func (e *Engine) InspectPrompt(ctx context.Context, id, version string) (*domain.PromptInspection, error) {
return e.runner.InspectPrompt(ctx, id, version) return e.runner.InspectPrompt(ctx, id, version)
} }