Files
narratio/internal/artifacts/catalog.go

226 lines
6.4 KiB
Go

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 {
ids := make([]string, 0, len(RuntimeTranscriptArtifacts())+1)
for _, transcript := range RuntimeTranscriptArtifacts() {
ids = append(ids, transcript.SourceID)
}
ids = append(ids, ArtifactBoundsSession)
return ids
}