Replace normalize stage with trim scaffold

This commit is contained in:
2026-05-08 16:51:01 +00:00
parent 4d646f161a
commit 38dae8b584
11 changed files with 88 additions and 45 deletions

View File

@@ -17,7 +17,7 @@ Implemented now:
Not implemented yet: Not implemented yet:
- real `normalize` behavior - real `trim` behavior
- real `archive` behavior - real `archive` behavior
- real `notify` behavior - real `notify` behavior
- additional analyze artifacts beyond `session_recap` - additional analyze artifacts beyond `session_recap`

View File

@@ -23,7 +23,7 @@ Implemented:
Still placeholder/future: Still placeholder/future:
- `normalize` stage behavior - `trim` stage behavior
- `archive` stage behavior - `archive` stage behavior
- `notify` stage behavior - `notify` stage behavior
- additional Scriptorium artifact types beyond `session_recap` - additional Scriptorium artifact types beyond `session_recap`
@@ -36,9 +36,9 @@ Canonical stage order:
1. `prepare` 1. `prepare`
2. `transcribe` 2. `transcribe`
3. `normalize` 3. `merge`
4. `merge` 4. `polish`
5. `polish` 5. `trim`
6. `analyze` 6. `analyze`
7. `archive` 7. `archive`
8. `notify` 8. `notify`

View File

