Finish render rollout with markdown publish defaults, analyze guidance, and docs updates

This commit is contained in:
2026-05-25 00:46:27 +00:00
parent 2fece10215
commit 88cee96d8d
20 changed files with 206 additions and 23 deletions

View File

@@ -1002,6 +1002,8 @@ func TestExecutePublishLoadsRemoteLocks(t *testing.T) {
_ = stageName
}
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.trimmed.json"), `{"segments":[]}`)
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.md"), "# final\n")
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.trimmed.md"), "# final trimmed\n")
var stdout bytes.Buffer
var stderr bytes.Buffer

View File

@@ -273,7 +273,7 @@ func TestExecuteStagesPublishSkipsRequiredUnselectedConfiguredOutput(t *testing.
manifestPath := manifestPathFor(cfg)
seed := manifest.New(cfg.Session.SessionID, time.Now().UTC())
seed.Campaign = cfg.Session.Campaign
for _, stageName := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim"} {
for _, stageName := range []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "render"} {
seed.MarkStageSucceeded(stageName, time.Now().UTC(), nil)
}
if err := os.MkdirAll(filepath.Dir(manifestPath), 0o755); err != nil {

View File

@@ -67,6 +67,14 @@ func TestResolvePublishedDestination(t *testing.T) {
t.Fatalf("built-in destination = %q, want transcripts/final.trimmed.json", got)
}
got, err = ResolvePublishedDestination("narratio.transcript.final_markdown", "", configured)
if err != nil {
t.Fatalf("ResolvePublishedDestination(markdown built-in) error = %v", err)
}
if got != "transcripts/final.md" {
t.Fatalf("markdown built-in destination = %q, want transcripts/final.md", got)
}
got, err = ResolvePublishedDestination("narratio.artifact.session_recap", "", configured)
if err != nil {
t.Fatalf("ResolvePublishedDestination(configured) error = %v", err)
@@ -103,6 +111,7 @@ func TestDescribeScriptoriumInputSource(t *testing.T) {
wantErrLike string
}{
{name: "built in", source: "narratio.transcript.final_trimmed", wantKind: SourceKindBuiltIn},
{name: "built in markdown", source: "narratio.transcript.final_markdown", wantKind: SourceKindBuiltIn},
{name: "configured", source: "narratio.artifact.session_recap", wantKind: SourceKindConfiguredArtifact, wantKey: "session_recap"},
{name: "previous", source: "narratio.previous_session.artifact.session_recap", wantKind: SourceKindPreviousArtifact, wantKey: "session_recap", wantPrev: true},
{name: "invalid previous", source: "narratio.previous_session.artifact.", wantErr: ErrInvalidPreviousSessionSource},

View File

@@ -86,6 +86,8 @@ const (
// Callers should copy this slice before mutating.
var DefaultPublishOutputs = []PublishOutputRule{
{Source: artifactmodel.SourceTranscriptFinalTrimmed, Dest: PathTranscriptFinalTrimmed},
{Source: artifactmodel.SourceTranscriptFinalMarkdown, Dest: artifactmodel.TranscriptPathFinalMarkdown},
{Source: artifactmodel.SourceTranscriptFinalTrimmedMarkdown, Dest: artifactmodel.TranscriptPathFinalTrimmedMarkdown},
}
// DefaultPipelineConfigSearchPaths defines the default search order for

View File

@@ -200,6 +200,21 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
transcript:
source: narratio.transcript.final_trimmed
required: true
`,
},
{
name: "markdown built in artifact source is accepted",
scriptoriumYAML: `scriptorium:
binary: scriptorium
artifacts:
session_recap:
enabled: true
prompt_id: dnd.session_recap
output_path: artifacts/session_recap.md
inputs:
transcript_markdown:
source: narratio.transcript.final_markdown
required: true
`,
},
{

View File

@@ -179,18 +179,29 @@ func TestSpoolAndPublishDefaults(t *testing.T) {
if cfg.Pipeline.Publish.UploadRun == nil || !*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))
if len(cfg.Pipeline.Publish.Outputs) != 3 {
t.Fatalf("publish.outputs len = %d, want 3 defaults", len(cfg.Pipeline.Publish.Outputs))
}
item := cfg.Pipeline.Publish.Outputs[0]
if item.Required == nil || !*item.Required {
t.Fatalf("publish.outputs[0].required = %#v, want true", item.Required)
wantBySource := map[string]string{
"narratio.transcript.final_trimmed": "transcripts/final.trimmed.json",
"narratio.transcript.final_markdown": "transcripts/final.md",
"narratio.transcript.final_trimmed_markdown": "transcripts/final.trimmed.md",
}
if item.Source != "narratio.transcript.final_trimmed" {
t.Fatalf("publish.outputs[0].source = %q, want narratio.transcript.final_trimmed", item.Source)
for i, item := range cfg.Pipeline.Publish.Outputs {
if item.Required == nil || !*item.Required {
t.Fatalf("publish.outputs[%d].required = %#v, want true", i, item.Required)
}
wantDest, ok := wantBySource[item.Source]
if !ok {
t.Fatalf("publish.outputs[%d].source = %q, want known default source", i, item.Source)
}
if item.Dest != wantDest {
t.Fatalf("publish.outputs[%d].dest = %q, want %q", i, item.Dest, wantDest)
}
delete(wantBySource, item.Source)
}
if item.Dest != "transcripts/final.trimmed.json" {
t.Fatalf("publish.outputs[0].dest = %q, want transcripts/final.trimmed.json", item.Dest)
if len(wantBySource) != 0 {
t.Fatalf("missing default publish outputs for sources: %#v", wantBySource)
}
}
@@ -337,6 +348,15 @@ publish:
`,
wantDest: "artifacts/session_recap.md",
},
{
name: "markdown built in derives canonical destination",
pipelineYML: testPipelineBaseYAML + `
publish:
outputs:
- source: narratio.transcript.final_markdown
`,
wantDest: "transcripts/final.md",
},
}
for _, tt := range tests {

View File

@@ -670,6 +670,12 @@ func resolveScriptoriumInput(
return "", false, nil, fmt.Errorf("normalized transcript input is unavailable; run normalize stage first")
case artifacts.ArtifactTranscriptFinalTrimmed:
return "", false, nil, fmt.Errorf("trimmed transcript input is unavailable; run trim stage first")
case artifacts.ArtifactTranscriptFinalMarkdown, artifacts.ArtifactTranscriptFinalTrimmedMarkdown:
return "", false, nil, fmt.Errorf(
"rendered markdown transcript input is unavailable for source %q; run narratio run-stage --force render %s",
descriptor.Source.ID,
paths.SessionID,
)
default:
return "", false, nil, nil
}

View File

@@ -961,6 +961,31 @@ func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) {
}
}
func TestAnalyzeSupportsRenderedMarkdownTranscriptSourceWhenConfigured(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)
markdownPath := filepath.Join(paths.TranscriptsDir, "final.md")
writeAnalyzeFile(t, markdownPath, "# Session Transcript\n")
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
Source: "narratio.transcript.final_markdown",
Required: true,
}
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if len(fake.RunRequests) != 1 {
t.Fatalf("run requests = %d, want 1", len(fake.RunRequests))
}
if fake.RunRequests[0].InputPaths["transcript"] != markdownPath {
t.Fatalf("transcript input = %q, want markdown transcript path", fake.RunRequests[0].InputPaths["transcript"])
}
}
func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)
@@ -1045,6 +1070,42 @@ func TestAnalyzeFailsWhenNormalizedTranscriptMissing(t *testing.T) {
}
}
func TestAnalyzeFailsWhenRenderedMarkdownTranscriptMissing(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t)
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
Source: "narratio.transcript.final_markdown",
Required: true,
}
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "run narratio run-stage --force render") {
t.Fatalf("error = %q, want render guidance", err.Error())
}
}
func TestAnalyzeFailsWhenRenderedTrimmedMarkdownTranscriptMissing(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t)
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
Source: "narratio.transcript.final_trimmed_markdown",
Required: true,
}
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
_, err := (analyzeStage{}).Run(context.Background(), env, m)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "run narratio run-stage --force render") {
t.Fatalf("error = %q, want render guidance", err.Error())
}
}
func TestAnalyzeFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)

View File

@@ -32,6 +32,7 @@ var publishPrerequisiteStages = []string{
"polish",
"normalize",
"trim",
"render",
"analyze",
}