Add render artifact model, config, and Seriatim adapter contracts

This commit is contained in:
2026-05-25 00:28:01 +00:00
parent a51228c803
commit 0658f2f642
18 changed files with 915 additions and 43 deletions

View File

@@ -8,6 +8,7 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/subprocess"
)
@@ -384,6 +385,96 @@ func (r *SubprocessRunner) Normalize(ctx context.Context, req NormalizeRequest)
}, nil
}
// Render executes Seriatim render with deterministic flags and validates non-empty text output.
func (r *SubprocessRunner) Render(ctx context.Context, req RenderRequest) (RenderResult, error) {
if r == nil {
return RenderResult{}, fmt.Errorf("seriatim subprocess runner is nil")
}
if strings.TrimSpace(req.InputTranscriptPath) == "" {
return RenderResult{}, fmt.Errorf("seriatim render input path is required")
}
if strings.TrimSpace(req.OutputRenderedPath) == "" {
return RenderResult{}, fmt.Errorf("seriatim render output path is required")
}
format := strings.TrimSpace(req.Format)
if format == "" {
format = "markdown"
}
if format != "markdown" {
return RenderResult{}, fmt.Errorf("seriatim render format %q is unsupported", req.Format)
}
binary := r.binary
if strings.TrimSpace(req.Binary) != "" {
binary = strings.TrimSpace(req.Binary)
}
timeout := r.timeout
if req.Timeout < 0 {
return RenderResult{}, fmt.Errorf("seriatim render timeout must be >= 0")
}
if req.Timeout > 0 {
timeout = req.Timeout
}
args := buildRenderArgs(req, format)
if req.GeneratedConfigPath != "" {
if err := writeRenderInvocationConfig(req, args, binary, timeout, format); err != nil {
return RenderResult{}, fmt.Errorf("write seriatim render invocation config %q: %w", req.GeneratedConfigPath, err)
}
}
runRes, err := subprocess.Run(ctx, subprocess.RunRequest{
Executable: binary,
Args: args,
Timeout: timeout,
StdoutLogPath: req.StdoutLogPath,
StderrLogPath: req.StderrLogPath,
})
if err != nil {
return RenderResult{
OutputRenderedPath: req.OutputRenderedPath,
StdoutLogPath: req.StdoutLogPath,
StderrLogPath: req.StderrLogPath,
GeneratedConfigPath: req.GeneratedConfigPath,
ExitCode: runRes.ExitCode,
Duration: runRes.Duration,
InvokedBinary: binary,
Format: format,
Title: req.Title,
}, fmt.Errorf("run seriatim render (binary=%q): %w", binary, err)
}
if err := validateNonEmptyTextFile(req.OutputRenderedPath); err != nil {
return RenderResult{
OutputRenderedPath: req.OutputRenderedPath,
StdoutLogPath: req.StdoutLogPath,
StderrLogPath: req.StderrLogPath,
GeneratedConfigPath: req.GeneratedConfigPath,
ExitCode: runRes.ExitCode,
Duration: runRes.Duration,
InvokedBinary: binary,
Format: format,
Title: req.Title,
}, fmt.Errorf("validate seriatim rendered output %q: %w", req.OutputRenderedPath, err)
}
return RenderResult{
OutputRenderedPath: req.OutputRenderedPath,
StdoutLogPath: req.StdoutLogPath,
StderrLogPath: req.StderrLogPath,
GeneratedConfigPath: req.GeneratedConfigPath,
ExitCode: runRes.ExitCode,
Duration: runRes.Duration,
InvokedBinary: binary,
Format: format,
Title: req.Title,
Metadata: map[string]any{
"adapter": "seriatim_subprocess",
},
}, nil
}
func (r *SubprocessRunner) buildMergeArgs(req MergeRequest) []string {
args := []string{"merge"}
@@ -480,6 +571,22 @@ func buildNormalizeArgs(req NormalizeRequest, outputSchema string) []string {
return args
}
func buildRenderArgs(req RenderRequest, format string) []string {
args := []string{
"render",
"--input-file", req.InputTranscriptPath,
"--output-file", req.OutputRenderedPath,
"--format", format,
"--include-timestamps", strconv.FormatBool(req.IncludeTimestamps),
"--include-segment-ids", strconv.FormatBool(req.IncludeSegmentIDs),
"--include-metadata", strconv.FormatBool(req.IncludeMetadata),
}
if strings.TrimSpace(req.Title) != "" {
args = append(args, "--title", req.Title)
}
return args
}
func writeTrimInvocationConfig(req TrimRequest, args []string, binary string, timeout time.Duration) error {
payload := map[string]any{
"schema": "seriatim.generated.v1",
@@ -509,6 +616,24 @@ func writeNormalizeInvocationConfig(req NormalizeRequest, args []string, binary
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644)
}
func writeRenderInvocationConfig(req RenderRequest, args []string, binary string, timeout time.Duration, format string) error {
payload := map[string]any{
"schema": "seriatim.generated.v1",
"command": "render",
"binary": binary,
"args": args,
"timeout": timeout.String(),
"input_path": req.InputTranscriptPath,
"output_path": req.OutputRenderedPath,
"format": format,
"title": req.Title,
"include_timestamps": req.IncludeTimestamps,
"include_segment_ids": req.IncludeSegmentIDs,
"include_metadata": req.IncludeMetadata,
}
return subprocess.WriteYAMLAtomic(req.GeneratedConfigPath, payload, 0o644)
}
func validateJSONFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
@@ -541,3 +666,20 @@ func validateJSONFileWithSegments(path string) error {
}
return nil
}
func validateNonEmptyTextFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read file: %w", err)
}
if len(data) == 0 {
return fmt.Errorf("file is empty")
}
if !utf8.Valid(data) {
return fmt.Errorf("file is not valid utf-8 text")
}
if strings.TrimSpace(string(data)) == "" {
return fmt.Errorf("file has no non-whitespace content")
}
return nil
}