Rename publish runtime terminology to published outputs

This commit is contained in:
2026-05-23 04:42:08 +00:00
parent df2c765b7f
commit 79737edf79
32 changed files with 354 additions and 354 deletions

View File

@@ -239,13 +239,13 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
store, storeErr := objectStoreIfConfigured(ctx, cfg) store, storeErr := objectStoreIfConfigured(ctx, cfg)
if storeErr != nil { if storeErr != nil {
fmt.Fprintf(out, "Remote archive: unavailable: %v\n", storeErr) fmt.Fprintf(out, "Remote publish: unavailable: %v\n", storeErr)
} else if store != nil { } else if store != nil {
current, err := discoverRemoteCurrentStateFn(ctx, cfg, store) current, err := discoverRemoteCurrentStateFn(ctx, cfg, store)
if err != nil { if err != nil {
fmt.Fprintf(out, "Remote archive: missing or unavailable: %v\n", err) fmt.Fprintf(out, "Remote publish: missing or unavailable: %v\n", err)
} else { } else {
fmt.Fprintf(out, "Remote archive: current run %s\n", current.RunID) fmt.Fprintf(out, "Remote publish: current run %s\n", current.RunID)
fmt.Fprintf(out, "Remote manifest: %s\n", current.CurrentManifestKey) fmt.Fprintf(out, "Remote manifest: %s\n", current.CurrentManifestKey)
} }
} }
@@ -261,15 +261,15 @@ func Status(ctx context.Context, args []string, out io.Writer) error {
All: staticArchiveLocks(cfg), All: staticArchiveLocks(cfg),
} }
} }
promotedRemoteState := map[string]string{} publishedRemoteState := map[string]string{}
if store != nil { if store != nil {
promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog) publishedRemoteState = remotePublishedOutputAvailability(ctx, cfg, store, catalog)
} }
fmt.Fprintln(out, "Remote outputs:") fmt.Fprintln(out, "Remote outputs:")
writeArtifactList(out, cfg, catalog, catalogLocks, promotedRemoteState) writeArtifactList(out, cfg, catalog, catalogLocks, publishedRemoteState)
} }
if err != nil { if err != nil {
fmt.Fprintf(out, "Archive locks: error: %v\n", err) fmt.Fprintf(out, "Publish locks: error: %v\n", err)
} else { } else {
writeLocks(out, cfg, locks) writeLocks(out, cfg, locks)
} }
@@ -437,11 +437,11 @@ func ArtifactsList(ctx context.Context, args []string, out io.Writer) error {
if err != nil { if err != nil {
return fmt.Errorf("artifacts list: %w", err) return fmt.Errorf("artifacts list: %w", err)
} }
promotedRemoteState := map[string]string{} publishedRemoteState := map[string]string{}
if remote && store != nil { if remote && store != nil {
promotedRemoteState = remotePromotionAvailability(ctx, cfg, store, catalog) publishedRemoteState = remotePublishedOutputAvailability(ctx, cfg, store, catalog)
} }
writeArtifactList(out, cfg, catalog, locks, promotedRemoteState) writeArtifactList(out, cfg, catalog, locks, publishedRemoteState)
return nil return nil
} }
@@ -532,7 +532,7 @@ func LocksAdd(ctx context.Context, args []string, out io.Writer) error {
if err != nil { if err != nil {
return fmt.Errorf("locks add: %w", err) return fmt.Errorf("locks add: %w", err)
} }
if _, err := config.ValidateArchiveLockRules([]config.ArchiveLockRule{{Source: source}}, cfg.Pipeline.Scriptorium, "locks add"); err != nil { if _, err := config.ValidatePublishLockRules([]config.PublishLockRule{{Source: source}}, cfg.Pipeline.Scriptorium, "locks add"); err != nil {
return fmt.Errorf("locks add: %w", err) return fmt.Errorf("locks add: %w", err)
} }
if _, ok := lockSourceSet(locks.Static)[source]; ok { if _, ok := lockSourceSet(locks.Static)[source]; ok {
@@ -542,12 +542,12 @@ func LocksAdd(ctx context.Context, args []string, out io.Writer) error {
if _, exists := remoteSet[source]; exists && !force { if _, exists := remoteSet[source]; exists && !force {
return fmt.Errorf("locks add: remote lock for %q already exists; pass --force to update", source) return fmt.Errorf("locks add: remote lock for %q already exists; pass --force to update", source)
} }
remoteSet[source] = config.ArchiveLockRule{Source: source, Reason: strings.TrimSpace(reason)} remoteSet[source] = config.PublishLockRule{Source: source, Reason: strings.TrimSpace(reason)}
remoteLocks := lockMapValues(remoteSet) remoteLocks := lockMapValues(remoteSet)
if _, err := config.ValidateArchiveLockRules(remoteLocks, cfg.Pipeline.Scriptorium, "locks"); err != nil { if _, err := config.ValidatePublishLockRules(remoteLocks, cfg.Pipeline.Scriptorium, "locks"); err != nil {
return fmt.Errorf("locks add: %w", err) return fmt.Errorf("locks add: %w", err)
} }
if err := uploadRemoteLockStore(ctx, store, locks.Key, &config.ArchiveLockStore{Locks: remoteLocks}); err != nil { if err := uploadRemoteLockStore(ctx, store, locks.Key, &config.PublishLockStore{Locks: remoteLocks}); err != nil {
return fmt.Errorf("locks add: %w", err) return fmt.Errorf("locks add: %w", err)
} }
_, err = fmt.Fprintf(out, "narratio session locks add: locked %s\n", source) _, err = fmt.Fprintf(out, "narratio session locks add: locked %s\n", source)
@@ -589,7 +589,7 @@ func LocksRemove(ctx context.Context, args []string, out io.Writer) error {
if err != nil { if err != nil {
return fmt.Errorf("locks remove: %w", err) return fmt.Errorf("locks remove: %w", err)
} }
if _, err := config.ValidateArchiveLockRules([]config.ArchiveLockRule{{Source: source}}, cfg.Pipeline.Scriptorium, "locks remove"); err != nil { if _, err := config.ValidatePublishLockRules([]config.PublishLockRule{{Source: source}}, cfg.Pipeline.Scriptorium, "locks remove"); err != nil {
return fmt.Errorf("locks remove: %w", err) return fmt.Errorf("locks remove: %w", err)
} }
remoteSet := lockSourceSet(locks.Remote) remoteSet := lockSourceSet(locks.Remote)
@@ -601,7 +601,7 @@ func LocksRemove(ctx context.Context, args []string, out io.Writer) error {
} }
delete(remoteSet, source) delete(remoteSet, source)
remoteLocks := lockMapValues(remoteSet) remoteLocks := lockMapValues(remoteSet)
if err := uploadRemoteLockStore(ctx, store, locks.Key, &config.ArchiveLockStore{Locks: remoteLocks}); err != nil { if err := uploadRemoteLockStore(ctx, store, locks.Key, &config.PublishLockStore{Locks: remoteLocks}); err != nil {
return fmt.Errorf("locks remove: %w", err) return fmt.Errorf("locks remove: %w", err)
} }
_, err = fmt.Fprintf(out, "narratio session locks remove: unlocked %s\n", source) _, err = fmt.Fprintf(out, "narratio session locks remove: unlocked %s\n", source)
@@ -980,7 +980,7 @@ func buildHelperArtifactCatalog(cfg *config.Config) (*artifacts.ArtifactCatalog,
return catalog, nil return catalog, nil
} }
func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.ArtifactCatalog, locks *effectiveLocks, promotedRemoteState map[string]string) { func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.ArtifactCatalog, locks *effectiveLocks, publishedRemoteState map[string]string) {
lockSet := lockSourceSet(locks.All) lockSet := lockSourceSet(locks.All)
fmt.Fprintln(out, "Built-in:") fmt.Fprintln(out, "Built-in:")
for _, id := range []string{ for _, id := range []string{
@@ -1000,13 +1000,13 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art
for _, req := range artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg)) { for _, req := range artifacts.CollectPreviousArtifactRequirements(configuredScriptoriumArtifacts(cfg)) {
fmt.Fprintf(out, "- narratio.previous_session.artifact.%s required=%t\n", req.Name, req.Required) fmt.Fprintf(out, "- narratio.previous_session.artifact.%s required=%t\n", req.Name, req.Required)
} }
fmt.Fprintln(out, "Promoted:") fmt.Fprintln(out, "Published:")
for _, rule := range cfg.Pipeline.Publish.PromoteArtifacts { for _, rule := range cfg.Pipeline.Publish.Outputs {
writePromotedArtifactLine(out, rule, catalog, lockSet, promotedRemoteState) writePublishedOutputLine(out, rule, catalog, lockSet, publishedRemoteState)
} }
} }
func writeArtifactLine(out io.Writer, source string, lockSet map[string]config.ArchiveLockRule) { func writeArtifactLine(out io.Writer, source string, lockSet map[string]config.PublishLockRule) {
parts := []string{source} parts := []string{source}
if _, ok := lockSet[source]; ok { if _, ok := lockSet[source]; ok {
parts = append(parts, "locked") parts = append(parts, "locked")
@@ -1014,13 +1014,13 @@ func writeArtifactLine(out io.Writer, source string, lockSet map[string]config.A
fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) fmt.Fprintf(out, "- %s\n", strings.Join(parts, " "))
} }
func writePromotedArtifactLine(out io.Writer, rule config.ArchivePromotionRule, catalog *artifacts.ArtifactCatalog, lockSet map[string]config.ArchiveLockRule, remoteState map[string]string) { func writePublishedOutputLine(out io.Writer, rule config.PublishOutputRule, catalog *artifacts.ArtifactCatalog, lockSet map[string]config.PublishLockRule, remoteState map[string]string) {
source := strings.TrimSpace(rule.Source) source := strings.TrimSpace(rule.Source)
parts := []string{source} parts := []string{source}
if _, ok := lockSet[source]; ok { if _, ok := lockSet[source]; ok {
parts = append(parts, "locked") parts = append(parts, "locked")
} }
dest, showDest, err := helperPromotionDest(rule, catalog) dest, showDest, err := helperPublishedOutputDest(rule, catalog)
if err != nil { if err != nil {
parts = append(parts, "remote=error") parts = append(parts, "remote=error")
fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) fmt.Fprintf(out, "- %s\n", strings.Join(parts, " "))
@@ -1029,35 +1029,35 @@ func writePromotedArtifactLine(out io.Writer, rule config.ArchivePromotionRule,
if showDest { if showDest {
parts = append(parts, "dest="+dest) parts = append(parts, "dest="+dest)
} }
if state := remoteState[promotionRemoteStateKey(source, dest)]; state != "" { if state := remoteState[publishedOutputRemoteStateKey(source, dest)]; state != "" {
parts = append(parts, state) parts = append(parts, state)
} }
fmt.Fprintf(out, "- %s\n", strings.Join(parts, " ")) fmt.Fprintf(out, "- %s\n", strings.Join(parts, " "))
} }
func remotePromotionAvailability(ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog) map[string]string { func remotePublishedOutputAvailability(ctx context.Context, cfg *config.Config, store storage.ObjectStore, catalog *artifacts.ArtifactCatalog) map[string]string {
out := map[string]string{} out := map[string]string{}
sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID) sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID)
for _, rule := range cfg.Pipeline.Publish.PromoteArtifacts { for _, rule := range cfg.Pipeline.Publish.Outputs {
source := strings.TrimSpace(rule.Source) source := strings.TrimSpace(rule.Source)
dest, _, err := helperPromotionDest(rule, catalog) dest, _, err := helperPublishedOutputDest(rule, catalog)
if err != nil { if err != nil {
out[promotionRemoteStateKey(source, "")] = "remote=error" out[publishedOutputRemoteStateKey(source, "")] = "remote=error"
continue continue
} }
key := artifacts.S3PromotedArtifactKey(sessionPrefix, dest) key := artifacts.S3PublishedOutputKey(sessionPrefix, dest)
if exists, err := store.Exists(ctx, key); err == nil && exists { if exists, err := store.Exists(ctx, key); err == nil && exists {
out[promotionRemoteStateKey(source, dest)] = "remote=promoted" out[publishedOutputRemoteStateKey(source, dest)] = "remote=published"
} else if err != nil { } else if err != nil {
out[promotionRemoteStateKey(source, dest)] = "remote=error" out[publishedOutputRemoteStateKey(source, dest)] = "remote=error"
} else { } else {
out[promotionRemoteStateKey(source, dest)] = "remote=missing" out[publishedOutputRemoteStateKey(source, dest)] = "remote=missing"
} }
} }
return out return out
} }
func helperPromotionDest(rule config.ArchivePromotionRule, catalog *artifacts.ArtifactCatalog) (string, bool, error) { func helperPublishedOutputDest(rule config.PublishOutputRule, catalog *artifacts.ArtifactCatalog) (string, bool, error) {
source := strings.TrimSpace(rule.Source) source := strings.TrimSpace(rule.Source)
dest := strings.TrimSpace(rule.Dest) dest := strings.TrimSpace(rule.Dest)
if dest == "" { if dest == "" {
@@ -1094,20 +1094,20 @@ func normalizeHelperArchiveRelativePath(rel string) (string, error) {
return cleaned, nil return cleaned, nil
} }
func promotionRemoteStateKey(source, dest string) string { func publishedOutputRemoteStateKey(source, dest string) string {
return strings.TrimSpace(source) + "\x00" + strings.TrimSpace(dest) return strings.TrimSpace(source) + "\x00" + strings.TrimSpace(dest)
} }
func writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) { func writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) {
if locks == nil || len(locks.All) == 0 { if locks == nil || len(locks.All) == 0 {
fmt.Fprintln(out, "Archive locks: none") fmt.Fprintln(out, "Publish locks: none")
return return
} }
fmt.Fprintln(out, "Archive locks:") fmt.Fprintln(out, "Publish locks:")
promoted := map[string]config.ArchivePromotionRule{} published := map[string]config.PublishOutputRule{}
if cfg != nil && cfg.Pipeline != nil && cfg.Pipeline.Publish != nil { if cfg != nil && cfg.Pipeline != nil && cfg.Pipeline.Publish != nil {
for _, rule := range cfg.Pipeline.Publish.PromoteArtifacts { for _, rule := range cfg.Pipeline.Publish.Outputs {
promoted[strings.TrimSpace(rule.Source)] = rule published[strings.TrimSpace(rule.Source)] = rule
} }
} }
staticSet := lockSourceSet(locks.Static) staticSet := lockSourceSet(locks.Static)
@@ -1116,9 +1116,9 @@ func writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) {
if _, ok := staticSet[lock.Source]; ok { if _, ok := staticSet[lock.Source]; ok {
origin = "pipeline" origin = "pipeline"
} }
promo := "not-promoted" promo := "not-published"
if _, ok := promoted[lock.Source]; ok { if _, ok := published[lock.Source]; ok {
promo = "promoted" promo = "published"
} }
reason := strings.TrimSpace(lock.Reason) reason := strings.TrimSpace(lock.Reason)
if reason == "" { if reason == "" {
@@ -1128,13 +1128,13 @@ func writeLocks(out io.Writer, cfg *config.Config, locks *effectiveLocks) {
} }
} }
func lockMapValues(in map[string]config.ArchiveLockRule) []config.ArchiveLockRule { func lockMapValues(in map[string]config.PublishLockRule) []config.PublishLockRule {
keys := make([]string, 0, len(in)) keys := make([]string, 0, len(in))
for key := range in { for key := range in {
keys = append(keys, key) keys = append(keys, key)
} }
sort.Strings(keys) sort.Strings(keys)
out := make([]config.ArchiveLockRule, 0, len(keys)) out := make([]config.PublishLockRule, 0, len(keys))
for _, key := range keys { for _, key := range keys {
item := in[key] item := in[key]
item.Source = key item.Source = key

View File

@@ -423,7 +423,7 @@ inputs:
} }
fake := &storage.FakeBackend{} fake := &storage.FakeBackend{}
audioKey := artifacts.S3PromotedArtifactKey(artifacts.S3AudioPrefix(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "audio/"), "alice.flac") audioKey := artifacts.S3PublishedOutputKey(artifacts.S3AudioPrefix(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "audio/"), "alice.flac")
fake.SeedObject(storage.FakeObject{Key: audioKey, Data: []byte("audio")}) fake.SeedObject(storage.FakeObject{Key: audioKey, Data: []byte("audio")})
origStoreFn := newObjectStoreFromConfigFn origStoreFn := newObjectStoreFromConfigFn
newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) { newObjectStoreFromConfigFn = func(context.Context, *config.Config) (storage.ObjectStore, error) {
@@ -501,9 +501,9 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) {
if code != 0 { if code != 0 {
t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String()) t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String())
} }
store, err := config.LoadArchiveLockStoreBytes("locks.yml", fake.Objects[key].Data, nil) store, err := config.LoadPublishLockStoreBytes("locks.yml", fake.Objects[key].Data, nil)
if err != nil { if err != nil {
t.Fatalf("LoadArchiveLockStoreBytes() error = %v", err) t.Fatalf("LoadPublishLockStoreBytes() error = %v", err)
} }
if len(store.Locks) != 0 { if len(store.Locks) != 0 {
t.Fatalf("locks after remove = %#v, want empty", store.Locks) t.Fatalf("locks after remove = %#v, want empty", store.Locks)
@@ -693,7 +693,7 @@ func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
required: true required: true
`) `)
fake := &storage.FakeBackend{} fake := &storage.FakeBackend{}
trimmedKey := artifacts.S3PromotedArtifactKey( trimmedKey := artifacts.S3PublishedOutputKey(
artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"),
"transcripts/final.trimmed.json", "transcripts/final.trimmed.json",
) )
@@ -713,7 +713,7 @@ func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
if code != 0 { if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
} }
if !strings.Contains(stdout.String(), "narratio.transcript.final_trimmed remote=promoted") { if !strings.Contains(stdout.String(), "narratio.transcript.final_trimmed remote=published") {
t.Fatalf("stdout = %q, want promoted remote availability", stdout.String()) t.Fatalf("stdout = %q, want promoted remote availability", stdout.String())
} }
} }
@@ -732,8 +732,8 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) {
`) `)
fake := &storage.FakeBackend{} fake := &storage.FakeBackend{}
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03") sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
fake.SeedObject(storage.FakeObject{Key: artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/full.json"), Data: []byte(`{"segments":[]}`)}) fake.SeedObject(storage.FakeObject{Key: artifacts.S3PublishedOutputKey(sessionPrefix, "transcripts/full.json"), Data: []byte(`{"segments":[]}`)})
fake.SeedObject(storage.FakeObject{Key: artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/bounds.json"), Data: []byte(`{}`)}) fake.SeedObject(storage.FakeObject{Key: artifacts.S3PublishedOutputKey(sessionPrefix, "transcripts/bounds.json"), Data: []byte(`{}`)})
var storeInitCalls int var storeInitCalls int
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
@@ -759,8 +759,8 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) {
} }
} }
for _, want := range []string{ for _, want := range []string{
"narratio.transcript.final dest=transcripts/full.json remote=promoted", "narratio.transcript.final dest=transcripts/full.json remote=published",
"narratio.bounds.session dest=transcripts/bounds.json remote=promoted", "narratio.bounds.session dest=transcripts/bounds.json remote=published",
} { } {
if !strings.Contains(out, want) { if !strings.Contains(out, want) {
t.Fatalf("stdout = %q, want %q", out, want) t.Fatalf("stdout = %q, want %q", out, want)
@@ -783,8 +783,8 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
fake := &storage.FakeBackend{} fake := &storage.FakeBackend{}
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03") sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix)
trimmedKey := artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/final.trimmed.json") trimmedKey := artifacts.S3PublishedOutputKey(sessionPrefix, "transcripts/final.trimmed.json")
fullKey := artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/full.json") fullKey := artifacts.S3PublishedOutputKey(sessionPrefix, "transcripts/full.json")
lockKey := artifacts.S3SessionLocksKey(sessionPrefix) lockKey := artifacts.S3SessionLocksKey(sessionPrefix)
fake.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")}) fake.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
fake.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "2026-05-03", "sample-campaign")}) fake.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "2026-05-03", "sample-campaign")})
@@ -811,10 +811,10 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
"Built-in:", "Built-in:",
"Configured:", "Configured:",
"Previous-session:", "Previous-session:",
"Promoted:", "Published:",
"narratio.transcript.final_trimmed locked", "narratio.transcript.final_trimmed locked",
"narratio.transcript.final_trimmed locked remote=promoted", "narratio.transcript.final_trimmed locked remote=published",
"narratio.transcript.final dest=transcripts/full.json remote=promoted", "narratio.transcript.final dest=transcripts/full.json remote=published",
} { } {
if !strings.Contains(out, want) { if !strings.Contains(out, want) {
t.Fatalf("stdout = %q, want %q", out, want) t.Fatalf("stdout = %q, want %q", out, want)
@@ -850,13 +850,13 @@ func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testin
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
} }
out := stdout.String() out := stdout.String()
if !strings.Contains(out, "Remote archive: missing or unavailable:") { if !strings.Contains(out, "Remote publish: missing or unavailable:") {
t.Fatalf("stdout = %q, want remote archive unavailable state", out) t.Fatalf("stdout = %q, want remote archive unavailable state", out)
} }
if !strings.Contains(out, "Remote outputs:") || !strings.Contains(out, "narratio.transcript.final_trimmed remote=error") { if !strings.Contains(out, "Remote outputs:") || !strings.Contains(out, "narratio.transcript.final_trimmed remote=error") {
t.Fatalf("stdout = %q, want remote output error state", out) t.Fatalf("stdout = %q, want remote output error state", out)
} }
if !strings.Contains(out, "Archive locks: error:") { if !strings.Contains(out, "Publish locks: error:") {
t.Fatalf("stdout = %q, want archive locks error", out) t.Fatalf("stdout = %q, want archive locks error", out)
} }
} }
@@ -883,7 +883,7 @@ func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) {
if code != 0 { if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
} }
promotedKey := artifacts.S3PromotedArtifactKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "transcripts/final.trimmed.json") promotedKey := artifacts.S3PublishedOutputKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "transcripts/final.trimmed.json")
if _, ok := fake.Objects[promotedKey]; ok { if _, ok := fake.Objects[promotedKey]; ok {
t.Fatalf("locked promoted key %q was uploaded", promotedKey) t.Fatalf("locked promoted key %q was uploaded", promotedKey)
} }

