package artifacts import ( "fmt" "sort" "strings" "gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy" "gitea.maximumdirect.net/eric/narratio/internal/config" ) const ( ArtifactProvenanceGeneratedCurrentAnalyzeRun = "generated.current_analyze_run" ArtifactProvenanceDisabledFromDisk = "filesystem.disabled_artifact_output" ArtifactProvenanceCurrentExtractManifest = "manifest.current_extract_run" ) // ConfiguredArtifactDefinition describes one configured analyze artifact. type ConfiguredArtifactDefinition struct { Enabled bool OutputPath string } // ExtractionArtifactDefinition describes one configured Notarius output lane. type ExtractionArtifactDefinition struct { LaneID string PipelineID string MediaType string SchemaID string SchemaVersion string ModuleKey string } // ExtractionDefinitionsFromConfig converts the effective Notarius output map into catalog definitions. func ExtractionDefinitionsFromConfig(cfg *config.NotariusConfig) map[string]ExtractionArtifactDefinition { if cfg == nil || len(cfg.Outputs) == 0 { return nil } definitions := make(map[string]ExtractionArtifactDefinition, len(cfg.Outputs)) for key, output := range cfg.Outputs { definitions[key] = ExtractionArtifactDefinition{ LaneID: output.LaneID, PipelineID: cfg.PipelineID, MediaType: output.MediaType, SchemaID: output.SchemaID, SchemaVersion: output.SchemaVersion, ModuleKey: output.ModuleKey, } } return definitions } // CatalogEntry is one runtime catalog entry resolved by source ID. type CatalogEntry struct { SourceID string ConfiguredKey string ExtractionKey string CanonicalRelPath string ProducerStage string OutputKind string Planned bool Executable bool Available bool Path string Provenance string ProducerRunID string } // ArtifactCatalog tracks built-in, configured, and extraction artifact definitions and runtime state. type ArtifactCatalog struct { entries map[string]CatalogEntry configuredIndex map[string]string extractionIndex map[string]string } // NewArtifactCatalog returns an empty runtime artifact catalog. func NewArtifactCatalog() *ArtifactCatalog { return &ArtifactCatalog{ entries: map[string]CatalogEntry{}, configuredIndex: map[string]string{}, extractionIndex: map[string]string{}, } } // RegisterExtractionArtifacts registers the configured Notarius output lanes. func (c *ArtifactCatalog) RegisterExtractionArtifacts(configured map[string]ExtractionArtifactDefinition) error { keys := make([]string, 0, len(configured)) for key := range configured { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { trimmed := strings.TrimSpace(key) if !artifactpolicy.IsConfiguredKey(trimmed) { return fmt.Errorf("extraction artifact key %q must match ^[a-z][a-z0-9_]*$", key) } if _, exists := c.extractionIndex[trimmed]; exists { return fmt.Errorf("duplicate extraction artifact key %q", trimmed) } sourceID := ExtractionArtifactSourceID(trimmed) if err := c.addEntry(CatalogEntry{ SourceID: sourceID, ExtractionKey: trimmed, ProducerStage: "extract", OutputKind: "notarius_lane", Planned: true, }); err != nil { return fmt.Errorf("register extraction artifact %q: %w", trimmed, err) } c.extractionIndex[trimmed] = sourceID } return nil } // 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. New runtime // callers should resolve selection first and use RegisterEffectiveConfiguredArtifacts. func (c *ArtifactCatalog) RegisterConfiguredArtifacts( configured map[string]ConfiguredArtifactDefinition, selected ...[]string, ) error { var requested []string if len(selected) > 0 { requested = selected[0] } effective, err := ResolveEffectiveArtifactSet(configured, requested) if err != nil { return err } return c.RegisterEffectiveConfiguredArtifacts(configured, effective) } // RegisterEffectiveConfiguredArtifacts registers configured artifacts using one // resolved analyze execution set. func (c *ArtifactCatalog) RegisterEffectiveConfiguredArtifacts( configured map[string]ConfiguredArtifactDefinition, effective EffectiveArtifactSet, ) error { keys := make([]string, 0, len(configured)) for key := range configured { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { trimmed := strings.TrimSpace(key) if !artifactpolicy.IsConfiguredKey(trimmed) { return fmt.Errorf("configured artifact key %q must match ^[a-z][a-z0-9_]*$", key) } def := configured[key] sourceID := ConfiguredArtifactSourceID(trimmed) if _, exists := c.configuredIndex[trimmed]; exists { return fmt.Errorf("duplicate configured artifact key %q", trimmed) } if err := c.addEntry(CatalogEntry{ SourceID: sourceID, ConfiguredKey: trimmed, CanonicalRelPath: strings.TrimSpace(def.OutputPath), ProducerStage: "analyze", OutputKind: "scriptorium_artifact", Planned: true, Executable: effective.Includes(trimmed), }); err != nil { return fmt.Errorf("register configured artifact %q: %w", trimmed, err) } c.configuredIndex[trimmed] = sourceID } return nil } // BootstrapRuntimeCatalog creates the deterministic catalog definitions shared // by runtime consumers. Availability remains the caller's responsibility. func BootstrapRuntimeCatalog( configured map[string]ConfiguredArtifactDefinition, effective EffectiveArtifactSet, extraction map[string]ExtractionArtifactDefinition, ) (*ArtifactCatalog, error) { catalog := NewArtifactCatalog() if err := catalog.RegisterBuiltIns(); err != nil { return nil, err } if err := catalog.RegisterEffectiveConfiguredArtifacts(configured, effective); err != nil { return nil, err } if err := catalog.RegisterExtractionArtifacts(extraction); err != nil { return nil, err } return catalog, 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 } // SourceIDForExtractionKey returns the canonical source ID for one Notarius output key. func (c *ArtifactCatalog) SourceIDForExtractionKey(key string) (string, bool) { if c == nil { return "", false } sourceID, ok := c.extractionIndex[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 } // ListExtraction returns extraction entries sorted by configured output key. func (c *ArtifactCatalog) ListExtraction() []CatalogEntry { if c == nil || len(c.extractionIndex) == 0 { return nil } keys := make([]string, 0, len(c.extractionIndex)) for key := range c.extractionIndex { keys = append(keys, key) } sort.Strings(keys) out := make([]CatalogEntry, 0, len(keys)) for _, key := range keys { out = append(out, c.entries[c.extractionIndex[key]]) } 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) markAvailableFromExtractManifest(sourceID, path, producerRunID string) error { if err := c.markAvailable(sourceID, path, ArtifactProvenanceCurrentExtractManifest); err != nil { return err } entry := c.entries[strings.TrimSpace(sourceID)] entry.ProducerRunID = strings.TrimSpace(producerRunID) c.entries[entry.SourceID] = entry return nil } 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 { ids := make([]string, 0, len(RuntimeTranscriptArtifacts())+1) for _, transcript := range RuntimeTranscriptArtifacts() { ids = append(ids, transcript.SourceID) } ids = append(ids, ArtifactBoundsSession) return ids }