From 38dae8b5848fc7b2b9dc288a41759869e76beaa4 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 8 May 2026 16:51:01 +0000 Subject: [PATCH] Replace normalize stage with trim scaffold --- README.md | 2 +- architecture.md | 8 ++--- internal/app/commands_test.go | 18 +++++++++- internal/app/plan_test.go | 8 ++--- internal/app/planner_test.go | 2 +- internal/app/resume_run_stage_test.go | 35 ++++++++++++++++--- internal/app/runner_test.go | 6 ++-- internal/artifacts/local.go | 2 +- internal/artifacts/local_test.go | 2 +- internal/artifacts/paths.go | 48 +++++++++++++-------------- internal/stage/placeholders.go | 2 +- 11 files changed, 88 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 7ecf49d..d39a9c2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Implemented now: Not implemented yet: -- real `normalize` behavior +- real `trim` behavior - real `archive` behavior - real `notify` behavior - additional analyze artifacts beyond `session_recap` diff --git a/architecture.md b/architecture.md index 1742cdc..04a4bfb 100644 --- a/architecture.md +++ b/architecture.md @@ -23,7 +23,7 @@ Implemented: Still placeholder/future: -- `normalize` stage behavior +- `trim` stage behavior - `archive` stage behavior - `notify` stage behavior - additional Scriptorium artifact types beyond `session_recap` @@ -36,9 +36,9 @@ Canonical stage order: 1. `prepare` 2. `transcribe` -3. `normalize` -4. `merge` -5. `polish` +3. `merge` +4. `polish` +5. `trim` 6. `analyze` 7. `archive` 8. `notify` diff --git a/internal/app/commands_test.go b/internal/app/commands_test.go index b84f093..f90c186 100644 --- a/internal/app/commands_test.go +++ b/internal/app/commands_test.go @@ -32,7 +32,7 @@ func TestExecuteValidCommands(t *testing.T) { wantOut string }{ {name: "run", args: []string{"run", "--config", pipelinePath, "--session", sessionPath}, wantOut: "narratio run: session 2026-05-03; executed=8 skipped=0; manifest="}, - {name: "plan", args: []string{"plan", "--config", pipelinePath, "--session", sessionPath}, wantOut: "prepare: skip\ntranscribe: skip\nnormalize: skip\nmerge: skip\npolish: skip\nanalyze: skip\narchive: skip\nnotify: skip"}, + {name: "plan", args: []string{"plan", "--config", pipelinePath, "--session", sessionPath}, wantOut: "prepare: skip\ntranscribe: skip\nmerge: skip\npolish: skip\ntrim: skip\nanalyze: skip\narchive: skip\nnotify: skip"}, {name: "status", args: []string{"status", "--manifest", manifestPath}, wantOut: "session_id: 2026-05-03"}, {name: "resume", args: []string{"resume", "--config", pipelinePath, "--session", sessionPath}, wantOut: "narratio resume: session 2026-05-03 has no remaining stages"}, {name: "run-stage", args: []string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "polish"}, wantOut: "narratio run-stage: stage=polish executed=0 skipped=1 force=false; manifest="}, @@ -106,6 +106,22 @@ func TestExecuteRunStageUnknownFails(t *testing.T) { } } +func TestExecuteRunStageNormalizeIsRejected(t *testing.T) { + workspaceRoot := t.TempDir() + pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe") + + var stdout bytes.Buffer + var stderr bytes.Buffer + + code := Execute([]string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "normalize"}, &stdout, &stderr) + if code == 0 { + t.Fatal("exit code = 0, want non-zero") + } + if !strings.Contains(stderr.String(), "unknown stage") { + t.Fatalf("stderr = %q, want unknown stage error", stderr.String()) + } +} + func TestExecuteRunStageTranscribeUsesConfiguredWhisperXServer(t *testing.T) { workspaceRoot := t.TempDir() var serverCalls int diff --git a/internal/app/plan_test.go b/internal/app/plan_test.go index 69552a4..c8d3c73 100644 --- a/internal/app/plan_test.go +++ b/internal/app/plan_test.go @@ -27,7 +27,7 @@ func TestPlanCreatesAndReusesWorkdir(t *testing.T) { if !strings.Contains(got, "narratio plan: workdir prepared at") { t.Fatalf("first output = %q, want workdir prepared", got) } - for _, name := range []string{"prepare", "transcribe", "normalize", "merge", "polish", "analyze", "archive", "notify"} { + for _, name := range []string{"prepare", "transcribe", "merge", "polish", "trim", "analyze", "archive", "notify"} { if !strings.Contains(got, name+": run") { t.Fatalf("first output = %q, missing stage %q", got, name) } @@ -42,7 +42,7 @@ func TestPlanCreatesAndReusesWorkdir(t *testing.T) { filepath.Join(sessionWorkdir, "inputs"), filepath.Join(sessionWorkdir, "audio"), filepath.Join(sessionWorkdir, "transcripts", "raw"), - filepath.Join(sessionWorkdir, "transcripts", "normalized"), + filepath.Join(sessionWorkdir, "transcripts", "trimmed"), filepath.Join(sessionWorkdir, "artifacts"), filepath.Join(sessionWorkdir, "config"), filepath.Join(sessionWorkdir, "logs"), @@ -81,8 +81,8 @@ func TestPlanShowsRunAndSkipFromManifest(t *testing.T) { if !strings.Contains(got, "prepare: skip") || !strings.Contains(got, "transcribe: skip") { t.Fatalf("output = %q, want prepare/transcribe skipped", got) } - if !strings.Contains(got, "normalize: run") { - t.Fatalf("output = %q, want normalize run", got) + if !strings.Contains(got, "trim: run") { + t.Fatalf("output = %q, want trim run", got) } if !strings.Contains(got, "totals: run=6 skip=2") { t.Fatalf("output = %q, want totals run=6 skip=2", got) diff --git a/internal/app/planner_test.go b/internal/app/planner_test.go index 8423fdf..4940f8e 100644 --- a/internal/app/planner_test.go +++ b/internal/app/planner_test.go @@ -4,7 +4,7 @@ import "testing" func TestBuildFullPlanOrder(t *testing.T) { got := BuildFullPlan() - want := []string{"prepare", "transcribe", "normalize", "merge", "polish", "analyze", "archive", "notify"} + want := []string{"prepare", "transcribe", "merge", "polish", "trim", "analyze", "archive", "notify"} if len(got) != len(want) { t.Fatalf("len(plan) = %d, want %d", len(got), len(want)) } diff --git a/internal/app/resume_run_stage_test.go b/internal/app/resume_run_stage_test.go index 2adbe87..0f7a6ea 100644 --- a/internal/app/resume_run_stage_test.go +++ b/internal/app/resume_run_stage_test.go @@ -44,8 +44,8 @@ func TestResumeStartsAfterCompletedStages(t *testing.T) { if err != nil { t.Fatalf("load manifest: %v", err) } - if loaded.Stages["normalize"] == nil || loaded.Stages["normalize"].Status != manifest.StatusSucceeded { - t.Fatalf("normalize stage = %#v, want succeeded", loaded.Stages["normalize"]) + if loaded.Stages["trim"] == nil || loaded.Stages["trim"].Status != manifest.StatusSucceeded { + t.Fatalf("trim stage = %#v, want succeeded", loaded.Stages["trim"]) } } @@ -56,7 +56,7 @@ func TestResumeNoRemainingStages(t *testing.T) { store := &manifest.LocalStore{} m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) - for _, name := range []string{"prepare", "transcribe", "normalize", "merge", "polish", "analyze", "archive", "notify"} { + for _, name := range []string{"prepare", "transcribe", "merge", "polish", "trim", "analyze", "archive", "notify"} { m.MarkStageSucceeded(name, time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil) } if err := store.Save(context.Background(), manifestPath, m); err != nil { @@ -85,7 +85,7 @@ func TestResumeForceRerunsSucceeded(t *testing.T) { store := &manifest.LocalStore{} m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) - for _, name := range []string{"prepare", "transcribe", "normalize", "merge", "polish", "analyze", "archive", "notify"} { + for _, name := range []string{"prepare", "transcribe", "merge", "polish", "trim", "analyze", "archive", "notify"} { m.MarkStageSucceeded(name, time.Date(2026, 5, 3, 10, 1, 0, 0, time.UTC), nil) } if err := store.Save(context.Background(), manifestPath, m); err != nil { @@ -165,3 +165,30 @@ func TestRunStageSkipAndForce(t *testing.T) { t.Fatalf("output = %q, want force rerun", out.String()) } } + +func TestRunStageTrimPlaceholderExecutes(t *testing.T) { + workspaceRoot := t.TempDir() + pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot) + manifestPath := filepath.Join(workspaceRoot, "work", "2026-05-03", "manifest.json") + + var out bytes.Buffer + err := RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "trim"}, &out) + if err != nil { + t.Fatalf("RunStage(trim) error = %v", err) + } + if !strings.Contains(out.String(), "stage=trim executed=1 skipped=0") { + t.Fatalf("output = %q, want stage=trim executed", out.String()) + } + + store := &manifest.LocalStore{} + m, err := store.Load(context.Background(), manifestPath) + if err != nil { + t.Fatalf("load manifest: %v", err) + } + if m.Stages["trim"] == nil || m.Stages["trim"].Status != manifest.StatusSucceeded { + t.Fatalf("trim stage = %#v, want succeeded", m.Stages["trim"]) + } + if m.Stages["trim"].Metadata == nil || m.Stages["trim"].Metadata["placeholder"] != true { + t.Fatalf("trim stage metadata = %#v, want placeholder=true", m.Stages["trim"].Metadata) + } +} diff --git a/internal/app/runner_test.go b/internal/app/runner_test.go index b24e525..2249a06 100644 --- a/internal/app/runner_test.go +++ b/internal/app/runner_test.go @@ -61,7 +61,7 @@ func TestExecuteStagesPlaceholderSuccessUpdatesManifest(t *testing.T) { t.Fatalf("Load manifest error = %v", err) } - for _, name := range []string{"prepare", "transcribe", "normalize", "merge", "polish", "analyze", "archive", "notify"} { + for _, name := range []string{"prepare", "transcribe", "merge", "polish", "trim", "analyze", "archive", "notify"} { sr := m.Stages[name] if sr == nil { t.Fatalf("missing stage record %q", name) @@ -238,8 +238,8 @@ func TestExecuteStagesFailureUpdatesManifest(t *testing.T) { if got := m.Stages["transcribe"]; got == nil || got.Status != manifest.StatusFailed { t.Fatalf("transcribe status = %#v, want failed", got) } - if got := m.Stages["normalize"]; got != nil { - t.Fatalf("normalize should not run, got %#v", got) + if got := m.Stages["merge"]; got != nil { + t.Fatalf("merge should not run, got %#v", got) } } diff --git a/internal/artifacts/local.go b/internal/artifacts/local.go index ebc0d0f..0f4b822 100644 --- a/internal/artifacts/local.go +++ b/internal/artifacts/local.go @@ -51,7 +51,7 @@ func (s *LocalStore) EnsureLayout(sessionID string) (SessionPaths, error) { paths.AudioDir, paths.TranscriptsDir, paths.TranscriptsRawDir, - paths.TranscriptsNormalizedDir, + paths.TranscriptsTrimmedDir, paths.ArtifactsDir, paths.ConfigDir, paths.LogsDir, diff --git a/internal/artifacts/local_test.go b/internal/artifacts/local_test.go index 30a68b4..e6b8d9f 100644 --- a/internal/artifacts/local_test.go +++ b/internal/artifacts/local_test.go @@ -20,7 +20,7 @@ func TestEnsureLayoutCreatesExpectedDirectories(t *testing.T) { checkDirExists(t, paths.AudioDir) checkDirExists(t, paths.TranscriptsDir) checkDirExists(t, paths.TranscriptsRawDir) - checkDirExists(t, paths.TranscriptsNormalizedDir) + checkDirExists(t, paths.TranscriptsTrimmedDir) checkDirExists(t, paths.ArtifactsDir) checkDirExists(t, paths.ConfigDir) checkDirExists(t, paths.LogsDir) diff --git a/internal/artifacts/paths.go b/internal/artifacts/paths.go index a1395e0..888efa4 100644 --- a/internal/artifacts/paths.go +++ b/internal/artifacts/paths.go @@ -4,18 +4,18 @@ import "path/filepath" // SessionPaths contains canonical local paths for one session work directory. type SessionPaths struct { - WorkspaceRoot string - Root string - InputsDir string - AudioDir string - TranscriptsDir string - TranscriptsRawDir string - TranscriptsNormalizedDir string - ArtifactsDir string - ConfigDir string - LogsDir string - ManifestPath string - LockPath string + WorkspaceRoot string + Root string + InputsDir string + AudioDir string + TranscriptsDir string + TranscriptsRawDir string + TranscriptsTrimmedDir string + ArtifactsDir string + ConfigDir string + LogsDir string + ManifestPath string + LockPath string } // SessionWorkDir returns the work directory for one session. @@ -27,17 +27,17 @@ func buildSessionPaths(workspaceRoot, sessionID string) SessionPaths { root := SessionWorkDir(workspaceRoot, sessionID) transcripts := filepath.Join(root, "transcripts") return SessionPaths{ - WorkspaceRoot: workspaceRoot, - Root: root, - InputsDir: filepath.Join(root, "inputs"), - AudioDir: filepath.Join(root, "audio"), - TranscriptsDir: transcripts, - TranscriptsRawDir: filepath.Join(transcripts, "raw"), - TranscriptsNormalizedDir: filepath.Join(transcripts, "normalized"), - ArtifactsDir: filepath.Join(root, "artifacts"), - ConfigDir: filepath.Join(root, "config"), - LogsDir: filepath.Join(root, "logs"), - ManifestPath: filepath.Join(root, "manifest.json"), - LockPath: filepath.Join(root, ".lock"), + WorkspaceRoot: workspaceRoot, + Root: root, + InputsDir: filepath.Join(root, "inputs"), + AudioDir: filepath.Join(root, "audio"), + TranscriptsDir: transcripts, + TranscriptsRawDir: filepath.Join(transcripts, "raw"), + TranscriptsTrimmedDir: filepath.Join(transcripts, "trimmed"), + ArtifactsDir: filepath.Join(root, "artifacts"), + ConfigDir: filepath.Join(root, "config"), + LogsDir: filepath.Join(root, "logs"), + ManifestPath: filepath.Join(root, "manifest.json"), + LockPath: filepath.Join(root, ".lock"), } } diff --git a/internal/stage/placeholders.go b/internal/stage/placeholders.go index 77fac75..f60e643 100644 --- a/internal/stage/placeholders.go +++ b/internal/stage/placeholders.go @@ -90,9 +90,9 @@ func All() []Stage { return []Stage{ prepareStage{}, transcribeStage{}, - placeholderStage{name: "normalize"}, mergeStage{}, polishStage{}, + placeholderStage{name: "trim"}, analyzeStage{}, placeholderStage{name: "archive"}, placeholderStage{name: "notify"},