View File

@@ -196,7 +196,7 @@ func TestPostArchiveCleanupNotRunWhenPromotionIsMissing(t *testing.T) {
cfg, seed, runID := archiveStageCleanupFixture(t) cfg, seed, runID := archiveStageCleanupFixture(t)
cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Spool.DeleteAudioAfterPublish = true
cfg.Pipeline.Workspace.CleanupAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true
cfg.Pipeline.Publish.PromoteArtifacts = []config.ArchivePromotionRule{ cfg.Pipeline.Publish.Outputs = []config.PublishOutputRule{
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)}, {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)},
} }
@@ -205,8 +205,8 @@ func TestPostArchiveCleanupNotRunWhenPromotionIsMissing(t *testing.T) {
t.Fatalf("Select(publish) error = %v", err) t.Fatalf("Select(publish) error = %v", err)
} }
_, err = executeStages(context.Background(), cfg, []stage.Stage{archiveStageImpl}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) _, err = executeStages(context.Background(), cfg, []stage.Stage{archiveStageImpl}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}})
if err == nil || !strings.Contains(err.Error(), "required promotion source unavailable") { if err == nil || !strings.Contains(err.Error(), "required output source unavailable") {
t.Fatalf("executeStages() error = %v, want promotion-missing failure", err) t.Fatalf("executeStages() error = %v, want required output source unavailable failure", err)
} }
assertExists(t, seed.spoolAudioDir) assertExists(t, seed.spoolAudioDir)
@@ -270,7 +270,7 @@ func cleanupFixtureConfig(t *testing.T) (*config.Config, cleanupSeed) {
t.Helper() t.Helper()
cfg := testConfig(t) cfg := testConfig(t)
cfg.Pipeline.Publish = &config.ArchiveConfig{Enabled: boolPtr(true), UploadRun: boolPtr(true)} cfg.Pipeline.Publish = &config.PublishConfig{Enabled: boolPtr(true), UploadRun: boolPtr(true)}
cfg.Pipeline.Spool.Root = filepath.Join(t.TempDir(), "spool") cfg.Pipeline.Spool.Root = filepath.Join(t.TempDir(), "spool")
runID := "20260516T010203Z-1a2b3c4d" runID := "20260516T010203Z-1a2b3c4d"
@@ -329,10 +329,10 @@ func archiveStageCleanupFixture(t *testing.T) (*config.Config, cleanupSeed, stri
Bucket: "my-dnd-archive", Bucket: "my-dnd-archive",
RootPrefix: "dnd", RootPrefix: "dnd",
} }
cfg.Pipeline.Publish = &config.ArchiveConfig{ cfg.Pipeline.Publish = &config.PublishConfig{
Enabled: boolPtr(true), Enabled: boolPtr(true),
UploadRun: boolPtr(true), UploadRun: boolPtr(true),
PromoteArtifacts: []config.ArchivePromotionRule{ Outputs: []config.PublishOutputRule{
{Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)}, {Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)},
{Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)}, {Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)},
}, },

View File