@@ -32,7 +32,7 @@ func TestExecuteValidCommands(t *testing.T) {
wantOut string wantOut string
}{ }{
{name: "run", args: []string{"run", "--config", pipelinePath, "--session", sessionPath}, wantOut: "narratio run: session 2026-05-03; executed=8 skipped=0; manifest="}, {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: "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: "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="}, {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) { func TestExecuteRunStageTranscribeUsesConfiguredWhisperXServer(t *testing.T) {
workspaceRoot := t.TempDir() workspaceRoot := t.TempDir()
var serverCalls int var serverCalls int

View File

@@ -27,7 +27,7 @@ func TestPlanCreatesAndReusesWorkdir(t *testing.T) {
if !strings.Contains(got, "narratio plan: workdir prepared at") { if !strings.Contains(got, "narratio plan: workdir prepared at") {
t.Fatalf("first output = %q, want workdir prepared", got) 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") { if !strings.Contains(got, name+": run") {
t.Fatalf("first output = %q, missing stage %q", got, name) 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, "inputs"),
filepath.Join(sessionWorkdir, "audio"), filepath.Join(sessionWorkdir, "audio"),
filepath.Join(sessionWorkdir, "transcripts", "raw"), filepath.Join(sessionWorkdir, "transcripts", "raw"),
filepath.Join(sessionWorkdir, "transcripts", "normalized"), filepath.Join(sessionWorkdir, "transcripts", "trimmed"),
filepath.Join(sessionWorkdir, "artifacts"), filepath.Join(sessionWorkdir, "artifacts"),
filepath.Join(sessionWorkdir, "config"), filepath.Join(sessionWorkdir, "config"),
filepath.Join(sessionWorkdir, "logs"), filepath.Join(sessionWorkdir, "logs"),
@@ -81,8 +81,8 @@ func TestPlanShowsRunAndSkipFromManifest(t *testing.T) {
if !strings.Contains(got, "prepare: skip") || !strings.Contains(got, "transcribe: skip") { if !strings.Contains(got, "prepare: skip") || !strings.Contains(got, "transcribe: skip") {
t.Fatalf("output = %q, want prepare/transcribe skipped", got) t.Fatalf("output = %q, want prepare/transcribe skipped", got)
} }
if !strings.Contains(got, "normalize: run") { if !strings.Contains(got, "trim: run") {
t.Fatalf("output = %q, want normalize run", got) t.Fatalf("output = %q, want trim run", got)
} }
if !strings.Contains(got, "totals: run=6 skip=2") { if !strings.Contains(got, "totals: run=6 skip=2") {
t.Fatalf("output = %q, want totals run=6 skip=2", got) t.Fatalf("output = %q, want totals run=6 skip=2", got)

View File

@@ -4,7 +4,7 @@ import "testing"
func TestBuildFullPlanOrder(t *testing.T) { func TestBuildFullPlanOrder(t *testing.T) {
got := BuildFullPlan() 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) { if len(got) != len(want) {
t.Fatalf("len(plan) = %d, want %d", len(got), len(want)) t.Fatalf("len(plan) = %d, want %d", len(got), len(want))
} }

View File

@@ -44,8 +44,8 @@ func TestResumeStartsAfterCompletedStages(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("load manifest: %v", err) t.Fatalf("load manifest: %v", err)
} }
if loaded.Stages["normalize"] == nil || loaded.Stages["normalize"].Status != manifest.StatusSucceeded { if loaded.Stages["trim"] == nil || loaded.Stages["trim"].Status != manifest.StatusSucceeded {
t.Fatalf("normalize stage = %#v, want succeeded", loaded.Stages["normalize"]) t.Fatalf("trim stage = %#v, want succeeded", loaded.Stages["trim"])
} }
} }
@@ -56,7 +56,7 @@ func TestResumeNoRemainingStages(t *testing.T) {
store := &manifest.LocalStore{} store := &manifest.LocalStore{}
m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) 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) 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 { if err := store.Save(context.Background(), manifestPath, m); err != nil {
@@ -85,7 +85,7 @@ func TestResumeForceRerunsSucceeded(t *testing.T) {
store := &manifest.LocalStore{} store := &manifest.LocalStore{}
m := manifest.New("2026-05-03", time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)) 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) 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 { 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()) 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)
}
}

View File

@@ -61,7 +61,7 @@ func TestExecuteStagesPlaceholderSuccessUpdatesManifest(t *testing.T) {
t.Fatalf("Load manifest error = %v", err) 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] sr := m.Stages[name]
if sr == nil { if sr == nil {
t.Fatalf("missing stage record %q", name) 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 { if got := m.Stages["transcribe"]; got == nil || got.Status != manifest.StatusFailed {
t.Fatalf("transcribe status = %#v, want failed", got) t.Fatalf("transcribe status = %#v, want failed", got)
} }
if got := m.Stages["normalize"]; got != nil { if got := m.Stages["merge"]; got != nil {
t.Fatalf("normalize should not run, got %#v", got) t.Fatalf("merge should not run, got %#v", got)
} }
} }

View File

@@ -51,7 +51,7 @@ func (s *LocalStore) EnsureLayout(sessionID string) (SessionPaths, error) {
paths.AudioDir, paths.AudioDir,
paths.TranscriptsDir, paths.TranscriptsDir,
paths.TranscriptsRawDir, paths.TranscriptsRawDir,
paths.TranscriptsNormalizedDir, paths.TranscriptsTrimmedDir,
paths.ArtifactsDir, paths.ArtifactsDir,
paths.ConfigDir, paths.ConfigDir,
paths.LogsDir, paths.LogsDir,

View File

@@ -20,7 +20,7 @@ func TestEnsureLayoutCreatesExpectedDirectories(t *testing.T) {
checkDirExists(t, paths.AudioDir) checkDirExists(t, paths.AudioDir)
checkDirExists(t, paths.TranscriptsDir) checkDirExists(t, paths.TranscriptsDir)
checkDirExists(t, paths.TranscriptsRawDir) checkDirExists(t, paths.TranscriptsRawDir)
checkDirExists(t, paths.TranscriptsNormalizedDir) checkDirExists(t, paths.TranscriptsTrimmedDir)
checkDirExists(t, paths.ArtifactsDir) checkDirExists(t, paths.ArtifactsDir)
checkDirExists(t, paths.ConfigDir) checkDirExists(t, paths.ConfigDir)
checkDirExists(t, paths.LogsDir) checkDirExists(t, paths.LogsDir)

View File

@@ -4,18 +4,18 @@ import "path/filepath"
// SessionPaths contains canonical local paths for one session work directory. // SessionPaths contains canonical local paths for one session work directory.
type SessionPaths struct { type SessionPaths struct {
WorkspaceRoot string WorkspaceRoot string
Root string Root string
InputsDir string InputsDir string
AudioDir string AudioDir string
TranscriptsDir string TranscriptsDir string
TranscriptsRawDir string TranscriptsRawDir string
TranscriptsNormalizedDir string TranscriptsTrimmedDir string
ArtifactsDir string ArtifactsDir string
ConfigDir string ConfigDir string
LogsDir string LogsDir string
ManifestPath string ManifestPath string
LockPath string LockPath string
} }
// SessionWorkDir returns the work directory for one session. // SessionWorkDir returns the work directory for one session.
@@ -27,17 +27,17 @@ func buildSessionPaths(workspaceRoot, sessionID string) SessionPaths {
root := SessionWorkDir(workspaceRoot, sessionID) root := SessionWorkDir(workspaceRoot, sessionID)
transcripts := filepath.Join(root, "transcripts") transcripts := filepath.Join(root, "transcripts")
return SessionPaths{ return SessionPaths{
WorkspaceRoot: workspaceRoot, WorkspaceRoot: workspaceRoot,
Root: root, Root: root,
InputsDir: filepath.Join(root, "inputs"), InputsDir: filepath.Join(root, "inputs"),
AudioDir: filepath.Join(root, "audio"), AudioDir: filepath.Join(root, "audio"),
TranscriptsDir: transcripts, TranscriptsDir: transcripts,
TranscriptsRawDir: filepath.Join(transcripts, "raw"), TranscriptsRawDir: filepath.Join(transcripts, "raw"),
TranscriptsNormalizedDir: filepath.Join(transcripts, "normalized"), TranscriptsTrimmedDir: filepath.Join(transcripts, "trimmed"),
ArtifactsDir: filepath.Join(root, "artifacts"), ArtifactsDir: filepath.Join(root, "artifacts"),
ConfigDir: filepath.Join(root, "config"), ConfigDir: filepath.Join(root, "config"),
LogsDir: filepath.Join(root, "logs"), LogsDir: filepath.Join(root, "logs"),
ManifestPath: filepath.Join(root, "manifest.json"), ManifestPath: filepath.Join(root, "manifest.json"),
LockPath: filepath.Join(root, ".lock"), LockPath: filepath.Join(root, ".lock"),
} }
} }

View File

@@ -90,9 +90,9 @@ func All() []Stage {
return []Stage{ return []Stage{
prepareStage{}, prepareStage{},
transcribeStage{}, transcribeStage{},
placeholderStage{name: "normalize"},
mergeStage{}, mergeStage{},
polishStage{}, polishStage{},
placeholderStage{name: "trim"},
analyzeStage{}, analyzeStage{},
placeholderStage{name: "archive"}, placeholderStage{name: "archive"},
placeholderStage{name: "notify"}, placeholderStage{name: "notify"},