Initialize narratio Go module and CLI scaffold

This commit is contained in:
2026-05-02 10:37:38 -05:00
parent 48c51e1c77
commit 902e6bc994
42 changed files with 1486 additions and 1 deletions

View File

@@ -1,3 +1,30 @@
# narratio
Narratio is a pipeline orchestration tool that turns D&D session audio into useful artifacts like transcripts, campaign logs, and analysis.
`narratio` is a Go-based orchestration application for processing D&D session audio into transcripts and downstream artifacts.
This repository is currently an **initial scaffold** that establishes:
- Go module and package boundaries
- Placeholder CLI commands: `run`, `plan`, `status`, `resume`, `run-stage`
- Basic structured logging setup via `log/slog`
- Stub adapter, stage, config, manifest, artifact, and contract packages
## Current State
The CLI commands are intentionally placeholders and print `not yet implemented` messages.
Example:
```bash
go run ./cmd/narratio plan
```
## Non-Goals (Current Pass)
This scaffold does not yet implement:
- real config loading/validation behavior
- manifest persistence behavior
- stage execution behavior
- real WhisperX/seriatim/audita/analyzer integrations
- real S3 or notification integrations

753
architecture.md Normal file
View File

@@ -0,0 +1,753 @@
# D&D Session Orchestrator Architecture
## Purpose
This repository implements a Go-based orchestration application called `narratio` for processing recorded Dungeons & Dragons session audio into durable transcripts and downstream analysis artifacts.
The orchestrator coordinates an existing pipeline consisting of:
1. Per-speaker FLAC audio recordings from Mumble.
2. A self-hosted WhisperX HTTP transcription service.
3. `seriatim`, a deterministic transcript merger.
4. `audita`, an LLM-backed transcript polisher.
5. A future (not yet implemented, name subject to change) `dnd-session-analyzer`, which generates final D&D session artifacts such as session logs, event logs, meta-analysis, and table-read reports.
6. Long-term storage (which may be local filesystem, S3-compatible object storage, SFTP endpoint, or similar) for all inputs, intermediate outputs, logs, generated configs, and final artifacts.
The orchestrator is not responsible for implementing transcription, transcript merging, transcript polishing, or artifact generation. Its job is to coordinate those components reliably, maintain durable state, validate stage boundaries, manage local and remote artifacts, and make the pipeline resumable.
## Design Goals
The application should be:
- **Modular**: Each stage and external component should be isolated behind a narrow interface.
- **Composable**: Stages should consume and produce declared artifacts.
- **Resumable**: Long-running workflows should be restartable without repeating completed work.
- **Idempotent**: Re-running the same pipeline should not corrupt or duplicate outputs.
- **Observable**: The orchestrator should produce structured logs, captured subprocess logs, and a durable run manifest.
- **Config-driven**: Most user-adjustable behavior should live in configuration files, not hardcoded logic.
- **Strict at boundaries**: Inputs and outputs should be validated at every major stage boundary.
- **Boring and reliable**: Prefer explicit, maintainable code over clever workflow abstractions.
The orchestrator should be written in Go.
## Non-Goals
The orchestrator should not:
- Reimplement WhisperX.
- Reimplement `seriatim`.
- Reimplement `audita`.
- Contain D&D-specific prompt logic directly in the core orchestration layer.
- Parse or modify LLM outputs except for high-level validation of expected files or schemas.
- Become a general-purpose distributed workflow engine.
- Require a database for v1.
- Hide important run state only in logs.
## High-Level Pipeline
The intended pipeline is:
```text
prepare
transcribe speaker tracks in parallel
normalize speaker transcripts
merge transcripts with seriatim
polish transcript with audita
generate D&D artifacts with dnd-session-analyzer
archive outputs
notify user
```
Some stages may be implemented in the initial scaffold only as interfaces or placeholders.
The initial implementation should create the application framework and contracts without implementing the full real behavior of all stages.
## Primary Concepts
### Configuration
Configuration is user-authored and describes how the pipeline should run.
There should be two primary configuration files:
pipeline.yml
session.yml
pipeline.yml contains durable/default pipeline configuration:
```text
workspace root
S3 bucket/prefix
WhisperX service settings
seriatim binary path and options
audita binary path and options
analyzer binary path and artifact settings
notification settings
concurrency and timeout defaults
```
session.yml contains per-session inputs and metadata:
```text
session ID
campaign ID
session date/title
audio input directory or explicit audio files
speakers.yml
autocorrect.yml
glossary.yml
references to previous-session context
```
The application should strictly decode config files and reject unknown fields. Config validation should happen before any stage is run.
Environment variables may be used for secrets and deployment-specific credentials, but ordinary pipeline behavior should live in config files.
### Manifest
The manifest is machine-authored durable run state.
Each session run should have a manifest.json in the local work directory and eventually mirrored to S3.
The manifest should track:
```text
session ID
pipeline version
created/updated timestamps
resolved input files
checksums for important inputs/configs
stage statuses
stage timestamps
stage outputs
captured logs
generated config files
component versions where available
errors
artifact locations
S3 object keys
```
Stage statuses should be explicit:
```text
pending
running
succeeded
failed
skipped
stale
interrupted
```
The manifest is the source of truth for resume/skip decisions.
### Stages
A stage is a pipeline unit that consumes declared inputs and produces declared outputs.
Examples:
```text
prepare
transcribe
normalize
merge
polish
analyze
archive
notify
```
Stages should be organized around the lifecycle of a session, not around executable names.
For example, the merge stage may call seriatim, but the stage should be named for the pipeline operation, not the tool.
Every stage should:
- Determine required inputs.
- Check whether expected outputs already exist.
- Decide whether to run, skip, or fail.
- Execute through an adapter or local logic.
- Validate expected outputs.
- Write outputs to the local work directory.
- Record stage status and metadata in the manifest.
- Allow safe rerun with `--force`.
### Adapters
Adapters isolate interactions with external components.
The orchestrator should have thin adapters for:
- WhisperX HTTP API
- seriatim
- audita
- future dnd-session-analyzer
- storage layer (local filesystem, S3-compatible, SFTP, etc.)
- optional email/notification backend
Adapters should hide details such as:
- CLI argument construction
- generated config file format
- HTTP request/response details
- stdout/stderr capture
- process exit handling
- timeout and cancellation behavior
The stage layer should call adapter methods and should not know implementation details of the underlying external tool.
### Artifact Store
The orchestrator should use a local work directory as the primary working area.
Each stage writes outputs locally first. The orchestrator then validates and archives those outputs to S3.
The storage layer should expose an abstraction for artifact references and common operations:
write local artifact
read local artifact
calculate checksum
upload to S3
record local path and S3 key
validate existence
For v1, local filesystem + S3 is enough. A database is not required.
#### Expected Local Work Directory Layout
Each session should have an isolated local work directory.
Example:
```text
work/{session_id}/
inputs/
speakers.yml
autocorrect.yml
glossary.yml
session.yml
pipeline.resolved.yml
audio/
adam.flac
eric.flac
other-speaker.flac
transcripts/
raw/
adam.json
eric.json
other-speaker.json
normalized/
adam.json
eric.json
other-speaker.json
merged.json
processed.json
artifacts/
event-log.json
session-log.md
meta-analysis.md
table-read.md
config/
seriatim.generated.yml
audita.generated.yml
analyzer.event-log.generated.yml
analyzer.session-log.generated.yml
logs/
whisperx.adam.log
whisperx.eric.log
seriatim.stdout.log
seriatim.stderr.log
audita.stdout.log
audita.stderr.log
analyzer.session-log.stdout.log
analyzer.session-log.stderr.log
manifest.json
.lock
```
The exact layout may evolve, but the distinction between inputs, audio, transcripts, artifacts, generated config, logs, and manifest should remain.
#### Expected S3 Layout
S3 should mirror the conceptual local layout.
Example:
```text
sessions/{session_id}/
inputs/
audio/
transcripts/
raw/
normalized/
merged.json
processed.json
artifacts/
config/
logs/
manifest.json
```
The orchestrator should be able to upload outputs after each successful stage, not only at the end of the full workflow.
## Suggested Go Package Layout
Initial scaffold should use a package layout *similar* to:
```text
cmd/narratio/
main.go
internal/app/
app.go
plan.go
run.go
commands.go
internal/config/
config.go
load.go
validate.go
internal/manifest/
manifest.go
store.go
status.go
internal/stage/
stage.go
prepare.go
transcribe.go
normalize.go
merge.go
polish.go
analyze.go
archive.go
notify.go
internal/adapters/
whisperx/
client.go
seriatim/
runner.go
audita/
runner.go
analyzer/
runner.go
notify/
sender.go
internal/artifacts/
store.go
local.go
s3.go
paths.go
checksum.go
internal/contracts/
transcript.go
session.go
artifact.go
internal/logging/
logging.go
```
The initial build step should create these packages and core interfaces, but should not fully implement all real external behavior.
## CLI Shape
The CLI should eventually support commands like:
```bash
dnd-orchestrator run --config pipeline.yml --session session.yml
dnd-orchestrator plan --config pipeline.yml --session session.yml
dnd-orchestrator status --session-id 2026-05-03
dnd-orchestrator resume --session-id 2026-05-03
dnd-orchestrator run-stage polish --session-id 2026-05-03 --force
```
For the initial scaffold, it is acceptable to implement only partial command behavior, but the command structure should anticipate these operations.
```
run
```
Loads config, validates inputs, creates or loads a manifest, builds a plan, and executes the pipeline.
```
plan
```
Prints the stages that would run and which stages would be skipped.
```
status
```
Reads a manifest and prints stage status.
```
resume
```
Continues a previous run from the latest valid manifest state.
```
run-stage
```
Runs one stage explicitly, with optional --force.
## Core Stage Interface
The application should define a narrow stage interface.
Example shape:
```go
type Stage interface {
Name() string
Run(ctx context.Context, env *Env, manifest *manifest.Manifest) (*StageResult, error)
}
```
The exact types may vary, but the idea should remain:
- a stage has a stable name
- a stage runs with context
- a stage receives shared dependencies through an environment object
- a stage may inspect/update manifest state through controlled methods
- a stage returns declared outputs and metadata
Suggested supporting type:
```go
type StageResult struct {
Outputs []artifacts.Ref
Metadata map[string]any
}
```
## Application Environment
The application should pass shared dependencies through an explicit environment object rather than globals.
Example shape:
```go
type Env struct {
Config *config.Config
ArtifactStore artifacts.Store
Logger *slog.Logger
WhisperX whisperx.Client
Seriatim seriatim.Runner
Audita audita.Runner
Analyzer analyzer.Runner
Notifier notify.Sender
}
```
Avoid package-level mutable state.
## Main Loop
The main application loop should be a simple state machine.
Conceptual behavior:
```text
load configuration
validate configuration
create/load manifest
acquire session lock
resolve input files
build execution plan
for each stage:
decide whether stage should run
mark stage running
run stage
validate outputs
archive outputs as appropriate
mark stage succeeded or failed
release lock
```
The implementation should prefer clear control flow over a generic workflow engine.
## Long-Running Stage Handling
`audita` can run for more than an hour. `seriatim` is very fast (typically less than a second). Each `whisperx` file transcription job can take anywhere from 5 to 30 minutes.
The orchestrator should therefore treat long-running stages as normal and expected.
Long-running subprocess stages should:
- receive a generous configurable timeout
- use context.Context for cancellation
- capture stdout and stderr continuously
- write logs to the work directory
- update manifest status before and after execution
- optionally update heartbeat timestamps while running
- preserve temporary work directories on failure
- avoid treating partial output as success
A stage should write to a temporary output path first, then promote to the final output path only after validation.
Example:
```text
processed.json.tmp
processed.json
```
## Idempotency and Resume Behavior
Every stage should be designed around the question:
```text
If this stage runs twice, what happens?
```
Preferred behavior:
- If expected output exists and validates, skip the stage.
- If expected output exists but input/config checksum changed, mark stale.
- If `--force` is supplied, rerun and replace output atomically.
- If a previous run failed, preserve logs and temporary files for inspection.
- If rerunning, do not delete unrelated successful outputs.
The manifest should allow the application to resume after interruption without rerunning completed expensive stages.
## Locking
The orchestrator should prevent two processes from operating on the same session work directory at the same time.
For v1, a lock file in the work directory is acceptable:
```text
work/{session_id}/.lock
```
The lock should be acquired before mutating manifest or stage outputs.
## Logging
The orchestrator should use structured logs, preferably Go's log/slog.
Subprocess logs should be captured separately:
```text
orchestrator structured log
stdout log per external tool invocation
stderr log per external tool invocation
```
Do not combine all component logs into one global text file.
## External Component Boundaries
### WhisperX
The `whisperx` adapter should handle:
- HTTP submission of audio files
- polling or waiting for completion, depending on API shape
- timeout and retry policy
- returning raw transcript JSON
- per-speaker parallelism with bounded concurrency
The orchestrator should not assume WhisperX internals beyond the adapter contract.
### Seriatim
`seriatim` is a deterministic transcript merger.
The `seriatim` adapter should handle:
- generating Seriatim config
- invoking the binary
- passing input transcript paths
- passing output path
- capturing logs
- returning merged transcript path
The orchestrator should not implement `seriatim` merge logic.
### Audita
`audita` is an LLM-backed transcript polisher.
The `audita` adapter should handle:
- generating `audita` config
- invoking the binary or service
- passing merged transcript path
- passing glossary/autocorrect paths
- capturing logs
- preserving Audita work/checkpoint files
- returning processed transcript path
The orchestrator should not implement LLM correction logic.
### D&D Session Analyzer
The analyzer is a future component.
The initial orchestrator should include interfaces/placeholders for it, but should not need a complete implementation.
The analyzer adapter should eventually handle:
- artifact type selection
- generated analyzer config
- prompt template references
- processed transcript path
- prior session context references
- output artifact paths
- structured validation where appropriate
The orchestrator should not contain prompt-specific D&D logic. The analyzer should own artifact-specific prompt behavior.
## Contracts and Schema Versioning
Core data artifacts should include schema versions where practical.
Important contracts include:
- SpeakerTranscript (`whisperx` output)
- CanonicalTranscript (`seriatim` output)
- ProcessedTranscript (`autida` output)
- ArtifactResult
- SessionManifest
The orchestrator may not need to deeply understand every field in every artifact, but it should validate enough to know that expected outputs were produced and are parseable.
Schema versions should appear in JSON artifacts where possible:
```json
{
"schema": "processed_transcript.v1",
"session_id": "2026-05-03",
"segments": []
}
```
### Validation Boundaries
Validate at each boundary:
```text
input config is valid
audio files exist
speakers.yml maps expected filenames
WhisperX output exists and is parseable
normalized speaker transcripts exist
Seriatim merged transcript exists and is parseable
Audita processed transcript exists and is parseable
analyzer artifacts exist in expected formats
```
For the initial scaffold, validation methods may be stubs or simple parse/existence checks, but the architecture should make validation a first-class concern.
## Testing Strategy
The orchestrator should be testable without running `whisperx`, `seriatim`, `audita`, or LLMs.
Use fake adapters for tests:
- fake `whisperx` returns canned transcript JSON
- fake `seriatim` writes a known merged transcript
- fake `audita` writes a known processed transcript
- fake `analyzer` writes fixed artifacts
- fake artifact store writes to a temp directory
Tests should cover:
- config loading and validation
- plan generation
- manifest creation/update
- stage skip behavior
- force rerun behavior
- failure handling
- resume behavior
- subprocess adapter construction where practical
### Testing Non-Goals
Tests must **not** do any of the following:
- include hard-coded assertions of specific default configuration values (although a test may validate that a default value is defined and has the correct type, it may not require it to equal any particualar value)
## Initial Implementation Scope
The first implementation pass should create the application framework and interfaces, not the full working pipeline.
Implement:
```
Go module setup
CLI skeleton
config structs and loader
manifest structs and local manifest store
artifact reference/store interfaces
stage interface
app environment object
plan/main-loop skeleton
placeholder stages
adapter interfaces
fake/no-op adapter implementations where helpful
basic structured logging
basic tests for config/manifest/stage planning
```
Do not yet implement:
```
real WhisperX HTTP API behavior
real Seriatim subprocess execution
real Audita subprocess execution
real S3 upload/download
real email notification
full transcript schema validation
full analyzer behavior
D&D prompt logic
```
The initial code should compile, include clear TODOs, and establish durable package boundaries.
## Engineering Preferences
Use idiomatic Go.
Prefer:
```
context.Context
log/slog
explicit interfaces
small packages
table-driven tests
strict config decoding
clear errors with context
local filesystem abstractions where useful
standard library where sufficient
```
Avoid:
```
global mutable state
reflection-heavy frameworks unless justified
premature generic DAG engines
hidden side effects in constructors
mixing orchestration logic with adapter-specific implementation details
writing business logic directly in main.go
```
### Architectural Invariant
The most important invariant of the application is:
Every stage consumes declared artifacts, produces declared artifacts, validates them, records provenance, and can be safely skipped or rerun.
All design and implementation choices should support that invariant.

