Add integrated sequence catalog examples

This commit is contained in:
2026-10-23 21:50:10 +00:00
parent 5af71a7533
commit d4133f23f5
11 changed files with 462 additions and 1 deletions

View File

@@ -0,0 +1,134 @@
package consumers
import (
"errors"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
)
// SequenceCatalogConfig is a source-backed prompt catalog with an application
// supplied run label and a value derived from that label during resolution.
type SequenceCatalogConfig struct {
RunLabel string `yaml:"run_label"`
ResolvedLabel string `yaml:"resolved_label"`
Prompts []SequenceCatalogEntry `yaml:"prompts"`
}
// SequenceCatalogEntry keeps prompt meaning, resources, and credentials in the
// consuming application while ConfigKit handles collection assembly.
type SequenceCatalogEntry struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Messages []string `yaml:"messages"`
Resource string `yaml:"resource"`
Credential configkit.Secret `yaml:"credential"`
}
// LoadSequenceCatalog assembles imported main fragments over a file-backed
// underlay, applies one selected collection overlay, then resolves and validates
// the resulting application configuration.
func LoadSequenceCatalog(mainPath, overlayName, runLabel string) (*configkit.Result[SequenceCatalogConfig], error) {
root := filepath.Dir(mainPath)
filesystem := configkit.NewFilesystem("sequence catalog", os.DirFS(root))
filesystemContext := &configkit.FilesystemContext{BaseDir: ".", Root: ".", FS: &filesystem}
definition := configkit.Definition[SequenceCatalogConfig]{
SequenceIdentities: []configkit.SequenceIdentity{
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"),
},
Resolve: func(configuration *SequenceCatalogConfig, resolution *configkit.Resolution) error {
configuration.ResolvedLabel = "for:" + configuration.RunLabel
resolution.Record(configkit.Keys("resolved_label"), configkit.ChangeNormalization,
"derive the resolved label from the selected run label", configkit.Keys("run_label"))
return nil
},
Validate: validateSequenceCatalog,
}
return configkit.LoadLayered(configkit.Input{
Source: filesystem.File(filepath.Base(mainPath)),
Imports: true,
}, configkit.LayeredOptions[SequenceCatalogConfig]{
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
Source: filesystem.File("underlay.yml"),
}}}},
Overlay: overlayName,
OverlayFilesystem: filesystemContext,
Overrides: []configkit.Override{{
Path: configkit.Keys("run_label"), Value: runLabel, Flag: "--run-label",
}},
Definition: definition,
})
}
// ReadSequenceCatalogResource follows the final prompt field's source context,
// including when record inheritance retained a resource from an underlay.
func ReadSequenceCatalogResource(result *configkit.Result[SequenceCatalogConfig], promptID string) (string, error) {
if result == nil {
return "", errors.New("sequence catalog result is nil")
}
for index, prompt := range result.Config.Prompts {
if prompt.ID != promptID {
continue
}
resourcePath := configkit.Keys("prompts").Index(index).Key("resource")
info, found := result.Metadata().Lookup(resourcePath)
if !found || len(info.Origin.Locations) != 1 {
return "", errors.New("prompt resource has no single declaring source")
}
context, found := result.SourceContext(info.Origin.Locations[0].SourceID)
if !found {
return "", errors.New("prompt resource source context is unavailable")
}
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, prompt.Resource))
if err != nil {
return "", fmt.Errorf("read prompt resource: %w", err)
}
return string(data), nil
}
return "", fmt.Errorf("prompt %q is unavailable", promptID)
}
func validateSequenceCatalog(configuration *SequenceCatalogConfig, validation *configkit.Validation) {
for index, prompt := range configuration.Prompts {
if strings.TrimSpace(prompt.Name) == "" {
validation.Error(configkit.Keys("prompts").Index(index).Key("name"), "must not be empty")
}
if len(prompt.Messages) == 0 {
validation.Error(configkit.Keys("prompts").Index(index).Key("messages"), "must contain at least one message")
}
}
if len(configuration.Prompts) == 0 {
validation.Error(configkit.Keys("prompts"), "must contain at least one prompt")
}
}
// NamespacedMapCatalogConfig demonstrates typed application maps whose
// independent prompt sequences use root and dynamic namespace registrations.
type NamespacedMapCatalogConfig struct {
Prompts []map[string]any `yaml:"prompts"`
Collections map[string]map[string]any `yaml:"collections"`
}
// LoadNamespacedMapCatalog applies the same scalar identity constraint to one
// root collection and each namespace's independently scoped collection.
func LoadNamespacedMapCatalog(source configkit.Source) (*configkit.Result[NamespacedMapCatalogConfig], error) {
selector := configkit.SequenceSelector{
configkit.SequenceLiteralKey("collections"),
configkit.SequenceAnyMappingKey(),
configkit.SequenceLiteralKey("prompts"),
}
return configkit.Load(source, configkit.Options[NamespacedMapCatalogConfig]{
Definition: configkit.Definition[NamespacedMapCatalogConfig]{
SequenceIdentities: []configkit.SequenceIdentity{
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"),
configkit.SequenceIdentityMatching(selector, "id"),
},
},
})
}