@@ -13,9 +13,9 @@ import (
) )
type effectiveLocks struct { type effectiveLocks struct {
Static []config.ArchiveLockRule Static []config.PublishLockRule
Remote []config.ArchiveLockRule Remote []config.PublishLockRule
All []config.ArchiveLockRule All []config.PublishLockRule
Key string Key string
} }
@@ -34,7 +34,7 @@ func remoteLocksKey(cfg *config.Config) (string, error) {
return artifacts.S3SessionLocksKey(sessionPrefix), nil return artifacts.S3SessionLocksKey(sessionPrefix), nil
} }
func loadRemoteLockStore(ctx context.Context, cfg *config.Config, store storage.ObjectStore) (*config.ArchiveLockStore, string, error) { func loadRemoteLockStore(ctx context.Context, cfg *config.Config, store storage.ObjectStore) (*config.PublishLockStore, string, error) {
key, err := remoteLocksKey(cfg) key, err := remoteLocksKey(cfg)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@@ -44,7 +44,7 @@ func loadRemoteLockStore(ctx context.Context, cfg *config.Config, store storage.
return nil, key, fmt.Errorf("check remote locks %q: %w", key, err) return nil, key, fmt.Errorf("check remote locks %q: %w", key, err)
} }
if !exists { if !exists {
return &config.ArchiveLockStore{}, key, nil return &config.PublishLockStore{}, key, nil
} }
tmp, err := downloadObjectToTemp(ctx, store, key, "narratio-locks-*.yml") tmp, err := downloadObjectToTemp(ctx, store, key, "narratio-locks-*.yml")
if err != nil { if err != nil {
@@ -55,7 +55,7 @@ func loadRemoteLockStore(ctx context.Context, cfg *config.Config, store storage.
if err != nil { if err != nil {
return nil, key, fmt.Errorf("read remote locks %q: %w", key, err) return nil, key, fmt.Errorf("read remote locks %q: %w", key, err)
} }
lockStore, err := config.LoadArchiveLockStoreBytes("s3://"+s3BucketName(cfg.Pipeline)+"/"+key, data, cfg.Pipeline.Scriptorium) lockStore, err := config.LoadPublishLockStoreBytes("s3://"+s3BucketName(cfg.Pipeline)+"/"+key, data, cfg.Pipeline.Scriptorium)
if err != nil { if err != nil {
return nil, key, err return nil, key, err
} }
@@ -67,41 +67,41 @@ func loadEffectiveLocks(ctx context.Context, cfg *config.Config, store storage.O
if store == nil { if store == nil {
return &effectiveLocks{ return &effectiveLocks{
Static: staticLocks, Static: staticLocks,
All: append([]config.ArchiveLockRule(nil), staticLocks...), All: append([]config.PublishLockRule(nil), staticLocks...),
}, nil }, nil
} }
lockStore, key, err := loadRemoteLockStore(ctx, cfg, store) lockStore, key, err := loadRemoteLockStore(ctx, cfg, store)
if err != nil { if err != nil {
return nil, err return nil, err
} }
remoteLocks := append([]config.ArchiveLockRule(nil), lockStore.Locks...) remoteLocks := append([]config.PublishLockRule(nil), lockStore.Locks...)
return &effectiveLocks{ return &effectiveLocks{
Static: staticLocks, Static: staticLocks,
Remote: remoteLocks, Remote: remoteLocks,
All: config.MergeArchiveLockRules(staticLocks, remoteLocks), All: config.MergePublishLockRules(staticLocks, remoteLocks),
Key: key, Key: key,
}, nil }, nil
} }
func staticArchiveLocks(cfg *config.Config) []config.ArchiveLockRule { func staticArchiveLocks(cfg *config.Config) []config.PublishLockRule {
if cfg == nil || cfg.Pipeline == nil || cfg.Pipeline.Publish == nil { if cfg == nil || cfg.Pipeline == nil || cfg.Pipeline.Publish == nil {
return nil return nil
} }
return append([]config.ArchiveLockRule(nil), cfg.Pipeline.Publish.Locks...) return append([]config.PublishLockRule(nil), cfg.Pipeline.Publish.Locks...)
} }
func applyEffectiveLocks(cfg *config.Config, locks []config.ArchiveLockRule) { func applyEffectiveLocks(cfg *config.Config, locks []config.PublishLockRule) {
if cfg == nil || cfg.Pipeline == nil { if cfg == nil || cfg.Pipeline == nil {
return return
} }
if cfg.Pipeline.Publish == nil { if cfg.Pipeline.Publish == nil {
cfg.Pipeline.Publish = &config.ArchiveConfig{} cfg.Pipeline.Publish = &config.PublishConfig{}
} }
cfg.Pipeline.Publish.Locks = append([]config.ArchiveLockRule(nil), locks...) cfg.Pipeline.Publish.Locks = append([]config.PublishLockRule(nil), locks...)
} }
func uploadRemoteLockStore(ctx context.Context, store storage.ObjectStore, key string, lockStore *config.ArchiveLockStore) error { func uploadRemoteLockStore(ctx context.Context, store storage.ObjectStore, key string, lockStore *config.PublishLockStore) error {
data, err := config.MarshalArchiveLockStore(lockStore) data, err := config.MarshalPublishLockStore(lockStore)
if err != nil { if err != nil {
return err return err
} }
@@ -124,8 +124,8 @@ func uploadRemoteLockStore(ctx context.Context, store storage.ObjectStore, key s
return nil return nil
} }
func lockSourceSet(locks []config.ArchiveLockRule) map[string]config.ArchiveLockRule { func lockSourceSet(locks []config.PublishLockRule) map[string]config.PublishLockRule {
out := make(map[string]config.ArchiveLockRule, len(locks)) out := make(map[string]config.PublishLockRule, len(locks))
for _, lock := range locks { for _, lock := range locks {
source := strings.TrimSpace(lock.Source) source := strings.TrimSpace(lock.Source)
if source == "" { if source == "" {

View File

@@ -254,10 +254,10 @@ func TestExecuteStagesArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testi
Bucket: "my-dnd-archive", Bucket: "my-dnd-archive",
RootPrefix: "dnd", RootPrefix: "dnd",
} }
cfg.Pipeline.Publish = &config.ArchiveConfig{ cfg.Pipeline.Publish = &config.PublishConfig{
Enabled: boolPtr(true), Enabled: boolPtr(true),
UploadRun: boolPtr(true), UploadRun: boolPtr(true),
PromoteArtifacts: []config.ArchivePromotionRule{ Outputs: []config.PublishOutputRule{
{Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)}, {Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)},
}, },
} }
@@ -312,9 +312,9 @@ func TestExecuteStagesArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testi
t.Fatalf("Load manifest error = %v", err) t.Fatalf("Load manifest error = %v", err)
} }
meta := loadedManifest.Stages["publish"].Metadata meta := loadedManifest.Stages["publish"].Metadata
skipped, ok := meta["skipped_unselected_promotions"].([]any) skipped, ok := meta["skipped_unselected_outputs"].([]any)
if !ok || len(skipped) != 1 { if !ok || len(skipped) != 1 {
t.Fatalf("skipped_unselected_promotions = %#v, want one item", meta["skipped_unselected_promotions"]) t.Fatalf("skipped_unselected_outputs = %#v, want one item", meta["skipped_unselected_outputs"])
} }
item, ok := skipped[0].(map[string]any) item, ok := skipped[0].(map[string]any)
if !ok { if !ok {
@@ -950,7 +950,7 @@ func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) {
} }
} }
if tc.name == "publish" { if tc.name == "publish" {
cfg.Pipeline.Publish = &config.ArchiveConfig{ cfg.Pipeline.Publish = &config.PublishConfig{
Enabled: boolPtr(true), Enabled: boolPtr(true),
UploadRun: boolPtr(true), UploadRun: boolPtr(true),
} }

View File

@@ -246,7 +246,7 @@ func TestExecuteSessionSubcommandsAcceptPositionalSessionID(t *testing.T) {
{ {
name: "locks", name: "locks",
args: []string{"session", "locks", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, args: []string{"session", "locks", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath},
want: "Archive locks:", want: "Publish locks:",
}, },
} }
@@ -325,9 +325,9 @@ func TestExecuteSessionLocksMutationAcceptsPositionalSessionID(t *testing.T) {
if code != 0 { if code != 0 {
t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String()) t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String())
} }
store, err := config.LoadArchiveLockStoreBytes("locks.yml", fake.Objects[key].Data, nil) store, err := config.LoadPublishLockStoreBytes("locks.yml", fake.Objects[key].Data, nil)
if err != nil { if err != nil {
t.Fatalf("LoadArchiveLockStoreBytes() error = %v", err) t.Fatalf("LoadPublishLockStoreBytes() error = %v", err)
} }
if len(store.Locks) != 0 { if len(store.Locks) != 0 {
t.Fatalf("locks after remove = %#v, want empty", store.Locks) t.Fatalf("locks after remove = %#v, want empty", store.Locks)

View File

@@ -58,9 +58,9 @@ func S3CurrentRunPointerKey(sessionPrefix string) string {
return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), config.S3CurrentSegment, config.S3RunIDFile) return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), config.S3CurrentSegment, config.S3RunIDFile)
} }
// S3PromotedArtifactKey returns the destination key for one promoted artifact. // S3PublishedOutputKey returns the destination key for one published output.
// Format: {session_prefix}/{promotion.to} // Format: {session_prefix}/{output.dest}
func S3PromotedArtifactKey(sessionPrefix, to string) string { func S3PublishedOutputKey(sessionPrefix, to string) string {
return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), cleanS3Key(to)) return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), cleanS3Key(to))
} }

View File

@@ -43,7 +43,7 @@ func TestS3KeyConstruction(t *testing.T) {
t.Fatalf("manifest key = %q", manifestKey) t.Fatalf("manifest key = %q", manifestKey)
} }
promoted := S3PromotedArtifactKey(sessionPrefix, "transcripts/final.trimmed.json") promoted := S3PublishedOutputKey(sessionPrefix, "transcripts/final.trimmed.json")
if promoted != "dnd/campaigns/forsaken/sessions/2026-04-19/transcripts/final.trimmed.json" { if promoted != "dnd/campaigns/forsaken/sessions/2026-04-19/transcripts/final.trimmed.json" {
t.Fatalf("promoted key = %q", promoted) t.Fatalf("promoted key = %q", promoted)
} }

View File

@@ -21,7 +21,7 @@ type PipelineConfig struct {
Storage StorageConfig `yaml:"storage"` Storage StorageConfig `yaml:"storage"`
Spool SpoolConfig `yaml:"spool"` Spool SpoolConfig `yaml:"spool"`
Cache CacheConfig `yaml:"cache"` Cache CacheConfig `yaml:"cache"`
Publish *ArchiveConfig `yaml:"publish"` Publish *PublishConfig `yaml:"publish"`
Secrets *SecretsConfig `yaml:"secrets"` Secrets *SecretsConfig `yaml:"secrets"`
WhisperX WhisperXConfig `yaml:"whisperx"` WhisperX WhisperXConfig `yaml:"whisperx"`
Seriatim SeriatimConfig `yaml:"seriatim"` Seriatim SeriatimConfig `yaml:"seriatim"`
@@ -102,31 +102,31 @@ type CacheConfig struct {
S3Audio *bool `yaml:"s3_audio"` S3Audio *bool `yaml:"s3_audio"`
} }
// ArchiveConfig configures archive behavior and artifact promotions. // PublishConfig configures publish behavior and source-based output uploads.
type ArchiveConfig struct { type PublishConfig struct {
Enabled *bool `yaml:"enabled"` Enabled *bool `yaml:"enabled"`
UploadRun *bool `yaml:"upload_run"` UploadRun *bool `yaml:"upload_run"`
PromoteArtifacts []ArchivePromotionRule `yaml:"outputs"` Outputs []PublishOutputRule `yaml:"outputs"`
Locks []ArchiveLockRule `yaml:"locks"` Locks []PublishLockRule `yaml:"locks"`
} }
// ArchivePromotionRule configures one artifact promotion mapping. // PublishOutputRule configures one source-to-destination publish mapping.
type ArchivePromotionRule struct { type PublishOutputRule struct {
Source string `yaml:"source"` Source string `yaml:"source"`
Dest string `yaml:"dest"` Dest string `yaml:"dest"`
Required *bool `yaml:"required"` Required *bool `yaml:"required"`
} }
// ArchiveLockRule prevents one source-based promotion from overwriting its // PublishLockRule prevents one source from overwriting its top-level published
// top-level archive destination. // destination.
type ArchiveLockRule struct { type PublishLockRule struct {
Source string `yaml:"source"` Source string `yaml:"source"`
Reason string `yaml:"reason"` Reason string `yaml:"reason"`
} }
// ArchiveLockStore is the mutable per-session remote lock store. // PublishLockStore is the mutable per-session remote lock store.
type ArchiveLockStore struct { type PublishLockStore struct {
Locks []ArchiveLockRule `yaml:"locks"` Locks []PublishLockRule `yaml:"locks"`
} }
// WhisperXConfig configures WhisperX adapter settings. // WhisperXConfig configures WhisperX adapter settings.

View File

@@ -76,9 +76,9 @@ const (
S3RunIDFile = "run_id.txt" S3RunIDFile = "run_id.txt"
) )
// DefaultArchivePromoteArtifacts defines the default archive promotion rules. // DefaultPublishOutputs defines the default publish output rules.
// Callers should copy this slice before mutating. // Callers should copy this slice before mutating.
var DefaultArchivePromoteArtifacts = []ArchivePromotionRule{ var DefaultPublishOutputs = []PublishOutputRule{
{Source: artifactmodel.SourceTranscriptFinalTrimmed, Dest: PathTranscriptFinalTrimmed}, {Source: artifactmodel.SourceTranscriptFinalTrimmed, Dest: PathTranscriptFinalTrimmed},
} }

View File