11
cmd/narratio/main.go Normal file
View File

@@ -0,0 +1,11 @@
package main
import (
"os"
"gitea.maximumdirect.net/eric/narratio/internal/app"
)
func main() {
os.Exit(app.Execute(os.Args[1:], os.Stdout, os.Stderr))
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module gitea.maximumdirect.net/eric/narratio
go 1.25.0

View File

@@ -0,0 +1,20 @@
// Package analyzer declares the adapter contract for artifact analysis generation.
package analyzer
import "context"
// Runner is a placeholder adapter interface for session artifact generation.
type Runner interface {
Run(ctx context.Context, req AnalysisRequest) (AnalysisResult, error)
}
// AnalysisRequest is a placeholder analyzer input.
type AnalysisRequest struct {
ProcessedTranscriptPath string
OutputDir string
}
// AnalysisResult is a placeholder analyzer output.
type AnalysisResult struct {
ArtifactPaths []string
}

View File

@@ -0,0 +1,20 @@
// Package audita declares the adapter contract for transcript polishing.
package audita
import "context"
// Runner is a placeholder adapter interface for audita invocation.
type Runner interface {
Run(ctx context.Context, req PolishRequest) (PolishResult, error)
}
// PolishRequest is a placeholder polish input.
type PolishRequest struct {
MergedTranscriptPath string
OutputPath string
}
// PolishResult is a placeholder polish output.
type PolishResult struct {
ProcessedPath string
}

View File

@@ -0,0 +1,15 @@
// Package notify declares the adapter contract for run notifications.
package notify
import "context"
// Sender is a placeholder adapter interface for notifications.
type Sender interface {
Send(ctx context.Context, msg Message) error
}
// Message is a placeholder notification payload.
type Message struct {
Subject string
Body string
}

View File

@@ -0,0 +1,20 @@
// Package seriatim declares the adapter contract for transcript merge execution.
package seriatim
import "context"
// Runner is a placeholder adapter interface for seriatim invocation.
type Runner interface {
Run(ctx context.Context, req MergeRequest) (MergeResult, error)
}
// MergeRequest is a placeholder merge input.
type MergeRequest struct {
TranscriptPaths []string
OutputPath string
}
// MergeResult is a placeholder merge output.
type MergeResult struct {
MergedPath string
}

View File

@@ -0,0 +1,20 @@
// Package whisperx declares the adapter contract for WhisperX transcription.
package whisperx
import "context"
// Client is a placeholder adapter interface for WhisperX interactions.
type Client interface {
Transcribe(ctx context.Context, req TranscriptionRequest) (TranscriptionResult, error)
}
// TranscriptionRequest is a placeholder transcription input.
type TranscriptionRequest struct {
Speaker string
AudioPath string
}
// TranscriptionResult is a placeholder transcription output.
type TranscriptionResult struct {
TranscriptPath string
}

26
internal/app/app.go Normal file
View File

@@ -0,0 +1,26 @@
package app
import (
"log/slog"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/analyzer"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/audita"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/notify"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/seriatim"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/whisperx"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/config"
)
// Env is the shared dependency container passed to orchestrator components.
type Env struct {
Config *config.Config
ArtifactStore artifacts.Store
Logger *slog.Logger
WhisperX whisperx.Client
Seriatim seriatim.Runner
Audita audita.Runner
Analyzer analyzer.Runner
Notifier notify.Sender
}

56
internal/app/commands.go Normal file
View File

@@ -0,0 +1,56 @@
package app
import (
"context"
"fmt"
"io"
"strings"
)
var supportedCommands = []string{"run", "plan", "status", "resume", "run-stage"}
// Execute dispatches CLI commands and returns a process exit code.
func Execute(args []string, stdout, stderr io.Writer) int {
if len(args) == 0 {
printUsage(stderr)
return 1
}
ctx := context.Background()
cmd := args[0]
cmdArgs := args[1:]
var err error
switch cmd {
case "run":
err = Run(ctx, cmdArgs, stdout)
case "plan":
err = Plan(ctx, cmdArgs, stdout)
case "status":
err = Status(ctx, cmdArgs, stdout)
case "resume":
err = Resume(ctx, cmdArgs, stdout)
case "run-stage":
err = RunStage(ctx, cmdArgs, stdout)
default:
fmt.Fprintf(stderr, "unknown command: %q\n\n", cmd)
printUsage(stderr)
return 1
}
if err != nil {
fmt.Fprintf(stderr, "%v\n", err)
return 1
}
return 0
}
func printUsage(w io.Writer) {
fmt.Fprintf(w, "Usage: narratio <%s>\n", strings.Join(supportedCommands, "|"))
}
func placeholder(out io.Writer, command string) error {
_, err := fmt.Fprintf(out, "narratio %s: not yet implemented\n", command)
return err
}

View File

@@ -0,0 +1,75 @@
package app
import (
"bytes"
"strings"
"testing"
)
func TestExecuteValidCommands(t *testing.T) {
cases := []struct {
name string
args []string
wantOut string
}{
{name: "run", args: []string{"run"}, wantOut: "narratio run: not yet implemented"},
{name: "plan", args: []string{"plan"}, wantOut: "narratio plan: not yet implemented"},
{name: "status", args: []string{"status"}, wantOut: "narratio status: not yet implemented"},
{name: "resume", args: []string{"resume"}, wantOut: "narratio resume: not yet implemented"},
{name: "run-stage", args: []string{"run-stage", "polish"}, wantOut: "narratio run-stage: not yet implemented"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute(tc.args, &stdout, &stderr)
if code != 0 {
t.Fatalf("exit code = %d, want 0", code)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
if !strings.Contains(stdout.String(), tc.wantOut) {
t.Fatalf("stdout = %q, want to contain %q", stdout.String(), tc.wantOut)
}
})
}
}
func TestExecuteInvalidCommand(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute([]string{"bogus"}, &stdout, &stderr)
if code == 0 {
t.Fatalf("exit code = 0, want non-zero")
}
if stdout.Len() != 0 {
t.Fatalf("stdout = %q, want empty", stdout.String())
}
out := stderr.String()
if !strings.Contains(out, "unknown command") {
t.Fatalf("stderr = %q, want unknown command message", out)
}
if !strings.Contains(out, "Usage: narratio") {
t.Fatalf("stderr = %q, want usage message", out)
}
}
func TestExecuteMissingCommand(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
code := Execute(nil, &stdout, &stderr)
if code == 0 {
t.Fatalf("exit code = 0, want non-zero")
}
if stdout.Len() != 0 {
t.Fatalf("stdout = %q, want empty", stdout.String())
}
if !strings.Contains(stderr.String(), "Usage: narratio") {
t.Fatalf("stderr = %q, want usage message", stderr.String())
}
}

3
internal/app/doc.go Normal file
View File

@@ -0,0 +1,3 @@
// Package app contains CLI command wiring and top-level application orchestration
// primitives for narratio.
package app

11
internal/app/plan.go Normal file
View File

@@ -0,0 +1,11 @@
package app
import (
"context"
"io"
)
// Plan is a placeholder for future stage planning behavior.
func Plan(_ context.Context, _ []string, out io.Writer) error {
return placeholder(out, "plan")
}

11
internal/app/resume.go Normal file
View File

@@ -0,0 +1,11 @@
package app
import (
"context"
"io"
)
// Resume is a placeholder for future resume-from-manifest behavior.
func Resume(_ context.Context, _ []string, out io.Writer) error {
return placeholder(out, "resume")
}

11
internal/app/run.go Normal file
View File

@@ -0,0 +1,11 @@
package app
import (
"context"
"io"
)
// Run is a placeholder for the future end-to-end pipeline execution command.
func Run(_ context.Context, _ []string, out io.Writer) error {
return placeholder(out, "run")
}

11
internal/app/run_stage.go Normal file
View File

@@ -0,0 +1,11 @@
package app
import (
"context"
"io"
)
// RunStage is a placeholder for future single-stage execution behavior.
func RunStage(_ context.Context, _ []string, out io.Writer) error {
return placeholder(out, "run-stage")
}

11
internal/app/status.go Normal file
View File

@@ -0,0 +1,11 @@
package app
import (
"context"
"io"
)
// Status is a placeholder for future manifest status inspection behavior.
func Status(_ context.Context, _ []string, out io.Writer) error {
return placeholder(out, "status")
}

View File

@@ -0,0 +1,24 @@
package artifacts
import (
"crypto/sha256"
"encoding/hex"
"io"
"os"
)
// SHA256File returns the SHA-256 hex digest of a file.
func SHA256File(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}

View File

@@ -0,0 +1,2 @@
// Package artifacts defines artifact references and storage contracts.
package artifacts

View File

@@ -0,0 +1,31 @@
package artifacts
import (
"context"
"fmt"
)
// LocalStore is a placeholder local filesystem artifact store.
type LocalStore struct {
RootDir string
}
// WriteLocal returns a not-yet-implemented error in the scaffold.
func (s *LocalStore) WriteLocal(_ context.Context, _ Ref, _ []byte) error {
return fmt.Errorf("artifacts local write: not yet implemented")
}
// ReadLocal returns a not-yet-implemented error in the scaffold.
func (s *LocalStore) ReadLocal(_ context.Context, _ Ref) ([]byte, error) {
return nil, fmt.Errorf("artifacts local read: not yet implemented")
}
// ExistsLocal returns a not-yet-implemented error in the scaffold.
func (s *LocalStore) ExistsLocal(_ context.Context, _ Ref) (bool, error) {
return false, fmt.Errorf("artifacts local exists: not yet implemented")
}
// Upload returns a not-yet-implemented error in the scaffold.
func (s *LocalStore) Upload(_ context.Context, _ Ref) (string, error) {
return "", fmt.Errorf("artifacts local upload: not yet implemented")
}

View File

@@ -0,0 +1,8 @@
package artifacts
import "path/filepath"
// SessionWorkDir returns the work directory for one session.
func SessionWorkDir(rootDir, sessionID string) string {
return filepath.Join(rootDir, "work", sessionID)
}

32
internal/artifacts/s3.go Normal file
View File

@@ -0,0 +1,32 @@
package artifacts
import (
"context"
"fmt"
)
// S3Store is a placeholder S3-compatible artifact store.
type S3Store struct {
Bucket string
Prefix string
}
// WriteLocal returns a not-yet-implemented error in the scaffold.
func (s *S3Store) WriteLocal(_ context.Context, _ Ref, _ []byte) error {
return fmt.Errorf("artifacts s3 write local: not yet implemented")
}
// ReadLocal returns a not-yet-implemented error in the scaffold.
func (s *S3Store) ReadLocal(_ context.Context, _ Ref) ([]byte, error) {
return nil, fmt.Errorf("artifacts s3 read local: not yet implemented")
}
// ExistsLocal returns a not-yet-implemented error in the scaffold.
func (s *S3Store) ExistsLocal(_ context.Context, _ Ref) (bool, error) {
return false, fmt.Errorf("artifacts s3 exists local: not yet implemented")
}
// Upload returns a not-yet-implemented error in the scaffold.
func (s *S3Store) Upload(_ context.Context, _ Ref) (string, error) {
return "", fmt.Errorf("artifacts s3 upload: not yet implemented")
}

View File

@@ -0,0 +1,19 @@
package artifacts
import "context"
// Ref identifies a pipeline artifact and optional remote location.
type Ref struct {
Kind string
LocalPath string
RemoteKey string
Checksum string
}
// Store is a placeholder artifact storage abstraction.
type Store interface {
WriteLocal(ctx context.Context, ref Ref, data []byte) error
ReadLocal(ctx context.Context, ref Ref) ([]byte, error)
ExistsLocal(ctx context.Context, ref Ref) (bool, error)
Upload(ctx context.Context, ref Ref) (string, error)
}

15
internal/config/config.go Normal file
View File

@@ -0,0 +1,15 @@
package config
// Config contains loaded pipeline and session configuration.
type Config struct {
Pipeline *Pipeline
Session *Session
}
// Pipeline represents durable pipeline-level settings.
type Pipeline struct{}
// Session represents per-session inputs and metadata.
type Session struct {
SessionID string
}

2
internal/config/doc.go Normal file
View File

@@ -0,0 +1,2 @@
// Package config contains pipeline and session configuration contracts.
package config

8
internal/config/load.go Normal file
View File

@@ -0,0 +1,8 @@
package config
import "fmt"
// Load is a placeholder for strict pipeline/session config decoding.
func Load(_ string, _ string) (*Config, error) {
return nil, fmt.Errorf("config load: not yet implemented")
}

View File

@@ -0,0 +1,8 @@
package config
import "fmt"
// Validate is a placeholder for configuration validation logic.
func Validate(_ *Config) error {
return fmt.Errorf("config validation: not yet implemented")
}

View File

@@ -0,0 +1,7 @@
package contracts
// ArtifactResult is a placeholder generated artifact contract.
type ArtifactResult struct {
Schema string `json:"schema"`
Path string `json:"path"`
}

View File

@@ -0,0 +1,2 @@
// Package contracts defines shared high-level artifact contracts.
package contracts

View File

@@ -0,0 +1,7 @@
package contracts
// SessionManifest is a placeholder durable session-run contract.
type SessionManifest struct {
Schema string `json:"schema"`
SessionID string `json:"session_id"`
}

View File

@@ -0,0 +1,19 @@
package contracts
// SpeakerTranscript is a placeholder transcription artifact contract.
type SpeakerTranscript struct {
Schema string `json:"schema"`
SessionID string `json:"session_id"`
}
// CanonicalTranscript is a placeholder merged transcript contract.
type CanonicalTranscript struct {
Schema string `json:"schema"`
SessionID string `json:"session_id"`
}
// ProcessedTranscript is a placeholder polished transcript contract.
type ProcessedTranscript struct {
Schema string `json:"schema"`
SessionID string `json:"session_id"`
}

2
internal/logging/doc.go Normal file
View File

@@ -0,0 +1,2 @@
// Package logging provides structured logger construction for narratio.
package logging

View File

@@ -0,0 +1,17 @@
package logging
import (
"io"
"log/slog"
"os"
)
// NewLogger creates a text slog logger.
func NewLogger(out io.Writer, level slog.Level) *slog.Logger {
if out == nil {
out = os.Stderr
}
handler := slog.NewTextHandler(out, &slog.HandlerOptions{Level: level})
return slog.New(handler)
}

View File

@@ -0,0 +1,31 @@
package logging
import (
"bytes"
"log/slog"
"strings"
"testing"
)
func TestNewLoggerWritesOutput(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger(&buf, slog.LevelInfo)
if logger == nil {
t.Fatal("logger is nil")
}
logger.Info("hello", "component", "test")
out := buf.String()
if !strings.Contains(out, "hello") {
t.Fatalf("output = %q, want to contain message", out)
}
}
func TestNewLoggerNilWriter(t *testing.T) {
logger := NewLogger(nil, slog.LevelInfo)
if logger == nil {
t.Fatal("logger is nil")
}
logger.Info("should not panic")
}

2
internal/manifest/doc.go Normal file
View File

@@ -0,0 +1,2 @@
// Package manifest defines durable run-state tracking for narratio sessions.
package manifest

View File

@@ -0,0 +1,18 @@
package manifest
import "time"
// StageState tracks status and metadata for one stage execution.
type StageState struct {
Status StageStatus
UpdatedAt time.Time
Error string
}
// Manifest is the durable state record for a session run.
type Manifest struct {
SessionID string
CreatedAt time.Time
UpdatedAt time.Time
Stages map[string]StageState
}

View File

@@ -0,0 +1,14 @@
package manifest
// StageStatus is the lifecycle state of a pipeline stage.
type StageStatus string
const (
StatusPending StageStatus = "pending"
StatusRunning StageStatus = "running"
StatusSucceeded StageStatus = "succeeded"
StatusFailed StageStatus = "failed"
StatusSkipped StageStatus = "skipped"
StatusStale StageStatus = "stale"
StatusInterrupted StageStatus = "interrupted"
)

View File

@@ -0,0 +1,27 @@
package manifest
import (
"context"
"fmt"
)
// Store is a placeholder interface for manifest persistence.
type Store interface {
Load(ctx context.Context, sessionID string) (*Manifest, error)
Save(ctx context.Context, m *Manifest) error
}
// LocalStore is a placeholder local-filesystem manifest store.
type LocalStore struct {
RootDir string
}
// Load returns a not-yet-implemented error in the scaffold.
func (s *LocalStore) Load(_ context.Context, _ string) (*Manifest, error) {
return nil, fmt.Errorf("manifest local load: not yet implemented")
}
// Save returns a not-yet-implemented error in the scaffold.
func (s *LocalStore) Save(_ context.Context, _ *Manifest) error {
return fmt.Errorf("manifest local save: not yet implemented")
}

2
internal/stage/doc.go Normal file
View File

@@ -0,0 +1,2 @@
// Package stage defines pipeline stage contracts and placeholder stage types.
package stage

View File

@@ -0,0 +1,63 @@
package stage
import (
"context"
"fmt"
"gitea.maximumdirect.net/eric/narratio/internal/app"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
func notImplemented(name string) error {
return fmt.Errorf("stage %q: not yet implemented", name)
}
type Prepare struct{}
type Transcribe struct{}
type Normalize struct{}
type Merge struct{}
type Polish struct{}
type Analyze struct{}
type Archive struct{}
type Notify struct{}
func (Prepare) Name() string { return "prepare" }
func (Transcribe) Name() string { return "transcribe" }
func (Normalize) Name() string { return "normalize" }
func (Merge) Name() string { return "merge" }
func (Polish) Name() string { return "polish" }
func (Analyze) Name() string { return "analyze" }
func (Archive) Name() string { return "archive" }
func (Notify) Name() string { return "notify" }
func (s Prepare) Run(_ context.Context, _ *app.Env, _ *manifest.Manifest) (*Result, error) {
return nil, notImplemented(s.Name())
}
func (s Transcribe) Run(_ context.Context, _ *app.Env, _ *manifest.Manifest) (*Result, error) {
return nil, notImplemented(s.Name())
}
func (s Normalize) Run(_ context.Context, _ *app.Env, _ *manifest.Manifest) (*Result, error) {
return nil, notImplemented(s.Name())
}
func (s Merge) Run(_ context.Context, _ *app.Env, _ *manifest.Manifest) (*Result, error) {
return nil, notImplemented(s.Name())
}
func (s Polish) Run(_ context.Context, _ *app.Env, _ *manifest.Manifest) (*Result, error) {
return nil, notImplemented(s.Name())
}
func (s Analyze) Run(_ context.Context, _ *app.Env, _ *manifest.Manifest) (*Result, error) {
return nil, notImplemented(s.Name())
}
func (s Archive) Run(_ context.Context, _ *app.Env, _ *manifest.Manifest) (*Result, error) {
return nil, notImplemented(s.Name())
}
func (s Notify) Run(_ context.Context, _ *app.Env, _ *manifest.Manifest) (*Result, error) {
return nil, notImplemented(s.Name())
}

21
internal/stage/stage.go Normal file
View File

@@ -0,0 +1,21 @@
package stage
import (
"context"
"gitea.maximumdirect.net/eric/narratio/internal/app"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
)
// Stage is the pipeline unit contract.
type Stage interface {
Name() string
Run(ctx context.Context, env *app.Env, m *manifest.Manifest) (*Result, error)
}
// Result is the declared output of a stage execution.
type Result struct {
Outputs []artifacts.Ref
Metadata map[string]any
}