package config import ( "fmt" "sort" "strings" configkit "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy" ) // EffectiveArtifactRecord describes one concrete artifact after configuration // resolution and family expansion. type EffectiveArtifactRecord struct { Key string SourceID string DefinitionSources []string Enabled bool OutputPath string Dependencies []string FamilyKey string CharacterID string FamilySource string PartySource string } // EffectiveArtifactCatalog returns the deterministic concrete artifact catalog // consumed by runtime planning. func EffectiveArtifactCatalog(cfg *PipelineConfig, party ResolvedParty) ([]EffectiveArtifactRecord, error) { if cfg == nil { return nil, fmt.Errorf("pipeline config is required") } if cfg.Scriptorium == nil || len(cfg.Scriptorium.Artifacts) == 0 { return []EffectiveArtifactRecord{}, nil } keys := make([]string, 0, len(cfg.Scriptorium.Artifacts)) for key := range cfg.Scriptorium.Artifacts { keys = append(keys, key) } sort.Strings(keys) records := make([]EffectiveArtifactRecord, 0, len(keys)) for _, key := range keys { artifact := cfg.Scriptorium.Artifacts[key] record := EffectiveArtifactRecord{ Key: key, SourceID: artifactpolicy.ConfiguredSourceID(key), Enabled: artifact.Enabled, OutputPath: artifact.OutputPath, Dependencies: append([]string(nil), artifact.DependsOn...), } if member, ok := cfg.familyCatalog.Members[key]; ok { record.DefinitionSources = artifactFamilyDefinitionSources(cfg, member.Family) record.FamilyKey = member.Family record.CharacterID = member.CharacterID record.FamilySource = member.Source record.PartySource = party.Source.Path } else { record.DefinitionSources = artifactDefinitionSources(cfg, key) } record.Dependencies = nonEmptySortedStrings(record.Dependencies) records = append(records, record) } return records, nil } func artifactDefinitionSources(cfg *PipelineConfig, key string) []string { return declarationDefinitionSources(cfg, configkit.Keys("scriptorium", "artifacts", key), "scriptorium.artifacts."+key, ) } func artifactFamilyDefinitionSources(cfg *PipelineConfig, key string) []string { return declarationDefinitionSources(cfg, configkit.Keys("scriptorium", "artifact_families", key), "scriptorium.artifact_families."+key, ) } func declarationDefinitionSources(cfg *PipelineConfig, declaration configkit.Path, prefix string) []string { if cfg == nil || cfg.resolution == nil { return nil } pathsByID := make(map[string]string, len(cfg.resolution.configkitSources)) for _, source := range cfg.resolution.configkitSources { path := source.Path if path == "" { path = source.Label } pathsByID[source.ID] = path } seen := map[string]struct{}{} for _, change := range cfg.resolution.configkitMetadata.History(declaration) { if change.Origin.Layer != configkit.LayerBase && change.Origin.Layer != configkit.LayerProfile { continue } for _, location := range change.Origin.Locations { source := pathsByID[location.SourceID] if source == "" { source = location.SourceID } seen[source] = struct{}{} } } if len(seen) == 0 { for _, ownership := range cfg.resolution.ownership { if ownership.path != prefix && !strings.HasPrefix(ownership.path, prefix+".") { continue } for _, source := range ownership.sources { if source != pipelineDefaultOwnershipSource && source != "resolution" { seen[source] = struct{}{} } } } } if len(seen) == 0 && cfg.resolution.rootPath != "" { seen[cfg.resolution.rootPath] = struct{}{} } result := make([]string, 0, len(seen)) for source := range seen { result = append(result, source) } sort.Strings(result) return result } func nonEmptySortedStrings(values []string) []string { seen := map[string]struct{}{} for _, value := range values { value = strings.TrimSpace(value) if value != "" { seen[value] = struct{}{} } } result := make([]string, 0, len(seen)) for value := range seen { result = append(result, value) } sort.Strings(result) return result }