package artifacts import ( "fmt" "sort" "strings" "gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy" ) const ( ArtifactProvenanceGeneratedCurrentAnalyzeRun = "generated.current_analyze_run" ArtifactProvenanceDisabledFromDisk = "filesystem.disabled_artifact_output" ) // ConfiguredArtifactDefinition describes one configured analyze artifact. type ConfiguredArtifactDefinition struct { Enabled bool OutputPath string } // CatalogEntry is one runtime catalog entry resolved by source ID. type CatalogEntry struct { SourceID string ConfiguredKey string CanonicalRelPath string ProducerStage string OutputKind string Planned bool Executable bool Available bool Path string Provenance string } // ArtifactCatalog tracks built-in and configured artifact definitions and runtime state. type ArtifactCatalog struct { entries map[string]CatalogEntry configuredIndex map[string]string } // NewArtifactCatalog returns an empty runtime artifact catalog. func NewArtifactCatalog() *ArtifactCatalog { return &ArtifactCatalog{ entries: map[string]CatalogEntry{}, configuredIndex: map[string]string{}, } } // ConfiguredArtifactSourceID converts a configured artifact key into canonical source ID. func ConfiguredArtifactSourceID(key string) string { return artifactpolicy.ConfiguredSourceID(key) } // RegisterBuiltIns registers built-in source definitions used by runtime artifact resolution. func (c *ArtifactCatalog) RegisterBuiltIns() error { for _, id := range runtimeBuiltInArtifactIDs() { spec, ok := artifactRegistry[id] if !ok { return fmt.Errorf("register built-ins: source %q not found in artifact registry", id) } if err := c.addEntry(CatalogEntry{ SourceID: spec.ID, CanonicalRelPath: spec.CanonicalRelPath, ProducerStage: spec.ProducerStage, OutputKind: spec.OutputKind, Planned: true, }); err != nil { return fmt.Errorf("register built-ins: %w", err) } } return nil } // RegisterConfiguredArtifacts registers configured artifacts and applies executable selection. func (c *ArtifactCatalog) RegisterConfiguredArtifacts( configured map[string]ConfiguredArtifactDefinition, selected []string, ) error { keys := make([]string, 0, len(configured)) for key := range configured { keys = append(keys, key) } sort.Strings(keys) selectedSet := map[string]struct{}{} for _, key := range selected { trimmed := strings.TrimSpace(key) if trimmed == "" { return fmt.Errorf("selected artifact keys must be non-empty") } selectedSet[trimmed] = struct{}{} } for _, key := range keys { trimmed := strings.TrimSpace(key) if trimmed == "" { return fmt.Errorf("configured artifact keys must be non-empty") } def := configured[key] sourceID := ConfiguredArtifactSourceID(trimmed) if _, exists := c.configuredIndex[trimmed]; exists { return fmt.Errorf("duplicate configured artifact key %q", trimmed) } executable := def.Enabled if len(selectedSet) > 0 { _, executable = selectedSet[trimmed] } if err := c.addEntry(CatalogEntry{ SourceID: sourceID, ConfiguredKey: trimmed, CanonicalRelPath: strings.TrimSpace(def.OutputPath), ProducerStage: "analyze", OutputKind: "scriptorium_artifact", Planned: true, Executable: executable, }); err != nil { return fmt.Errorf("register configured artifact %q: %w", trimmed, err) } c.configuredIndex[trimmed] = sourceID } if len(selectedSet) > 0 { for key := range selectedSet { if _, ok := c.configuredIndex[key]; !ok { return fmt.Errorf("selected artifact %q is not configured", key) } } } return nil } // Lookup returns one catalog entry by source ID. func (c *ArtifactCatalog) Lookup(sourceID string) (CatalogEntry, bool) { if c == nil { return CatalogEntry{}, false } entry, ok := c.entries[strings.TrimSpace(sourceID)] return entry, ok } // SourceIDForConfiguredKey returns canonical source ID for one configured key. func (c *ArtifactCatalog) SourceIDForConfiguredKey(key string) (string, bool) { if c == nil { return "", false } sourceID, ok := c.configuredIndex[strings.TrimSpace(key)] return sourceID, ok } // ListConfigured returns configured entries sorted by configured key. func (c *ArtifactCatalog) ListConfigured() []CatalogEntry { if c == nil || len(c.configuredIndex) == 0 { return nil } keys := make([]string, 0, len(c.configuredIndex)) for key := range c.configuredIndex { keys = append(keys, key) } sort.Strings(keys) out := make([]CatalogEntry, 0, len(keys)) for _, key := range keys { sourceID := c.configuredIndex[key] out = append(out, c.entries[sourceID]) } return out } // MarkAvailableGenerated marks one source as available in current analyze execution. func (c *ArtifactCatalog) MarkAvailableGenerated(sourceID, path string) error { return c.markAvailable(sourceID, path, ArtifactProvenanceGeneratedCurrentAnalyzeRun) } // MarkAvailableFromDisk marks one source as available from disabled artifact on disk. func (c *ArtifactCatalog) MarkAvailableFromDisk(sourceID, path string) error { return c.markAvailable(sourceID, path, ArtifactProvenanceDisabledFromDisk) } func (c *ArtifactCatalog) markAvailable(sourceID, path, provenance string) error { if c == nil { return fmt.Errorf("artifact catalog is nil") } normalizedID := strings.TrimSpace(sourceID) entry, ok := c.entries[normalizedID] if !ok { return fmt.Errorf("unknown artifact source %q", sourceID) } trimmedPath := strings.TrimSpace(path) if trimmedPath == "" { return fmt.Errorf("artifact path is required") } entry.Available = true entry.Path = trimmedPath entry.Provenance = provenance c.entries[normalizedID] = entry return nil } func (c *ArtifactCatalog) addEntry(entry CatalogEntry) error { if c == nil { return fmt.Errorf("artifact catalog is nil") } sourceID := strings.TrimSpace(entry.SourceID) if sourceID == "" { return fmt.Errorf("source id is required") } if _, exists := c.entries[sourceID]; exists { return fmt.Errorf("source id %q is already registered", sourceID) } entry.SourceID = sourceID c.entries[sourceID] = entry return nil } func runtimeBuiltInArtifactIDs() []string { return []string{ ArtifactTranscriptBase, ArtifactTranscriptPolished, ArtifactTranscriptFinal, ArtifactTranscriptFinalTrimmed, ArtifactBoundsSession, } }