Enforce requested adapter output paths
This commit is contained in:
@@ -26,6 +26,8 @@ Generate raw per-speaker transcripts from prepared audio using WhisperX.
|
|||||||
- prepared audio identities must be unique; prepare disambiguates distinct
|
- prepared audio identities must be unique; prepare disambiguates distinct
|
||||||
source paths that share a basename.
|
source paths that share a basename.
|
||||||
- output path returned by adapter must match requested output path.
|
- output path returned by adapter must match requested output path.
|
||||||
|
- an empty adapter result path means the requested path; adapters cannot select
|
||||||
|
an alternate destination.
|
||||||
- each successful output is validated before stage success, and cancellation or
|
- each successful output is validated before stage success, and cancellation or
|
||||||
incomplete dispatch cannot be reported as a successful result.
|
incomplete dispatch cannot be reported as a successful result.
|
||||||
|
|
||||||
|
|||||||
@@ -869,6 +869,8 @@ transcripts through one artifact-owned contract.
|
|||||||
no output, multiple transcripts, unsafe paths, adapter conformance, and plural raw
|
no output, multiple transcripts, unsafe paths, adapter conformance, and plural raw
|
||||||
sources. No adapter-selected alternate path may become authoritative.
|
sources. No adapter-selected alternate path may become authoritative.
|
||||||
|
|
||||||
|
**Status:** Completed.
|
||||||
|
|
||||||
## Stage 26 — Centralize typed extraction-bundle evidence
|
## Stage 26 — Centralize typed extraction-bundle evidence
|
||||||
|
|
||||||
**Read first:** `audit-findings.md` lines 3538–3568 (DUP-007) and 3907–3932
|
**Read first:** `audit-findings.md` lines 3538–3568 (DUP-007) and 3907–3932
|
||||||
|
|||||||
@@ -438,7 +438,10 @@ func executeAnalyzeArtifact(
|
|||||||
if renderRes.ValidationFailed {
|
if renderRes.ValidationFailed {
|
||||||
return nil, fmt.Errorf("analyze: scriptorium render returned validation_failed=true for artifact %q", artifactName)
|
return nil, fmt.Errorf("analyze: scriptorium render returned validation_failed=true for artifact %q", artifactName)
|
||||||
}
|
}
|
||||||
finalRenderOutputPath := coalesceString(renderRes.OutputPath, renderReq.OutputPath)
|
finalRenderOutputPath, err := authoritativeOutputPath(renderReq.OutputPath, renderRes.OutputPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("analyze: %w", err)
|
||||||
|
}
|
||||||
if err := requireNonEmptyFile(finalRenderOutputPath, artifactName+" render output"); err != nil {
|
if err := requireNonEmptyFile(finalRenderOutputPath, artifactName+" render output"); err != nil {
|
||||||
return nil, fmt.Errorf("analyze: %w", err)
|
return nil, fmt.Errorf("analyze: %w", err)
|
||||||
}
|
}
|
||||||
@@ -487,7 +490,7 @@ func executeAnalyzeArtifact(
|
|||||||
"analyze: scriptorium validation failed (artifact=%q, prompt_id=%q, output_path=%q, exit_code=%d, stdout_log=%q, stderr_log=%q): %w",
|
"analyze: scriptorium validation failed (artifact=%q, prompt_id=%q, output_path=%q, exit_code=%d, stdout_log=%q, stderr_log=%q): %w",
|
||||||
artifactName,
|
artifactName,
|
||||||
req.PromptID,
|
req.PromptID,
|
||||||
coalesceString(res.OutputPath, req.OutputPath),
|
req.OutputPath,
|
||||||
res.ExitCode,
|
res.ExitCode,
|
||||||
coalesceString(res.StdoutLogPath, req.StdoutLogPath),
|
coalesceString(res.StdoutLogPath, req.StdoutLogPath),
|
||||||
coalesceString(res.StderrLogPath, req.StderrLogPath),
|
coalesceString(res.StderrLogPath, req.StderrLogPath),
|
||||||
@@ -500,7 +503,10 @@ func executeAnalyzeArtifact(
|
|||||||
return nil, fmt.Errorf("analyze: scriptorium run returned validation_failed=true for artifact %q", artifactName)
|
return nil, fmt.Errorf("analyze: scriptorium run returned validation_failed=true for artifact %q", artifactName)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalOutputPath := coalesceString(res.OutputPath, req.OutputPath)
|
finalOutputPath, err := authoritativeOutputPath(req.OutputPath, res.OutputPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("analyze: %w", err)
|
||||||
|
}
|
||||||
if err := requireNonEmptyFile(finalOutputPath, artifactName+" output"); err != nil {
|
if err := requireNonEmptyFile(finalOutputPath, artifactName+" output"); err != nil {
|
||||||
return nil, fmt.Errorf("analyze: %w", err)
|
return nil, fmt.Errorf("analyze: %w", err)
|
||||||
}
|
}
|
||||||
@@ -562,37 +568,7 @@ func configuredArtifactNameFromSourceID(sourceID string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func discoverProcessedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths) (string, string, error) {
|
func discoverProcessedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths) (string, string, error) {
|
||||||
candidates := []string{}
|
return resolveSingletonTranscript(m, paths, artifacts.ArtifactTranscriptPolished)
|
||||||
if m != nil && m.Stages != nil {
|
|
||||||
if sr := m.Stages["polish"]; sr != nil {
|
|
||||||
for _, out := range sr.Outputs {
|
|
||||||
if out.Kind != "transcript_polished" {
|
|
||||||
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.polish.outputs", nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fallback := filepath.Join(paths.TranscriptsDir, "polished.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.polish.outputs", nil
|
|
||||||
}
|
|
||||||
return "", "", nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type analyzeTranscriptInputs struct {
|
type analyzeTranscriptInputs struct {
|
||||||
|
|||||||
@@ -133,17 +133,17 @@ func (mergeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Sta
|
|||||||
return nil, fmt.Errorf("merge: seriatim merge failed: %w", err)
|
return nil, fmt.Errorf("merge: seriatim merge failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalMergedPath := mergedPath
|
finalMergedPath, err := authoritativeOutputPath(mergedPath, res.MergedTranscriptPath)
|
||||||
if strings.TrimSpace(res.MergedTranscriptPath) != "" {
|
if err != nil {
|
||||||
finalMergedPath = res.MergedTranscriptPath
|
return nil, fmt.Errorf("merge: %w", err)
|
||||||
}
|
}
|
||||||
if err := validateTranscriptJSONFile(finalMergedPath); err != nil {
|
if err := validateTranscriptJSONFile(finalMergedPath); err != nil {
|
||||||
return nil, fmt.Errorf("merge: merged transcript %q invalid: %w", finalMergedPath, err)
|
return nil, fmt.Errorf("merge: merged transcript %q invalid: %w", finalMergedPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalReportPath := req.ReportPath
|
finalReportPath, err := authoritativeOutputPath(req.ReportPath, res.ReportPath)
|
||||||
if strings.TrimSpace(res.ReportPath) != "" {
|
if err != nil {
|
||||||
finalReportPath = res.ReportPath
|
return nil, fmt.Errorf("merge: %w", err)
|
||||||
}
|
}
|
||||||
if reportEnabled {
|
if reportEnabled {
|
||||||
if err := validateTranscriptJSONFile(finalReportPath); err != nil {
|
if err := validateTranscriptJSONFile(finalReportPath); err != nil {
|
||||||
@@ -290,9 +290,9 @@ func normalizeMergeInputs(
|
|||||||
return nil, nil, nil, nil, fmt.Errorf("merge: normalize input %q failed: %w", input, err)
|
return nil, nil, nil, nil, fmt.Errorf("merge: normalize input %q failed: %w", input, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalOutputPath := outPath
|
finalOutputPath, err := authoritativeOutputPath(outPath, res.OutputNormalizedPath)
|
||||||
if strings.TrimSpace(res.OutputNormalizedPath) != "" {
|
if err != nil {
|
||||||
finalOutputPath = strings.TrimSpace(res.OutputNormalizedPath)
|
return nil, nil, nil, nil, fmt.Errorf("merge: normalize input %q: %w", input, err)
|
||||||
}
|
}
|
||||||
if err := validateTranscriptJSONFile(finalOutputPath); err != nil {
|
if err := validateTranscriptJSONFile(finalOutputPath); err != nil {
|
||||||
return nil, nil, nil, nil, fmt.Errorf("merge: normalized transcript %q invalid (input %q): %w", finalOutputPath, input, err)
|
return nil, nil, nil, nil, fmt.Errorf("merge: normalized transcript %q invalid (input %q): %w", finalOutputPath, input, err)
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ func TestMergeStageFailsWhenNormalizedOutputInvalid(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "normalized transcript") {
|
if !strings.Contains(err.Error(), "output path") {
|
||||||
t.Fatalf("error = %q", err.Error())
|
t.Fatalf("error = %q", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package stage
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -121,12 +120,18 @@ func (normalizeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (
|
|||||||
return nil, fmt.Errorf("normalize: seriatim normalize failed: %w", err)
|
return nil, fmt.Errorf("normalize: seriatim normalize failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalNormalizedPath := coalesceString(res.OutputNormalizedPath, req.OutputNormalizedPath)
|
finalNormalizedPath, err := authoritativeOutputPath(req.OutputNormalizedPath, res.OutputNormalizedPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("normalize: %w", err)
|
||||||
|
}
|
||||||
if err := validateProcessedTranscriptOutput(finalNormalizedPath); err != nil {
|
if err := validateProcessedTranscriptOutput(finalNormalizedPath); err != nil {
|
||||||
return nil, fmt.Errorf("normalize: normalized transcript %q invalid: %w", finalNormalizedPath, err)
|
return nil, fmt.Errorf("normalize: normalized transcript %q invalid: %w", finalNormalizedPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalReportPath := coalesceString(res.ReportPath, req.ReportPath)
|
finalReportPath, err := authoritativeOutputPath(req.ReportPath, res.ReportPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("normalize: %w", err)
|
||||||
|
}
|
||||||
if reportEnabled {
|
if reportEnabled {
|
||||||
if err := validateJSONFile(finalReportPath); err != nil {
|
if err := validateJSONFile(finalReportPath); err != nil {
|
||||||
return nil, fmt.Errorf("normalize: report %q invalid: %w", finalReportPath, err)
|
return nil, fmt.Errorf("normalize: report %q invalid: %w", finalReportPath, err)
|
||||||
@@ -209,32 +214,5 @@ func normalizeConfigOrDefault(cfg *config.NormalizeConfig) *config.NormalizeConf
|
|||||||
}
|
}
|
||||||
|
|
||||||
func discoverNormalizedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths) (string, string, error) {
|
func discoverNormalizedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths) (string, string, error) {
|
||||||
candidates := []string{}
|
return resolveSingletonTranscript(m, paths, artifacts.ArtifactTranscriptFinal)
|
||||||
if m != nil && m.Stages != nil {
|
|
||||||
if sr := m.Stages["normalize"]; sr != nil {
|
|
||||||
for _, out := range sr.Outputs {
|
|
||||||
if out.Kind != "transcript_final" {
|
|
||||||
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.normalize.outputs", nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fallback := filepath.Join(paths.TranscriptsDir, "final.json")
|
|
||||||
if info, err := os.Stat(fallback); err == nil && !info.IsDir() {
|
|
||||||
return filepath.Clean(fallback), "fallback.transcripts_dir", nil
|
|
||||||
}
|
|
||||||
return "", "", nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ func TestNormalizeStageFailsWhenNormalizedOutputInvalid(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "normalized transcript") {
|
if !strings.Contains(err.Error(), "output path") {
|
||||||
t.Fatalf("error = %q", err.Error())
|
t.Fatalf("error = %q", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
internal/stage/output_path.go
Normal file
27
internal/stage/output_path.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package stage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// authoritativeOutputPath accepts an omitted adapter result path as the requested
|
||||||
|
// path and rejects any adapter attempt to redirect the stage-owned destination.
|
||||||
|
func authoritativeOutputPath(requested, returned string) (string, error) {
|
||||||
|
requested = filepath.Clean(strings.TrimSpace(requested))
|
||||||
|
if requested == "" {
|
||||||
|
if strings.TrimSpace(returned) == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("adapter returned output path %q without a requested destination", returned)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(returned) == "" {
|
||||||
|
return requested, nil
|
||||||
|
}
|
||||||
|
returned = filepath.Clean(strings.TrimSpace(returned))
|
||||||
|
if returned != requested {
|
||||||
|
return "", fmt.Errorf("adapter output path %q did not match requested path %q", returned, requested)
|
||||||
|
}
|
||||||
|
return requested, nil
|
||||||
|
}
|
||||||
30
internal/stage/output_path_test.go
Normal file
30
internal/stage/output_path_test.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package stage
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAuthoritativeOutputPath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
requested string
|
||||||
|
returned string
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "empty result", requested: "outputs/result.json", want: "outputs/result.json"},
|
||||||
|
{name: "exact result", requested: "outputs/result.json", returned: "outputs/result.json", want: "outputs/result.json"},
|
||||||
|
{name: "clean equivalent", requested: "outputs/result.json", returned: "outputs/next/../result.json", want: "outputs/result.json"},
|
||||||
|
{name: "different result", requested: "outputs/result.json", returned: "other/result.json", wantErr: true},
|
||||||
|
{name: "unexpected result without destination", returned: "other/result.json", wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := authoritativeOutputPath(tt.requested, tt.returned)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Fatalf("authoritativeOutputPath(%q, %q) error = %v", tt.requested, tt.returned, err)
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Fatalf("authoritativeOutputPath(%q, %q) = %q, want %q", tt.requested, tt.returned, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -131,17 +130,17 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
|||||||
return nil, fmt.Errorf("polish: audita polish failed: %w", err)
|
return nil, fmt.Errorf("polish: audita polish failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalProcessedPath := processedPath
|
finalProcessedPath, err := authoritativeOutputPath(processedPath, res.ProcessedTranscriptPath)
|
||||||
if strings.TrimSpace(res.ProcessedTranscriptPath) != "" {
|
if err != nil {
|
||||||
finalProcessedPath = res.ProcessedTranscriptPath
|
return nil, fmt.Errorf("polish: %w", err)
|
||||||
}
|
}
|
||||||
if err := validateProcessedTranscriptOutput(finalProcessedPath); err != nil {
|
if err := validateProcessedTranscriptOutput(finalProcessedPath); err != nil {
|
||||||
return nil, fmt.Errorf("polish: processed transcript %q invalid: %w", finalProcessedPath, err)
|
return nil, fmt.Errorf("polish: processed transcript %q invalid: %w", finalProcessedPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalReportPath := req.ReportPath
|
finalReportPath, err := authoritativeOutputPath(req.ReportPath, res.ReportPath)
|
||||||
if strings.TrimSpace(res.ReportPath) != "" {
|
if err != nil {
|
||||||
finalReportPath = res.ReportPath
|
return nil, fmt.Errorf("polish: %w", err)
|
||||||
}
|
}
|
||||||
if reportEnabled {
|
if reportEnabled {
|
||||||
if err := validateTranscriptJSONFile(finalReportPath); err != nil {
|
if err := validateTranscriptJSONFile(finalReportPath); err != nil {
|
||||||
@@ -246,38 +245,7 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
|||||||
}
|
}
|
||||||
|
|
||||||
func discoverMergedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths) (string, string, error) {
|
func discoverMergedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths) (string, string, error) {
|
||||||
candidates := make([]string, 0)
|
return resolveSingletonTranscript(m, paths, artifacts.ArtifactTranscriptBase)
|
||||||
if m != nil && m.Stages != nil {
|
|
||||||
if sr := m.Stages["merge"]; sr != nil {
|
|
||||||
for _, out := range sr.Outputs {
|
|
||||||
if out.Kind != "transcript_base" {
|
|
||||||
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.merge.outputs", nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fallback := filepath.Join(paths.TranscriptsDir, "base.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.merge.outputs", nil
|
|
||||||
}
|
|
||||||
return "", "", nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateProcessedTranscriptOutput(path string) error {
|
func validateProcessedTranscriptOutput(path string) error {
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ func TestPolishStageFailsWhenProcessedOutputInvalid(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "processed transcript") {
|
if !strings.Contains(err.Error(), "output path") {
|
||||||
t.Fatalf("error = %q", err.Error())
|
t.Fatalf("error = %q", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,7 +157,10 @@ func (renderStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("render: seriatim render failed for %q: %w", artifacts.ArtifactTranscriptFinal, err)
|
return nil, fmt.Errorf("render: seriatim render failed for %q: %w", artifacts.ArtifactTranscriptFinal, err)
|
||||||
}
|
}
|
||||||
finalRenderedPath := coalesceString(finalRes.OutputRenderedPath, finalReq.OutputRenderedPath)
|
finalRenderedPath, err := authoritativeOutputPath(finalReq.OutputRenderedPath, finalRes.OutputRenderedPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("render: %w", err)
|
||||||
|
}
|
||||||
if err := requireNonEmptyFile(finalRenderedPath, "final transcript markdown output"); err != nil {
|
if err := requireNonEmptyFile(finalRenderedPath, "final transcript markdown output"); err != nil {
|
||||||
return nil, fmt.Errorf("render: %w", err)
|
return nil, fmt.Errorf("render: %w", err)
|
||||||
}
|
}
|
||||||
@@ -180,7 +183,10 @@ func (renderStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("render: seriatim render failed for %q: %w", artifacts.ArtifactTranscriptFinalTrimmed, err)
|
return nil, fmt.Errorf("render: seriatim render failed for %q: %w", artifacts.ArtifactTranscriptFinalTrimmed, err)
|
||||||
}
|
}
|
||||||
finalTrimmedRenderedPath := coalesceString(finalTrimmedRes.OutputRenderedPath, finalTrimmedReq.OutputRenderedPath)
|
finalTrimmedRenderedPath, err := authoritativeOutputPath(finalTrimmedReq.OutputRenderedPath, finalTrimmedRes.OutputRenderedPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("render: %w", err)
|
||||||
|
}
|
||||||
if err := requireNonEmptyFile(finalTrimmedRenderedPath, "final trimmed transcript markdown output"); err != nil {
|
if err := requireNonEmptyFile(finalTrimmedRenderedPath, "final trimmed transcript markdown output"); err != nil {
|
||||||
return nil, fmt.Errorf("render: %w", err)
|
return nil, fmt.Errorf("render: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,13 +139,10 @@ func (transcribeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(res.OutputRawTranscriptPath) == "" {
|
if _, err := authoritativeOutputPath(j.outPath, res.OutputRawTranscriptPath); err != nil {
|
||||||
res.OutputRawTranscriptPath = j.outPath
|
|
||||||
}
|
|
||||||
if filepath.Clean(res.OutputRawTranscriptPath) != filepath.Clean(j.outPath) {
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
if firstErr == nil {
|
if firstErr == nil {
|
||||||
firstErr = fmt.Errorf("speaker %q: adapter output path %q did not match expected %q", j.speakerID, res.OutputRawTranscriptPath, j.outPath)
|
firstErr = fmt.Errorf("speaker %q: %w", j.speakerID, err)
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|||||||
19
internal/stage/transcript_resolution.go
Normal file
19
internal/stage/transcript_resolution.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package stage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolveSingletonTranscript(m *manifest.Manifest, paths artifacts.SessionPaths, source string) (string, string, error) {
|
||||||
|
resolved, err := artifacts.ResolveSessionArtifact(paths, m, source)
|
||||||
|
if errors.Is(err, artifacts.ErrSessionArtifactNotFound) {
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return resolved.Path, resolved.Provenance, nil
|
||||||
|
}
|
||||||
@@ -200,7 +200,10 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
|
|||||||
if renderRes.ValidationFailed {
|
if renderRes.ValidationFailed {
|
||||||
return nil, fmt.Errorf("trim: scriptorium bounds render returned validation_failed=true")
|
return nil, fmt.Errorf("trim: scriptorium bounds render returned validation_failed=true")
|
||||||
}
|
}
|
||||||
finalRenderOutputPath := coalesceString(renderRes.OutputPath, renderReq.OutputPath)
|
finalRenderOutputPath, err := authoritativeOutputPath(renderReq.OutputPath, renderRes.OutputPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("trim: %w", err)
|
||||||
|
}
|
||||||
if err := requireNonEmptyFile(finalRenderOutputPath, "bounds render output"); err != nil {
|
if err := requireNonEmptyFile(finalRenderOutputPath, "bounds render output"); err != nil {
|
||||||
return nil, fmt.Errorf("trim: %w", err)
|
return nil, fmt.Errorf("trim: %w", err)
|
||||||
}
|
}
|
||||||
@@ -242,7 +245,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
|
|||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"trim: scriptorium bounds validation failed (prompt_id=%q, output_path=%q, exit_code=%d, stdout_log=%q, stderr_log=%q): %w",
|
"trim: scriptorium bounds validation failed (prompt_id=%q, output_path=%q, exit_code=%d, stdout_log=%q, stderr_log=%q): %w",
|
||||||
boundsReq.PromptID,
|
boundsReq.PromptID,
|
||||||
coalesceString(boundsRes.OutputPath, boundsReq.OutputPath),
|
boundsReq.OutputPath,
|
||||||
boundsRes.ExitCode,
|
boundsRes.ExitCode,
|
||||||
coalesceString(boundsRes.StdoutLogPath, boundsReq.StdoutLogPath),
|
coalesceString(boundsRes.StdoutLogPath, boundsReq.StdoutLogPath),
|
||||||
coalesceString(boundsRes.StderrLogPath, boundsReq.StderrLogPath),
|
coalesceString(boundsRes.StderrLogPath, boundsReq.StderrLogPath),
|
||||||
@@ -255,7 +258,10 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
|
|||||||
return nil, fmt.Errorf("trim: scriptorium bounds run returned validation_failed=true")
|
return nil, fmt.Errorf("trim: scriptorium bounds run returned validation_failed=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
finalBoundsOutputPath := coalesceString(boundsRes.OutputPath, boundsReq.OutputPath)
|
finalBoundsOutputPath, err := authoritativeOutputPath(boundsReq.OutputPath, boundsRes.OutputPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("trim: %w", err)
|
||||||
|
}
|
||||||
if err := requireNonEmptyFile(finalBoundsOutputPath, "session bounds output"); err != nil {
|
if err := requireNonEmptyFile(finalBoundsOutputPath, "session bounds output"); err != nil {
|
||||||
return nil, fmt.Errorf("trim: %w", err)
|
return nil, fmt.Errorf("trim: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user