226 lines
6.4 KiB
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
|
|
}
|