@@ -84,29 +84,29 @@ func LoadSessionBytesWithOptions(label string, data []byte, opts SessionLoadOpti
return &cfg, nil return &cfg, nil
} }
// LoadArchiveLockStoreBytes loads a mutable session lock store with strict // LoadPublishLockStoreBytes loads a mutable session lock store with strict
// field checking and source validation. // field checking and source validation.
func LoadArchiveLockStoreBytes(label string, data []byte, scriptorium *ScriptoriumConfig) (*ArchiveLockStore, error) { func LoadPublishLockStoreBytes(label string, data []byte, scriptorium *ScriptoriumConfig) (*PublishLockStore, error) {
var store ArchiveLockStore var store PublishLockStore
if err := decodeStrictYAMLFromReader("archive lock store", label, strings.NewReader(string(data)), &store); err != nil { if err := decodeStrictYAMLFromReader("publish lock store", label, strings.NewReader(string(data)), &store); err != nil {
return nil, fmt.Errorf("load archive lock store: %w", err) return nil, fmt.Errorf("load publish lock store: %w", err)
} }
locks, err := ValidateArchiveLockRules(store.Locks, scriptorium, "locks") locks, err := ValidatePublishLockRules(store.Locks, scriptorium, "locks")
if err != nil { if err != nil {
return nil, fmt.Errorf("load archive lock store: %w", err) return nil, fmt.Errorf("load publish lock store: %w", err)
} }
store.Locks = locks store.Locks = locks
return &store, nil return &store, nil
} }
// MarshalArchiveLockStore serializes a mutable lock store as strict-compatible YAML. // MarshalPublishLockStore serializes a mutable lock store as strict-compatible YAML.
func MarshalArchiveLockStore(store *ArchiveLockStore) ([]byte, error) { func MarshalPublishLockStore(store *PublishLockStore) ([]byte, error) {
if store == nil { if store == nil {
store = &ArchiveLockStore{} store = &PublishLockStore{}
} }
data, err := yaml.Marshal(store) data, err := yaml.Marshal(store)
if err != nil { if err != nil {
return nil, fmt.Errorf("marshal archive lock store: %w", err) return nil, fmt.Errorf("marshal publish lock store: %w", err)
} }
return data, nil return data, nil
} }
@@ -328,7 +328,7 @@ func applyPipelineDefaults(cfg *PipelineConfig) {
applyStorageDefaults(&cfg.Storage) applyStorageDefaults(&cfg.Storage)
applySpoolDefaults(&cfg.Spool) applySpoolDefaults(&cfg.Spool)
applyCacheDefaults(&cfg.Cache) applyCacheDefaults(&cfg.Cache)
applyArchiveDefaults(&cfg.Publish) applyPublishDefaults(&cfg.Publish)
applyWhisperXDefaults(&cfg.WhisperX) applyWhisperXDefaults(&cfg.WhisperX)
applySeriatimDefaults(&cfg.Seriatim) applySeriatimDefaults(&cfg.Seriatim)
applyAuditaDefaults(&cfg.Audita) applyAuditaDefaults(&cfg.Audita)
@@ -397,12 +397,12 @@ func applyCacheDefaults(cfg *CacheConfig) {
} }
} }
func applyArchiveDefaults(cfg **ArchiveConfig) { func applyPublishDefaults(cfg **PublishConfig) {
if cfg == nil { if cfg == nil {
return return
} }
if *cfg == nil { if *cfg == nil {
*cfg = &ArchiveConfig{} *cfg = &PublishConfig{}
} }
if (*cfg).Enabled == nil { if (*cfg).Enabled == nil {
@@ -411,12 +411,12 @@ func applyArchiveDefaults(cfg **ArchiveConfig) {
if (*cfg).UploadRun == nil { if (*cfg).UploadRun == nil {
(*cfg).UploadRun = boolPtr(DefaultArchiveUploadRun) (*cfg).UploadRun = boolPtr(DefaultArchiveUploadRun)
} }
if len((*cfg).PromoteArtifacts) == 0 { if len((*cfg).Outputs) == 0 {
(*cfg).PromoteArtifacts = append([]ArchivePromotionRule(nil), DefaultArchivePromoteArtifacts...) (*cfg).Outputs = append([]PublishOutputRule(nil), DefaultPublishOutputs...)
} }
for i := range (*cfg).PromoteArtifacts { for i := range (*cfg).Outputs {
if (*cfg).PromoteArtifacts[i].Required == nil { if (*cfg).Outputs[i].Required == nil {
(*cfg).PromoteArtifacts[i].Required = boolPtr(true) (*cfg).Outputs[i].Required = boolPtr(true)
} }
} }
} }

View File

@@ -179,10 +179,10 @@ func TestSpoolAndArchiveDefaults(t *testing.T) {
if cfg.Pipeline.Publish.UploadRun == nil || !*cfg.Pipeline.Publish.UploadRun { if cfg.Pipeline.Publish.UploadRun == nil || !*cfg.Pipeline.Publish.UploadRun {
t.Fatalf("archive.upload_run = %#v, want true", cfg.Pipeline.Publish.UploadRun) t.Fatalf("archive.upload_run = %#v, want true", cfg.Pipeline.Publish.UploadRun)
} }
if len(cfg.Pipeline.Publish.PromoteArtifacts) != 1 { if len(cfg.Pipeline.Publish.Outputs) != 1 {
t.Fatalf("publish.outputs len = %d, want 1 default", len(cfg.Pipeline.Publish.PromoteArtifacts)) t.Fatalf("publish.outputs len = %d, want 1 default", len(cfg.Pipeline.Publish.Outputs))
} }
item := cfg.Pipeline.Publish.PromoteArtifacts[0] item := cfg.Pipeline.Publish.Outputs[0]
if item.Required == nil || !*item.Required { if item.Required == nil || !*item.Required {
t.Fatalf("publish.outputs[0].required = %#v, want true", item.Required) t.Fatalf("publish.outputs[0].required = %#v, want true", item.Required)
} }
@@ -236,7 +236,7 @@ func TestArchivePromotionValidation(t *testing.T) {
- source: "narratio.transcript.final" - source: "narratio.transcript.final"
dest: "artifacts/shared.md" dest: "artifacts/shared.md"
`, `,
wantErr: "duplicates another archive promotion destination", wantErr: "duplicates another publish output destination",
}, },
{ {
name: "configured source requires configured artifact key", name: "configured source requires configured artifact key",
@@ -349,11 +349,11 @@ publish:
if err := Validate(cfg); err != nil { if err := Validate(cfg); err != nil {
t.Fatalf("Validate() error = %v", err) t.Fatalf("Validate() error = %v", err)
} }
if len(cfg.Pipeline.Publish.PromoteArtifacts) != 1 { if len(cfg.Pipeline.Publish.Outputs) != 1 {
t.Fatalf("publish.outputs len = %d, want 1", len(cfg.Pipeline.Publish.PromoteArtifacts)) t.Fatalf("publish.outputs len = %d, want 1", len(cfg.Pipeline.Publish.Outputs))
} }
if cfg.Pipeline.Publish.PromoteArtifacts[0].Dest != tt.wantDest { if cfg.Pipeline.Publish.Outputs[0].Dest != tt.wantDest {
t.Fatalf("publish.outputs[0].dest = %q, want %q", cfg.Pipeline.Publish.PromoteArtifacts[0].Dest, tt.wantDest) t.Fatalf("publish.outputs[0].dest = %q, want %q", cfg.Pipeline.Publish.Outputs[0].Dest, tt.wantDest)
} }
}) })
} }
@@ -414,7 +414,7 @@ publish:
- source: narratio.transcript.final_trimmed - source: narratio.transcript.final_trimmed
- source: " narratio.transcript.final_trimmed " - source: " narratio.transcript.final_trimmed "
`, `,
wantErr: "duplicates another archive lock source", wantErr: "duplicates another publish lock source",
}, },
} }
@@ -496,18 +496,18 @@ publish:
} }
func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) { func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) {
store, err := LoadArchiveLockStoreBytes("locks.yml", []byte(`locks: store, err := LoadPublishLockStoreBytes("locks.yml", []byte(`locks:
- source: narratio.transcript.final_trimmed - source: narratio.transcript.final_trimmed
reason: reviewed reason: reviewed
`), nil) `), nil)
if err != nil { if err != nil {
t.Fatalf("LoadArchiveLockStoreBytes() error = %v", err) t.Fatalf("LoadPublishLockStoreBytes() error = %v", err)
} }
if len(store.Locks) != 1 || store.Locks[0].Source != "narratio.transcript.final_trimmed" || store.Locks[0].Reason != "reviewed" { if len(store.Locks) != 1 || store.Locks[0].Source != "narratio.transcript.final_trimmed" || store.Locks[0].Reason != "reviewed" {
t.Fatalf("locks = %#v", store.Locks) t.Fatalf("locks = %#v", store.Locks)
} }
_, err = LoadArchiveLockStoreBytes("locks.yml", []byte(`locks: _, err = LoadPublishLockStoreBytes("locks.yml", []byte(`locks:
- source: narratio.transcript.final_trimmed - source: narratio.transcript.final_trimmed
dest: transcripts/final.trimmed.json dest: transcripts/final.trimmed.json
`), nil) `), nil)
@@ -515,19 +515,19 @@ func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) {
t.Fatalf("unknown field error = %v, want strict decode failed", err) t.Fatalf("unknown field error = %v, want strict decode failed", err)
} }
_, err = LoadArchiveLockStoreBytes("locks.yml", []byte(`locks: _, err = LoadPublishLockStoreBytes("locks.yml", []byte(`locks:
- source: narratio.transcript.final_trimmed - source: narratio.transcript.final_trimmed
- source: narratio.transcript.final_trimmed - source: narratio.transcript.final_trimmed
`), nil) `), nil)
if err == nil || !strings.Contains(err.Error(), "duplicates another archive lock source") { if err == nil || !strings.Contains(err.Error(), "duplicates another publish lock source") {
t.Fatalf("duplicate error = %v", err) t.Fatalf("duplicate error = %v", err)
} }
} }
func TestMergeArchiveLockRulesStaticWins(t *testing.T) { func TestMergeArchiveLockRulesStaticWins(t *testing.T) {
merged := MergeArchiveLockRules( merged := MergePublishLockRules(
[]ArchiveLockRule{{Source: "narratio.transcript.final_trimmed", Reason: "static"}}, []PublishLockRule{{Source: "narratio.transcript.final_trimmed", Reason: "static"}},
[]ArchiveLockRule{ []PublishLockRule{
{Source: "narratio.transcript.final_trimmed", Reason: "remote"}, {Source: "narratio.transcript.final_trimmed", Reason: "remote"},
{Source: "narratio.transcript.final", Reason: "remote full"}, {Source: "narratio.transcript.final", Reason: "remote full"},
}, },

View File

@@ -68,7 +68,7 @@ func validatePipeline(cfg *PipelineConfig) error {
if err := validateCache(cfg.Cache); err != nil { if err := validateCache(cfg.Cache); err != nil {
return err return err
} }
if err := validateArchive(cfg.Publish, cfg.Scriptorium); err != nil { if err := validatePublish(cfg.Publish, cfg.Scriptorium); err != nil {
return err return err
} }
if err := validateWhisperX(cfg.WhisperX); err != nil { if err := validateWhisperX(cfg.WhisperX); err != nil {
@@ -129,39 +129,39 @@ func validateCache(cfg CacheConfig) error {
return nil return nil
} }
func validateArchive(cfg *ArchiveConfig, scriptorium *ScriptoriumConfig) error { func validatePublish(cfg *PublishConfig, scriptorium *ScriptoriumConfig) error {
if cfg == nil { if cfg == nil {
return nil return nil
} }
seenDest := map[string]struct{}{} seenDest := map[string]struct{}{}
for i, item := range cfg.PromoteArtifacts { for i, item := range cfg.Outputs {
prefix := fmt.Sprintf("pipeline.publish.outputs[%d]", i) prefix := fmt.Sprintf("pipeline.publish.outputs[%d]", i)
source := strings.TrimSpace(item.Source) source := strings.TrimSpace(item.Source)
if source == "" { if source == "" {
return fmt.Errorf("%s.source is required", prefix) return fmt.Errorf("%s.source is required", prefix)
} }
if _, err := archiveSourceKnown(source, scriptorium); err != nil { if _, err := publishSourceKnown(source, scriptorium); err != nil {
return fmt.Errorf("%s.source %q is unsupported: %w", prefix, item.Source, err) return fmt.Errorf("%s.source %q is unsupported: %w", prefix, item.Source, err)
} }
dest := strings.TrimSpace(item.Dest) dest := strings.TrimSpace(item.Dest)
if dest == "" { if dest == "" {
derivedDest, err := deriveArchivePromotionDest(source, scriptorium) derivedDest, err := derivePublishOutputDest(source, scriptorium)
if err != nil { if err != nil {
return fmt.Errorf("%s.dest is required when destination cannot be derived from %q: %w", prefix, source, err) return fmt.Errorf("%s.dest is required when destination cannot be derived from %q: %w", prefix, source, err)
} }
dest = derivedDest dest = derivedDest
cfg.PromoteArtifacts[i].Dest = derivedDest cfg.Outputs[i].Dest = derivedDest
} }
if err := validateRelativeSafePath(prefix+".dest", dest); err != nil { if err := validateRelativeSafePath(prefix+".dest", dest); err != nil {
return err return err
} }
normalizedDest := filepath.ToSlash(filepath.Clean(dest)) normalizedDest := filepath.ToSlash(filepath.Clean(dest))
if _, ok := seenDest[normalizedDest]; ok { if _, ok := seenDest[normalizedDest]; ok {
return fmt.Errorf("%s.dest %q duplicates another archive promotion destination", prefix, dest) return fmt.Errorf("%s.dest %q duplicates another publish output destination", prefix, dest)
} }
seenDest[normalizedDest] = struct{}{} seenDest[normalizedDest] = struct{}{}
} }
locks, err := ValidateArchiveLockRules(cfg.Locks, scriptorium, "pipeline.publish.locks") locks, err := ValidatePublishLockRules(cfg.Locks, scriptorium, "pipeline.publish.locks")
if err != nil { if err != nil {
return err return err
} }
@@ -169,10 +169,10 @@ func validateArchive(cfg *ArchiveConfig, scriptorium *ScriptoriumConfig) error {
return nil return nil
} }
// ValidateArchiveLockRules validates and normalizes source-based archive locks. // ValidatePublishLockRules validates and normalizes source-based publish locks.
func ValidateArchiveLockRules(locks []ArchiveLockRule, scriptorium *ScriptoriumConfig, label string) ([]ArchiveLockRule, error) { func ValidatePublishLockRules(locks []PublishLockRule, scriptorium *ScriptoriumConfig, label string) ([]PublishLockRule, error) {
seenLocks := map[string]struct{}{} seenLocks := map[string]struct{}{}
out := make([]ArchiveLockRule, 0, len(locks)) out := make([]PublishLockRule, 0, len(locks))
if strings.TrimSpace(label) == "" { if strings.TrimSpace(label) == "" {
label = "publish.locks" label = "publish.locks"
} }
@@ -182,14 +182,14 @@ func ValidateArchiveLockRules(locks []ArchiveLockRule, scriptorium *ScriptoriumC
if source == "" { if source == "" {
return nil, fmt.Errorf("%s.source is required", prefix) return nil, fmt.Errorf("%s.source is required", prefix)
} }
if _, err := archiveSourceKnown(source, scriptorium); err != nil { if _, err := publishSourceKnown(source, scriptorium); err != nil {
return nil, fmt.Errorf("%s.source %q is unsupported: %w", prefix, item.Source, err) return nil, fmt.Errorf("%s.source %q is unsupported: %w", prefix, item.Source, err)
} }
if _, ok := seenLocks[source]; ok { if _, ok := seenLocks[source]; ok {
return nil, fmt.Errorf("%s.source %q duplicates another archive lock source", prefix, source) return nil, fmt.Errorf("%s.source %q duplicates another publish lock source", prefix, source)
} }
seenLocks[source] = struct{}{} seenLocks[source] = struct{}{}
out = append(out, ArchiveLockRule{ out = append(out, PublishLockRule{
Source: source, Source: source,
Reason: strings.TrimSpace(item.Reason), Reason: strings.TrimSpace(item.Reason),
}) })
@@ -197,17 +197,17 @@ func ValidateArchiveLockRules(locks []ArchiveLockRule, scriptorium *ScriptoriumC
return out, nil return out, nil
} }
// MergeArchiveLockRules returns the union of static and remote locks. Static // MergePublishLockRules returns the union of static and remote locks. Static
// locks win when both sources contain the same lock. // locks win when both sources contain the same lock.
func MergeArchiveLockRules(staticLocks, remoteLocks []ArchiveLockRule) []ArchiveLockRule { func MergePublishLockRules(staticLocks, remoteLocks []PublishLockRule) []PublishLockRule {
out := make([]ArchiveLockRule, 0, len(staticLocks)+len(remoteLocks)) out := make([]PublishLockRule, 0, len(staticLocks)+len(remoteLocks))
seen := map[string]struct{}{} seen := map[string]struct{}{}
for _, item := range staticLocks { for _, item := range staticLocks {
source := strings.TrimSpace(item.Source) source := strings.TrimSpace(item.Source)
if source == "" { if source == "" {
continue continue
} }
out = append(out, ArchiveLockRule{Source: source, Reason: strings.TrimSpace(item.Reason)}) out = append(out, PublishLockRule{Source: source, Reason: strings.TrimSpace(item.Reason)})
seen[source] = struct{}{} seen[source] = struct{}{}
} }
for _, item := range remoteLocks { for _, item := range remoteLocks {
@@ -218,13 +218,13 @@ func MergeArchiveLockRules(staticLocks, remoteLocks []ArchiveLockRule) []Archive
if _, ok := seen[source]; ok { if _, ok := seen[source]; ok {
continue continue
} }
out = append(out, ArchiveLockRule{Source: source, Reason: strings.TrimSpace(item.Reason)}) out = append(out, PublishLockRule{Source: source, Reason: strings.TrimSpace(item.Reason)})
seen[source] = struct{}{} seen[source] = struct{}{}
} }
return out return out
} }
func archiveSourceKnown(source string, scriptorium *ScriptoriumConfig) (string, error) { func publishSourceKnown(source string, scriptorium *ScriptoriumConfig) (string, error) {
trimmed := strings.TrimSpace(source) trimmed := strings.TrimSpace(source)
if _, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok { if _, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok {
return "", nil return "", nil
@@ -247,7 +247,7 @@ func archiveSourceKnown(source string, scriptorium *ScriptoriumConfig) (string,
return artifactKey, nil return artifactKey, nil
} }
func deriveArchivePromotionDest(source string, scriptorium *ScriptoriumConfig) (string, error) { func derivePublishOutputDest(source string, scriptorium *ScriptoriumConfig) (string, error) {
trimmed := strings.TrimSpace(source) trimmed := strings.TrimSpace(source)
if spec, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok { if spec, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok {
return spec.CanonicalRelPath, nil return spec.CanonicalRelPath, nil
@@ -256,7 +256,7 @@ func deriveArchivePromotionDest(source string, scriptorium *ScriptoriumConfig) (
case "narratio.bounds.session": case "narratio.bounds.session":
return filepath.ToSlash(filepath.Join(PathArtifactsDirSegment, "session_bounds.json")), nil return filepath.ToSlash(filepath.Join(PathArtifactsDirSegment, "session_bounds.json")), nil
} }
artifactKey, err := archiveSourceKnown(trimmed, scriptorium) artifactKey, err := publishSourceKnown(trimmed, scriptorium)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -629,14 +629,14 @@ func validateCrossConfig(pipeline *PipelineConfig, session *SessionConfig) error
} }
audioS3Enabled := session.Inputs.AudioS3 != nil audioS3Enabled := session.Inputs.AudioS3 != nil
archiveUploadEnabled := archiveUploadConfiguredForS3(pipeline) publishUploadEnabled := publishUploadConfiguredForS3(pipeline)
if (audioS3Enabled || archiveUploadEnabled) && strings.TrimSpace(pipeline.Storage.S3.Bucket) == "" { if (audioS3Enabled || publishUploadEnabled) && strings.TrimSpace(pipeline.Storage.S3.Bucket) == "" {
return fmt.Errorf("pipeline.storage.s3.bucket is required when S3 session audio or archive upload is enabled") return fmt.Errorf("pipeline.storage.s3.bucket is required when S3 session audio or publish upload is enabled")
} }
return nil return nil
} }
func archiveUploadConfiguredForS3(pipeline *PipelineConfig) bool { func publishUploadConfiguredForS3(pipeline *PipelineConfig) bool {
if pipeline == nil || pipeline.Publish == nil { if pipeline == nil || pipeline.Publish == nil {
return false return false
} }

View File

@@ -18,7 +18,7 @@ import (
const ( const (
InputKindManifest = "previous_manifest" InputKindManifest = "previous_manifest"
InputKindArtifact = "previous_artifact" InputKindArtifact = "previous_artifact"
InputSource = "previous_session_archive.current" InputSource = "previous_session_publish.current"
) )
type Plan struct { type Plan struct {
@@ -199,7 +199,7 @@ func BuildPlan(
selectedRel := "" selectedRel := ""
selectedKey := "" selectedKey := ""
for _, candidate := range candidates { for _, candidate := range candidates {
remoteKey := artifacts.S3PromotedArtifactKey(previousSessionPrefix, candidate) remoteKey := artifacts.S3PublishedOutputKey(previousSessionPrefix, candidate)
exists, err := store.Exists(ctx, remoteKey) exists, err := store.Exists(ctx, remoteKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("check previous-session artifact object %q: %w", remoteKey, err) return nil, fmt.Errorf("check previous-session artifact object %q: %w", remoteKey, err)
@@ -289,9 +289,9 @@ func artifactRelativePathCandidates(
if rel, ok := manifestArtifactRelativePathBySourceID(previousManifest, sourceID); ok { if rel, ok := manifestArtifactRelativePathBySourceID(previousManifest, sourceID); ok {
appendCandidate(rel) appendCandidate(rel)
base := path.Base(rel) base := path.Base(rel)
for _, promoted := range manifestPromotedPaths(previousManifest) { for _, published := range manifestPublishedPaths(previousManifest) {
if path.Base(promoted) == base { if path.Base(published) == base {
appendCandidate(promoted) appendCandidate(published)
} }
} }
} }
@@ -395,15 +395,15 @@ func manifestSessionRoot(previousManifest *manifest.Manifest) (string, bool) {
return filepath.Dir(runsDir), true return filepath.Dir(runsDir), true
} }
func manifestPromotedPaths(previousManifest *manifest.Manifest) []string { func manifestPublishedPaths(previousManifest *manifest.Manifest) []string {
if previousManifest == nil || len(previousManifest.Stages) == 0 { if previousManifest == nil || len(previousManifest.Stages) == 0 {
return nil return nil
} }
sr := previousManifest.Stages["archive"] sr := previousManifest.Stages["publish"]
if sr == nil || sr.Metadata == nil { if sr == nil || sr.Metadata == nil {
return nil return nil
} }
raw, ok := sr.Metadata["promoted_paths"] raw, ok := sr.Metadata["published_paths"]
if !ok { if !ok {
return nil return nil
} }

View File

@@ -155,10 +155,10 @@ func previousManifestWithOutput(t *testing.T, cfg *config.Config, rel string, pr
}) })
} }
if promoted != nil { if promoted != nil {
if m.Stages["archive"] == nil { if m.Stages["publish"] == nil {
m.MarkStageSucceeded("archive", time.Date(2026, 4, 26, 10, 2, 0, 0, time.UTC), nil) m.MarkStageSucceeded("publish", time.Date(2026, 4, 26, 10, 2, 0, 0, time.UTC), nil)
} }
m.Stages["archive"].Metadata = map[string]any{"promoted_paths": promoted} m.Stages["publish"].Metadata = map[string]any{"published_paths": promoted}
} }
return m return m
} }

View File

@@ -496,13 +496,13 @@ func executeAnalyzeArtifact(
if err := requireNonEmptyFile(finalOutputPath, artifactName+" output"); err != nil { if err := requireNonEmptyFile(finalOutputPath, artifactName+" output"); err != nil {
return nil, fmt.Errorf("analyze: %w", err) return nil, fmt.Errorf("analyze: %w", err)
} }
promotedArtifact, err := promoteRunLocalOutput(env.ArtifactStore, finalOutputPath, canonicalOutputPath, artifacts.Ref{ materializedArtifact, err := materializeRunLocalOutput(env.ArtifactStore, finalOutputPath, canonicalOutputPath, artifacts.Ref{
Kind: artifactName, Kind: artifactName,
Category: "artifacts", Category: "artifacts",
SessionID: sessionID, SessionID: sessionID,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("analyze: promote artifact output for %q: %w", artifactName, err) return nil, fmt.Errorf("analyze: materialize artifact output for %q: %w", artifactName, err)
} }
logPaths = append(logPaths, stdoutLogPath, stderrLogPath) logPaths = append(logPaths, stdoutLogPath, stderrLogPath)
@@ -529,7 +529,7 @@ func executeAnalyzeArtifact(
} }
return &analyzeArtifactExecutionResult{ return &analyzeArtifactExecutionResult{
Output: promotedArtifact, Output: materializedArtifact,
Logs: logPaths, Logs: logPaths,
GeneratedConfigs: generatedConfigs, GeneratedConfigs: generatedConfigs,
Metadata: meta, Metadata: meta,

View File

@@ -277,7 +277,7 @@ func TestAnalyzeUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
t.Fatalf("outputs len = %d, want 1", len(result.Outputs)) t.Fatalf("outputs len = %d, want 1", len(result.Outputs))
} }
if strings.Contains(result.Outputs[0].AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) { if strings.Contains(result.Outputs[0].AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) {
t.Fatalf("promoted output path = %q, want canonical session path", result.Outputs[0].AbsolutePath) t.Fatalf("materialized output path = %q, want canonical session path", result.Outputs[0].AbsolutePath)
} }
} }

View File

@@ -72,7 +72,7 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
}, nil }, nil
} }
if err := validateArchivePrerequisites(m); err != nil { if err := validatePublishPrerequisites(m); err != nil {
return nil, fmt.Errorf("publish: %w", err) return nil, fmt.Errorf("publish: %w", err)
} }
if env.ObjectStore == nil { if env.ObjectStore == nil {
@@ -126,17 +126,17 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
if err != nil { if err != nil {
return nil, fmt.Errorf("publish: build runtime artifact catalog: %w", err) return nil, fmt.Errorf("publish: build runtime artifact catalog: %w", err)
} }
promotions, skippedOptional, skippedUnselected, lockedPromotions, err := resolveArchivePromotions( publishOutputs, skippedOptionalOutputs, skippedUnselectedOutputs, lockedOutputs, err := resolvePublishOutputs(
sessionPaths, sessionPaths,
m, m,
runtimeCatalog, runtimeCatalog,
env.Config.Pipeline.Publish.PromoteArtifacts, env.Config.Pipeline.Publish.Outputs,
env.Config.Pipeline.Publish.Locks, env.Config.Pipeline.Publish.Locks,
env.SelectedArtifactKeys, env.SelectedArtifactKeys,
sessionPrefix, sessionPrefix,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("publish: resolve promotion rules: %w", err) return nil, fmt.Errorf("publish: resolve publish output rules: %w", err)
} }
runUploaded := make([]string, 0, len(runFiles)) runUploaded := make([]string, 0, len(runFiles))
for _, file := range runFiles { for _, file := range runFiles {
@@ -147,18 +147,18 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
runUploaded = append(runUploaded, file.RelativePath) runUploaded = append(runUploaded, file.RelativePath)
} }
promotedUploaded := make([]string, 0, len(promotions)) publishedUploaded := make([]string, 0, len(publishOutputs))
for _, promotion := range promotions { for _, promotion := range publishOutputs {
key := artifacts.S3PromotedArtifactKey(sessionPrefix, promotion.Dest) key := artifacts.S3PublishedOutputKey(sessionPrefix, promotion.Dest)
if _, err := env.ObjectStore.Upload(ctx, promotion.LocalPath, key, storage.UploadOptions{}); err != nil { if _, err := env.ObjectStore.Upload(ctx, promotion.LocalPath, key, storage.UploadOptions{}); err != nil {
return nil, fmt.Errorf("publish: upload promoted output source %q to %q: %w", promotion.Source, key, err) return nil, fmt.Errorf("publish: upload published output source %q to %q: %w", promotion.Source, key, err)
} }
promotedUploaded = append(promotedUploaded, promotion.Dest) publishedUploaded = append(publishedUploaded, promotion.Dest)
} }
previousUploaded := make([]string, 0, len(previousFiles)) previousUploaded := make([]string, 0, len(previousFiles))
for _, file := range previousFiles { for _, file := range previousFiles {
key := artifacts.S3PromotedArtifactKey(sessionPrefix, file.RelativePath) key := artifacts.S3PublishedOutputKey(sessionPrefix, file.RelativePath)
if _, err := env.ObjectStore.Upload(ctx, file.LocalPath, key, storage.UploadOptions{}); err != nil { if _, err := env.ObjectStore.Upload(ctx, file.LocalPath, key, storage.UploadOptions{}); err != nil {
return nil, fmt.Errorf("publish: upload previous file %q to %q: %w", file.RelativePath, key, err) return nil, fmt.Errorf("publish: upload previous file %q to %q: %w", file.RelativePath, key, err)
} }
@@ -171,11 +171,11 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
runPrefix, runPrefix,
sessionPrefix, sessionPrefix,
runUploaded, runUploaded,
promotedUploaded, publishedUploaded,
previousUploaded, previousUploaded,
skippedOptional, skippedOptionalOutputs,
skippedUnselected, skippedUnselectedOutputs,
lockedPromotions, lockedOutputs,
currentManifestKey, currentManifestKey,
)) ))
if err != nil { if err != nil {
@@ -203,29 +203,29 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
return &StageResult{ return &StageResult{
Metadata: map[string]any{ Metadata: map[string]any{
"stage": "publish", "stage": "publish",
"uploaded": true, "uploaded": true,
"s3_bucket": bucket, "s3_bucket": bucket,
"s3_run_prefix": runPrefix, "s3_run_prefix": runPrefix,
"run_files_uploaded": len(runUploaded), "run_files_uploaded": len(runUploaded),
"run_uploaded_paths": runUploaded, "run_uploaded_paths": runUploaded,
"promoted_files_uploaded": len(promotedUploaded), "published_files_uploaded": len(publishedUploaded),
"promoted_paths": promotedUploaded, "published_paths": publishedUploaded,
"previous_files_uploaded": len(previousUploaded), "previous_files_uploaded": len(previousUploaded),
"previous_uploaded_paths": previousUploaded, "previous_uploaded_paths": previousUploaded,
"skipped_optional_promotions": skippedOptional, "skipped_optional_outputs": skippedOptionalOutputs,
"skipped_unselected_promotions": skippedUnselectedPromotionMetadata(skippedUnselected), "skipped_unselected_outputs": skippedUnselectedOutputMetadata(skippedUnselectedOutputs),
"locked_promotion_count": len(lockedPromotions), "locked_output_count": len(lockedOutputs),
"locked_promotions": lockedPromotionMetadata(lockedPromotions), "locked_outputs": lockedOutputMetadata(lockedOutputs),
"current_manifest_key": currentManifestKey, "current_manifest_key": currentManifestKey,
"current_run_id_key": currentRunPointerKey, "current_run_id_key": currentRunPointerKey,
"current_pointer_written": true, "current_pointer_written": true,
"audio_upload_skipped": true, "audio_upload_skipped": true,
}, },
}, nil }, nil
} }
type archivePromotion struct { type publishOutput struct {
Source string Source string
Dest string Dest string
Required bool Required bool
@@ -233,7 +233,7 @@ type archivePromotion struct {
Provenance string Provenance string
} }
type archiveLockedPromotion struct { type publishLockedOutput struct {
Source string Source string
Dest string Dest string
RemoteKey string RemoteKey string
@@ -243,7 +243,7 @@ type archiveLockedPromotion struct {
Provenance string Provenance string
} }
type archiveSkippedUnselectedPromotion struct { type publishSkippedUnselectedOutput struct {
Source string Source string
Dest string Dest string
Required bool Required bool
@@ -265,7 +265,7 @@ func archiveRunUploadDisabled(env *Env) bool {
return cfg.UploadRun != nil && !*cfg.UploadRun return cfg.UploadRun != nil && !*cfg.UploadRun
} }
func validateArchivePrerequisites(m *manifest.Manifest) error { func validatePublishPrerequisites(m *manifest.Manifest) error {
if m == nil { if m == nil {
return fmt.Errorf("manifest is required") return fmt.Errorf("manifest is required")
} }
@@ -343,32 +343,32 @@ func archiveSessionPaths(env *Env, m *manifest.Manifest) artifacts.SessionPaths
return store.SessionPathsFor(campaign, sessionID) return store.SessionPathsFor(campaign, sessionID)
} }
func resolveArchivePromotions( func resolvePublishOutputs(
paths artifacts.SessionPaths, paths artifacts.SessionPaths,
m *manifest.Manifest, m *manifest.Manifest,
catalog *artifacts.ArtifactCatalog, catalog *artifacts.ArtifactCatalog,
rules []config.ArchivePromotionRule, rules []config.PublishOutputRule,
locks []config.ArchiveLockRule, locks []config.PublishLockRule,
selectedArtifactKeys []string, selectedArtifactKeys []string,
sessionPrefix string, sessionPrefix string,
) ([]archivePromotion, []string, []archiveSkippedUnselectedPromotion, []archiveLockedPromotion, error) { ) ([]publishOutput, []string, []publishSkippedUnselectedOutput, []publishLockedOutput, error) {
out := make([]archivePromotion, 0, len(rules)) out := make([]publishOutput, 0, len(rules))
skippedOptional := make([]string, 0) skippedOptionalOutputs := make([]string, 0)
skippedUnselected := make([]archiveSkippedUnselectedPromotion, 0) skippedUnselectedOutputs := make([]publishSkippedUnselectedOutput, 0)
lockedPromotions := make([]archiveLockedPromotion, 0) lockedOutputs := make([]publishLockedOutput, 0)
lockSet := archiveLockSet(locks) lockSet := archiveLockSet(locks)
selectedSet := archiveSelectedArtifactSet(selectedArtifactKeys) selectedSet := archiveSelectedArtifactSet(selectedArtifactKeys)
for _, rule := range rules { for _, rule := range rules {
source := strings.TrimSpace(rule.Source) source := strings.TrimSpace(rule.Source)
required := rule.Required == nil || *rule.Required required := rule.Required == nil || *rule.Required
dest, err := resolveArchivePromotionDest(rule, catalog) dest, err := resolvePublishOutputDest(rule, catalog)
if err != nil { if err != nil {
return nil, nil, nil, nil, fmt.Errorf("source %q: %w", source, err) return nil, nil, nil, nil, fmt.Errorf("source %q: %w", source, err)
} }
if len(selectedSet) > 0 { if len(selectedSet) > 0 {
if key, ok := artifacts.ConfiguredArtifactName(source); ok { if key, ok := artifacts.ConfiguredArtifactName(source); ok {
if _, selected := selectedSet[key]; !selected { if _, selected := selectedSet[key]; !selected {
skippedUnselected = append(skippedUnselected, archiveSkippedUnselectedPromotion{ skippedUnselectedOutputs = append(skippedUnselectedOutputs, publishSkippedUnselectedOutput{
Source: source, Source: source,
Dest: dest, Dest: dest,
Required: required, Required: required,
@@ -381,29 +381,29 @@ func resolveArchivePromotions(
resolved, err := artifacts.ResolveSessionArtifactWithCatalog(paths, m, source, catalog) resolved, err := artifacts.ResolveSessionArtifactWithCatalog(paths, m, source, catalog)
if err != nil { if err != nil {
if locked { if locked {
lockedPromotions = append(lockedPromotions, archiveLockedPromotion{ lockedOutputs = append(lockedOutputs, publishLockedOutput{
Source: source, Source: source,
Dest: dest, Dest: dest,
RemoteKey: artifacts.S3PromotedArtifactKey(sessionPrefix, dest), RemoteKey: artifacts.S3PublishedOutputKey(sessionPrefix, dest),
Reason: strings.TrimSpace(lock.Reason), Reason: strings.TrimSpace(lock.Reason),
Required: required, Required: required,
}) })
continue continue
} }
if errors.Is(err, artifacts.ErrSessionArtifactNotFound) && !required { if errors.Is(err, artifacts.ErrSessionArtifactNotFound) && !required {
skippedOptional = append(skippedOptional, dest) skippedOptionalOutputs = append(skippedOptionalOutputs, dest)
continue continue
} }
if errors.Is(err, artifacts.ErrSessionArtifactNotFound) { if errors.Is(err, artifacts.ErrSessionArtifactNotFound) {
return nil, nil, nil, nil, fmt.Errorf("required promotion source unavailable: %q", source) return nil, nil, nil, nil, fmt.Errorf("required output source unavailable: %q", source)
} }
return nil, nil, nil, nil, fmt.Errorf("resolve source %q: %w", source, err) return nil, nil, nil, nil, fmt.Errorf("resolve source %q: %w", source, err)
} }
if locked { if locked {
lockedPromotions = append(lockedPromotions, archiveLockedPromotion{ lockedOutputs = append(lockedOutputs, publishLockedOutput{
Source: source, Source: source,
Dest: dest, Dest: dest,
RemoteKey: artifacts.S3PromotedArtifactKey(sessionPrefix, dest), RemoteKey: artifacts.S3PublishedOutputKey(sessionPrefix, dest),
Reason: strings.TrimSpace(lock.Reason), Reason: strings.TrimSpace(lock.Reason),
Required: required, Required: required,
LocalPath: resolved.Path, LocalPath: resolved.Path,
@@ -411,7 +411,7 @@ func resolveArchivePromotions(
}) })
continue continue
} }
out = append(out, archivePromotion{ out = append(out, publishOutput{
Source: source, Source: source,
Dest: dest, Dest: dest,
Required: required, Required: required,
@@ -419,7 +419,7 @@ func resolveArchivePromotions(
Provenance: resolved.Provenance, Provenance: resolved.Provenance,
}) })
} }
return out, skippedOptional, skippedUnselected, lockedPromotions, nil return out, skippedOptionalOutputs, skippedUnselectedOutputs, lockedOutputs, nil
} }
func archiveSelectedArtifactSet(selected []string) map[string]struct{} { func archiveSelectedArtifactSet(selected []string) map[string]struct{} {
@@ -437,8 +437,8 @@ func archiveSelectedArtifactSet(selected []string) map[string]struct{} {
return out return out
} }
func archiveLockSet(locks []config.ArchiveLockRule) map[string]config.ArchiveLockRule { func archiveLockSet(locks []config.PublishLockRule) map[string]config.PublishLockRule {
out := make(map[string]config.ArchiveLockRule, len(locks)) out := make(map[string]config.PublishLockRule, len(locks))
for _, lock := range locks { for _, lock := range locks {
source := strings.TrimSpace(lock.Source) source := strings.TrimSpace(lock.Source)
if source == "" { if source == "" {
@@ -451,7 +451,7 @@ func archiveLockSet(locks []config.ArchiveLockRule) map[string]config.ArchiveLoc
return out return out
} }
func resolveArchivePromotionDest(rule config.ArchivePromotionRule, catalog *artifacts.ArtifactCatalog) (string, error) { func resolvePublishOutputDest(rule config.PublishOutputRule, catalog *artifacts.ArtifactCatalog) (string, error) {
dest := strings.TrimSpace(rule.Dest) dest := strings.TrimSpace(rule.Dest)
if dest == "" { if dest == "" {
entry, ok := catalog.Lookup(strings.TrimSpace(rule.Source)) entry, ok := catalog.Lookup(strings.TrimSpace(rule.Source))
@@ -760,36 +760,36 @@ func writeCurrentRunIDPointer(runID string) (string, error) {
func archiveMetadataPreview( func archiveMetadataPreview(
bucket, runPrefix, sessionPrefix string, bucket, runPrefix, sessionPrefix string,
runUploaded []string, runUploaded []string,
promotedUploaded []string, publishedUploaded []string,
previousUploaded []string, previousUploaded []string,
skippedOptional []string, skippedOptionalOutputs []string,
skippedUnselected []archiveSkippedUnselectedPromotion, skippedUnselectedOutputs []publishSkippedUnselectedOutput,
lockedPromotions []archiveLockedPromotion, lockedOutputs []publishLockedOutput,
currentManifestKey string, currentManifestKey string,
) map[string]any { ) map[string]any {
return map[string]any{ return map[string]any{
"stage": "publish", "stage": "publish",
"uploaded": true, "uploaded": true,
"s3_bucket": bucket, "s3_bucket": bucket,
"s3_run_prefix": runPrefix, "s3_run_prefix": runPrefix,
"run_files_uploaded": len(runUploaded), "run_files_uploaded": len(runUploaded),
"run_uploaded_paths": append([]string(nil), runUploaded...), "run_uploaded_paths": append([]string(nil), runUploaded...),
"promoted_files_uploaded": len(promotedUploaded), "published_files_uploaded": len(publishedUploaded),
"promoted_paths": append([]string(nil), promotedUploaded...), "published_paths": append([]string(nil), publishedUploaded...),
"previous_files_uploaded": len(previousUploaded), "previous_files_uploaded": len(previousUploaded),
"previous_uploaded_paths": append([]string(nil), previousUploaded...), "previous_uploaded_paths": append([]string(nil), previousUploaded...),
"skipped_optional_promotions": append([]string(nil), skippedOptional...), "skipped_optional_outputs": append([]string(nil), skippedOptionalOutputs...),
"skipped_unselected_promotions": skippedUnselectedPromotionMetadata(skippedUnselected), "skipped_unselected_outputs": skippedUnselectedOutputMetadata(skippedUnselectedOutputs),
"locked_promotion_count": len(lockedPromotions), "locked_output_count": len(lockedOutputs),
"locked_promotions": lockedPromotionMetadata(lockedPromotions), "locked_outputs": lockedOutputMetadata(lockedOutputs),
"current_manifest_key": currentManifestKey, "current_manifest_key": currentManifestKey,
"current_run_id_key": artifacts.S3CurrentRunPointerKey(sessionPrefix), "current_run_id_key": artifacts.S3CurrentRunPointerKey(sessionPrefix),
"current_pointer_written": false, "current_pointer_written": false,
"audio_upload_skipped": true, "audio_upload_skipped": true,
} }
} }
func skippedUnselectedPromotionMetadata(skipped []archiveSkippedUnselectedPromotion) []map[string]any { func skippedUnselectedOutputMetadata(skipped []publishSkippedUnselectedOutput) []map[string]any {
out := make([]map[string]any, 0, len(skipped)) out := make([]map[string]any, 0, len(skipped))
for _, item := range skipped { for _, item := range skipped {
out = append(out, map[string]any{ out = append(out, map[string]any{
@@ -801,7 +801,7 @@ func skippedUnselectedPromotionMetadata(skipped []archiveSkippedUnselectedPromot
return out return out
} }
func lockedPromotionMetadata(locked []archiveLockedPromotion) []map[string]any { func lockedOutputMetadata(locked []publishLockedOutput) []map[string]any {
out := make([]map[string]any, 0, len(locked)) out := make([]map[string]any, 0, len(locked))
for _, item := range locked { for _, item := range locked {
out = append(out, map[string]any{ out = append(out, map[string]any{

View File

@@ -128,8 +128,8 @@ func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) {
if result.Metadata["current_pointer_written"] != true { if result.Metadata["current_pointer_written"] != true {
t.Fatalf("metadata = %#v, want current_pointer_written=true", result.Metadata) t.Fatalf("metadata = %#v, want current_pointer_written=true", result.Metadata)
} }
if result.Metadata["promoted_files_uploaded"] != 2 { if result.Metadata["published_files_uploaded"] != 2 {
t.Fatalf("metadata promoted_files_uploaded = %#v, want 2", result.Metadata["promoted_files_uploaded"]) t.Fatalf("metadata published_files_uploaded = %#v, want 2", result.Metadata["published_files_uploaded"])
} }
if result.Metadata["previous_files_uploaded"] != 0 { if result.Metadata["previous_files_uploaded"] != 0 {
t.Fatalf("metadata previous_files_uploaded = %#v, want 0", result.Metadata["previous_files_uploaded"]) t.Fatalf("metadata previous_files_uploaded = %#v, want 0", result.Metadata["previous_files_uploaded"])
@@ -179,7 +179,7 @@ func TestArchiveToleratesMissingPreviousCache(t *testing.T) {
func TestArchiveUsesCustomPromotionRules(t *testing.T) { func TestArchiveUsesCustomPromotionRules(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Publish.PromoteArtifacts = []config.ArchivePromotionRule{ env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{
{Source: "narratio.transcript.final_trimmed", Dest: "published/trimmed.json", Required: boolPtr(true)}, {Source: "narratio.transcript.final_trimmed", Dest: "published/trimmed.json", Required: boolPtr(true)},
{Source: "narratio.artifact.session_recap", Dest: "published/recap.md", Required: boolPtr(true)}, {Source: "narratio.artifact.session_recap", Dest: "published/recap.md", Required: boolPtr(true)},
} }
@@ -200,7 +200,7 @@ func TestArchiveUsesCustomPromotionRules(t *testing.T) {
func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) { func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Publish.PromoteArtifacts = []config.ArchivePromotionRule{ env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{
{Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)}, {Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)},
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(false)}, {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(false)},
} }
@@ -209,10 +209,10 @@ func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Run() error = %v", err) t.Fatalf("Run() error = %v", err)
} }
got, _ := result.Metadata["skipped_optional_promotions"].([]string) got, _ := result.Metadata["skipped_optional_outputs"].([]string)
want := []string{"transcripts/base.json"} want := []string{"transcripts/base.json"}
if !reflect.DeepEqual(got, want) { if !reflect.DeepEqual(got, want) {
t.Fatalf("skipped_optional_promotions = %#v, want %#v", got, want) t.Fatalf("skipped_optional_outputs = %#v, want %#v", got, want)
} }
} }
@@ -236,15 +236,15 @@ func TestArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) {
if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok { if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok {
t.Fatalf("unexpected unselected recap promotion upload") t.Fatalf("unexpected unselected recap promotion upload")
} }
skipped := result.Metadata["skipped_unselected_promotions"].([]map[string]any) skipped := result.Metadata["skipped_unselected_outputs"].([]map[string]any)
if len(skipped) != 1 { if len(skipped) != 1 {
t.Fatalf("skipped_unselected_promotions = %#v, want one item", skipped) t.Fatalf("skipped_unselected_outputs = %#v, want one item", skipped)
} }
if skipped[0]["source"] != "narratio.artifact.session_recap" || skipped[0]["dest"] != "artifacts/session_recap.md" || skipped[0]["required"] != true { if skipped[0]["source"] != "narratio.artifact.session_recap" || skipped[0]["dest"] != "artifacts/session_recap.md" || skipped[0]["required"] != true {
t.Fatalf("skipped_unselected_promotions[0] = %#v, want session recap", skipped[0]) t.Fatalf("skipped_unselected_outputs[0] = %#v, want session recap", skipped[0])
} }
if result.Metadata["locked_promotion_count"] != 0 { if result.Metadata["locked_output_count"] != 0 {
t.Fatalf("locked_promotion_count = %#v, want 0", result.Metadata["locked_promotion_count"]) t.Fatalf("locked_output_count = %#v, want 0", result.Metadata["locked_output_count"])
} }
if _, ok := fake.Objects[m.S3SessionPrefix+"current/run_id.txt"]; !ok { if _, ok := fake.Objects[m.S3SessionPrefix+"current/run_id.txt"]; !ok {
t.Fatalf("missing current pointer") t.Fatalf("missing current pointer")
@@ -264,15 +264,15 @@ func TestArchiveSelectedConfiguredPromotionStillFailsWhenMissing(t *testing.T) {
} }
_, err := archiveStage{}.Run(context.Background(), env, m) _, err := archiveStage{}.Run(context.Background(), env, m)
if err == nil || !strings.Contains(err.Error(), `required promotion source unavailable: "narratio.artifact.session_recap"`) { if err == nil || !strings.Contains(err.Error(), `required output source unavailable: "narratio.artifact.session_recap"`) {
t.Fatalf("Run() error = %v, want required selected promotion failure", err) t.Fatalf("Run() error = %v, want required selected output failure", err)
} }
} }
func TestArchiveLockedSelectedPromotionSkipsAsLocked(t *testing.T) { func TestArchiveLockedSelectedPromotionSkipsAsLocked(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.SelectedArtifactKeys = []string{"session_recap"} env.SelectedArtifactKeys = []string{"session_recap"}
env.Config.Pipeline.Publish.Locks = []config.ArchiveLockRule{ env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{
{Source: "narratio.artifact.session_recap", Reason: "reviewed"}, {Source: "narratio.artifact.session_recap", Reason: "reviewed"},
} }
fake := env.ObjectStore.(*storage.FakeBackend) fake := env.ObjectStore.(*storage.FakeBackend)
@@ -284,12 +284,12 @@ func TestArchiveLockedSelectedPromotionSkipsAsLocked(t *testing.T) {
if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok { if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok {
t.Fatalf("unexpected locked recap promotion upload") t.Fatalf("unexpected locked recap promotion upload")
} }
if result.Metadata["locked_promotion_count"] != 1 { if result.Metadata["locked_output_count"] != 1 {
t.Fatalf("locked_promotion_count = %#v, want 1", result.Metadata["locked_promotion_count"]) t.Fatalf("locked_output_count = %#v, want 1", result.Metadata["locked_output_count"])
} }
skipped := result.Metadata["skipped_unselected_promotions"].([]map[string]any) skipped := result.Metadata["skipped_unselected_outputs"].([]map[string]any)
if len(skipped) != 0 { if len(skipped) != 0 {
t.Fatalf("skipped_unselected_promotions = %#v, want empty", skipped) t.Fatalf("skipped_unselected_outputs = %#v, want empty", skipped)
} }
} }
@@ -301,7 +301,7 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing.
PromptID: "dnd.player_handout", PromptID: "dnd.player_handout",
OutputPath: "artifacts/player_handout.md", OutputPath: "artifacts/player_handout.md",
} }
env.Config.Pipeline.Publish.Locks = []config.ArchiveLockRule{ env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{
{Source: "narratio.artifact.session_recap", Reason: "reviewed"}, {Source: "narratio.artifact.session_recap", Reason: "reviewed"},
} }
@@ -309,18 +309,18 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing.
if err != nil { if err != nil {
t.Fatalf("Run() error = %v", err) t.Fatalf("Run() error = %v", err)
} }
if result.Metadata["locked_promotion_count"] != 0 { if result.Metadata["locked_output_count"] != 0 {
t.Fatalf("locked_promotion_count = %#v, want 0", result.Metadata["locked_promotion_count"]) t.Fatalf("locked_output_count = %#v, want 0", result.Metadata["locked_output_count"])
} }
skipped := result.Metadata["skipped_unselected_promotions"].([]map[string]any) skipped := result.Metadata["skipped_unselected_outputs"].([]map[string]any)
if len(skipped) != 1 || skipped[0]["source"] != "narratio.artifact.session_recap" { if len(skipped) != 1 || skipped[0]["source"] != "narratio.artifact.session_recap" {
t.Fatalf("skipped_unselected_promotions = %#v, want unselected recap", skipped) t.Fatalf("skipped_unselected_outputs = %#v, want unselected recap", skipped)
} }
} }
func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Publish.Locks = []config.ArchiveLockRule{ env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{
{Source: "narratio.transcript.final_trimmed", Reason: "human reviewed"}, {Source: "narratio.transcript.final_trimmed", Reason: "human reviewed"},
} }
fake := env.ObjectStore.(*storage.FakeBackend) fake := env.ObjectStore.(*storage.FakeBackend)
@@ -348,15 +348,15 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
t.Fatalf("last upload = %#v, want current run pointer %q", fake.Uploads, currentRunIDKey) t.Fatalf("last upload = %#v, want current run pointer %q", fake.Uploads, currentRunIDKey)
} }
if result.Metadata["promoted_files_uploaded"] != 1 { if result.Metadata["published_files_uploaded"] != 1 {
t.Fatalf("metadata promoted_files_uploaded = %#v, want 1", result.Metadata["promoted_files_uploaded"]) t.Fatalf("metadata published_files_uploaded = %#v, want 1", result.Metadata["published_files_uploaded"])
} }
if result.Metadata["locked_promotion_count"] != 1 { if result.Metadata["locked_output_count"] != 1 {
t.Fatalf("metadata locked_promotion_count = %#v, want 1", result.Metadata["locked_promotion_count"]) t.Fatalf("metadata locked_output_count = %#v, want 1", result.Metadata["locked_output_count"])
} }
locked := result.Metadata["locked_promotions"].([]map[string]any) locked := result.Metadata["locked_outputs"].([]map[string]any)
if len(locked) != 1 { if len(locked) != 1 {
t.Fatalf("locked_promotions = %#v, want one item", locked) t.Fatalf("locked_outputs = %#v, want one item", locked)
} }
if locked[0]["source"] != "narratio.transcript.final_trimmed" || if locked[0]["source"] != "narratio.transcript.final_trimmed" ||
locked[0]["dest"] != "transcripts/final.trimmed.json" || locked[0]["dest"] != "transcripts/final.trimmed.json" ||
@@ -376,21 +376,21 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
stages := current["stages"].(map[string]any) stages := current["stages"].(map[string]any)
archive := stages["publish"].(map[string]any) archive := stages["publish"].(map[string]any)
meta := archive["metadata"].(map[string]any) meta := archive["metadata"].(map[string]any)
if meta["locked_promotion_count"] != float64(1) { if meta["locked_output_count"] != float64(1) {
t.Fatalf("current manifest locked_promotion_count = %#v, want 1", meta["locked_promotion_count"]) t.Fatalf("current manifest locked_output_count = %#v, want 1", meta["locked_output_count"])
} }
items := meta["locked_promotions"].([]any) items := meta["locked_outputs"].([]any)
if len(items) != 1 { if len(items) != 1 {
t.Fatalf("current manifest locked_promotions = %#v, want one item", items) t.Fatalf("current manifest locked_outputs = %#v, want one item", items)
} }
} }
func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) { func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Publish.PromoteArtifacts = []config.ArchivePromotionRule{ env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)}, {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)},
} }
env.Config.Pipeline.Publish.Locks = []config.ArchiveLockRule{ env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{
{Source: "narratio.transcript.base", Reason: "manual merge is locked"}, {Source: "narratio.transcript.base", Reason: "manual merge is locked"},
} }
@@ -407,9 +407,9 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
if _, ok := fake.Objects[m.S3SessionPrefix+"current/run_id.txt"]; !ok { if _, ok := fake.Objects[m.S3SessionPrefix+"current/run_id.txt"]; !ok {
t.Fatalf("current run pointer should be written for locked missing promotion") t.Fatalf("current run pointer should be written for locked missing promotion")
} }
locked := result.Metadata["locked_promotions"].([]map[string]any) locked := result.Metadata["locked_outputs"].([]map[string]any)
if len(locked) != 1 { if len(locked) != 1 {
t.Fatalf("locked_promotions = %#v, want one item", locked) t.Fatalf("locked_outputs = %#v, want one item", locked)
} }
if locked[0]["local_path"] != "" || locked[0]["provenance"] != "" { if locked[0]["local_path"] != "" || locked[0]["provenance"] != "" {
t.Fatalf("locked missing promotion metadata = %#v, want empty local path/provenance", locked[0]) t.Fatalf("locked missing promotion metadata = %#v, want empty local path/provenance", locked[0])
@@ -418,7 +418,7 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) { func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Publish.Locks = []config.ArchiveLockRule{ env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{
{Source: "narratio.transcript.final_trimmed", Reason: "already published"}, {Source: "narratio.transcript.final_trimmed", Reason: "already published"},
} }
fake := env.ObjectStore.(*storage.FakeBackend) fake := env.ObjectStore.(*storage.FakeBackend)
@@ -442,13 +442,13 @@ func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) {
func TestArchiveFailsWhenRequiredPromotionMissing(t *testing.T) { func TestArchiveFailsWhenRequiredPromotionMissing(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Publish.PromoteArtifacts = []config.ArchivePromotionRule{ env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)}, {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)},
} }
_, err := archiveStage{}.Run(context.Background(), env, m) _, err := archiveStage{}.Run(context.Background(), env, m)
if err == nil || !strings.Contains(err.Error(), "required promotion source unavailable") { if err == nil || !strings.Contains(err.Error(), "required output source unavailable") {
t.Fatalf("Run() error = %v, want required promotion source unavailable failure", err) t.Fatalf("Run() error = %v, want required output source unavailable failure", err)
} }
} }
@@ -485,8 +485,8 @@ func TestArchiveDoesNotWriteCurrentPointerWhenPromotionUploadFails(t *testing.T)
env.ObjectStore = &promotionFailingStore{delegate: fake, failKey: failingKey} env.ObjectStore = &promotionFailingStore{delegate: fake, failKey: failingKey}
_, err := archiveStage{}.Run(context.Background(), env, m) _, err := archiveStage{}.Run(context.Background(), env, m)
if err == nil || !strings.Contains(err.Error(), "promoted output") { if err == nil || !strings.Contains(err.Error(), "upload published output source") {
t.Fatalf("Run() error = %v, want promotion upload failure", err) t.Fatalf("Run() error = %v, want published output upload failure", err)
} }
if _, ok := fake.Objects[m.S3SessionPrefix+"current/run_id.txt"]; ok { if _, ok := fake.Objects[m.S3SessionPrefix+"current/run_id.txt"]; ok {
t.Fatalf("unexpected current pointer write on promotion failure") t.Fatalf("unexpected current pointer write on promotion failure")
@@ -563,10 +563,10 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) {
RootPrefix: "dnd", RootPrefix: "dnd",
}, },
}, },
Publish: &config.ArchiveConfig{ Publish: &config.PublishConfig{
Enabled: boolPtr(true), Enabled: boolPtr(true),
UploadRun: boolPtr(true), UploadRun: boolPtr(true),
PromoteArtifacts: []config.ArchivePromotionRule{ Outputs: []config.PublishOutputRule{
{Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)}, {Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)},
{Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)}, {Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)},
}, },

View File

@@ -150,7 +150,7 @@ func (mergeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Sta
} }
} }
promotedMerged, err := promoteRunLocalOutput(env.ArtifactStore, finalMergedPath, canonicalMergedPath, artifacts.Ref{ materializedMerged, err := materializeRunLocalOutput(env.ArtifactStore, finalMergedPath, canonicalMergedPath, artifacts.Ref{
Kind: "transcript_base", Kind: "transcript_base",
Category: "transcripts", Category: "transcripts",
SessionID: sessionID, SessionID: sessionID,
@@ -158,9 +158,9 @@ func (mergeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Sta
if err != nil { if err != nil {
return nil, fmt.Errorf("merge: promote merged transcript: %w", err) return nil, fmt.Errorf("merge: promote merged transcript: %w", err)
} }
outputs := []artifacts.Ref{promotedMerged} outputs := []artifacts.Ref{materializedMerged}
if reportEnabled { if reportEnabled {
promotedReport, err := promoteRunLocalOutput(env.ArtifactStore, finalReportPath, canonicalReportPath, artifacts.Ref{ materializedReport, err := materializeRunLocalOutput(env.ArtifactStore, finalReportPath, canonicalReportPath, artifacts.Ref{
Kind: "seriatim_report", Kind: "seriatim_report",
Category: "artifacts", Category: "artifacts",
SessionID: sessionID, SessionID: sessionID,
@@ -168,7 +168,7 @@ func (mergeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Sta
if err != nil { if err != nil {
return nil, fmt.Errorf("merge: promote report: %w", err) return nil, fmt.Errorf("merge: promote report: %w", err)
} }
outputs = append(outputs, promotedReport) outputs = append(outputs, materializedReport)
} }
coalesceGap := any(nil) coalesceGap := any(nil)

View File

@@ -315,10 +315,10 @@ func TestMergeStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
t.Fatalf("run output path = %q, want run-local path", req.OutputMergedTranscriptPath) t.Fatalf("run output path = %q, want run-local path", req.OutputMergedTranscriptPath)
} }
if len(result.Outputs) == 0 { if len(result.Outputs) == 0 {
t.Fatalf("outputs = %#v, want promoted outputs", result.Outputs) t.Fatalf("outputs = %#v, want materialized outputs", result.Outputs)
} }
if strings.Contains(result.Outputs[0].AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) { if strings.Contains(result.Outputs[0].AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) {
t.Fatalf("promoted output path = %q, want canonical session path", result.Outputs[0].AbsolutePath) t.Fatalf("materialized output path = %q, want canonical session path", result.Outputs[0].AbsolutePath)
} }
} }

View File

@@ -133,7 +133,7 @@ func (normalizeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (
} }
} }
promotedNormalized, err := promoteRunLocalOutput(env.ArtifactStore, finalNormalizedPath, canonicalNormalizedPath, artifacts.Ref{ materializedNormalized, err := materializeRunLocalOutput(env.ArtifactStore, finalNormalizedPath, canonicalNormalizedPath, artifacts.Ref{
Kind: "transcript_final", Kind: "transcript_final",
Category: "transcripts", Category: "transcripts",
SessionID: sessionID, SessionID: sessionID,
@@ -141,9 +141,9 @@ func (normalizeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (
if err != nil { if err != nil {
return nil, fmt.Errorf("normalize: promote normalized transcript: %w", err) return nil, fmt.Errorf("normalize: promote normalized transcript: %w", err)
} }
outputs := []artifacts.Ref{promotedNormalized} outputs := []artifacts.Ref{materializedNormalized}
if reportEnabled { if reportEnabled {
promotedReport, err := promoteRunLocalOutput(env.ArtifactStore, finalReportPath, canonicalReportPath, artifacts.Ref{ materializedReport, err := materializeRunLocalOutput(env.ArtifactStore, finalReportPath, canonicalReportPath, artifacts.Ref{
Kind: "seriatim_normalize_report", Kind: "seriatim_normalize_report",
Category: "artifacts", Category: "artifacts",
SessionID: sessionID, SessionID: sessionID,
@@ -151,7 +151,7 @@ func (normalizeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (
if err != nil { if err != nil {
return nil, fmt.Errorf("normalize: promote report: %w", err) return nil, fmt.Errorf("normalize: promote report: %w", err)
} }
outputs = append(outputs, promotedReport) outputs = append(outputs, materializedReport)
} }
reportCanonicalPath := "" reportCanonicalPath := ""
if reportEnabled { if reportEnabled {

View File

@@ -231,10 +231,10 @@ func TestNormalizeStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
t.Fatalf("run output path = %q, want run-local path", req.OutputNormalizedPath) t.Fatalf("run output path = %q, want run-local path", req.OutputNormalizedPath)
} }
if len(result.Outputs) == 0 { if len(result.Outputs) == 0 {
t.Fatalf("outputs = %#v, want promoted outputs", result.Outputs) t.Fatalf("outputs = %#v, want materialized outputs", result.Outputs)
} }
if strings.Contains(result.Outputs[0].AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) { if strings.Contains(result.Outputs[0].AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) {
t.Fatalf("promoted output path = %q, want canonical session path", result.Outputs[0].AbsolutePath) t.Fatalf("materialized output path = %q, want canonical session path", result.Outputs[0].AbsolutePath)
} }
} }

View File

@@ -61,7 +61,7 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
RootPrefix: "dnd", RootPrefix: "dnd",
}, },
}, },
Publish: &config.ArchiveConfig{ Publish: &config.PublishConfig{
Enabled: boolPtr(true), Enabled: boolPtr(true),
UploadRun: boolPtr(true), UploadRun: boolPtr(true),
}, },

View File

@@ -149,7 +149,7 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
} }
} }
promotedProcessed, err := promoteRunLocalOutput(env.ArtifactStore, finalProcessedPath, canonicalProcessedPath, artifacts.Ref{ materializedProcessed, err := materializeRunLocalOutput(env.ArtifactStore, finalProcessedPath, canonicalProcessedPath, artifacts.Ref{
Kind: "transcript_polished", Kind: "transcript_polished",
Category: "transcripts", Category: "transcripts",
SessionID: sessionID, SessionID: sessionID,
@@ -157,9 +157,9 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
if err != nil { if err != nil {
return nil, fmt.Errorf("polish: promote processed transcript: %w", err) return nil, fmt.Errorf("polish: promote processed transcript: %w", err)
} }
outputs := []artifacts.Ref{promotedProcessed} outputs := []artifacts.Ref{materializedProcessed}
if reportEnabled { if reportEnabled {
promotedReport, err := promoteRunLocalOutput(env.ArtifactStore, finalReportPath, canonicalReportPath, artifacts.Ref{ materializedReport, err := materializeRunLocalOutput(env.ArtifactStore, finalReportPath, canonicalReportPath, artifacts.Ref{
Kind: "audita_report", Kind: "audita_report",
Category: "artifacts", Category: "artifacts",
SessionID: sessionID, SessionID: sessionID,
@@ -167,7 +167,7 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
if err != nil { if err != nil {
return nil, fmt.Errorf("polish: promote report: %w", err) return nil, fmt.Errorf("polish: promote report: %w", err)
} }
outputs = append(outputs, promotedReport) outputs = append(outputs, materializedReport)
} }
var validationConcurrency any var validationConcurrency any

View File

@@ -261,10 +261,10 @@ func TestPolishStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
t.Fatalf("run output path = %q, want run-local path", req.OutputProcessedPath) t.Fatalf("run output path = %q, want run-local path", req.OutputProcessedPath)
} }
if len(result.Outputs) == 0 { if len(result.Outputs) == 0 {
t.Fatalf("outputs = %#v, want promoted outputs", result.Outputs) t.Fatalf("outputs = %#v, want materialized outputs", result.Outputs)
} }
if strings.Contains(result.Outputs[0].AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) { if strings.Contains(result.Outputs[0].AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) {
t.Fatalf("promoted output path = %q, want canonical session path", result.Outputs[0].AbsolutePath) t.Fatalf("materialized output path = %q, want canonical session path", result.Outputs[0].AbsolutePath)
} }
} }

View File

@@ -300,7 +300,7 @@ func seedPreviousCurrentState(
}) })
} }
artifactKey := artifacts.S3PromotedArtifactKey(previousSessionPrefix, "artifacts/session_recap.md") artifactKey := artifacts.S3PublishedOutputKey(previousSessionPrefix, "artifacts/session_recap.md")
if options.includeArtifactObject { if options.includeArtifactObject {
body := options.artifactBody body := options.artifactBody
if body == "" { if body == "" {
@@ -365,9 +365,9 @@ func buildPreviousManifestForSeed(
LocalPath: artifactPath, LocalPath: artifactPath,
}, },
}) })
m.MarkStageSucceeded("archive", now, nil) m.MarkStageSucceeded("publish", now, nil)
m.Stages["archive"].Metadata = map[string]any{ m.Stages["publish"].Metadata = map[string]any{
"promoted_paths": []string{"artifacts/session_recap.md"}, "published_paths": []string{"artifacts/session_recap.md"},
} }
data, err := json.MarshalIndent(m, "", " ") data, err := json.MarshalIndent(m, "", " ")
if err != nil { if err != nil {

View File

@@ -110,7 +110,7 @@ func runLocalPathForCanonical(layout runStageLayout, sessionPaths artifacts.Sess
return localPath, nil return localPath, nil
} }
func promoteRunLocalOutput( func materializeRunLocalOutput(
store artifacts.Store, store artifacts.Store,
srcPath, canonicalPath string, srcPath, canonicalPath string,
ref artifacts.Ref, ref artifacts.Ref,
@@ -128,11 +128,11 @@ func promoteRunLocalOutput(
return artifacts.Ref{}, fmt.Errorf("read run-local output %q: %w", srcPath, err) return artifacts.Ref{}, fmt.Errorf("read run-local output %q: %w", srcPath, err)
} }
if err := store.WriteFileAtomic(canonicalPath, data, 0o644); err != nil { if err := store.WriteFileAtomic(canonicalPath, data, 0o644); err != nil {
return artifacts.Ref{}, fmt.Errorf("promote output to %q: %w", canonicalPath, err) return artifacts.Ref{}, fmt.Errorf("materialize output to %q: %w", canonicalPath, err)
} }
checksum, err := store.Checksum(canonicalPath) checksum, err := store.Checksum(canonicalPath)
if err != nil { if err != nil {
return artifacts.Ref{}, fmt.Errorf("checksum promoted output %q: %w", canonicalPath, err) return artifacts.Ref{}, fmt.Errorf("checksum materialized output %q: %w", canonicalPath, err)
} }
ref.AbsolutePath = canonicalPath ref.AbsolutePath = canonicalPath
ref.Checksum = checksum ref.Checksum = checksum

View File

@@ -213,11 +213,11 @@ dispatch:
ref := outputRef[speaker] ref := outputRef[speaker]
runOutputPaths = append(runOutputPaths, ref.AbsolutePath) runOutputPaths = append(runOutputPaths, ref.AbsolutePath)
canonicalOut := filepath.Join(paths.TranscriptsRawDir, speaker+".json") canonicalOut := filepath.Join(paths.TranscriptsRawDir, speaker+".json")
promoted, err := promoteRunLocalOutput(env.ArtifactStore, ref.AbsolutePath, canonicalOut, ref) materialized, err := materializeRunLocalOutput(env.ArtifactStore, ref.AbsolutePath, canonicalOut, ref)
if err != nil { if err != nil {
return nil, fmt.Errorf("transcribe: promote %q output: %w", speaker, err) return nil, fmt.Errorf("transcribe: materialize %q output: %w", speaker, err)
} }
outputs = append(outputs, promoted) outputs = append(outputs, materialized)
outputPaths = append(outputPaths, canonicalOut) outputPaths = append(outputPaths, canonicalOut)
orderedPerFile[speaker] = perFile[speaker] orderedPerFile[speaker] = perFile[speaker]
} }

View File

@@ -217,7 +217,7 @@ func TestTranscribeStageUsesRunLocalOutputAndPromotesCanonical(t *testing.T) {
t.Fatalf("outputs = %#v, want one output", result.Outputs) t.Fatalf("outputs = %#v, want one output", result.Outputs)
} }
if strings.Contains(result.Outputs[0].AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) { if strings.Contains(result.Outputs[0].AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) {
t.Fatalf("promoted output path = %q, want canonical session path", result.Outputs[0].AbsolutePath) t.Fatalf("materialized output path = %q, want canonical session path", result.Outputs[0].AbsolutePath)
} }
} }

View File

@@ -100,7 +100,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
if err := validateProcessedTranscriptOutput(trimmedPath); err != nil { if err := validateProcessedTranscriptOutput(trimmedPath); err != nil {
return nil, fmt.Errorf("trim: copied trimmed transcript %q invalid: %w", trimmedPath, err) return nil, fmt.Errorf("trim: copied trimmed transcript %q invalid: %w", trimmedPath, err)
} }
promotedTrimmed, err := promoteRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{ materializedTrimmed, err := materializeRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{
Kind: "transcript_final_trimmed", Kind: "transcript_final_trimmed",
Category: "transcripts", Category: "transcripts",
SessionID: sessionID, SessionID: sessionID,
@@ -110,7 +110,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
} }
metadata["trim_action"] = "copy_disabled" metadata["trim_action"] = "copy_disabled"
return &StageResult{ return &StageResult{
Outputs: []artifacts.Ref{promotedTrimmed}, Outputs: []artifacts.Ref{materializedTrimmed},
Metadata: metadata, Metadata: metadata,
}, nil }, nil
} }
@@ -351,7 +351,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
return nil, fmt.Errorf("trim: trimmed transcript %q invalid: %w", trimmedPath, err) return nil, fmt.Errorf("trim: trimmed transcript %q invalid: %w", trimmedPath, err)
} }
promotedTrimmed, err := promoteRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{ materializedTrimmed, err := materializeRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{
Kind: "transcript_final_trimmed", Kind: "transcript_final_trimmed",
Category: "transcripts", Category: "transcripts",
SessionID: sessionID, SessionID: sessionID,
@@ -359,7 +359,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
if err != nil { if err != nil {
return nil, fmt.Errorf("trim: promote trimmed transcript: %w", err) return nil, fmt.Errorf("trim: promote trimmed transcript: %w", err)
} }
promotedBounds, err := promoteRunLocalOutput(env.ArtifactStore, finalBoundsOutputPath, canonicalBoundsOutputPath, artifacts.Ref{ materializedBounds, err := materializeRunLocalOutput(env.ArtifactStore, finalBoundsOutputPath, canonicalBoundsOutputPath, artifacts.Ref{
Kind: "session_bounds", Kind: "session_bounds",
Category: "artifacts", Category: "artifacts",
SessionID: sessionID, SessionID: sessionID,
@@ -369,7 +369,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
} }
return &StageResult{ return &StageResult{
Outputs: []artifacts.Ref{promotedTrimmed, promotedBounds}, Outputs: []artifacts.Ref{materializedTrimmed, materializedBounds},
Logs: logPaths, Logs: logPaths,
GeneratedConfigs: generatedConfigs, GeneratedConfigs: generatedConfigs,
Metadata: metadata, Metadata: metadata,

View File

@@ -326,7 +326,7 @@ func TestTrimStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
} }
for _, out := range result.Outputs { for _, out := range result.Outputs {
if strings.Contains(out.AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) { if strings.Contains(out.AbsolutePath, string(filepath.Separator)+"runs"+string(filepath.Separator)) {
t.Fatalf("promoted output path = %q, want canonical session path", out.AbsolutePath) t.Fatalf("materialized output path = %q, want canonical session path", out.AbsolutePath)
} }
} }
} }