diff --git a/internal/app/analyze_artifacts_commands_test.go b/internal/app/analyze_artifacts_commands_test.go index d04e703..6d060c2 100644 --- a/internal/app/analyze_artifacts_commands_test.go +++ b/internal/app/analyze_artifacts_commands_test.go @@ -33,7 +33,7 @@ func TestExecuteRunStageArtifactsUnsupportedStageFails(t *testing.T) { } } -func TestExecuteRunStageArchivePropagatesSelectedArtifacts(t *testing.T) { +func TestExecuteRunStagePublishPropagatesSelectedArtifacts(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot) @@ -281,7 +281,7 @@ func TestExecuteAnalyzeMissingConfigUsesRunStageLoadingPath(t *testing.T) { } } -func TestExecutePublishForceRunsArchive(t *testing.T) { +func TestExecutePublishForceRunsPublish(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot) diff --git a/internal/app/operator_helpers.go b/internal/app/operator_helpers.go index 19b3843..5ce47a4 100644 --- a/internal/app/operator_helpers.go +++ b/internal/app/operator_helpers.go @@ -82,7 +82,7 @@ func Session(ctx context.Context, args []string, out io.Writer) error { } } -// SessionLocks dispatches session-oriented archive lock list and mutation +// SessionLocks dispatches session-oriented publish lock list and mutation // helpers while preserving the existing lock implementations. func SessionLocks(ctx context.Context, args []string, out io.Writer) error { if len(args) > 0 && !isCLIFlagToken(args[0]) { @@ -179,7 +179,7 @@ func SessionValidate(ctx context.Context, args []string, out io.Writer) error { if lockErr != nil { findings = append(findings, errorFinding("locks", lockErr.Error())) } else if len(locks.All) == 0 { - findings = append(findings, okFinding("locks", "no effective archive locks")) + findings = append(findings, okFinding("locks", "no effective publish locks")) } else { for _, lock := range locks.All { findings = append(findings, warnFinding("locks", fmt.Sprintf("%s locked: %s", lock.Source, strings.TrimSpace(lock.Reason)))) @@ -258,8 +258,8 @@ func Status(ctx context.Context, args []string, out io.Writer) error { catalogLocks := locks if err != nil { catalogLocks = &effectiveLocks{ - Static: staticArchiveLocks(cfg), - All: staticArchiveLocks(cfg), + Static: staticPublishLocks(cfg), + All: staticPublishLocks(cfg), } } publishedRemoteState := map[string]string{} @@ -411,7 +411,7 @@ func ArtifactsList(ctx context.Context, args []string, out io.Writer) error { var flags commonConfigFlags var remote bool addCommonConfigFlags(fs, &flags) - fs.BoolVar(&remote, "remote", false, "inspect remote archive availability") + fs.BoolVar(&remote, "remote", false, "inspect remote publish availability") if err := fs.Parse(args); err != nil { return fmt.Errorf("artifacts list: invalid flags: %w", err) } @@ -446,7 +446,7 @@ func ArtifactsList(ctx context.Context, args []string, out io.Writer) error { return nil } -// Locks dispatches archive lock list and mutation helpers. +// Locks dispatches publish lock list and mutation helpers. func Locks(ctx context.Context, args []string, out io.Writer) error { if len(args) > 0 && !strings.HasPrefix(args[0], "-") { switch args[0] { @@ -461,7 +461,7 @@ func Locks(ctx context.Context, args []string, out io.Writer) error { return LocksList(ctx, args, out) } -// LocksList lists effective archive locks. +// LocksList lists effective publish locks. func LocksList(ctx context.Context, args []string, out io.Writer) error { positionalSessionID, args := pullLeadingSessionID(args) fs := flag.NewFlagSet("locks", flag.ContinueOnError) @@ -919,7 +919,7 @@ func validateRemoteAudioFinding(ctx context.Context, cfg *config.Config, store s func validatePreviousArtifactFindings(ctx context.Context, cfg *config.Config, store storage.ObjectStore, requirements []artifacts.PreviousArtifactRequirement) []finding { out := []finding{} prefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID) - manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(prefix) + manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(prefix) for _, key := range []string{runIDKey, manifestKey} { exists, err := store.Exists(ctx, key) if err != nil { diff --git a/internal/app/operator_helpers_test.go b/internal/app/operator_helpers_test.go index 75b2e13..062d714 100644 --- a/internal/app/operator_helpers_test.go +++ b/internal/app/operator_helpers_test.go @@ -596,7 +596,7 @@ func TestExecuteLocksRequireSessionID(t *testing.T) { func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) - addStaticArchiveLockToPipelineConfig(t, pipelinePath, "narratio.transcript.final_trimmed") + addStaticPublishLockToPipelineConfig(t, pipelinePath, "narratio.transcript.final_trimmed") fake := &storage.FakeBackend{} var storeInitCalls int restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) @@ -683,10 +683,10 @@ func addSessionTemplateToCampaign(t *testing.T, campaignPath, templateFile strin } } -func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) { +func TestExecuteArtifactsListRemoteReportsPublishedAvailability(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) - addArchivePromotionsToPipeline(t, pipelinePath, ` + addPublishOutputsToPipeline(t, pipelinePath, ` outputs: - source: narratio.transcript.final_trimmed dest: transcripts/final.trimmed.json @@ -714,14 +714,14 @@ func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } 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 published remote availability", stdout.String()) } } -func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) { +func TestExecuteArtifactsListRemoteUsesPublishOutputDestinations(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) - addArchivePromotionsToPipeline(t, pipelinePath, ` + addPublishOutputsToPipeline(t, pipelinePath, ` outputs: - source: narratio.transcript.final dest: transcripts/full.json @@ -771,7 +771,7 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) { func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) - addArchivePromotionsToPipeline(t, pipelinePath, ` + addPublishOutputsToPipeline(t, pipelinePath, ` outputs: - source: narratio.transcript.final_trimmed dest: transcripts/final.trimmed.json @@ -782,7 +782,7 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) { `) fake := &storage.FakeBackend{} sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03") - manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) + manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(sessionPrefix) trimmedKey := artifacts.S3PublishedOutputKey(sessionPrefix, "transcripts/final.trimmed.json") fullKey := artifacts.S3PublishedOutputKey(sessionPrefix, "transcripts/full.json") lockKey := artifacts.S3SessionLocksKey(sessionPrefix) @@ -828,7 +828,7 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) { func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) - addArchivePromotionsToPipeline(t, pipelinePath, ` + addPublishOutputsToPipeline(t, pipelinePath, ` outputs: - source: narratio.transcript.final_trimmed dest: transcripts/final.trimmed.json @@ -861,9 +861,9 @@ func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testin } } -func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) { +func TestExecutePublishLoadsRemoteLocks(t *testing.T) { workspaceRoot := t.TempDir() - pipelinePath, campaignPath, sessionPath := writeValidArchiveConfigFiles(t, workspaceRoot) + pipelinePath, campaignPath, sessionPath := writeValidPublishRunConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} lockKey := artifacts.S3SessionLocksKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")) fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.final_trimmed\n reason: remote review\n")}) @@ -883,28 +883,28 @@ func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) { if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } - promotedKey := artifacts.S3PublishedOutputKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "transcripts/final.trimmed.json") - if _, ok := fake.Objects[promotedKey]; ok { - t.Fatalf("locked promoted key %q was uploaded", promotedKey) + publishedKey := artifacts.S3PublishedOutputKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "transcripts/final.trimmed.json") + if _, ok := fake.Objects[publishedKey]; ok { + t.Fatalf("locked published key %q was uploaded", publishedKey) } } -func addArchivePromotionsToPipeline(t *testing.T, pipelinePath, archiveYAML string) { +func addPublishOutputsToPipeline(t *testing.T, pipelinePath, publishYAML string) { t.Helper() data, err := os.ReadFile(pipelinePath) if err != nil { t.Fatalf("read pipeline: %v", err) } - updated := strings.Replace(string(data), " upload_run: false\n", " upload_run: false\n"+archiveYAML, 1) + updated := strings.Replace(string(data), " upload_run: false\n", " upload_run: false\n"+publishYAML, 1) if updated == string(data) { - t.Fatalf("pipeline %q did not contain archive upload_run marker", pipelinePath) + t.Fatalf("pipeline %q did not contain publish upload_run marker", pipelinePath) } if err := os.WriteFile(pipelinePath, []byte(updated), 0o644); err != nil { t.Fatalf("write pipeline: %v", err) } } -func writeValidArchiveConfigFiles(t *testing.T, workspaceRoot string) (string, string, string) { +func writeValidPublishRunConfigFiles(t *testing.T, workspaceRoot string) (string, string, string) { t.Helper() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) data, err := os.ReadFile(pipelinePath) @@ -941,7 +941,7 @@ func writeValidArchiveConfigFiles(t *testing.T, workspaceRoot string) (string, s return pipelinePath, campaignPath, sessionPath } -func addStaticArchiveLockToPipelineConfig(t *testing.T, pipelinePath, source string) { +func addStaticPublishLockToPipelineConfig(t *testing.T, pipelinePath, source string) { t.Helper() data, err := os.ReadFile(pipelinePath) if err != nil { @@ -954,7 +954,7 @@ func addStaticArchiveLockToPipelineConfig(t *testing.T, pipelinePath, source str 1, ) if updated == string(data) { - t.Fatalf("archive section not found in pipeline config") + t.Fatalf("publish section not found in pipeline config") } if err := os.WriteFile(pipelinePath, []byte(updated), 0o644); err != nil { t.Fatalf("write pipeline: %v", err) diff --git a/internal/app/post_archive_cleanup.go b/internal/app/post_publish_cleanup.go similarity index 94% rename from internal/app/post_archive_cleanup.go rename to internal/app/post_publish_cleanup.go index 3a9d953..7a1ec44 100644 --- a/internal/app/post_archive_cleanup.go +++ b/internal/app/post_publish_cleanup.go @@ -12,7 +12,7 @@ import ( "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) -func runPostArchiveCleanup(ctx context.Context, env *Env, manifestPath string, m *manifest.Manifest, executed []string) error { +func runPostPublishCleanup(ctx context.Context, env *Env, manifestPath string, m *manifest.Manifest, executed []string) error { if env == nil || env.Config == nil || env.Config.Pipeline == nil || m == nil { return nil } @@ -23,7 +23,7 @@ func runPostArchiveCleanup(ctx context.Context, env *Env, manifestPath string, m return nil } - sr := archiveStageRecordForCleanup(m, executed) + sr := publishStageRecordForCleanup(m, executed) if sr == nil { return nil } @@ -33,7 +33,7 @@ func runPostArchiveCleanup(ctx context.Context, env *Env, manifestPath string, m sr.Metadata["spool_cleanup_requested"] = spoolRequested sr.Metadata["workdir_cleanup_requested"] = workRequested - eligible, reason := archiveCleanupEligible(env.Config, sr) + eligible, reason := publishCleanupEligible(env.Config, sr) if !eligible { sr.Metadata["cleanup_skipped"] = true sr.Metadata["cleanup_skipped_reason"] = reason @@ -96,7 +96,7 @@ func runPostArchiveCleanup(ctx context.Context, env *Env, manifestPath string, m return nil } -func archiveStageRecordForCleanup(m *manifest.Manifest, executed []string) *manifest.StageRecord { +func publishStageRecordForCleanup(m *manifest.Manifest, executed []string) *manifest.StageRecord { if m == nil { return nil } @@ -117,7 +117,7 @@ func archiveStageRecordForCleanup(m *manifest.Manifest, executed []string) *mani return sr } -func archiveCleanupEligible(cfg *config.Config, sr *manifest.StageRecord) (bool, string) { +func publishCleanupEligible(cfg *config.Config, sr *manifest.StageRecord) (bool, string) { if cfg == nil || cfg.Pipeline == nil || cfg.Pipeline.Publish == nil { return false, "publish configuration is missing" } diff --git a/internal/app/post_archive_cleanup_test.go b/internal/app/post_publish_cleanup_test.go similarity index 86% rename from internal/app/post_archive_cleanup_test.go rename to internal/app/post_publish_cleanup_test.go index adb310c..a974929 100644 --- a/internal/app/post_archive_cleanup_test.go +++ b/internal/app/post_publish_cleanup_test.go @@ -16,13 +16,13 @@ import ( "gitea.maximumdirect.net/eric/narratio/internal/stage" ) -type archiveSuccessStage struct { +type publishSuccessStage struct { metadata map[string]any } -func (archiveSuccessStage) Name() string { return "publish" } -func (archiveSuccessStage) Declares() stage.IODecl { return stage.IODecl{} } -func (s archiveSuccessStage) Run(_ context.Context, _ *stage.Env, _ *manifest.Manifest) (*stage.StageResult, error) { +func (publishSuccessStage) Name() string { return "publish" } +func (publishSuccessStage) Declares() stage.IODecl { return stage.IODecl{} } +func (s publishSuccessStage) Run(_ context.Context, _ *stage.Env, _ *manifest.Manifest) (*stage.StageResult, error) { md := map[string]any{ "stage": "publish", "uploaded": true, @@ -43,12 +43,12 @@ func (notifyFailStage) Run(_ context.Context, _ *stage.Env, _ *manifest.Manifest return nil, errors.New("notify failed") } -func TestPostArchiveCleanupDisabledKeepsLocalDirs(t *testing.T) { +func TestPostPublishCleanupDisabledKeepsLocalDirs(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = false cfg.Pipeline.Workspace.CleanupAfterPublish = false - if _, err := executeStages(context.Background(), cfg, []stage.Stage{archiveSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { + if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } @@ -57,12 +57,12 @@ func TestPostArchiveCleanupDisabledKeepsLocalDirs(t *testing.T) { assertExists(t, seed.localSourceAudio) } -func TestPostArchiveCleanupSpoolOnly(t *testing.T) { +func TestPostPublishCleanupSpoolOnly(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = false - if _, err := executeStages(context.Background(), cfg, []stage.Stage{archiveSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { + if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } @@ -71,12 +71,12 @@ func TestPostArchiveCleanupSpoolOnly(t *testing.T) { assertExists(t, seed.localSourceAudio) } -func TestPostArchiveCleanupWorkdirOnly(t *testing.T) { +func TestPostPublishCleanupWorkdirOnly(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = false cfg.Pipeline.Workspace.CleanupAfterPublish = true - if _, err := executeStages(context.Background(), cfg, []stage.Stage{archiveSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { + if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } @@ -87,12 +87,12 @@ func TestPostArchiveCleanupWorkdirOnly(t *testing.T) { assertExists(t, seed.spoolAudioDir) } -func TestPostArchiveCleanupBothPolicies(t *testing.T) { +func TestPostPublishCleanupBothPolicies(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true - if _, err := executeStages(context.Background(), cfg, []stage.Stage{archiveSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { + if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } @@ -102,12 +102,12 @@ func TestPostArchiveCleanupBothPolicies(t *testing.T) { assertExists(t, seed.previousCachePath) } -func TestPostArchiveCleanupNotRunWhenArchiveFails(t *testing.T) { +func TestPostPublishCleanupNotRunWhenPublishFails(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true - _, err := executeStages(context.Background(), cfg, []stage.Stage{failingStage{name: "publish", err: errors.New("archive failed")}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) + _, err := executeStages(context.Background(), cfg, []stage.Stage{failingStage{name: "publish", err: errors.New("publish failed")}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) if err == nil || !strings.Contains(err.Error(), "stage \"publish\" failed") { t.Fatalf("executeStages() error = %v, want publish failure", err) } @@ -116,12 +116,12 @@ func TestPostArchiveCleanupNotRunWhenArchiveFails(t *testing.T) { assertExists(t, seed.runWorkDir) } -func TestPostArchiveCleanupNotRunWhenArchiveSkipped(t *testing.T) { +func TestPostPublishCleanupNotRunWhenPublishSkipped(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true - if _, err := executeStages(context.Background(), cfg, []stage.Stage{archiveSuccessStage{metadata: map[string]any{"skipped": true}}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { + if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{metadata: map[string]any{"skipped": true}}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } @@ -129,12 +129,12 @@ func TestPostArchiveCleanupNotRunWhenArchiveSkipped(t *testing.T) { assertExists(t, seed.runWorkDir) } -func TestPostArchiveCleanupNotRunWhenCurrentPointerMissing(t *testing.T) { +func TestPostPublishCleanupNotRunWhenCurrentPointerMissing(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true - if _, err := executeStages(context.Background(), cfg, []stage.Stage{archiveSuccessStage{metadata: map[string]any{"current_pointer_written": false}}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { + if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{metadata: map[string]any{"current_pointer_written": false}}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } @@ -142,13 +142,13 @@ func TestPostArchiveCleanupNotRunWhenCurrentPointerMissing(t *testing.T) { assertExists(t, seed.runWorkDir) } -func TestPostArchiveCleanupNotRunWhenArchiveUploadDisabled(t *testing.T) { +func TestPostPublishCleanupNotRunWhenPublishUploadDisabled(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true cfg.Pipeline.Publish.UploadRun = boolPtr(false) - if _, err := executeStages(context.Background(), cfg, []stage.Stage{archiveSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { + if _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}); err != nil { t.Fatalf("executeStages() error = %v", err) } @@ -156,12 +156,12 @@ func TestPostArchiveCleanupNotRunWhenArchiveUploadDisabled(t *testing.T) { assertExists(t, seed.runWorkDir) } -func TestPostArchiveCleanupWaitsUntilAllStagesSucceed(t *testing.T) { +func TestPostPublishCleanupWaitsUntilAllStagesSucceed(t *testing.T) { cfg, seed := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true - _, err := executeStages(context.Background(), cfg, []stage.Stage{archiveSuccessStage{}, notifyFailStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) + _, err := executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{}, notifyFailStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) if err == nil || !strings.Contains(err.Error(), "stage \"notify\" failed") { t.Fatalf("executeStages() error = %v, want notify failure", err) } @@ -170,7 +170,7 @@ func TestPostArchiveCleanupWaitsUntilAllStagesSucceed(t *testing.T) { assertExists(t, seed.runWorkDir) } -func TestPostArchiveCleanupFailsOnUnsafePath(t *testing.T) { +func TestPostPublishCleanupFailsOnUnsafePath(t *testing.T) { cfg, _ := cleanupFixtureConfig(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = false @@ -186,25 +186,25 @@ func TestPostArchiveCleanupFailsOnUnsafePath(t *testing.T) { t.Fatalf("Save() error = %v", err) } - _, err = executeStages(context.Background(), cfg, []stage.Stage{archiveSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) + _, err = executeStages(context.Background(), cfg, []stage.Stage{publishSuccessStage{}}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) if err == nil || !strings.Contains(err.Error(), "refusing to delete path outside root") { t.Fatalf("executeStages() error = %v, want safe-path failure", err) } } -func TestPostArchiveCleanupNotRunWhenPromotionIsMissing(t *testing.T) { - cfg, seed, runID := archiveStageCleanupFixture(t) +func TestPostPublishCleanupNotRunWhenOutputIsMissing(t *testing.T) { + cfg, seed, runID := publishStageCleanupFixture(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true cfg.Pipeline.Publish.Outputs = []config.PublishOutputRule{ {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)}, } - archiveStageImpl, err := stage.Select("publish") + publishStageImpl, err := stage.Select("publish") if err != nil { 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{publishStageImpl}, RunOptions{Env: &Env{ObjectStore: &storage.FakeBackend{}}}) if err == nil || !strings.Contains(err.Error(), "required output source unavailable") { t.Fatalf("executeStages() error = %v, want required output source unavailable failure", err) } @@ -215,17 +215,17 @@ func TestPostArchiveCleanupNotRunWhenPromotionIsMissing(t *testing.T) { assertExists(t, artifacts.SessionRunRootForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, runID)) } -func TestPostArchiveCleanupNotRunWhenCurrentManifestUploadFails(t *testing.T) { - cfg, seed, _ := archiveStageCleanupFixture(t) +func TestPostPublishCleanupNotRunWhenCurrentManifestUploadFails(t *testing.T) { + cfg, seed, _ := publishStageCleanupFixture(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true failKey := seed.sessionPrefix + "current/manifest.json" - archiveStageImpl, err := stage.Select("publish") + publishStageImpl, err := stage.Select("publish") if err != nil { t.Fatalf("Select(publish) error = %v", err) } - _, err = executeStages(context.Background(), cfg, []stage.Stage{archiveStageImpl}, RunOptions{ + _, err = executeStages(context.Background(), cfg, []stage.Stage{publishStageImpl}, RunOptions{ Env: &Env{ObjectStore: &failKeyStore{delegate: &storage.FakeBackend{}, failKey: failKey}}, }) if err == nil || !strings.Contains(err.Error(), "current manifest") { @@ -236,17 +236,17 @@ func TestPostArchiveCleanupNotRunWhenCurrentManifestUploadFails(t *testing.T) { assertExists(t, seed.runWorkDir) } -func TestPostArchiveCleanupNotRunWhenCurrentPointerUploadFails(t *testing.T) { - cfg, seed, _ := archiveStageCleanupFixture(t) +func TestPostPublishCleanupNotRunWhenCurrentPointerUploadFails(t *testing.T) { + cfg, seed, _ := publishStageCleanupFixture(t) cfg.Pipeline.Spool.DeleteAudioAfterPublish = true cfg.Pipeline.Workspace.CleanupAfterPublish = true failKey := seed.sessionPrefix + "current/run_id.txt" - archiveStageImpl, err := stage.Select("publish") + publishStageImpl, err := stage.Select("publish") if err != nil { t.Fatalf("Select(publish) error = %v", err) } - _, err = executeStages(context.Background(), cfg, []stage.Stage{archiveStageImpl}, RunOptions{ + _, err = executeStages(context.Background(), cfg, []stage.Stage{publishStageImpl}, RunOptions{ Env: &Env{ObjectStore: &failKeyStore{delegate: &storage.FakeBackend{}, failKey: failKey}}, }) if err == nil || !strings.Contains(err.Error(), "current run pointer") { @@ -320,7 +320,7 @@ func cleanupFixtureConfig(t *testing.T) (*config.Config, cleanupSeed) { } } -func archiveStageCleanupFixture(t *testing.T) (*config.Config, cleanupSeed, string) { +func publishStageCleanupFixture(t *testing.T) (*config.Config, cleanupSeed, string) { t.Helper() cfg, seed := cleanupFixtureConfig(t) @@ -344,7 +344,7 @@ func archiveStageCleanupFixture(t *testing.T) (*config.Config, cleanupSeed, stri }, }, } - writeArchiveFixtureRunFiles( + writePublishFixtureRunFiles( t, seed.runWorkDir, artifacts.SessionWorkDirForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID), @@ -367,7 +367,7 @@ func archiveStageCleanupFixture(t *testing.T) (*config.Config, cleanupSeed, stri return cfg, seed, runID } -func writeArchiveFixtureRunFiles(t *testing.T, runWorkDir, sessionRoot string) { +func writePublishFixtureRunFiles(t *testing.T, runWorkDir, sessionRoot string) { t.Helper() mustWriteFile(t, filepath.Join(runWorkDir, "prepare", "inputs", "session.yml"), "session_id: 2026-05-03\n") mustWriteFile(t, filepath.Join(runWorkDir, "transcribe", "outputs", "transcripts", "raw", "speaker.json"), "{}\n") diff --git a/internal/app/remote_locks.go b/internal/app/remote_locks.go index 845d45b..bf4d9b3 100644 --- a/internal/app/remote_locks.go +++ b/internal/app/remote_locks.go @@ -63,7 +63,7 @@ func loadRemoteLockStore(ctx context.Context, cfg *config.Config, store storage. } func loadEffectiveLocks(ctx context.Context, cfg *config.Config, store storage.ObjectStore) (*effectiveLocks, error) { - staticLocks := staticArchiveLocks(cfg) + staticLocks := staticPublishLocks(cfg) if store == nil { return &effectiveLocks{ Static: staticLocks, @@ -83,7 +83,7 @@ func loadEffectiveLocks(ctx context.Context, cfg *config.Config, store storage.O }, nil } -func staticArchiveLocks(cfg *config.Config) []config.PublishLockRule { +func staticPublishLocks(cfg *config.Config) []config.PublishLockRule { if cfg == nil || cfg.Pipeline == nil || cfg.Pipeline.Publish == nil { return nil } diff --git a/internal/app/restore_discovery.go b/internal/app/restore_discovery.go index ed9cead..4e590cd 100644 --- a/internal/app/restore_discovery.go +++ b/internal/app/restore_discovery.go @@ -12,7 +12,7 @@ import ( "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) -// RemoteCurrentState captures discovered committed remote archive state for one session. +// RemoteCurrentState captures discovered committed remote published current state for one session. type RemoteCurrentState struct { Bucket string SessionPrefix string @@ -32,15 +32,15 @@ func discoverRemoteCurrentState(ctx context.Context, cfg *config.Config, store s return nil, fmt.Errorf("remote object store is required") } - bucket := artifacts.ResolveArchiveBucket(cfg, nil) + bucket := artifacts.ResolvePublishBucket(cfg, nil) if strings.TrimSpace(bucket) == "" { - return nil, fmt.Errorf("archive bucket is required") + return nil, fmt.Errorf("publish bucket is required") } - sessionPrefix, err := artifacts.ResolveArchiveSessionPrefix(cfg, nil) + sessionPrefix, err := artifacts.ResolvePublishSessionPrefix(cfg, nil) if err != nil { - return nil, fmt.Errorf("resolve archive session prefix: %w", err) + return nil, fmt.Errorf("resolve publish session prefix: %w", err) } - currentManifestKey, currentRunIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) + currentManifestKey, currentRunIDKey := artifacts.ResolveCurrentStateKeys(sessionPrefix) exists, err := store.Exists(ctx, currentRunIDKey) if err != nil { diff --git a/internal/app/restore_discovery_test.go b/internal/app/restore_discovery_test.go index d2f8185..4ea32c9 100644 --- a/internal/app/restore_discovery_test.go +++ b/internal/app/restore_discovery_test.go @@ -208,7 +208,7 @@ func restoreDiscoveryConfig() *config.Config { func restoreDiscoveryKeys(cfg *config.Config) (sessionPrefix, manifestKey, runIDKey string) { sessionPrefix = artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID) - manifestKey, runIDKey = artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) + manifestKey, runIDKey = artifacts.ResolveCurrentStateKeys(sessionPrefix) return sessionPrefix, manifestKey, runIDKey } diff --git a/internal/app/restore_execution_test.go b/internal/app/restore_execution_test.go index a144d2f..e75fa56 100644 --- a/internal/app/restore_execution_test.go +++ b/internal/app/restore_execution_test.go @@ -427,7 +427,7 @@ func seedRestoreCommittedState(t *testing.T, fake *storage.FakeBackend, pipeline } sessionPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.SessionID) - manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) + manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(sessionPrefix) seedRestoreObject(fake, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n")) seedRestoreObject(fake, manifestKey, restoreManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)) @@ -465,7 +465,7 @@ func seedRestorePreviousCurrent(t *testing.T, fake *storage.FakeBackend, cfg *co func seedRestorePreviousCurrentManifestOnly(t *testing.T, fake *storage.FakeBackend, cfg *config.Config) { t.Helper() previousPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID) - manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(previousPrefix) + manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(previousPrefix) previousRunID := "20260426T010203Z-a1b2c3d4" seedRestoreObject(fake, runIDKey, []byte(previousRunID+"\n")) diff --git a/internal/app/restore_plan_test.go b/internal/app/restore_plan_test.go index 1737c86..26243c5 100644 --- a/internal/app/restore_plan_test.go +++ b/internal/app/restore_plan_test.go @@ -325,7 +325,7 @@ func configureRestorePlanPreviousRequirement(cfg *config.Config, required bool) func restorePlanCurrentState(t *testing.T, cfg *config.Config) *RemoteCurrentState { t.Helper() sessionPrefix := artifacts.S3SessionPrefix("dnd", cfg.Session.Campaign, cfg.Session.SessionID) - manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) + manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(sessionPrefix) return &RemoteCurrentState{ Bucket: "test-bucket", SessionPrefix: sessionPrefix, diff --git a/internal/app/runner.go b/internal/app/runner.go index e4483b0..6a7d3ab 100644 --- a/internal/app/runner.go +++ b/internal/app/runner.go @@ -96,7 +96,7 @@ func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage if needsRemoteLocksForRun(env.Config, stages) { locks, err := loadEffectiveLocks(ctx, env.Config, env.ObjectStore) if err != nil { - return nil, fmt.Errorf("load remote archive locks: %w", err) + return nil, fmt.Errorf("load remote publish locks: %w", err) } applyEffectiveLocks(env.Config, locks.All) } @@ -234,14 +234,14 @@ func executeStages(ctx context.Context, cfg *config.Config, stages []stage.Stage env.Logger.Info("stage succeeded", "stage", s.Name()) } - if err := runPostArchiveCleanup(ctx, env, manifestPath, m, executed); err != nil { + if err := runPostPublishCleanup(ctx, env, manifestPath, m, executed); err != nil { failedAt := nowUTC() runManifest.MarkFailed(failedAt, err.Error()) syncRunManifestIdentityFromSession(m, runManifest) if saveErr := runManifestStore.SaveRun(ctx, runManifestPath, runManifest); saveErr != nil { - return nil, fmt.Errorf("post-archive cleanup failed (%v) and run-manifest save failed (%v)", err, saveErr) + return nil, fmt.Errorf("post-publish cleanup failed (%v) and run-manifest save failed (%v)", err, saveErr) } - return nil, fmt.Errorf("post-archive cleanup: %w", err) + return nil, fmt.Errorf("post-publish cleanup: %w", err) } completedAt := nowUTC() runManifest.MarkSucceeded(completedAt) diff --git a/internal/app/runner_test.go b/internal/app/runner_test.go index 4141276..3085b03 100644 --- a/internal/app/runner_test.go +++ b/internal/app/runner_test.go @@ -248,7 +248,7 @@ func TestNeedsObjectStoreForRunPrepareWithPreviousRequirements(t *testing.T) { } } -func TestExecuteStagesArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) { +func TestExecuteStagesPublishSkipsRequiredUnselectedConfiguredOutput(t *testing.T) { cfg := testConfig(t) cfg.Pipeline.Storage.S3 = &config.StorageS3Config{ Bucket: "my-dnd-archive", @@ -283,7 +283,7 @@ func TestExecuteStagesArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testi t.Fatalf("Save manifest error = %v", err) } - archiveStageImpl, err := stage.Select("publish") + publishStageImpl, err := stage.Select("publish") if err != nil { t.Fatalf("Select(publish) error = %v", err) } @@ -293,7 +293,7 @@ func TestExecuteStagesArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testi cfg, []stage.Stage{ selectedAnalyzeArtifactStage{expected: []string{"player_handout"}}, - archiveStageImpl, + publishStageImpl, }, RunOptions{ SelectedArtifacts: []string{"player_handout"}, @@ -304,7 +304,7 @@ func TestExecuteStagesArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testi t.Fatalf("executeStages() error = %v", err) } if len(summary.Executed) != 2 || summary.Executed[0] != "analyze" || summary.Executed[1] != "publish" { - t.Fatalf("executed = %#v, want analyze and archive", summary.Executed) + t.Fatalf("executed = %#v, want analyze and publish", summary.Executed) } loadedManifest, err := store.Load(context.Background(), summary.ManifestPath) @@ -321,7 +321,7 @@ func TestExecuteStagesArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testi t.Fatalf("skipped item = %#v, want object", skipped[0]) } if item["source"] != "narratio.artifact.session_recap" || item["dest"] != "artifacts/session_recap.md" || item["required"] != true { - t.Fatalf("skipped item = %#v, want required session_recap promotion", item) + t.Fatalf("skipped item = %#v, want required session_recap published output", item) } } @@ -427,10 +427,10 @@ func TestExecuteStagesPlaceholderSuccessUpdatesManifest(t *testing.T) { } if name == "publish" { if sr.Metadata == nil || sr.Metadata["stage"] != "publish" { - t.Fatalf("archive metadata missing stage=publish: %#v", sr.Metadata) + t.Fatalf("publish metadata missing stage=publish: %#v", sr.Metadata) } if sr.Metadata["skipped"] != true { - t.Fatalf("archive metadata missing skipped=true for test config without archive section: %#v", sr.Metadata) + t.Fatalf("publish metadata missing skipped=true for test config without publish section: %#v", sr.Metadata) } continue } @@ -729,7 +729,7 @@ func TestExecuteStagesRunManifestRecordsSkippedStage(t *testing.T) { } } -func TestExecuteStagesRunLocalArtifactsAndCanonicalPromotion(t *testing.T) { +func TestExecuteStagesRunLocalArtifactsAndCanonicalSync(t *testing.T) { cfg := testConfig(t) stages := []stage.Stage{ BuildFullPlan()[0], // prepare @@ -778,7 +778,7 @@ func TestExecuteStagesRunLocalArtifactsAndCanonicalPromotion(t *testing.T) { } for _, p := range canonicalChecks { if _, statErr := os.Stat(p); statErr != nil { - t.Fatalf("canonical promoted artifact missing at %q: %v", p, statErr) + t.Fatalf("canonical published artifact missing at %q: %v", p, statErr) } } @@ -864,7 +864,7 @@ func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) { {name: "merge", env: &Env{Seriatim: &seriatim.FakeRunner{Err: errors.New("merge fail")}}}, {name: "polish", env: &Env{Audita: &audita.FakeRunner{Err: errors.New("polish fail")}}}, {name: "analyze", env: &Env{Scriptorium: &scriptorium.FakeRunner{RunErr: errors.New("analyze fail")}}}, - {name: "publish", env: &Env{ObjectStore: &storage.FakeBackend{UploadErr: errors.New("archive fail")}}}, + {name: "publish", env: &Env{ObjectStore: &storage.FakeBackend{UploadErr: errors.New("publish fail")}}}, {name: "notify", env: &Env{Notifier: ¬ify.FakeSender{Err: errors.New("notify fail")}}}, } @@ -961,13 +961,13 @@ func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) { runID := "20260516T010203Z-0a1b2c3d" runWorkDir := filepath.Join(cfg.Pipeline.Workspace.Root, "work", cfg.Session.Campaign, cfg.Session.SessionID, runID) if err := os.MkdirAll(filepath.Join(runWorkDir, "inputs"), 0o755); err != nil { - t.Fatalf("mkdir archive inputs dir: %v", err) + t.Fatalf("mkdir publish inputs dir: %v", err) } if err := os.WriteFile(filepath.Join(runWorkDir, "inputs", "session.yml"), []byte("session_id: 2026-05-03\n"), 0o644); err != nil { - t.Fatalf("write archive fixture session.yml: %v", err) + t.Fatalf("write publish fixture session.yml: %v", err) } if err := os.WriteFile(filepath.Join(runWorkDir, "manifest.json"), []byte("{}\n"), 0o644); err != nil { - t.Fatalf("write archive fixture manifest.json: %v", err) + t.Fatalf("write publish fixture manifest.json: %v", err) } seed := manifest.New(cfg.Session.SessionID, time.Now().UTC()) @@ -981,7 +981,7 @@ func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) { seed.MarkStageSucceeded(name, time.Now().UTC(), nil) } if err := tc.env.ManifestStore.Save(context.Background(), manifestPathFor(cfg), seed); err != nil { - t.Fatalf("seed archive manifest: %v", err) + t.Fatalf("seed publish manifest: %v", err) } } diff --git a/internal/app/session_oriented_cli_test.go b/internal/app/session_oriented_cli_test.go index 1313f08..4dd8492 100644 --- a/internal/app/session_oriented_cli_test.go +++ b/internal/app/session_oriented_cli_test.go @@ -212,7 +212,7 @@ func TestExecuteSessionSubcommandsAcceptPositionalSessionID(t *testing.T) { pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03") - manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) + manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(sessionPrefix) 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")}) var storeInitCalls int diff --git a/internal/artifacts/paths_model_test.go b/internal/artifacts/paths_model_test.go index fcb53f5..54ca020 100644 --- a/internal/artifacts/paths_model_test.go +++ b/internal/artifacts/paths_model_test.go @@ -75,9 +75,9 @@ func TestSessionPreviousPathsForCampaign(t *testing.T) { t.Fatalf("SessionPreviousArtifactPathForCampaign() = %q, want %q", artifactPath, wantArtifactPath) } - archiveRelativeArtifactPath := SessionPreviousArtifactPathForCampaign(root, "forsaken", "2026-04-19", "artifacts/session_recap.md") - if archiveRelativeArtifactPath != wantArtifactPath { - t.Fatalf("SessionPreviousArtifactPathForCampaign(archive-relative) = %q, want %q", archiveRelativeArtifactPath, wantArtifactPath) + previousRelativeArtifactPath := SessionPreviousArtifactPathForCampaign(root, "forsaken", "2026-04-19", "artifacts/session_recap.md") + if previousRelativeArtifactPath != wantArtifactPath { + t.Fatalf("SessionPreviousArtifactPathForCampaign(previous-relative) = %q, want %q", previousRelativeArtifactPath, wantArtifactPath) } } @@ -102,7 +102,7 @@ func TestSessionPreviousPathsFromSessionPaths(t *testing.T) { got = SessionPreviousArtifactPath(paths, "artifacts/session_recap.md") want = filepath.Join(paths.PreviousArtifactsDir, "session_recap.md") if got != want { - t.Fatalf("SessionPreviousArtifactPath(archive-relative) = %q, want %q", got, want) + t.Fatalf("SessionPreviousArtifactPath(previous-relative) = %q, want %q", got, want) } } diff --git a/internal/artifacts/archive_identity.go b/internal/artifacts/publish_identity.go similarity index 74% rename from internal/artifacts/archive_identity.go rename to internal/artifacts/publish_identity.go index 1f6a6e1..aee094b 100644 --- a/internal/artifacts/archive_identity.go +++ b/internal/artifacts/publish_identity.go @@ -8,8 +8,8 @@ import ( "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) -// ResolveArchiveBucket resolves archive bucket identity with manifest-first precedence. -func ResolveArchiveBucket(cfg *config.Config, m *manifest.Manifest) string { +// ResolvePublishBucket resolves publish bucket identity with manifest-first precedence. +func ResolvePublishBucket(cfg *config.Config, m *manifest.Manifest) string { if m != nil && strings.TrimSpace(m.S3Bucket) != "" { return strings.TrimSpace(m.S3Bucket) } @@ -19,8 +19,8 @@ func ResolveArchiveBucket(cfg *config.Config, m *manifest.Manifest) string { return strings.TrimSpace(cfg.Pipeline.Storage.S3.Bucket) } -// ResolveArchiveSessionPrefix resolves archive session prefix with manifest-first precedence. -func ResolveArchiveSessionPrefix(cfg *config.Config, m *manifest.Manifest) (string, error) { +// ResolvePublishSessionPrefix resolves publish session prefix with manifest-first precedence. +func ResolvePublishSessionPrefix(cfg *config.Config, m *manifest.Manifest) (string, error) { if m != nil && strings.TrimSpace(m.S3SessionPrefix) != "" { return strings.TrimSpace(m.S3SessionPrefix), nil } @@ -47,8 +47,8 @@ func ResolveArchiveSessionPrefix(cfg *config.Config, m *manifest.Manifest) (stri return sessionPrefix, nil } -// ResolveArchiveRunPrefix resolves archive run prefix with manifest-first precedence. -func ResolveArchiveRunPrefix(cfg *config.Config, m *manifest.Manifest) (string, error) { +// ResolvePublishRunPrefix resolves publish run prefix with manifest-first precedence. +func ResolvePublishRunPrefix(cfg *config.Config, m *manifest.Manifest) (string, error) { if m != nil { runPrefix := strings.TrimSpace(m.S3RunPrefix) if runPrefix != "" { @@ -56,7 +56,7 @@ func ResolveArchiveRunPrefix(cfg *config.Config, m *manifest.Manifest) (string, } } - sessionPrefix, err := ResolveArchiveSessionPrefix(cfg, m) + sessionPrefix, err := ResolvePublishSessionPrefix(cfg, m) if err != nil { return "", err } @@ -71,7 +71,7 @@ func ResolveArchiveRunPrefix(cfg *config.Config, m *manifest.Manifest) (string, return S3RunPrefix(sessionPrefix, runID), nil } -// ResolveArchiveCurrentStateKeys returns current pointer keys for a session prefix. -func ResolveArchiveCurrentStateKeys(sessionPrefix string) (manifestKey, runIDKey string) { +// ResolveCurrentStateKeys returns current pointer keys for a session prefix. +func ResolveCurrentStateKeys(sessionPrefix string) (manifestKey, runIDKey string) { return S3CurrentManifestKey(sessionPrefix), S3CurrentRunPointerKey(sessionPrefix) } diff --git a/internal/artifacts/archive_identity_test.go b/internal/artifacts/publish_identity_test.go similarity index 72% rename from internal/artifacts/archive_identity_test.go rename to internal/artifacts/publish_identity_test.go index 27ae8f1..7dc90dc 100644 --- a/internal/artifacts/archive_identity_test.go +++ b/internal/artifacts/publish_identity_test.go @@ -8,7 +8,7 @@ import ( "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) -func TestResolveArchiveBucketPrefersManifestThenConfig(t *testing.T) { +func TestResolvePublishBucketPrefersManifestThenConfig(t *testing.T) { cfg := &config.Config{ Pipeline: &config.PipelineConfig{ Storage: config.StorageConfig{ @@ -17,15 +17,15 @@ func TestResolveArchiveBucketPrefersManifestThenConfig(t *testing.T) { }, } - if got := ResolveArchiveBucket(cfg, &manifest.Manifest{S3Bucket: "manifest-bucket"}); got != "manifest-bucket" { + if got := ResolvePublishBucket(cfg, &manifest.Manifest{S3Bucket: "manifest-bucket"}); got != "manifest-bucket" { t.Fatalf("bucket = %q, want manifest-bucket", got) } - if got := ResolveArchiveBucket(cfg, &manifest.Manifest{}); got != "cfg-bucket" { + if got := ResolvePublishBucket(cfg, &manifest.Manifest{}); got != "cfg-bucket" { t.Fatalf("bucket = %q, want cfg-bucket", got) } } -func TestResolveArchiveSessionPrefixPrefersManifestThenConfig(t *testing.T) { +func TestResolvePublishSessionPrefixPrefersManifestThenConfig(t *testing.T) { cfg := &config.Config{ Pipeline: &config.PipelineConfig{ Storage: config.StorageConfig{ @@ -39,17 +39,17 @@ func TestResolveArchiveSessionPrefixPrefersManifestThenConfig(t *testing.T) { } m := &manifest.Manifest{S3SessionPrefix: "manifest/session/prefix/"} - got, err := ResolveArchiveSessionPrefix(cfg, m) + got, err := ResolvePublishSessionPrefix(cfg, m) if err != nil { - t.Fatalf("ResolveArchiveSessionPrefix() error = %v", err) + t.Fatalf("ResolvePublishSessionPrefix() error = %v", err) } if got != "manifest/session/prefix/" { t.Fatalf("session prefix = %q, want manifest/session/prefix/", got) } - got, err = ResolveArchiveSessionPrefix(cfg, &manifest.Manifest{}) + got, err = ResolvePublishSessionPrefix(cfg, &manifest.Manifest{}) if err != nil { - t.Fatalf("ResolveArchiveSessionPrefix() error = %v", err) + t.Fatalf("ResolvePublishSessionPrefix() error = %v", err) } want := "dnd/campaigns/forsaken/sessions/2026-04-19/" if got != want { @@ -57,7 +57,7 @@ func TestResolveArchiveSessionPrefixPrefersManifestThenConfig(t *testing.T) { } } -func TestResolveArchiveRunPrefixPrefersManifestThenDerived(t *testing.T) { +func TestResolvePublishRunPrefixPrefersManifestThenDerived(t *testing.T) { cfg := &config.Config{ Pipeline: &config.PipelineConfig{ Storage: config.StorageConfig{ @@ -74,9 +74,9 @@ func TestResolveArchiveRunPrefixPrefersManifestThenDerived(t *testing.T) { RunID: "20260516T010203Z-1a2b3c4d", S3RunPrefix: "manifest/run/prefix/", } - got, err := ResolveArchiveRunPrefix(cfg, m) + got, err := ResolvePublishRunPrefix(cfg, m) if err != nil { - t.Fatalf("ResolveArchiveRunPrefix() error = %v", err) + t.Fatalf("ResolvePublishRunPrefix() error = %v", err) } if got != "manifest/run/prefix/" { t.Fatalf("run prefix = %q, want manifest/run/prefix/", got) @@ -85,9 +85,9 @@ func TestResolveArchiveRunPrefixPrefersManifestThenDerived(t *testing.T) { m = &manifest.Manifest{ RunID: "20260516T010203Z-1a2b3c4d", } - got, err = ResolveArchiveRunPrefix(cfg, m) + got, err = ResolvePublishRunPrefix(cfg, m) if err != nil { - t.Fatalf("ResolveArchiveRunPrefix() error = %v", err) + t.Fatalf("ResolvePublishRunPrefix() error = %v", err) } want := "dnd/campaigns/forsaken/sessions/2026-04-19/runs/20260516T010203Z-1a2b3c4d/" if got != want { @@ -95,12 +95,12 @@ func TestResolveArchiveRunPrefixPrefersManifestThenDerived(t *testing.T) { } } -func TestResolveArchiveIdentityErrorsAreDeterministic(t *testing.T) { +func TestResolvePublishIdentityErrorsAreDeterministic(t *testing.T) { cfgNoS3 := &config.Config{ Pipeline: &config.PipelineConfig{}, Session: &config.SessionConfig{SessionID: "2026-04-19", Campaign: "forsaken"}, } - _, err := ResolveArchiveSessionPrefix(cfgNoS3, &manifest.Manifest{}) + _, err := ResolvePublishSessionPrefix(cfgNoS3, &manifest.Manifest{}) if err == nil || !strings.Contains(err.Error(), "pipeline.storage.s3 configuration is required") { t.Fatalf("error = %v, want missing storage.s3", err) } @@ -113,14 +113,14 @@ func TestResolveArchiveIdentityErrorsAreDeterministic(t *testing.T) { }, Session: &config.SessionConfig{SessionID: "2026-04-19", Campaign: "forsaken"}, } - _, err = ResolveArchiveRunPrefix(cfg, &manifest.Manifest{}) + _, err = ResolvePublishRunPrefix(cfg, &manifest.Manifest{}) if err == nil || !strings.Contains(err.Error(), "run id is required") { t.Fatalf("error = %v, want missing run id", err) } } -func TestResolveArchiveCurrentStateKeys(t *testing.T) { - manifestKey, runIDKey := ResolveArchiveCurrentStateKeys("dnd/campaigns/forsaken/sessions/2026-04-19/") +func TestResolveCurrentStateKeys(t *testing.T) { + manifestKey, runIDKey := ResolveCurrentStateKeys("dnd/campaigns/forsaken/sessions/2026-04-19/") if manifestKey != "dnd/campaigns/forsaken/sessions/2026-04-19/current/manifest.json" { t.Fatalf("manifest key = %q", manifestKey) } diff --git a/internal/artifacts/s3_keys_test.go b/internal/artifacts/s3_keys_test.go index 25fa8b3..836d149 100644 --- a/internal/artifacts/s3_keys_test.go +++ b/internal/artifacts/s3_keys_test.go @@ -43,9 +43,9 @@ func TestS3KeyConstruction(t *testing.T) { t.Fatalf("manifest key = %q", manifestKey) } - promoted := S3PublishedOutputKey(sessionPrefix, "transcripts/final.trimmed.json") - if promoted != "dnd/campaigns/forsaken/sessions/2026-04-19/transcripts/final.trimmed.json" { - t.Fatalf("promoted key = %q", promoted) + publishedKey := S3PublishedOutputKey(sessionPrefix, "transcripts/final.trimmed.json") + if publishedKey != "dnd/campaigns/forsaken/sessions/2026-04-19/transcripts/final.trimmed.json" { + t.Fatalf("published key = %q", publishedKey) } runRelative := S3RunRelativeDestinationKey(runPrefix, `logs\whisperx.stdout.log`) diff --git a/internal/config/storage_archive_test.go b/internal/config/storage_publish_test.go similarity index 95% rename from internal/config/storage_archive_test.go rename to internal/config/storage_publish_test.go index 6526052..013b79f 100644 --- a/internal/config/storage_archive_test.go +++ b/internal/config/storage_publish_test.go @@ -153,7 +153,7 @@ storage: } } -func TestSpoolAndArchiveDefaults(t *testing.T) { +func TestSpoolAndPublishDefaults(t *testing.T) { pipelinePath, sessionPath := writeConfigFiles(t, testPipelineBaseYAML, testSessionBaseYAML) cfg, err := Load(pipelinePath, sessionPath) @@ -171,13 +171,13 @@ func TestSpoolAndArchiveDefaults(t *testing.T) { t.Fatalf("workspace.cleanup_after_publish = true, want false") } if cfg.Pipeline.Publish == nil { - t.Fatal("archive should be initialized by defaults") + t.Fatal("publish should be initialized by defaults") } if cfg.Pipeline.Publish.Enabled == nil || !*cfg.Pipeline.Publish.Enabled { - t.Fatalf("archive.enabled = %#v, want true", cfg.Pipeline.Publish.Enabled) + t.Fatalf("publish.enabled = %#v, want true", cfg.Pipeline.Publish.Enabled) } if cfg.Pipeline.Publish.UploadRun == nil || !*cfg.Pipeline.Publish.UploadRun { - t.Fatalf("archive.upload_run = %#v, want true", cfg.Pipeline.Publish.UploadRun) + t.Fatalf("publish.upload_run = %#v, want true", cfg.Pipeline.Publish.UploadRun) } if len(cfg.Pipeline.Publish.Outputs) != 1 { t.Fatalf("publish.outputs len = %d, want 1 default", len(cfg.Pipeline.Publish.Outputs)) @@ -194,7 +194,7 @@ func TestSpoolAndArchiveDefaults(t *testing.T) { } } -func TestArchivePromotionValidation(t *testing.T) { +func TestPublishOutputValidation(t *testing.T) { tests := []struct { name string ruleYML string @@ -278,7 +278,7 @@ publish: } } -func TestArchivePromotionLegacyTranscriptSourcesRejected(t *testing.T) { +func TestPublishOutputLegacyTranscriptSourcesRejected(t *testing.T) { legacyTranscriptSources := []string{ "narratio.transcript." + "merged", "narratio.transcript." + "full", @@ -307,7 +307,7 @@ publish: } } -func TestArchivePromotionDerivesDestinationWhenOmitted(t *testing.T) { +func TestPublishOutputDerivesDestinationWhenOmitted(t *testing.T) { tests := []struct { name string pipelineYML string @@ -359,7 +359,7 @@ publish: } } -func TestArchiveLockValidation(t *testing.T) { +func TestPublishLockValidation(t *testing.T) { tests := []struct { name string pipelineYML string @@ -439,7 +439,7 @@ publish: } } -func TestArchiveLockLegacyTranscriptSourcesRejected(t *testing.T) { +func TestPublishLockLegacyTranscriptSourcesRejected(t *testing.T) { legacyTranscriptSources := []string{ "narratio.transcript." + "merged", "narratio.transcript." + "full", @@ -467,7 +467,7 @@ publish: } } -func TestArchiveLockUnknownFieldFailsStrictDecode(t *testing.T) { +func TestPublishLockUnknownFieldFailsStrictDecode(t *testing.T) { pipelineYAML := testPipelineBaseYAML + ` publish: locks: @@ -481,7 +481,7 @@ publish: } } -func TestArchiveLegacyFromToFailsStrictDecode(t *testing.T) { +func TestPublishLegacyFromToFailsStrictDecode(t *testing.T) { pipelineYAML := testPipelineBaseYAML + ` publish: outputs: @@ -495,7 +495,7 @@ publish: } } -func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) { +func TestPublishLockStoreBytesStrictDecodeAndValidation(t *testing.T) { store, err := LoadPublishLockStoreBytes("locks.yml", []byte(`locks: - source: narratio.transcript.final_trimmed reason: reviewed @@ -524,7 +524,7 @@ func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) { } } -func TestMergeArchiveLockRulesStaticWins(t *testing.T) { +func TestMergePublishLockRulesStaticWins(t *testing.T) { merged := MergePublishLockRules( []PublishLockRule{{Source: "narratio.transcript.final_trimmed", Reason: "static"}}, []PublishLockRule{ diff --git a/internal/previouscache/previouscache.go b/internal/previouscache/previouscache.go index 0e1a733..cba048f 100644 --- a/internal/previouscache/previouscache.go +++ b/internal/previouscache/previouscache.go @@ -92,7 +92,7 @@ func BuildPlan( campaign, previousSessionID, ) - currentManifestKey, currentRunIDKey := artifacts.ResolveArchiveCurrentStateKeys(previousSessionPrefix) + currentManifestKey, currentRunIDKey := artifacts.ResolveCurrentStateKeys(previousSessionPrefix) result := &Plan{} @@ -190,7 +190,7 @@ func BuildPlan( if len(candidates) == 0 { if requirement.Required { return nil, fmt.Errorf( - "required previous-session artifact %q is unavailable in previous-session manifest/archive", + "required previous-session artifact %q is unavailable in previous-session manifest/published-state", requirement.Name, ) } @@ -216,7 +216,7 @@ func BuildPlan( if selectedRel == "" { if requirement.Required { return nil, fmt.Errorf( - "required previous-session artifact %q object missing from archive candidate keys", + "required previous-session artifact %q object missing from published candidate keys", requirement.Name, ) } diff --git a/internal/previouscache/previouscache_test.go b/internal/previouscache/previouscache_test.go index 2b5cd4e..0a44e17 100644 --- a/internal/previouscache/previouscache_test.go +++ b/internal/previouscache/previouscache_test.go @@ -134,7 +134,7 @@ func previousCacheTestConfig(t *testing.T) (*config.Config, artifacts.SessionPat func seedPreviousCurrent(t *testing.T, store *storage.FakeBackend, cfg *config.Config, m *manifest.Manifest) { t.Helper() previousPrefix := artifacts.S3SessionPrefix(cfg.Pipeline.Storage.S3.RootPrefix, cfg.Session.Campaign, cfg.Session.PreviousSessionID) - manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(previousPrefix) + manifestKey, runIDKey := artifacts.ResolveCurrentStateKeys(previousPrefix) store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("previous-run\n")}) data, err := marshalManifestForPreviousCacheTest(m) if err != nil { @@ -143,7 +143,7 @@ func seedPreviousCurrent(t *testing.T, store *storage.FakeBackend, cfg *config.C store.SeedObject(storage.FakeObject{Key: manifestKey, Data: data}) } -func previousManifestWithOutput(t *testing.T, cfg *config.Config, rel string, promoted []string) *manifest.Manifest { +func previousManifestWithOutput(t *testing.T, cfg *config.Config, rel string, publishedPaths []string) *manifest.Manifest { t.Helper() m := manifest.New(cfg.Session.PreviousSessionID, time.Date(2026, 4, 26, 10, 0, 0, 0, time.UTC)) m.Campaign = cfg.Session.Campaign @@ -154,11 +154,11 @@ func previousManifestWithOutput(t *testing.T, cfg *config.Config, rel string, pr {SourceID: "narratio.artifact.session_recap", LocalPath: filepath.Join(filepath.Dir(filepath.Dir(m.LocalWorkDir)), filepath.FromSlash(rel))}, }) } - if promoted != nil { + if publishedPaths != nil { if m.Stages["publish"] == nil { m.MarkStageSucceeded("publish", time.Date(2026, 4, 26, 10, 2, 0, 0, time.UTC), nil) } - m.Stages["publish"].Metadata = map[string]any{"published_paths": promoted} + m.Stages["publish"].Metadata = map[string]any{"published_paths": publishedPaths} } return m } diff --git a/internal/stage/placeholders.go b/internal/stage/placeholders.go index 50ba83a..d2ac0be 100644 --- a/internal/stage/placeholders.go +++ b/internal/stage/placeholders.go @@ -75,7 +75,7 @@ func All() []Stage { normalizeStage{}, trimStage{}, analyzeStage{}, - archiveStage{}, + publishStage{}, placeholderStage{name: "notify"}, } } diff --git a/internal/stage/placeholders_test.go b/internal/stage/placeholders_test.go index 472385e..ac7201d 100644 --- a/internal/stage/placeholders_test.go +++ b/internal/stage/placeholders_test.go @@ -197,10 +197,10 @@ func TestStagesReturnExpectedMetadata(t *testing.T) { } if s.Name() == "publish" { if result.Metadata["stage"] != "publish" { - t.Fatalf("archive metadata = %#v, want stage=publish", result.Metadata) + t.Fatalf("publish metadata = %#v, want stage=publish", result.Metadata) } if result.Metadata["uploaded"] != true { - t.Fatalf("archive metadata = %#v, want uploaded=true", result.Metadata) + t.Fatalf("publish metadata = %#v, want uploaded=true", result.Metadata) } continue } @@ -222,10 +222,10 @@ func TestStagesReturnExpectedMetadata(t *testing.T) { t.Fatalf("scriptorium run calls = %d, want 0 when scriptorium config is absent", len(sc.RunRequests)) } if len(st.Requests) != 0 { - t.Fatalf("storage archive calls = %d, want 0", len(st.Requests)) + t.Fatalf("storage publish calls = %d, want 0", len(st.Requests)) } if _, ok := st.Objects["dnd/campaigns/sample-campaign/sessions/2026-05-03/runs/"+m.RunID+"/manifest.json"]; !ok { - t.Fatalf("archive upload missing manifest key in fake object store") + t.Fatalf("publish upload missing manifest key in fake object store") } if len(nf.Requests) != 1 { t.Fatalf("notify calls = %d, want 1", len(nf.Requests)) diff --git a/internal/stage/prepare_previous_test.go b/internal/stage/prepare_previous_test.go index 7c03fb0..0a50e7b 100644 --- a/internal/stage/prepare_previous_test.go +++ b/internal/stage/prepare_previous_test.go @@ -193,7 +193,7 @@ func TestHydratePreviousSessionArtifactsDoesNotUseLocalPreviousWorkspaceState(t writeFile(t, filepath.Join(seed.PreviousSessionRoot, "artifacts", "session_recap.md"), "# local stale recap\n") _, err := hydratePreviousSessionArtifacts(context.Background(), env, sessionPaths, requirements) - if err == nil || !strings.Contains(err.Error(), "object missing from archive") { + if err == nil || !strings.Contains(err.Error(), "object missing from published candidate keys") { t.Fatalf("error = %v, want remote-object-missing failure", err) } } @@ -273,7 +273,7 @@ func seedPreviousCurrentState( campaign := strings.TrimSpace(env.Config.Session.Campaign) rootPrefix := strings.TrimSpace(env.Config.Pipeline.Storage.S3.RootPrefix) previousSessionPrefix := artifacts.S3SessionPrefix(rootPrefix, campaign, previousSessionID) - manifestKey, runPointerKey := artifacts.ResolveArchiveCurrentStateKeys(previousSessionPrefix) + manifestKey, runPointerKey := artifacts.ResolveCurrentStateKeys(previousSessionPrefix) previousRunID := "20260510T010203Z-a1b2c3d4" previousSessionRoot := filepath.Join(t.TempDir(), "work", campaign, previousSessionID) diff --git a/internal/stage/archive.go b/internal/stage/publish.go similarity index 89% rename from internal/stage/archive.go rename to internal/stage/publish.go index c0e432b..1107c42 100644 --- a/internal/stage/archive.go +++ b/internal/stage/publish.go @@ -19,13 +19,13 @@ import ( "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) -type archiveStage struct{} -type archiveUploadFile struct { +type publishStage struct{} +type publishUploadFile struct { RelativePath string LocalPath string } -var archivePrerequisiteStages = []string{ +var publishPrerequisiteStages = []string{ "prepare", "transcribe", "merge", @@ -35,9 +35,9 @@ var archivePrerequisiteStages = []string{ "analyze", } -func (archiveStage) Name() string { return "publish" } +func (publishStage) Name() string { return "publish" } -func (archiveStage) Declares() IODecl { +func (publishStage) Declares() IODecl { return IODecl{ Inputs: []artifacts.Ref{ {Kind: "manifest", Category: "input", RelativePath: "manifest.json"}, @@ -45,23 +45,23 @@ func (archiveStage) Declares() IODecl { } } -func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*StageResult, error) { +func (publishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*StageResult, error) { if env == nil || env.Config == nil || env.Config.Pipeline == nil || env.Config.Session == nil { return nil, fmt.Errorf("publish: resolved config must include pipeline and session") } - if archiveDisabled(env) { + if publishDisabled(env) { return &StageResult{ Metadata: map[string]any{ "stage": "publish", "skipped": true, - "archive_enabled": false, + "publish_enabled": false, "audio_upload_skipped": true, "current_pointer_written": false, }, }, nil } - if archiveRunUploadDisabled(env) { + if publishRunUploadDisabled(env) { return &StageResult{ Metadata: map[string]any{ "stage": "publish", @@ -80,7 +80,7 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S return nil, fmt.Errorf("publish: remote object store backend is required when publish run upload is enabled") } - runRoot, err := resolveArchiveRunRoot(env, m) + runRoot, err := resolvePublishRunRoot(env, m) if err != nil { return nil, fmt.Errorf("publish: resolve run root: %w", err) } @@ -92,15 +92,15 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S return nil, fmt.Errorf("publish: run root %q is not a directory", runRoot) } - runPrefix, err := artifacts.ResolveArchiveRunPrefix(env.Config, m) + runPrefix, err := artifacts.ResolvePublishRunPrefix(env.Config, m) if err != nil { return nil, fmt.Errorf("publish: resolve s3 run prefix: %w", err) } - sessionPrefix, err := artifacts.ResolveArchiveSessionPrefix(env.Config, m) + sessionPrefix, err := artifacts.ResolvePublishSessionPrefix(env.Config, m) if err != nil { return nil, fmt.Errorf("publish: resolve s3 session prefix: %w", err) } - bucket := artifacts.ResolveArchiveBucket(env.Config, m) + bucket := artifacts.ResolvePublishBucket(env.Config, m) if bucket == "" { return nil, fmt.Errorf("publish: resolve s3 bucket: bucket is required") } @@ -109,21 +109,21 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S return nil, fmt.Errorf("publish: run id is required") } - manifestSource, err := resolveArchiveRunManifestSource(runRoot) + manifestSource, err := resolvePublishRunManifestSource(runRoot) if err != nil { return nil, fmt.Errorf("publish: resolve run manifest source: %w", err) } - runFiles, err := collectArchiveRunFiles(runRoot, manifestSource) + runFiles, err := collectPublishRunFiles(runRoot, manifestSource) if err != nil { return nil, fmt.Errorf("publish: collect run files: %w", err) } - sessionPaths := archiveSessionPaths(env, m) - previousFiles, err := collectArchivePreviousFiles(sessionPaths.PreviousDir) + sessionPaths := publishSessionPaths(env, m) + previousFiles, err := collectPublishPreviousFiles(sessionPaths.PreviousDir) if err != nil { return nil, fmt.Errorf("publish: collect previous files: %w", err) } - runtimeCatalog, err := buildArchiveRuntimeArtifactCatalog(sessionPaths, env.Config.Pipeline.Scriptorium) + runtimeCatalog, err := buildPublishRuntimeArtifactCatalog(sessionPaths, env.Config.Pipeline.Scriptorium) if err != nil { return nil, fmt.Errorf("publish: build runtime artifact catalog: %w", err) } @@ -149,12 +149,12 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S } publishedUploaded := make([]string, 0, len(publishOutputs)) - for _, promotion := range publishOutputs { - key := artifacts.S3PublishedOutputKey(sessionPrefix, promotion.Dest) - if _, err := env.ObjectStore.Upload(ctx, promotion.LocalPath, key, storage.UploadOptions{}); err != nil { - return nil, fmt.Errorf("publish: upload published output source %q to %q: %w", promotion.Source, key, err) + for _, publishedOutput := range publishOutputs { + key := artifacts.S3PublishedOutputKey(sessionPrefix, publishedOutput.Dest) + if _, err := env.ObjectStore.Upload(ctx, publishedOutput.LocalPath, key, storage.UploadOptions{}); err != nil { + return nil, fmt.Errorf("publish: upload published output source %q to %q: %w", publishedOutput.Source, key, err) } - publishedUploaded = append(publishedUploaded, promotion.Dest) + publishedUploaded = append(publishedUploaded, publishedOutput.Dest) } previousUploaded := make([]string, 0, len(previousFiles)) @@ -166,8 +166,8 @@ func (archiveStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S previousUploaded = append(previousUploaded, file.RelativePath) } - currentManifestKey, currentRunPointerKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) - manifestTempPath, err := writeCurrentManifestSnapshot(m, archiveMetadataPreview( + currentManifestKey, currentRunPointerKey := artifacts.ResolveCurrentStateKeys(sessionPrefix) + manifestTempPath, err := writeCurrentManifestSnapshot(m, publishMetadataPreview( bucket, runPrefix, sessionPrefix, @@ -250,7 +250,7 @@ type publishSkippedUnselectedOutput struct { Required bool } -func archiveDisabled(env *Env) bool { +func publishDisabled(env *Env) bool { cfg := env.Config.Pipeline.Publish if cfg == nil { return true @@ -258,7 +258,7 @@ func archiveDisabled(env *Env) bool { return cfg.Enabled != nil && !*cfg.Enabled } -func archiveRunUploadDisabled(env *Env) bool { +func publishRunUploadDisabled(env *Env) bool { cfg := env.Config.Pipeline.Publish if cfg == nil { return true @@ -270,7 +270,7 @@ func validatePublishPrerequisites(m *manifest.Manifest) error { if m == nil { return fmt.Errorf("manifest is required") } - for _, stageName := range archivePrerequisiteStages { + for _, stageName := range publishPrerequisiteStages { sr := m.Stages[stageName] if sr == nil { return fmt.Errorf("prerequisite stage %q has not succeeded", stageName) @@ -282,7 +282,7 @@ func validatePublishPrerequisites(m *manifest.Manifest) error { return nil } -func resolveArchiveRunRoot(env *Env, m *manifest.Manifest) (string, error) { +func resolvePublishRunRoot(env *Env, m *manifest.Manifest) (string, error) { sessionID := strings.TrimSpace(env.Config.Session.SessionID) if sessionID == "" && m != nil { sessionID = strings.TrimSpace(m.SessionID) @@ -313,7 +313,7 @@ func resolveArchiveRunRoot(env *Env, m *manifest.Manifest) (string, error) { return canonical, nil } -func resolveArchiveSessionRoot(env *Env, m *manifest.Manifest) (string, error) { +func resolvePublishSessionRoot(env *Env, m *manifest.Manifest) (string, error) { sessionID := strings.TrimSpace(env.Config.Session.SessionID) if sessionID == "" && m != nil { sessionID = strings.TrimSpace(m.SessionID) @@ -331,7 +331,7 @@ func resolveArchiveSessionRoot(env *Env, m *manifest.Manifest) (string, error) { return filepath.Clean(artifacts.SessionWorkDirForCampaign(env.Config.Pipeline.Workspace.Root, campaign, sessionID)), nil } -func archiveSessionPaths(env *Env, m *manifest.Manifest) artifacts.SessionPaths { +func publishSessionPaths(env *Env, m *manifest.Manifest) artifacts.SessionPaths { sessionID := strings.TrimSpace(env.Config.Session.SessionID) if sessionID == "" && m != nil { sessionID = strings.TrimSpace(m.SessionID) @@ -357,8 +357,8 @@ func resolvePublishOutputs( skippedOptionalOutputs := make([]string, 0) skippedUnselectedOutputs := make([]publishSkippedUnselectedOutput, 0) lockedOutputs := make([]publishLockedOutput, 0) - lockSet := archiveLockSet(locks) - selectedSet := archiveSelectedArtifactSet(selectedArtifactKeys) + lockSet := publishLockSet(locks) + selectedSet := publishSelectedArtifactSet(selectedArtifactKeys) configuredOutputs := configuredOutputPathMapFromCatalog(catalog) for _, rule := range rules { source := strings.TrimSpace(rule.Source) @@ -424,7 +424,7 @@ func resolvePublishOutputs( return out, skippedOptionalOutputs, skippedUnselectedOutputs, lockedOutputs, nil } -func archiveSelectedArtifactSet(selected []string) map[string]struct{} { +func publishSelectedArtifactSet(selected []string) map[string]struct{} { if len(selected) == 0 { return nil } @@ -439,7 +439,7 @@ func archiveSelectedArtifactSet(selected []string) map[string]struct{} { return out } -func archiveLockSet(locks []config.PublishLockRule) map[string]config.PublishLockRule { +func publishLockSet(locks []config.PublishLockRule) map[string]config.PublishLockRule { out := make(map[string]config.PublishLockRule, len(locks)) for _, lock := range locks { source := strings.TrimSpace(lock.Source) @@ -471,7 +471,7 @@ func configuredOutputPathMapFromCatalog(catalog *artifacts.ArtifactCatalog) map[ return out } -func buildArchiveRuntimeArtifactCatalog( +func buildPublishRuntimeArtifactCatalog( paths artifacts.SessionPaths, scriptoriumCfg *config.ScriptoriumConfig, ) (*artifacts.ArtifactCatalog, error) { @@ -538,8 +538,8 @@ func resolveConfiguredArtifactLocalPath(paths artifacts.SessionPaths, configured return filepath.Join(paths.Root, rel), nil } -func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile, error) { - files := make([]archiveUploadFile, 0, 64) +func collectPublishRunFiles(runRoot, manifestPath string) ([]publishUploadFile, error) { + files := make([]publishUploadFile, 0, 64) err := filepath.WalkDir(runRoot, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr @@ -553,7 +553,7 @@ func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile, return fmt.Errorf("relative dir from %q to %q: %w", runRoot, path, err) } relDir = filepath.ToSlash(relDir) - // Preserve existing behavior: audio is not uploaded in archive run record. + // Preserve existing behavior: audio is not uploaded in publish run record. if relDir == "audio" || strings.HasPrefix(relDir, "audio/") { return filepath.SkipDir } @@ -564,7 +564,7 @@ func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile, return fmt.Errorf("relative path from %q to %q: %w", runRoot, path, err) } rel = filepath.ToSlash(rel) - files = append(files, archiveUploadFile{ + files = append(files, publishUploadFile{ RelativePath: rel, LocalPath: path, }) @@ -584,11 +584,11 @@ func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile, if manifestInfo.IsDir() { return nil, fmt.Errorf("manifest path %q is a directory", manifestPath) } - files = append(files, archiveUploadFile{ + files = append(files, publishUploadFile{ RelativePath: "manifest.json", LocalPath: manifestPath, }) - seen := map[string]archiveUploadFile{} + seen := map[string]publishUploadFile{} for _, file := range files { seen[file.RelativePath] = file } @@ -603,7 +603,7 @@ func collectArchiveRunFiles(runRoot, manifestPath string) ([]archiveUploadFile, return files, nil } -func collectArchivePreviousFiles(previousDir string) ([]archiveUploadFile, error) { +func collectPublishPreviousFiles(previousDir string) ([]publishUploadFile, error) { previousDir = filepath.Clean(strings.TrimSpace(previousDir)) if previousDir == "" { return nil, fmt.Errorf("previous directory is required") @@ -619,7 +619,7 @@ func collectArchivePreviousFiles(previousDir string) ([]archiveUploadFile, error return nil, fmt.Errorf("previous path %q is not a directory", previousDir) } - files := make([]archiveUploadFile, 0, 16) + files := make([]publishUploadFile, 0, 16) err = filepath.WalkDir(previousDir, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr @@ -632,7 +632,7 @@ func collectArchivePreviousFiles(previousDir string) ([]archiveUploadFile, error return fmt.Errorf("relative path from %q to %q: %w", previousDir, path, err) } rel = filepath.ToSlash(rel) - files = append(files, archiveUploadFile{ + files = append(files, publishUploadFile{ RelativePath: filepath.ToSlash(filepath.Join(config.PathPreviousDirSegment, rel)), LocalPath: path, }) @@ -648,7 +648,7 @@ func collectArchivePreviousFiles(previousDir string) ([]archiveUploadFile, error return files, nil } -func resolveArchiveRunManifestSource(runRoot string) (string, error) { +func resolvePublishRunManifestSource(runRoot string) (string, error) { path := filepath.Join(filepath.Clean(runRoot), "manifest.json") info, err := os.Stat(path) if err != nil { @@ -674,7 +674,7 @@ func directoryExists(path string) (bool, error) { return false, err } -func writeCurrentManifestSnapshot(m *manifest.Manifest, archiveMetadata map[string]any) (string, error) { +func writeCurrentManifestSnapshot(m *manifest.Manifest, publishMetadata map[string]any) (string, error) { if m == nil { return "", fmt.Errorf("manifest is required") } @@ -707,7 +707,7 @@ func writeCurrentManifestSnapshot(m *manifest.Manifest, archiveMetadata map[stri now := time.Now().UTC() clone.MarkStageSucceeded("publish", now, nil) if sr := clone.Stages["publish"]; sr != nil { - sr.Metadata = archiveMetadata + sr.Metadata = publishMetadata } data, err := json.MarshalIndent(&clone, "", " ") @@ -747,7 +747,7 @@ func writeCurrentRunIDPointer(runID string) (string, error) { return path, nil } -func archiveMetadataPreview( +func publishMetadataPreview( bucket, runPrefix, sessionPrefix string, runUploaded []string, publishedUploaded []string, diff --git a/internal/stage/archive_test.go b/internal/stage/publish_test.go similarity index 76% rename from internal/stage/archive_test.go rename to internal/stage/publish_test.go index 7b577c1..2d3b449 100644 --- a/internal/stage/archive_test.go +++ b/internal/stage/publish_test.go @@ -17,11 +17,11 @@ import ( "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) -func TestArchiveSkipsWhenDisabled(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishSkipsWhenDisabled(t *testing.T) { + env, m, _ := publishFixture(t) env.Config.Pipeline.Publish.Enabled = boolPtr(false) - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } @@ -29,15 +29,15 @@ func TestArchiveSkipsWhenDisabled(t *testing.T) { t.Fatalf("metadata = %#v, want skipped=true", result.Metadata) } if len(env.ObjectStore.(*storage.FakeBackend).Uploads) != 0 { - t.Fatalf("unexpected uploads when archive disabled") + t.Fatalf("unexpected uploads when publish disabled") } } -func TestArchiveSkipsRunUploadWhenDisabled(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishSkipsRunUploadWhenDisabled(t *testing.T) { + env, m, _ := publishFixture(t) env.Config.Pipeline.Publish.UploadRun = boolPtr(false) - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } @@ -49,11 +49,11 @@ func TestArchiveSkipsRunUploadWhenDisabled(t *testing.T) { } } -func TestArchiveFailsWhenPrerequisiteNotSucceeded(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishFailsWhenPrerequisiteNotSucceeded(t *testing.T) { + env, m, _ := publishFixture(t) m.Stages["trim"].Status = manifest.StatusFailed - _, err := archiveStage{}.Run(context.Background(), env, m) + _, err := publishStage{}.Run(context.Background(), env, m) if err == nil || !strings.Contains(err.Error(), `prerequisite stage "trim"`) { t.Fatalf("Run() error = %v, want prerequisite failure", err) } @@ -62,11 +62,11 @@ func TestArchiveFailsWhenPrerequisiteNotSucceeded(t *testing.T) { } } -func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishUploadsRunRecordPublishedOutputsAndCurrentPointer(t *testing.T) { + env, m, _ := publishFixture(t) fake := env.ObjectStore.(*storage.FakeBackend) - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } @@ -94,10 +94,10 @@ func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) { trimmedKey := sessionPrefix + "transcripts/final.trimmed.json" recapKey := sessionPrefix + "artifacts/session_recap.md" if _, ok := fake.Objects[trimmedKey]; !ok { - t.Fatalf("missing promoted key %q", trimmedKey) + t.Fatalf("missing published output key %q", trimmedKey) } if _, ok := fake.Objects[recapKey]; !ok { - t.Fatalf("missing promoted key %q", recapKey) + t.Fatalf("missing published output key %q", recapKey) } currentManifestKey := sessionPrefix + "current/manifest.json" @@ -136,8 +136,8 @@ func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) { } } -func TestArchiveUploadsPreviousCacheWhenPresent(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishUploadsPreviousCacheWhenPresent(t *testing.T) { + env, m, _ := publishFixture(t) fake := env.ObjectStore.(*storage.FakeBackend) sessionRoot := artifacts.SessionWorkDirForCampaign( env.Config.Pipeline.Workspace.Root, @@ -147,7 +147,7 @@ func TestArchiveUploadsPreviousCacheWhenPresent(t *testing.T) { writeStageTestFile(t, filepath.Join(sessionRoot, "previous", "manifest.json"), "{\"session_id\":\"2026-04-12\"}\n") writeStageTestFile(t, filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md"), "# previous recap\n") - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } @@ -155,20 +155,20 @@ func TestArchiveUploadsPreviousCacheWhenPresent(t *testing.T) { previousManifestKey := m.S3SessionPrefix + "previous/manifest.json" previousRecapKey := m.S3SessionPrefix + "previous/artifacts/session_recap.md" if _, ok := fake.Objects[previousManifestKey]; !ok { - t.Fatalf("missing archived previous manifest key %q", previousManifestKey) + t.Fatalf("missing published previous manifest key %q", previousManifestKey) } if _, ok := fake.Objects[previousRecapKey]; !ok { - t.Fatalf("missing archived previous artifact key %q", previousRecapKey) + t.Fatalf("missing published previous artifact key %q", previousRecapKey) } if result.Metadata["previous_files_uploaded"] != 2 { t.Fatalf("metadata previous_files_uploaded = %#v, want 2", result.Metadata["previous_files_uploaded"]) } } -func TestArchiveToleratesMissingPreviousCache(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishToleratesMissingPreviousCache(t *testing.T) { + env, m, _ := publishFixture(t) - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } @@ -177,35 +177,35 @@ func TestArchiveToleratesMissingPreviousCache(t *testing.T) { } } -func TestArchiveUsesCustomPromotionRules(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishUsesCustomOutputRules(t *testing.T) { + env, m, _ := publishFixture(t) env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{ {Source: "narratio.transcript.final_trimmed", Dest: "published/trimmed.json", Required: boolPtr(true)}, {Source: "narratio.artifact.session_recap", Dest: "published/recap.md", Required: boolPtr(true)}, } - _, err := archiveStage{}.Run(context.Background(), env, m) + _, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } fake := env.ObjectStore.(*storage.FakeBackend) if _, ok := fake.Objects[m.S3SessionPrefix+"published/trimmed.json"]; !ok { - t.Fatalf("missing custom promoted trimmed key") + t.Fatalf("missing custom published trimmed key") } if _, ok := fake.Objects[m.S3SessionPrefix+"published/recap.md"]; !ok { - t.Fatalf("missing custom promoted recap key") + t.Fatalf("missing custom published recap key") } } -func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishSkipsOptionalMissingOutput(t *testing.T) { + env, m, _ := publishFixture(t) env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{ {Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)}, {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(false)}, } - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } @@ -216,8 +216,8 @@ func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) { } } -func TestArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishSkipsRequiredUnselectedConfiguredOutput(t *testing.T) { + env, m, _ := publishFixture(t) env.SelectedArtifactKeys = []string{"player_handout"} env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{ Enabled: true, @@ -226,15 +226,15 @@ func TestArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) { } fake := env.ObjectStore.(*storage.FakeBackend) - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } if _, ok := fake.Objects[m.S3SessionPrefix+"transcripts/final.trimmed.json"]; !ok { - t.Fatalf("missing built-in promoted trimmed key") + t.Fatalf("missing built-in published trimmed key") } if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok { - t.Fatalf("unexpected unselected recap promotion upload") + t.Fatalf("unexpected unselected recap published output upload") } skipped := result.Metadata["skipped_unselected_outputs"].([]map[string]any) if len(skipped) != 1 { @@ -251,8 +251,8 @@ func TestArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) { } } -func TestArchiveSelectedConfiguredPromotionStillFailsWhenMissing(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishSelectedConfiguredOutputStillFailsWhenMissing(t *testing.T) { + env, m, _ := publishFixture(t) env.SelectedArtifactKeys = []string{"session_recap"} sessionRoot := artifacts.SessionWorkDirForCampaign( env.Config.Pipeline.Workspace.Root, @@ -263,26 +263,26 @@ func TestArchiveSelectedConfiguredPromotionStillFailsWhenMissing(t *testing.T) { t.Fatalf("remove recap: %v", err) } - _, err := archiveStage{}.Run(context.Background(), env, m) + _, err := publishStage{}.Run(context.Background(), env, m) if err == nil || !strings.Contains(err.Error(), `required output source unavailable: "narratio.artifact.session_recap"`) { t.Fatalf("Run() error = %v, want required selected output failure", err) } } -func TestArchiveLockedSelectedPromotionSkipsAsLocked(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishLockedSelectedOutputSkipsAsLocked(t *testing.T) { + env, m, _ := publishFixture(t) env.SelectedArtifactKeys = []string{"session_recap"} env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{ {Source: "narratio.artifact.session_recap", Reason: "reviewed"}, } fake := env.ObjectStore.(*storage.FakeBackend) - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok { - t.Fatalf("unexpected locked recap promotion upload") + t.Fatalf("unexpected locked recap published output upload") } if result.Metadata["locked_output_count"] != 1 { t.Fatalf("locked_output_count = %#v, want 1", result.Metadata["locked_output_count"]) @@ -293,8 +293,8 @@ func TestArchiveLockedSelectedPromotionSkipsAsLocked(t *testing.T) { } } -func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishLockedUnselectedConfiguredOutputSkipsAsUnselected(t *testing.T) { + env, m, _ := publishFixture(t) env.SelectedArtifactKeys = []string{"player_handout"} env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{ Enabled: true, @@ -305,7 +305,7 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing. {Source: "narratio.artifact.session_recap", Reason: "reviewed"}, } - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } @@ -318,25 +318,25 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing. } } -func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishSkipsLockedRequiredOutputAndCommits(t *testing.T) { + env, m, _ := publishFixture(t) env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{ {Source: "narratio.transcript.final_trimmed", Reason: "human reviewed"}, } fake := env.ObjectStore.(*storage.FakeBackend) - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json" if _, ok := fake.Objects[trimmedKey]; ok { - t.Fatalf("locked promotion key %q should not be uploaded", trimmedKey) + t.Fatalf("locked published output key %q should not be uploaded", trimmedKey) } recapKey := m.S3SessionPrefix + "artifacts/session_recap.md" if _, ok := fake.Objects[recapKey]; !ok { - t.Fatalf("unlocked promotion key %q should be uploaded", recapKey) + t.Fatalf("unlocked published output key %q should be uploaded", recapKey) } runTrimmedKey := m.S3RunPrefix + "trim/outputs/transcripts/final.trimmed.json" if _, ok := fake.Objects[runTrimmedKey]; !ok { @@ -365,7 +365,7 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { locked[0]["required"] != true || locked[0]["local_path"] == "" || locked[0]["provenance"] == "" { - t.Fatalf("locked promotion metadata = %#v", locked[0]) + t.Fatalf("locked published output metadata = %#v", locked[0]) } currentManifestKey := m.S3SessionPrefix + "current/manifest.json" @@ -374,8 +374,8 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { t.Fatalf("unmarshal current manifest: %v", err) } stages := current["stages"].(map[string]any) - archive := stages["publish"].(map[string]any) - meta := archive["metadata"].(map[string]any) + publishRecord := stages["publish"].(map[string]any) + meta := publishRecord["metadata"].(map[string]any) if meta["locked_output_count"] != float64(1) { t.Fatalf("current manifest locked_output_count = %#v, want 1", meta["locked_output_count"]) } @@ -385,8 +385,8 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { } } -func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishLockedRequiredMissingOutputSucceeds(t *testing.T) { + env, m, _ := publishFixture(t) env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{ {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)}, } @@ -394,7 +394,7 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) { {Source: "narratio.transcript.base", Reason: "manual merge is locked"}, } - result, err := archiveStage{}.Run(context.Background(), env, m) + result, err := publishStage{}.Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) } @@ -402,22 +402,22 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) { fake := env.ObjectStore.(*storage.FakeBackend) mergedKey := m.S3SessionPrefix + "transcripts/base.json" if _, ok := fake.Objects[mergedKey]; ok { - t.Fatalf("locked missing promotion key %q should not be uploaded", mergedKey) + t.Fatalf("locked missing published output key %q should not be uploaded", mergedKey) } 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 output") } locked := result.Metadata["locked_outputs"].([]map[string]any) if len(locked) != 1 { t.Fatalf("locked_outputs = %#v, want one item", locked) } 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 published output metadata = %#v, want empty local path/provenance", locked[0]) } } -func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishLockDoesNotOverwriteExistingOutput(t *testing.T) { + env, m, _ := publishFixture(t) env.Config.Pipeline.Publish.Locks = []config.PublishLockRule{ {Source: "narratio.transcript.final_trimmed", Reason: "already published"}, } @@ -425,40 +425,40 @@ func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) { trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json" fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte("previously published\n")}) - if _, err := (archiveStage{}).Run(context.Background(), env, m); err != nil { + if _, err := (publishStage{}).Run(context.Background(), env, m); err != nil { t.Fatalf("Run() error = %v", err) } got := string(fake.Objects[trimmedKey].Data) if got != "previously published\n" { - t.Fatalf("locked promotion object contents = %q, want existing object preserved", got) + t.Fatalf("locked published output object contents = %q, want existing object preserved", got) } for _, upload := range fake.Uploads { if upload.Key == trimmedKey { - t.Fatalf("locked promotion key %q was uploaded", trimmedKey) + t.Fatalf("locked published output key %q was uploaded", trimmedKey) } } } -func TestArchiveFailsWhenRequiredPromotionMissing(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishFailsWhenRequiredOutputMissing(t *testing.T) { + env, m, _ := publishFixture(t) env.Config.Pipeline.Publish.Outputs = []config.PublishOutputRule{ {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)}, } - _, err := archiveStage{}.Run(context.Background(), env, m) + _, err := publishStage{}.Run(context.Background(), env, m) if err == nil || !strings.Contains(err.Error(), "required output source unavailable") { t.Fatalf("Run() error = %v, want required output source unavailable failure", err) } } -func TestArchiveFailsWhenCanonicalRunRootMissing(t *testing.T) { - env, m, runRoot := archiveFixture(t) +func TestPublishFailsWhenCanonicalRunRootMissing(t *testing.T) { + env, m, runRoot := publishFixture(t) if err := os.RemoveAll(runRoot); err != nil { t.Fatalf("remove run root: %v", err) } - _, err := archiveStage{}.Run(context.Background(), env, m) + _, err := publishStage{}.Run(context.Background(), env, m) if err == nil { t.Fatal("expected missing run-root error, got nil") } @@ -467,8 +467,8 @@ func TestArchiveFailsWhenCanonicalRunRootMissing(t *testing.T) { } } -func TestArchiveDoesNotWriteCurrentPointerWhenPromotionUploadFails(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishDoesNotWriteCurrentPointerWhenOutputUploadFails(t *testing.T) { + env, m, _ := publishFixture(t) fake := env.ObjectStore.(*storage.FakeBackend) trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json" @@ -480,26 +480,26 @@ func TestArchiveDoesNotWriteCurrentPointerWhenPromotionUploadFails(t *testing.T) originalUpload := fake.Upload _ = originalUpload // Use UploadErr toggle by checking call sequence in postcondition. - // First failure point is promotion upload; simulate by setting error immediately before promotion key write. + // First failure point is published output upload; simulate by setting error immediately before published output key write. // We cannot hook FakeBackend per-key without changing public behavior; use dedicated backend wrapper instead. - env.ObjectStore = &promotionFailingStore{delegate: fake, failKey: failingKey} + env.ObjectStore = &publishedOutputFailingStore{delegate: fake, failKey: failingKey} - _, err := archiveStage{}.Run(context.Background(), env, m) + _, err := publishStage{}.Run(context.Background(), env, m) if err == nil || !strings.Contains(err.Error(), "upload published output source") { t.Fatalf("Run() error = %v, want published output upload failure", err) } 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 published output failure") } fake.UploadErr = origUploadErr } -func TestArchiveDoesNotWriteCurrentPointerWhenCurrentManifestUploadFails(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishDoesNotWriteCurrentPointerWhenCurrentManifestUploadFails(t *testing.T) { + env, m, _ := publishFixture(t) fake := env.ObjectStore.(*storage.FakeBackend) - env.ObjectStore = &promotionFailingStore{delegate: fake, failKey: m.S3SessionPrefix + "current/manifest.json"} + env.ObjectStore = &publishedOutputFailingStore{delegate: fake, failKey: m.S3SessionPrefix + "current/manifest.json"} - _, err := archiveStage{}.Run(context.Background(), env, m) + _, err := publishStage{}.Run(context.Background(), env, m) if err == nil || !strings.Contains(err.Error(), "current manifest") { t.Fatalf("Run() error = %v, want current manifest upload failure", err) } @@ -508,17 +508,17 @@ func TestArchiveDoesNotWriteCurrentPointerWhenCurrentManifestUploadFails(t *test } } -func TestArchiveFailsWithoutObjectStore(t *testing.T) { - env, m, _ := archiveFixture(t) +func TestPublishFailsWithoutObjectStore(t *testing.T) { + env, m, _ := publishFixture(t) env.ObjectStore = nil - _, err := archiveStage{}.Run(context.Background(), env, m) + _, err := publishStage{}.Run(context.Background(), env, m) if err == nil || !strings.Contains(err.Error(), "object store") { t.Fatalf("Run() error = %v, want object store backend failure", err) } } -func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) { +func publishFixture(t *testing.T) (*Env, *manifest.Manifest, string) { t.Helper() root := t.TempDir() @@ -549,7 +549,7 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) { m.S3Bucket = "my-dnd-archive" m.S3SessionPrefix = artifacts.S3SessionPrefix("dnd", campaign, sessionID) m.S3RunPrefix = artifacts.S3RunPrefix(m.S3SessionPrefix, runID) - for _, name := range archivePrerequisiteStages { + for _, name := range publishPrerequisiteStages { m.MarkStageSucceeded(name, time.Date(2026, 5, 16, 1, 2, 3, 0, time.UTC), nil) } @@ -591,27 +591,27 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) { return env, m, runRoot } -type promotionFailingStore struct { +type publishedOutputFailingStore struct { delegate *storage.FakeBackend failKey string } -func (s *promotionFailingStore) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) { +func (s *publishedOutputFailingStore) List(ctx context.Context, prefix string) ([]storage.ObjectInfo, error) { return s.delegate.List(ctx, prefix) } -func (s *promotionFailingStore) Download(ctx context.Context, key, localPath string) error { +func (s *publishedOutputFailingStore) Download(ctx context.Context, key, localPath string) error { return s.delegate.Download(ctx, key, localPath) } -func (s *promotionFailingStore) Upload(ctx context.Context, localPath, key string, opts storage.UploadOptions) (storage.ObjectInfo, error) { +func (s *publishedOutputFailingStore) Upload(ctx context.Context, localPath, key string, opts storage.UploadOptions) (storage.ObjectInfo, error) { if strings.TrimSpace(key) == strings.TrimSpace(s.failKey) { return storage.ObjectInfo{}, errors.New("forced upload failure") } return s.delegate.Upload(ctx, localPath, key, opts) } -func (s *promotionFailingStore) Exists(ctx context.Context, key string) (bool, error) { +func (s *publishedOutputFailingStore) Exists(ctx context.Context, key string) (bool, error) { return s.delegate.Exists(ctx, key) }