View File

@@ -0,0 +1,239 @@
package consumers_test
import (
"bytes"
"errors"
"fmt"
"os"
"strings"
"testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/ConfigKit/examples/consumers"
)
func Example_sequenceCatalog() {
const oldSecret = "CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET"
const newSecret = "CONFIGKIT_SEQUENCE_CATALOG_NEW_SECRET"
restoreOld, restoreNew := preserveEnvironment(oldSecret), preserveEnvironment(newSecret)
defer restoreOld()
defer restoreNew()
_ = os.Setenv(oldSecret, "disposable-old-example-credential")
_ = os.Setenv(newSecret, "disposable-new-example-credential")
result, err := consumers.LoadSequenceCatalog(fixture("sequence-catalog", "config.yml"), "upsert", "preview")
if err != nil {
fmt.Println(err)
return
}
ids := make([]string, 0, len(result.Config.Prompts))
for _, prompt := range result.Config.Prompts {
ids = append(ids, prompt.ID)
}
shared := configkit.Keys("prompts").Index(1).Key("name")
info, _ := result.Metadata().Lookup(shared)
resource, resourceErr := consumers.ReadSequenceCatalogResource(result, "concise")
fmt.Println(ids, result.Config.ResolvedLabel, info.Explicit, len(result.Metadata().History(shared)), resource, resourceErr == nil, err)
// Output: [foundation shared welcome concise extra] for:preview true 2 Foundation resource text.
// true <nil>
}
func TestSequenceCatalogSelectedOverlayOperations(t *testing.T) {
const oldSecret = "CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET"
const newSecret = "CONFIGKIT_SEQUENCE_CATALOG_NEW_SECRET"
t.Setenv(oldSecret, "sequence-catalog-old-canary")
t.Setenv(newSecret, "sequence-catalog-new-canary")
tests := []struct {
name string
wantIDs []string
shared *consumers.SequenceCatalogEntry
}{
{name: "append", wantIDs: []string{"foundation", "shared", "welcome", "concise", "extra"}},
{name: "upsert", wantIDs: []string{"foundation", "shared", "welcome", "concise", "extra"}, shared: &consumers.SequenceCatalogEntry{ID: "shared", Name: "Shared replacement", Messages: []string{"Replacement message"}, Credential: configkit.SecretEnv(newSecret)}},
{name: "replace", wantIDs: []string{"replacement", "last"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := consumers.LoadSequenceCatalog(fixture("sequence-catalog", "config.yml"), test.name, "test")
if err != nil {
var loadError *configkit.LoadError
if errors.As(err, &loadError) {
t.Fatalf("load selected overlay: %v (%#v)", err, loadError.Diagnostics())
}
t.Fatal(err)
}
gotIDs := make([]string, 0, len(result.Config.Prompts))
for _, prompt := range result.Config.Prompts {
gotIDs = append(gotIDs, prompt.ID)
}
if strings.Join(gotIDs, ",") != strings.Join(test.wantIDs, ",") {
t.Fatalf("prompt order = %v, want %v", gotIDs, test.wantIDs)
}
if test.shared == nil {
return
}
shared := result.Config.Prompts[1]
if shared.ID != test.shared.ID || shared.Name != test.shared.Name || len(shared.Messages) != 1 ||
shared.Messages[0] != test.shared.Messages[0] || shared.Description != "" || shared.Resource != "" || shared.Credential.EnvName() != newSecret {
t.Fatalf("upsert did not replace the complete matched record: %#v", shared)
}
})
}
}
func TestSequenceCatalogInspectionTracksResourcesAndSecretHistory(t *testing.T) {
const oldSecret = "CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET"
const newSecret = "CONFIGKIT_SEQUENCE_CATALOG_NEW_SECRET"
const oldCanary = "sequence-catalog-old-canary"
const newCanary = "sequence-catalog-new-canary"
t.Setenv(oldSecret, oldCanary)
t.Setenv(newSecret, newCanary)
result, err := consumers.LoadSequenceCatalog(fixture("sequence-catalog", "config.yml"), "upsert", "history")
if err != nil {
var loadError *configkit.LoadError
if errors.As(err, &loadError) {
t.Fatalf("load sequence catalog: %v (%#v)", err, loadError.Diagnostics())
}
t.Fatal(err)
}
resourcePath := configkit.Keys("prompts").Index(3).Key("resource")
resourceInfo, found := result.Metadata().Lookup(resourcePath)
if !found || len(resourceInfo.Origin.Locations) != 1 || len(resourceInfo.Origin.Inheritance) != 1 {
t.Fatalf("inherited resource inspection = %#v, %v", resourceInfo, found)
}
resource, err := consumers.ReadSequenceCatalogResource(result, "concise")
if err != nil || resource != "Foundation resource text.\n" {
t.Fatalf("resource from inherited source context = %q, %v", resource, err)
}
if len(result.Metadata().History(configkit.Keys("resolved_label"))) == 0 {
t.Fatal("recorded CLI-driven resolution was absent from final inspection")
}
credentialPath := configkit.Keys("prompts").Index(1).Key("credential")
credentialHistory := result.Metadata().History(credentialPath)
if len(credentialHistory) == 0 {
t.Fatal("upsert did not retain the replaced secret reference in history")
}
var explanation bytes.Buffer
if err := result.WriteExplanation(&explanation, credentialPath); err != nil {
t.Fatal(err)
}
if strings.Contains(explanation.String(), oldCanary) || strings.Contains(explanation.String(), newCanary) {
t.Fatalf("secret explanation exposed a credential value: %s", explanation.String())
}
}
func TestNamespacedTypedMapIdentityScopesAreIndependent(t *testing.T) {
source := configkit.Bytes("namespaced maps", []byte(`
prompts:
- id: shared
text: root prompt
collections:
team:
prompts:
- id: shared
text: team prompt
operations:
prompts:
- id: shared
text: operations prompt
`))
result, err := consumers.LoadNamespacedMapCatalog(source)
if err != nil {
t.Fatal(err)
}
if result.Config.Prompts[0]["text"] != "root prompt" ||
result.Config.Collections["team"]["prompts"].([]any)[0].(map[string]any)["text"] != "team prompt" ||
result.Config.Collections["operations"]["prompts"].([]any)[0].(map[string]any)["text"] != "operations prompt" {
t.Fatalf("typed map collections = %#v", result.Config)
}
}
func TestNamespacedTypedMapIdentityFindsCollectionsIntroducedByInheritance(t *testing.T) {
result, err := consumers.LoadNamespacedMapCatalog(configkit.Bytes("inherited namespace", []byte(`
collections:
source:
prompts:
- id: shared
text: inherited prompt
copied:
extends: source
`)))
if err != nil {
t.Fatal(err)
}
copied := result.Config.Collections["copied"]["prompts"].([]any)
if len(copied) != 1 || copied[0].(map[string]any)["text"] != "inherited prompt" {
t.Fatalf("inherited typed-map collection = %#v", result.Config.Collections["copied"])
}
}
func Example_yamlDeclaredIdentityOnly() {
result, err := configkit.Load(configkit.Bytes("YAML identity only", []byte(`
configkit:
sequences:
- destination: [prompts]
key: id
prompts:
- id: initial
`)), configkit.Options[map[string]any]{
Overrides: []configkit.Override{{
Path: configkit.Keys("prompts"),
Value: []any{
map[string]any{"id": "same"},
map[string]any{"id": "same"},
},
Flag: "--prompts",
}},
})
fmt.Println(len(result.Config["prompts"].([]any)), err)
// Output: 2 <nil>
}
func TestYAMLDeclaredIdentityDoesNotRegisterFinalTypedConstraint(t *testing.T) {
result, err := configkit.Load(configkit.Bytes("YAML identity only", []byte(`
configkit:
sequences:
- destination: [prompts]
key: id
prompts:
- id: initial
`)), configkit.Options[map[string]any]{
Overrides: []configkit.Override{{
Path: configkit.Keys("prompts"),
Value: []any{
map[string]any{"id": "same"},
map[string]any{"id": "same"},
},
Flag: "--prompts",
}},
})
if err != nil {
t.Fatalf("YAML-only identity incorrectly became an application constraint: %v", err)
}
prompts := result.Config["prompts"].([]any)
if len(prompts) != 2 || prompts[0].(map[string]any)["id"] != prompts[1].(map[string]any)["id"] {
t.Fatalf("CLI result = %#v", result.Config)
}
}
func TestNamespacedTypedMapRejectsDuplicatesWithinOneCollection(t *testing.T) {
_, err := consumers.LoadNamespacedMapCatalog(configkit.Bytes("duplicate namespace", []byte(`
collections:
team:
prompts:
- id: duplicate
- id: duplicate
`)))
var loadError *configkit.LoadError
if !errors.As(err, &loadError) || len(loadError.Diagnostics()) != 1 ||
loadError.Diagnostics()[0].Code != configkit.CodeComposeConflict ||
loadError.Diagnostics()[0].Path.String() != "/collections/team/prompts" {
diagnostics := []configkit.Diagnostic(nil)
if loadError != nil {
diagnostics = loadError.Diagnostics()
}
t.Fatalf("duplicate namespaced identity error = %v (%#v)", err, diagnostics)
}
}

