Update the analyze stage to utilize the new artifact package

This commit is contained in:
2026-05-18 01:29:18 +00:00
parent 7dc79e052f
commit 01fb02426c
6 changed files with 603 additions and 89 deletions

View File

@@ -3,6 +3,7 @@ package stage
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -87,27 +88,7 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
}, nil
}
processedTranscriptPath, processedSource, err := discoverProcessedTranscript(m, paths)
if err != nil {
return nil, fmt.Errorf("analyze: resolve processed transcript: %w", err)
}
normalizedTranscriptPath, normalizedSource, err := discoverNormalizedTranscript(m, paths)
if err != nil {
return nil, fmt.Errorf("analyze: resolve normalized transcript: %w", err)
}
trimmedTranscriptPath, trimmedSource, err := discoverTrimmedTranscript(m, paths)
if err != nil {
return nil, fmt.Errorf("analyze: resolve trimmed transcript: %w", err)
}
transcriptInputs := analyzeTranscriptInputs{
ProcessedPath: processedTranscriptPath,
ProcessedSource: processedSource,
NormalizedPath: normalizedTranscriptPath,
NormalizedSource: normalizedSource,
TrimmedPath: trimmedTranscriptPath,
TrimmedSource: trimmedSource,
}
transcriptRefs := discoverAnalyzeTranscriptRefs(m, paths)
inputPaths := map[string]string{}
omittedOptionalInputs := []string{}
@@ -115,7 +96,7 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
inputNames := sortedScriptoriumInputNames(artifactCfg.Inputs)
for _, inputName := range inputNames {
inputCfg := artifactCfg.Inputs[inputName]
resolvedPath, resolved, resolveErr := resolveScriptoriumInput(inputName, inputCfg, transcriptInputs, paths, sessionDir)
resolvedPath, resolved, resolveErr := resolveScriptoriumInput(inputName, inputCfg, m, paths, sessionDir)
if resolveErr != nil {
return nil, fmt.Errorf("analyze: resolve input %q: %w", inputName, resolveErr)
}
@@ -170,12 +151,12 @@ func (analyzeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
"omitted_optional_inputs": omittedOptionalInputs,
"vars": vars,
"timeout": timeout.String(),
"processed_transcript_path": processedTranscriptPath,
"processed_transcript_source": processedSource,
"normalized_transcript_path": normalizedTranscriptPath,
"normalized_transcript_source": normalizedSource,
"trimmed_transcript_path": trimmedTranscriptPath,
"trimmed_transcript_source": trimmedSource,
"processed_transcript_path": transcriptRefs.ProcessedPath,
"processed_transcript_source": transcriptRefs.ProcessedSource,
"normalized_transcript_path": transcriptRefs.NormalizedPath,
"normalized_transcript_source": transcriptRefs.NormalizedSource,
"trimmed_transcript_path": transcriptRefs.TrimmedPath,
"trimmed_transcript_source": transcriptRefs.TrimmedSource,
"render_debug_enabled": resolveRenderDebugEnabled(env.Config.Pipeline.Scriptorium.RenderDebug, artifactCfg.RenderDebug),
}
@@ -373,40 +354,6 @@ func discoverProcessedTranscript(m *manifest.Manifest, paths artifacts.SessionPa
return "", "", nil
}
func discoverTrimmedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths) (string, string, error) {
candidates := []string{}
if m != nil && m.Stages != nil {
if sr := m.Stages["trim"]; sr != nil {
for _, out := range sr.Outputs {
if out.Kind != "transcript_trimmed" {
continue
}
p := strings.TrimSpace(out.LocalPath)
if p == "" {
continue
}
resolved := artifacts.ResolveSessionLocalPathForRead(paths, p)
candidates = append(candidates, filepath.Clean(resolved))
}
}
}
deduped := dedupeAndSortPaths(candidates)
for _, p := range deduped {
if info, err := os.Stat(p); err == nil && !info.IsDir() {
return p, "manifest.trim.outputs", nil
}
}
fallback := filepath.Join(paths.TranscriptsDir, "trimmed.json")
if info, err := os.Stat(fallback); err == nil && !info.IsDir() {
return filepath.Clean(fallback), "fallback.transcripts_dir", nil
}
if len(deduped) > 0 {
return deduped[0], "manifest.trim.outputs", nil
}
return "", "", nil
}
type analyzeTranscriptInputs struct {
ProcessedPath string
ProcessedSource string
@@ -416,38 +363,36 @@ type analyzeTranscriptInputs struct {
TrimmedSource string
}
func discoverAnalyzeTranscriptRefs(m *manifest.Manifest, paths artifacts.SessionPaths) analyzeTranscriptInputs {
processedPath, processedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptPolished)
normalizedPath, normalizedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptFull)
trimmedPath, trimmedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptTrimmed)
return analyzeTranscriptInputs{
ProcessedPath: processedPath,
ProcessedSource: processedSource,
NormalizedPath: normalizedPath,
NormalizedSource: normalizedSource,
TrimmedPath: trimmedPath,
TrimmedSource: trimmedSource,
}
}
func discoverAnalyzeArtifactRef(m *manifest.Manifest, paths artifacts.SessionPaths, source string) (string, string) {
resolved, err := artifacts.ResolveSessionArtifact(paths, m, source)
if err != nil {
return "", ""
}
return resolved.Path, resolved.Provenance
}
func resolveScriptoriumInput(
inputName string,
inputCfg config.ScriptoriumInputConfig,
transcriptInputs analyzeTranscriptInputs,
m *manifest.Manifest,
paths artifacts.SessionPaths,
sessionDir string,
) (string, bool, error) {
switch strings.TrimSpace(inputCfg.Source) {
case "processed_transcript":
if strings.TrimSpace(transcriptInputs.ProcessedPath) == "" {
return "", false, nil
}
if err := validateProcessedTranscriptOutput(transcriptInputs.ProcessedPath); err != nil {
return "", false, fmt.Errorf("processed transcript %q invalid: %w", transcriptInputs.ProcessedPath, err)
}
return transcriptInputs.ProcessedPath, true, nil
case "normalized_transcript":
if strings.TrimSpace(transcriptInputs.NormalizedPath) == "" {
return "", false, fmt.Errorf("normalized transcript input is unavailable; run normalize stage first")
}
if err := validateProcessedTranscriptOutput(transcriptInputs.NormalizedPath); err != nil {
return "", false, fmt.Errorf("normalized transcript %q invalid: %w", transcriptInputs.NormalizedPath, err)
}
return transcriptInputs.NormalizedPath, true, nil
case "trimmed_transcript":
if strings.TrimSpace(transcriptInputs.TrimmedPath) == "" {
return "", false, fmt.Errorf("trimmed transcript input is unavailable; run trim stage first")
}
if err := validateProcessedTranscriptOutput(transcriptInputs.TrimmedPath); err != nil {
return "", false, fmt.Errorf("trimmed transcript %q invalid: %w", transcriptInputs.TrimmedPath, err)
}
return transcriptInputs.TrimmedPath, true, nil
case "previous_session_artifact":
if strings.TrimSpace(inputCfg.Path) == "" {
return "", false, nil
@@ -458,7 +403,27 @@ func resolveScriptoriumInput(
}
return resolved, true, nil
default:
return "", false, fmt.Errorf("unsupported source %q", inputCfg.Source)
resolved, err := artifacts.ResolveSessionArtifact(paths, m, inputCfg.Source)
if err == nil {
return resolved.Path, true, nil
}
if errors.Is(err, artifacts.ErrSessionArtifactNotFound) {
normalized, normalizeErr := artifacts.NormalizeSessionArtifactSource(inputCfg.Source)
if normalizeErr != nil {
return "", false, normalizeErr
}
switch normalized {
case artifacts.ArtifactTranscriptPolished:
return "", false, nil
case artifacts.ArtifactTranscriptFull:
return "", false, fmt.Errorf("normalized transcript input is unavailable; run normalize stage first")
case artifacts.ArtifactTranscriptTrimmed:
return "", false, fmt.Errorf("trimmed transcript input is unavailable; run trim stage first")
default:
return "", false, nil
}
}
return "", false, err
}
}

View File

@@ -473,6 +473,30 @@ func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) {
}
}
func TestAnalyzeSupportsCanonicalTrimmedTranscriptSourceWhenConfigured(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
Source: "narratio.transcript.trimmed",
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"] != filepath.Join(paths.TranscriptsDir, "trimmed.json") {
t.Fatalf("transcript input = %q, want trimmed transcript path", fake.RunRequests[0].InputPaths["transcript"])
}
}
func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)
@@ -498,6 +522,36 @@ func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) {
}
}
func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)
fallbackPath := filepath.Join(paths.TranscriptsDir, "normalized.json")
manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json")
writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`)
writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`)
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
{Kind: "transcript_normalized", LocalPath: manifestPath},
})
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
Source: "narratio.transcript.full",
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"] != manifestPath {
t.Fatalf("transcript input = %q, want manifest normalized transcript path", fake.RunRequests[0].InputPaths["transcript"])
}
}
func TestAnalyzeSupportsNormalizedTranscriptSourceFromManifestOutput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID)