Add abstractions for the internal artifact catalog

This commit is contained in:
2026-05-19 18:43:55 -05:00
parent c63ecbab32
commit 859ae1ae10
3 changed files with 440 additions and 0 deletions

View File

@@ -76,6 +76,34 @@ func TestRunStageArtifactsDoesNotImplyForce(t *testing.T) {
}
}
func TestResumeArtifactsWithSucceededAnalyzeSkipsUnlessForced(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
store := &manifest.LocalStore{}
seed := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC))
for _, stageName := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "analyze", "archive", "notify"} {
seed.MarkStageSucceeded(stageName, time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil)
}
if err := store.Save(context.Background(), manifestPath, seed); err != nil {
t.Fatalf("save manifest: %v", err)
}
var out bytes.Buffer
err := Resume(
context.Background(),
[]string{"--config", pipelinePath, "--session", sessionPath, "--artifacts", "session_recap"},
&out,
)
if err != nil {
t.Fatalf("Resume() error = %v", err)
}
if !strings.Contains(out.String(), "has no remaining stages") {
t.Fatalf("output = %q, want no remaining stages", out.String())
}
}
func writeValidConfigFilesWithScriptoriumArtifacts(t *testing.T, workspaceRoot string) (string, string) {
t.Helper()

View File

@@ -0,0 +1,224 @@
package artifacts
import (
"fmt"
"sort"
"strings"
)
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 "narratio.artifact." + strings.TrimSpace(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{
ArtifactTranscriptMerged,
ArtifactTranscriptPolished,
ArtifactTranscriptFull,
ArtifactTranscriptTrimmed,
ArtifactBoundsSession,
}
}

View File

@@ -0,0 +1,188 @@
package artifacts
import "testing"
func TestArtifactCatalogRegisterBuiltInsAndLookup(t *testing.T) {
catalog := NewArtifactCatalog()
if err := catalog.RegisterBuiltIns(); err != nil {
t.Fatalf("RegisterBuiltIns() error = %v", err)
}
entry, ok := catalog.Lookup(ArtifactTranscriptFull)
if !ok {
t.Fatalf("Lookup(%q) ok = false, want true", ArtifactTranscriptFull)
}
if !entry.Planned {
t.Fatalf("entry.Planned = false, want true")
}
if entry.Executable {
t.Fatalf("entry.Executable = true, want false")
}
if entry.CanonicalRelPath != "transcripts/normalized.json" {
t.Fatalf("entry.CanonicalRelPath = %q, want transcripts/normalized.json", entry.CanonicalRelPath)
}
}
func TestArtifactCatalogRegisterConfiguredArtifactsDefaultsToEnabled(t *testing.T) {
catalog := NewArtifactCatalog()
if err := catalog.RegisterConfiguredArtifacts(map[string]ConfiguredArtifactDefinition{
"player_handout": {Enabled: false, OutputPath: "artifacts/player_handout.md"},
"session_recap": {Enabled: true, OutputPath: "artifacts/session_recap.md"},
}, nil); err != nil {
t.Fatalf("RegisterConfiguredArtifacts() error = %v", err)
}
recapID, ok := catalog.SourceIDForConfiguredKey("session_recap")
if !ok {
t.Fatal("SourceIDForConfiguredKey(session_recap) ok = false, want true")
}
recap, ok := catalog.Lookup(recapID)
if !ok {
t.Fatalf("Lookup(%q) ok = false, want true", recapID)
}
if !recap.Executable {
t.Fatalf("recap.Executable = false, want true")
}
handoutID, ok := catalog.SourceIDForConfiguredKey("player_handout")
if !ok {
t.Fatal("SourceIDForConfiguredKey(player_handout) ok = false, want true")
}
handout, ok := catalog.Lookup(handoutID)
if !ok {
t.Fatalf("Lookup(%q) ok = false, want true", handoutID)
}
if handout.Executable {
t.Fatalf("handout.Executable = true, want false")
}
}
func TestArtifactCatalogRegisterConfiguredArtifactsSelectedSetOverridesEnabled(t *testing.T) {
catalog := NewArtifactCatalog()
if err := catalog.RegisterConfiguredArtifacts(
map[string]ConfiguredArtifactDefinition{
"session_recap": {Enabled: true, OutputPath: "artifacts/session_recap.md"},
"player_handout": {Enabled: false, OutputPath: "artifacts/player_handout.md"},
},
[]string{"player_handout"},
); err != nil {
t.Fatalf("RegisterConfiguredArtifacts() error = %v", err)
}
entries := catalog.ListConfigured()
if len(entries) != 2 {
t.Fatalf("ListConfigured() len = %d, want 2", len(entries))
}
if entries[0].ConfiguredKey != "player_handout" || entries[0].Executable != true {
t.Fatalf("entries[0] = %+v, want player_handout executable", entries[0])
}
if entries[1].ConfiguredKey != "session_recap" || entries[1].Executable != false {
t.Fatalf("entries[1] = %+v, want session_recap disabled by selection", entries[1])
}
}
func TestArtifactCatalogRejectsSelectedUnknownArtifact(t *testing.T) {
catalog := NewArtifactCatalog()
err := catalog.RegisterConfiguredArtifacts(
map[string]ConfiguredArtifactDefinition{
"session_recap": {Enabled: true, OutputPath: "artifacts/session_recap.md"},
},
[]string{"unknown"},
)
if err == nil {
t.Fatal("RegisterConfiguredArtifacts() error = nil, want non-nil")
}
}
func TestArtifactCatalogRejectsConfiguredSourceConflictAcrossRegistrations(t *testing.T) {
catalog := NewArtifactCatalog()
if err := catalog.RegisterConfiguredArtifacts(
map[string]ConfiguredArtifactDefinition{
"session_recap": {Enabled: true, OutputPath: "artifacts/session_recap.md"},
},
nil,
); err != nil {
t.Fatalf("RegisterConfiguredArtifacts() first call error = %v", err)
}
err := catalog.RegisterConfiguredArtifacts(
map[string]ConfiguredArtifactDefinition{
"session_recap": {Enabled: true, OutputPath: "artifacts/session_recap.md"},
},
nil,
)
if err == nil {
t.Fatal("RegisterConfiguredArtifacts() error = nil, want non-nil")
}
}
func TestArtifactCatalogMarkAvailableGenerated(t *testing.T) {
catalog := NewArtifactCatalog()
if err := catalog.RegisterConfiguredArtifacts(
map[string]ConfiguredArtifactDefinition{
"session_recap": {Enabled: true, OutputPath: "artifacts/session_recap.md"},
},
nil,
); err != nil {
t.Fatalf("RegisterConfiguredArtifacts() error = %v", err)
}
sourceID, _ := catalog.SourceIDForConfiguredKey("session_recap")
if err := catalog.MarkAvailableGenerated(sourceID, "/tmp/session_recap.md"); err != nil {
t.Fatalf("MarkAvailableGenerated() error = %v", err)
}
entry, _ := catalog.Lookup(sourceID)
if !entry.Available {
t.Fatalf("entry.Available = false, want true")
}
if entry.Provenance != ArtifactProvenanceGeneratedCurrentAnalyzeRun {
t.Fatalf("entry.Provenance = %q, want %q", entry.Provenance, ArtifactProvenanceGeneratedCurrentAnalyzeRun)
}
}
func TestArtifactCatalogMarkAvailableFromDisk(t *testing.T) {
catalog := NewArtifactCatalog()
if err := catalog.RegisterConfiguredArtifacts(
map[string]ConfiguredArtifactDefinition{
"session_recap": {Enabled: false, OutputPath: "artifacts/session_recap.md"},
},
nil,
); err != nil {
t.Fatalf("RegisterConfiguredArtifacts() error = %v", err)
}
sourceID, _ := catalog.SourceIDForConfiguredKey("session_recap")
if err := catalog.MarkAvailableFromDisk(sourceID, "/tmp/session_recap.md"); err != nil {
t.Fatalf("MarkAvailableFromDisk() error = %v", err)
}
entry, _ := catalog.Lookup(sourceID)
if !entry.Available {
t.Fatalf("entry.Available = false, want true")
}
if entry.Provenance != ArtifactProvenanceDisabledFromDisk {
t.Fatalf("entry.Provenance = %q, want %q", entry.Provenance, ArtifactProvenanceDisabledFromDisk)
}
}
func TestArtifactCatalogLookupPlannedButUnavailable(t *testing.T) {
catalog := NewArtifactCatalog()
if err := catalog.RegisterConfiguredArtifacts(
map[string]ConfiguredArtifactDefinition{
"session_recap": {Enabled: true, OutputPath: "artifacts/session_recap.md"},
},
nil,
); err != nil {
t.Fatalf("RegisterConfiguredArtifacts() error = %v", err)
}
sourceID, _ := catalog.SourceIDForConfiguredKey("session_recap")
entry, ok := catalog.Lookup(sourceID)
if !ok {
t.Fatalf("Lookup(%q) ok = false, want true", sourceID)
}
if !entry.Planned {
t.Fatalf("entry.Planned = false, want true")
}
if entry.Available {
t.Fatalf("entry.Available = true, want false")
}
}