From a3986cf0d673b89281316519cfa981bbb1ecff0f Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 17 May 2026 07:53:51 -0500 Subject: [PATCH] Centralized defaults into internal/config/defaults.go --- README.md | 2 ++ architecture.md | 1 + internal/artifacts/paths.go | 29 +++++++++-------- internal/artifacts/s3_keys.go | 12 ++++--- internal/config/defaults.go | 61 +++++++++++++++++++++++++++++++++++ internal/config/load.go | 53 ++++++++++++++---------------- internal/config/normalize.go | 5 --- 7 files changed, 111 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 3d39195..1a93eaa 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ Optional secrets-from-files config: YAML decoding is strict (`KnownFields(true)`), so unknown fields fail fast. +Maintainer note: application defaults are centralized in [`internal/config/defaults.go`](internal/config/defaults.go). + ## Storage And Archive Foundations Narratio now includes configuration and path-model foundations for archive support, plus implemented prepare-stage S3 audio input. diff --git a/architecture.md b/architecture.md index 7db51a2..92d0eb7 100644 --- a/architecture.md +++ b/architecture.md @@ -114,6 +114,7 @@ CLI pipeline config path resolution: - when `--config` is omitted, Narratio searches defaults in order: - `/usr/local/etc/narratio/pipeline.yml` - `/etc/narratio/pipeline.yml` +- default values are centralized in `internal/config/defaults.go` CLI session config path resolution: diff --git a/internal/artifacts/paths.go b/internal/artifacts/paths.go index a84ff2a..e5ba59e 100644 --- a/internal/artifacts/paths.go +++ b/internal/artifacts/paths.go @@ -2,6 +2,8 @@ package artifacts import ( "path/filepath" + + "gitea.maximumdirect.net/eric/narratio/internal/config" ) // SessionPaths contains canonical local paths for one session work directory. @@ -22,34 +24,33 @@ type SessionPaths struct { // SessionWorkDir returns the work directory for one session. func SessionWorkDir(rootDir, sessionID string) string { - return filepath.Join(rootDir, "work", sessionID) + return filepath.Join(rootDir, config.PathWorkDirSegment, sessionID) } // SessionRunWorkDir returns the campaign/session/run scoped local work directory. func SessionRunWorkDir(rootDir, campaign, sessionID, runID string) string { - return filepath.Join(rootDir, "work", campaign, sessionID, runID) + return filepath.Join(rootDir, config.PathWorkDirSegment, campaign, sessionID, runID) } // SessionSpoolAudioDir returns the campaign/session/run scoped local spool audio path. func SessionSpoolAudioDir(spoolRoot, campaign, sessionID, runID string) string { - return filepath.Join(spoolRoot, campaign, sessionID, runID, "audio") + return filepath.Join(spoolRoot, campaign, sessionID, runID, config.PathAudioDirSegment) } func buildSessionPaths(workspaceRoot, sessionID string) SessionPaths { root := SessionWorkDir(workspaceRoot, sessionID) - transcripts := filepath.Join(root, "transcripts") return SessionPaths{ WorkspaceRoot: workspaceRoot, Root: root, - InputsDir: filepath.Join(root, "inputs"), - AudioDir: filepath.Join(root, "audio"), - TranscriptsDir: transcripts, - TranscriptsRawDir: filepath.Join(transcripts, "raw"), - TranscriptsTrimmedDir: filepath.Join(transcripts, "trimmed"), - ArtifactsDir: filepath.Join(root, "artifacts"), - ConfigDir: filepath.Join(root, "config"), - LogsDir: filepath.Join(root, "logs"), - ManifestPath: filepath.Join(root, "manifest.json"), - LockPath: filepath.Join(root, ".lock"), + InputsDir: filepath.Join(root, config.PathInputsDirSegment), + AudioDir: filepath.Join(root, config.PathAudioDirSegment), + TranscriptsDir: filepath.Join(root, config.PathTranscriptsSegment), + TranscriptsRawDir: filepath.Join(root, filepath.FromSlash(config.PathTranscriptsRaw)), + TranscriptsTrimmedDir: filepath.Join(root, filepath.FromSlash(config.PathTranscriptsTrimmed)), + ArtifactsDir: filepath.Join(root, config.PathArtifactsDirSegment), + ConfigDir: filepath.Join(root, config.PathConfigDirSegment), + LogsDir: filepath.Join(root, config.PathLogsDirSegment), + ManifestPath: filepath.Join(root, config.PathManifestFile), + LockPath: filepath.Join(root, config.PathLockFile), } } diff --git a/internal/artifacts/s3_keys.go b/internal/artifacts/s3_keys.go index 3e7030c..fceb63e 100644 --- a/internal/artifacts/s3_keys.go +++ b/internal/artifacts/s3_keys.go @@ -3,6 +3,8 @@ package artifacts import ( "path" "strings" + + "gitea.maximumdirect.net/eric/narratio/internal/config" ) // S3SessionPrefix builds the canonical S3 session prefix. @@ -10,9 +12,9 @@ import ( func S3SessionPrefix(rootPrefix, campaign, sessionID string) string { prefix := path.Join( cleanS3PathPart(rootPrefix), - "campaigns", + config.S3CampaignsSegment, cleanS3PathPart(campaign), - "sessions", + config.S3SessionsSegment, cleanS3PathPart(sessionID), ) return ensureS3TrailingSlash(prefix) @@ -21,7 +23,7 @@ func S3SessionPrefix(rootPrefix, campaign, sessionID string) string { // S3RunPrefix builds the canonical S3 run prefix. // Format: {session_prefix}/runs/{run_id}/ func S3RunPrefix(sessionPrefix, runID string) string { - prefix := path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), "runs", cleanS3PathPart(runID)) + prefix := path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), config.S3RunsSegment, cleanS3PathPart(runID)) return ensureS3TrailingSlash(prefix) } @@ -35,13 +37,13 @@ func S3AudioPrefix(sessionPrefix, audioPrefix string) string { // S3CurrentManifestKey returns the current manifest pointer key. // Format: {session_prefix}/current/manifest.json func S3CurrentManifestKey(sessionPrefix string) string { - return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), "current", "manifest.json") + return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), config.S3CurrentSegment, config.S3ManifestFile) } // S3CurrentRunPointerKey returns the current run pointer key. // Format: {session_prefix}/current/run_id.txt func S3CurrentRunPointerKey(sessionPrefix string) string { - return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), "current", "run_id.txt") + return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), config.S3CurrentSegment, config.S3RunIDFile) } // S3PromotedArtifactKey returns the destination key for one promoted artifact. diff --git a/internal/config/defaults.go b/internal/config/defaults.go index f69b53f..6af6f3d 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -10,8 +10,69 @@ const ( DefaultSessionConfigPathEtc = "/etc/narratio/session.yml" DefaultS3AccessKeyIDEnv = "OBJECT_STORAGE_KEY_ID" DefaultS3SecretAccessKeyEnv = "OBJECT_STORAGE_KEY" + DefaultStorageS3RootPrefix = "dnd" + DefaultSpoolRoot = "/var/spool/narratio" + + DefaultWhisperXLanguage = "en" + DefaultWhisperXTimeout = "30m" + DefaultWhisperXRetryDelay = "2s" + DefaultWhisperXConcurrency = 2 + DefaultWhisperXRetries = 3 + + DefaultSeriatimBinary = "seriatim" + DefaultSeriatimTimeout = "10m" + DefaultSeriatimOutputSchema = "seriatim-intermediate" + DefaultSeriatimCoalesceGap = 3.0 + DefaultSeriatimReport = true + + DefaultAuditaBinary = "audita" + DefaultAuditaTimeout = "3h" + DefaultAuditaReport = true + + DefaultScriptoriumBinary = "scriptorium" + DefaultScriptoriumTimeout = "10m" + + DefaultTrimBoundsTimeout = "10m" + DefaultTrimSeriatimReport = false + + DefaultNormalizeOutputPath = "transcripts/normalized.json" + DefaultNormalizeOutputSchema = "seriatim-intermediate" + DefaultNormalizeReport = true + + DefaultArchiveEnabled = true + DefaultArchiveUploadRun = true + + PathWorkDirSegment = "work" + PathInputsDirSegment = "inputs" + PathAudioDirSegment = "audio" + PathTranscriptsSegment = "transcripts" + PathTranscriptsRaw = "transcripts/raw" + PathTranscriptsTrimmed = "transcripts/trimmed" + PathArtifactsDirSegment = "artifacts" + PathConfigDirSegment = "config" + PathLogsDirSegment = "logs" + PathManifestFile = "manifest.json" + PathLockFile = ".lock" + PathTranscriptMerged = "transcripts/merged.json" + PathTranscriptProcessed = "transcripts/processed.json" + PathTranscriptNormalized = "transcripts/normalized.json" + PathTranscriptTrimmed = "transcripts/trimmed.json" + + S3CampaignsSegment = "campaigns" + S3SessionsSegment = "sessions" + S3RunsSegment = "runs" + S3CurrentSegment = "current" + S3ManifestFile = "manifest.json" + S3RunIDFile = "run_id.txt" ) +// DefaultArchivePromoteArtifacts defines the default archive promotion rules. +// Callers should copy this slice before mutating. +var DefaultArchivePromoteArtifacts = []ArchivePromotionRule{ + {From: PathTranscriptTrimmed, To: PathTranscriptTrimmed}, + {From: "artifacts/session_recap.md", To: "artifacts/session_recap.md"}, +} + // DefaultPipelineConfigSearchPaths defines the default search order for // pipeline.yml when callers do not provide an explicit path. // diff --git a/internal/config/load.go b/internal/config/load.go index 2aaed4b..06c7ee7 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -174,7 +174,7 @@ func applyStorageDefaults(cfg *StorageConfig) { cfg.S3 = &StorageS3Config{} } if cfg.S3.RootPrefix == "" { - cfg.S3.RootPrefix = "dnd" + cfg.S3.RootPrefix = DefaultStorageS3RootPrefix } if cfg.S3.AccessKeyIDEnv == "" { cfg.S3.AccessKeyIDEnv = DefaultS3AccessKeyIDEnv @@ -189,7 +189,7 @@ func applySpoolDefaults(cfg *SpoolConfig) { return } if cfg.Root == "" { - cfg.Root = "/var/spool/narratio" + cfg.Root = DefaultSpoolRoot } } @@ -202,16 +202,13 @@ func applyArchiveDefaults(cfg **ArchiveConfig) { } if (*cfg).Enabled == nil { - (*cfg).Enabled = boolPtr(true) + (*cfg).Enabled = boolPtr(DefaultArchiveEnabled) } if (*cfg).UploadRun == nil { - (*cfg).UploadRun = boolPtr(true) + (*cfg).UploadRun = boolPtr(DefaultArchiveUploadRun) } if len((*cfg).PromoteArtifacts) == 0 { - (*cfg).PromoteArtifacts = []ArchivePromotionRule{ - {From: "transcripts/trimmed.json", To: "transcripts/trimmed.json", Required: boolPtr(true)}, - {From: "artifacts/session_recap.md", To: "artifacts/session_recap.md", Required: boolPtr(true)}, - } + (*cfg).PromoteArtifacts = append([]ArchivePromotionRule(nil), DefaultArchivePromoteArtifacts...) } for i := range (*cfg).PromoteArtifacts { if (*cfg).PromoteArtifacts[i].Required == nil { @@ -225,19 +222,19 @@ func applyWhisperXDefaults(cfg *WhisperXConfig) { return } if cfg.Language == "" { - cfg.Language = "en" + cfg.Language = DefaultWhisperXLanguage } if cfg.Timeout == "" { - cfg.Timeout = "30m" + cfg.Timeout = DefaultWhisperXTimeout } if cfg.RetryDelay == "" { - cfg.RetryDelay = "2s" + cfg.RetryDelay = DefaultWhisperXRetryDelay } if cfg.Concurrency == nil { - cfg.Concurrency = intPtr(2) + cfg.Concurrency = intPtr(DefaultWhisperXConcurrency) } if cfg.Retries == nil { - cfg.Retries = intPtr(3) + cfg.Retries = intPtr(DefaultWhisperXRetries) } } @@ -251,19 +248,19 @@ func applySeriatimDefaults(cfg *SeriatimConfig) { return } if cfg.Binary == "" { - cfg.Binary = "seriatim" + cfg.Binary = DefaultSeriatimBinary } if cfg.Timeout == "" { - cfg.Timeout = "10m" + cfg.Timeout = DefaultSeriatimTimeout } if cfg.OutputSchema == "" { - cfg.OutputSchema = "seriatim-intermediate" + cfg.OutputSchema = DefaultSeriatimOutputSchema } if cfg.CoalesceGap == nil { - cfg.CoalesceGap = float64Ptr(3.0) + cfg.CoalesceGap = float64Ptr(DefaultSeriatimCoalesceGap) } if cfg.Report == nil { - cfg.Report = boolPtr(true) + cfg.Report = boolPtr(DefaultSeriatimReport) } } @@ -272,13 +269,13 @@ func applyAuditaDefaults(cfg *AuditaConfig) { return } if cfg.Binary == "" { - cfg.Binary = "audita" + cfg.Binary = DefaultAuditaBinary } if cfg.Timeout == "" { - cfg.Timeout = "3h" + cfg.Timeout = DefaultAuditaTimeout } if cfg.Report == nil { - cfg.Report = boolPtr(true) + cfg.Report = boolPtr(DefaultAuditaReport) } } @@ -287,10 +284,10 @@ func applyScriptoriumDefaults(cfg *ScriptoriumConfig) { return } if cfg.Binary == "" { - cfg.Binary = "scriptorium" + cfg.Binary = DefaultScriptoriumBinary } if cfg.Timeout == "" { - cfg.Timeout = "10m" + cfg.Timeout = DefaultScriptoriumTimeout } } @@ -299,10 +296,10 @@ func applyTrimDefaults(cfg *TrimConfig) { return } if cfg.Bounds.Timeout == "" { - cfg.Bounds.Timeout = "10m" + cfg.Bounds.Timeout = DefaultTrimBoundsTimeout } if cfg.Seriatim.Report == nil { - cfg.Seriatim.Report = boolPtr(false) + cfg.Seriatim.Report = boolPtr(DefaultTrimSeriatimReport) } } @@ -311,13 +308,13 @@ func applyNormalizeDefaults(cfg *NormalizeConfig) { return } if cfg.OutputSchema == "" { - cfg.OutputSchema = defaultNormalizeOutputSchema + cfg.OutputSchema = DefaultNormalizeOutputSchema } if cfg.OutputPath == "" && !cfg.outputPathWasSet() { - cfg.OutputPath = defaultNormalizeOutputPath + cfg.OutputPath = DefaultNormalizeOutputPath } if cfg.Report == nil { - cfg.Report = boolPtr(true) + cfg.Report = boolPtr(DefaultNormalizeReport) } } diff --git a/internal/config/normalize.go b/internal/config/normalize.go index 7ce33e7..3b2dd7e 100644 --- a/internal/config/normalize.go +++ b/internal/config/normalize.go @@ -6,11 +6,6 @@ import ( "gopkg.in/yaml.v3" ) -const ( - defaultNormalizeOutputPath = "transcripts/normalized.json" - defaultNormalizeOutputSchema = "seriatim-intermediate" -) - // UnmarshalYAML tracks explicit normalize.output_path presence so validation can // distinguish omitted vs explicitly empty values. func (cfg *NormalizeConfig) UnmarshalYAML(node *yaml.Node) error {