Add collection input helper

This commit is contained in:
2026-10-17 06:09:52 +00:00
parent 13b60a72ac
commit 8dfe35a1f0
3 changed files with 97 additions and 0 deletions

View File

@@ -8,9 +8,25 @@ import (
"path/filepath" "path/filepath"
"gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
"gitea.maximumdirect.net/eric/promptkit/internal/filecatalog" "gitea.maximumdirect.net/eric/promptkit/internal/filecatalog"
) )
// CollectionInput mounts source at the selected collection's configuration
// destination and enables ConfigKit imports for that input. The source is not
// read or validated until ConfigKit loads it. Callers may adjust the returned
// input, including its filesystem context or imports setting.
func CollectionInput(namespace string, source configkit.Source) (configkit.Input, error) {
if !domain.IsValidResourceNamespace(namespace) {
return configkit.Input{}, fmt.Errorf("%w: invalid collection namespace %q", ErrInvalidConfig, namespace)
}
return configkit.Input{
Source: source,
Destination: configkit.Keys("collections", namespace),
Imports: true,
}, nil
}
// DirectoryInputs selects the YAML files beneath an explicitly chosen host // DirectoryInputs selects the YAML files beneath an explicitly chosen host
// directory as one strict-composition group. The returned order is deterministic; // directory as one strict-composition group. The returned order is deterministic;
// it does not assign precedence among those files. Discovery is recursive over // it does not assign precedence among those files. Discovery is recursive over

View File

@@ -1,6 +1,8 @@
package promptkit_test package promptkit_test
import ( import (
"context"
"errors"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
@@ -16,6 +18,78 @@ type sourceDocument struct {
Second string `yaml:"second"` Second string `yaml:"second"`
} }
func TestCollectionInputValidatesWithoutReadingSource(t *testing.T) {
input, err := promptkit.CollectionInput("docs", configkit.File(filepath.Join(t.TempDir(), "not-read.yaml")))
if err != nil {
t.Fatal(err)
}
if input.Destination.String() != "/collections/docs" || !input.Imports {
t.Fatalf("collection input = %+v, want destination /collections/docs with imports enabled", input)
}
for _, namespace := range []string{"", " ", "docs/api"} {
if _, err := promptkit.CollectionInput(namespace, configkit.NoSource()); !errors.Is(err, promptkit.ErrInvalidConfig) {
t.Errorf("namespace %q error = %v, want ErrInvalidConfig", namespace, err)
}
}
}
func TestCollectionInputLoadsNativeImportsAndLaterOverride(t *testing.T) {
directory := t.TempDir()
mainPath := filepath.Join(directory, "collection.yaml")
defaultsPath := filepath.Join(directory, "defaults.yaml")
if err := os.WriteFile(mainPath, []byte("configkit:\n imports:\n - path: defaults.yaml\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(defaultsPath, []byte("prompts:\n greeting:\n version: imported\n messages:\n - role: user\n content: Hello\n output:\n format: text\n validation_mode: none\n"), 0o600); err != nil {
t.Fatal(err)
}
input, err := promptkit.CollectionInput("docs", configkit.File(mainPath))
if err != nil {
t.Fatal(err)
}
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}},
Main: &configkit.Input{
Source: configkit.Mapping("later collection override", map[string]any{"prompts": map[string]any{"greeting": map[string]any{"version": "overridden"}}}),
Destination: configkit.Keys("collections", "docs"),
},
}})
if err != nil {
t.Fatalf("construct engine with native collection import: %v", err)
}
inspection, err := engine.InspectPrompt(context.Background(), "docs/greeting")
if err != nil || inspection.PromptVersion == nil || *inspection.PromptVersion != "overridden" {
t.Fatalf("inspect imported and overlaid prompt = (%+v, %v)", inspection, err)
}
}
func TestCollectionInputLoadsEmbeddedImportsAndLaterOverride(t *testing.T) {
files := fstest.MapFS{
"collection.yaml": {Data: []byte("configkit:\n imports:\n - path: defaults.yaml\n")},
"defaults.yaml": {Data: []byte("prompts:\n greeting:\n version: imported\n messages:\n - role: user\n content: Hello\n output:\n format: text\n validation_mode: none\n")},
}
filesystem := configkit.NewFilesystem("embedded collection", files)
input, err := promptkit.CollectionInput("docs", filesystem.File("collection.yaml"))
if err != nil {
t.Fatal(err)
}
input.Filesystem = &configkit.FilesystemContext{FS: &filesystem, Root: "."}
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}},
Main: &configkit.Input{
Source: configkit.Mapping("later collection override", map[string]any{"prompts": map[string]any{"greeting": map[string]any{"version": "overridden"}}}),
Destination: configkit.Keys("collections", "docs"),
},
}})
if err != nil {
t.Fatalf("construct engine with embedded collection import: %v", err)
}
inspection, err := engine.InspectPrompt(context.Background(), "docs/greeting")
if err != nil || inspection.PromptVersion == nil || *inspection.PromptVersion != "overridden" {
t.Fatalf("inspect imported and overlaid prompt = (%+v, %v)", inspection, err)
}
}
func TestDirectoryInputsSelectsSortedFilesWithRootContext(t *testing.T) { func TestDirectoryInputsSelectsSortedFilesWithRootContext(t *testing.T) {
root := t.TempDir() root := t.TempDir()
if err := os.Mkdir(filepath.Join(root, "nested"), 0o755); err != nil { if err := os.Mkdir(filepath.Join(root, "nested"), 0o755); err != nil {

View File

@@ -593,6 +593,13 @@ Validation: source-helper contract tests and focused configuration/root tests.
Verify helper construction itself performs no source read using an existing Verify helper construction itself performs no source read using an existing
controlled source seam where needed, rather than adding a production interface. controlled source seam where needed, rather than adding a production interface.
Completion: added `CollectionInput` with shared namespace validation, a
`collections/<namespace>` destination, and imports enabled without reading the
source. Contract tests cover invalid namespaces and deferred source loading;
native-file and embedded-filesystem imports assemble successfully, preserve
their source context, and accept a later override. Validation passed:
`GOWORK=off go test ./...`, `GOWORK=off go vet .`, and `git diff --check`.
## Stage 12: Add Immutable Scopes To Ordinary Execution ## Stage 12: Add Immutable Scopes To Ordinary Execution
Scope: public `Namespaces`/`WithNamespaces` and shared engine-boundary routing. Scope: public `Namespaces`/`WithNamespaces` and shared engine-boundary routing.