View File

@@ -0,0 +1,7 @@
run_label: source-default
configkit:
imports:
- dir: fragments
pattern: '*.yml'
destination: [prompts]
placement: sequence-item

View File

@@ -0,0 +1 @@
Foundation resource text.

View File

@@ -0,0 +1,4 @@
id: welcome
name: Welcome
messages: [Welcome message]
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET

View File

@@ -0,0 +1,3 @@
id: concise
extends: {id: foundation}
name: Concise

View File

@@ -0,0 +1,10 @@
configkit:
sequences:
- destination: [prompts]
key: id
mode: append
prompts:
- id: extra
name: Extra
messages: [Extra message]
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET

View File

@@ -0,0 +1,14 @@
configkit:
sequences:
- destination: [prompts]
key: id
mode: replace
prompts:
- id: replacement
name: Replacement
messages: [Replacement message]
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET
- id: last
name: Last
messages: [Last message]
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET

View File

@@ -0,0 +1,14 @@
configkit:
sequences:
- destination: [prompts]
key: id
mode: upsert
prompts:
- id: shared
name: Shared replacement
messages: [Replacement message]
credential: CONFIGKIT_SEQUENCE_CATALOG_NEW_SECRET
- id: extra
name: Extra
messages: [Extra message]
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET

View File

@@ -0,0 +1,18 @@
configkit:
sequences:
- destination: [prompts]
key: id
mode: append
prompts:
- id: foundation
name: Foundation
description: Shared foundation instruction
messages: [Foundation message]
resource: foundation.txt
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET
- id: shared
name: Shared draft
description: This field must not survive a whole-record replacement
messages: [Draft message]
resource: shared.txt
credential: CONFIGKIT_SEQUENCE_CATALOG_OLD_SECRET