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 searches its caller-selected main path before a custom // fallback directory, assembles imported fragments over a file-backed underlay, // applies one selected collection overlay, then resolves and validates once. func LoadSequenceCatalog(mainPath, overlayName, runLabel string) (*configkit.Result[SequenceCatalogConfig], error) { absoluteMain, err := filepath.Abs(mainPath) if err != nil { return nil, err } root := filepath.Dir(absoluteMain) filesystem := configkit.NewFilesystem("sequence catalog", os.DirFS(root)) filesystemContext := &configkit.FilesystemContext{BaseDir: ".", Root: ".", FS: &filesystem} mainSearch := configkit.SearchConfig("", configkit.Searches( configkit.Paths(absoluteMain), configkit.SearchDirectory(filepath.Join(root, "fallback"), configkit.DirectorySearchOptions{ Filenames: []configkit.FilenameGroup{configkit.CustomFilenames(filepath.Base(absoluteMain))}, }), ), configkit.SearchOptions{Required: true}) 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: mainSearch, Imports: true, Filesystem: &configkit.FilesystemContext{Root: root}, }, 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"), }, }, }) }