package app import ( "fmt" "sort" "strings" "gitea.maximumdirect.net/eric/narratio/internal/artifacts" "gitea.maximumdirect.net/eric/narratio/internal/config" ) type artifactSelectionFlag struct { values []string } func (f *artifactSelectionFlag) String() string { return strings.Join(f.values, ",") } func (f *artifactSelectionFlag) Set(value string) error { f.values = append(f.values, value) return nil } func (f *artifactSelectionFlag) Normalize() ([]string, error) { if len(f.values) == 0 { return nil, nil } seen := map[string]struct{}{} out := make([]string, 0, len(f.values)) for _, raw := range f.values { for _, part := range strings.Split(raw, ",") { name := strings.TrimSpace(part) if name == "" { return nil, fmt.Errorf("artifact names must be non-empty") } if _, ok := seen[name]; ok { continue } seen[name] = struct{}{} out = append(out, name) } } sort.Strings(out) return out, nil } func validateSelectedArtifacts(cfg *config.Config, selected []string) error { _, err := resolveEffectiveArtifacts(cfg, selected) return err } func resolveEffectiveArtifacts(cfg *config.Config, selected []string) (artifacts.EffectiveArtifactSet, error) { if cfg == nil || cfg.Pipeline == nil || cfg.Pipeline.Scriptorium == nil { if len(selected) == 0 { return artifacts.ResolveEffectiveArtifactSet(nil, nil) } return artifacts.EffectiveArtifactSet{}, fmt.Errorf("--artifacts requires pipeline.scriptorium.artifacts to be configured") } configured := artifacts.ConfiguredArtifactDefinitions(cfg.Pipeline.Scriptorium.Artifacts) normalized, err := normalizeArtifactSelection(cfg, selected) if err != nil { return artifacts.EffectiveArtifactSet{}, err } if len(normalized) > 0 && len(configured) == 0 { return artifacts.EffectiveArtifactSet{}, fmt.Errorf("--artifacts requires at least one configured artifact in pipeline.scriptorium.artifacts") } effective, err := artifacts.ResolveEffectiveArtifactSet(configured, normalized) if err != nil { if strings.Contains(err.Error(), "is not configured") { return artifacts.EffectiveArtifactSet{}, fmt.Errorf("--artifacts includes unknown artifact %q", selectedArtifactName(err)) } return artifacts.EffectiveArtifactSet{}, err } if err := validateEffectiveArtifactConfiguration(cfg.Pipeline.Scriptorium.Artifacts, effective); err != nil { return artifacts.EffectiveArtifactSet{}, err } return effective.WithOrigins(effectiveArtifactOrigins(cfg.Pipeline)), nil } func normalizeArtifactSelection(cfg *config.Config, selected []string) ([]string, error) { if len(selected) == 0 { return nil, nil } configured := cfg.Pipeline.Scriptorium.Artifacts families := config.ArtifactFamilies(cfg.Pipeline).Families set := make(map[string]struct{}, len(selected)) for _, raw := range selected { key := strings.TrimSpace(raw) if key == "" { return nil, fmt.Errorf("artifact names must be non-empty") } if family, ok := families[key]; ok { for _, member := range family.Members { set[member] = struct{}{} } continue } if _, ok := configured[key]; !ok { return nil, fmt.Errorf("--artifacts includes unknown artifact %q", key) } set[key] = struct{}{} } normalized := make([]string, 0, len(set)) for key := range set { normalized = append(normalized, key) } sort.Strings(normalized) return normalized, nil } func effectiveArtifactOrigins(pipeline *config.PipelineConfig) map[string]artifacts.EffectiveArtifactOrigin { catalog := config.ArtifactFamilies(pipeline) origins := make(map[string]artifacts.EffectiveArtifactOrigin, len(catalog.Members)) for key, member := range catalog.Members { origins[key] = artifacts.EffectiveArtifactOrigin{Family: member.Family, CharacterID: member.CharacterID} } return origins } func selectedArtifactName(err error) string { message := err.Error() start := strings.Index(message, "\"") if start < 0 { return "" } end := strings.Index(message[start+1:], "\"") if end < 0 { return "" } return message[start+1 : start+1+end] } func validateEffectiveArtifactConfiguration(configured map[string]config.ScriptoriumArtifactConfig, effective artifacts.EffectiveArtifactSet) error { for _, name := range effective.Keys() { artifactCfg := configured[name] if strings.TrimSpace(artifactCfg.PromptID) == "" { return fmt.Errorf("pipeline.scriptorium.artifacts.%s.prompt_id is required when selected", name) } if strings.TrimSpace(artifactCfg.OutputPath) == "" { return fmt.Errorf("pipeline.scriptorium.artifacts.%s.output_path is required when selected", name) } } return nil }