824 lines
24 KiB
Go
824 lines
24 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// LoadPipeline loads pipeline configuration from a YAML file with strict field checking.
|
|
func LoadPipeline(path string) (*PipelineConfig, error) {
|
|
return LoadPipelineWithOptions(path, PipelineLoadOptions{})
|
|
}
|
|
|
|
// PipelineLoadOptions carries an optional explicit profile selection. A nil
|
|
// Profile means the caller omitted selection; a non-nil empty value is an
|
|
// explicit invalid selection.
|
|
type PipelineLoadOptions struct {
|
|
Profile *string
|
|
}
|
|
|
|
// LoadPipelineWithOptions loads pipeline configuration with strict field
|
|
// checking and optional named-profile selection.
|
|
func LoadPipelineWithOptions(path string, opts PipelineLoadOptions) (*PipelineConfig, error) {
|
|
cfg, err := loadComposedPipeline(path, opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load pipeline config: %w", err)
|
|
}
|
|
cfg.resolution.publishDeclared = cfg.Publish != nil
|
|
applyPipelineDefaults(cfg)
|
|
if err := resolveNotariusPaths(cfg, path); err != nil {
|
|
return nil, fmt.Errorf("load pipeline config: %w", err)
|
|
}
|
|
if err := finalizePipelineResolution(cfg); err != nil {
|
|
return nil, fmt.Errorf("load pipeline config: %w", err)
|
|
}
|
|
retainArtifactFamilyDeclarations(cfg)
|
|
return cfg, nil
|
|
}
|
|
|
|
// LoadCampaign loads campaign configuration from a YAML file with strict field checking.
|
|
func LoadCampaign(path string) (*CampaignConfig, error) {
|
|
var cfg CampaignConfig
|
|
if err := decodeStrictYAML("campaign", path, &cfg); err != nil {
|
|
return nil, fmt.Errorf("load campaign config: %w", err)
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
|
|
// LoadSession loads session configuration from a YAML file with strict field checking.
|
|
func LoadSession(path string) (*SessionConfig, error) {
|
|
return LoadSessionWithOptions(path, SessionLoadOptions{})
|
|
}
|
|
|
|
// SessionLoadOptions configures expected session identity checks.
|
|
type SessionLoadOptions struct {
|
|
SessionID string
|
|
PreviousSessionID string
|
|
Profile *string
|
|
}
|
|
|
|
// LoadSessionWithOptions loads session configuration from a YAML file with
|
|
// strict field checking.
|
|
func LoadSessionWithOptions(path string, opts SessionLoadOptions) (*SessionConfig, error) {
|
|
sessionBytes, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load session config: session file %q: open: %w", path, err)
|
|
}
|
|
return LoadSessionBytesWithOptions(path, sessionBytes, opts)
|
|
}
|
|
|
|
// LoadSessionBytesWithOptions loads session configuration from YAML bytes with
|
|
// strict field checking.
|
|
func LoadSessionBytesWithOptions(label string, data []byte, opts SessionLoadOptions) (*SessionConfig, error) {
|
|
if err := rejectSessionTemplatePlaceholders(label, string(data)); err != nil {
|
|
return nil, fmt.Errorf("load session config: %w", err)
|
|
}
|
|
|
|
var cfg SessionConfig
|
|
if err := decodeStrictYAMLFromReader("session", label, strings.NewReader(string(data)), &cfg); err != nil {
|
|
return nil, fmt.Errorf("load session config: %w", err)
|
|
}
|
|
if strings.TrimSpace(opts.SessionID) != "" && strings.TrimSpace(cfg.SessionID) != "" && strings.TrimSpace(cfg.SessionID) != strings.TrimSpace(opts.SessionID) {
|
|
return nil, fmt.Errorf(
|
|
"load session config: session file %q: session_id mismatch: --session-id %q does not match session_id %q",
|
|
label,
|
|
strings.TrimSpace(opts.SessionID),
|
|
strings.TrimSpace(cfg.SessionID),
|
|
)
|
|
}
|
|
if expectedPreviousSessionID := strings.TrimSpace(opts.PreviousSessionID); expectedPreviousSessionID != "" {
|
|
actualPreviousSessionID := strings.TrimSpace(cfg.PreviousSessionID)
|
|
if actualPreviousSessionID == "" {
|
|
return nil, fmt.Errorf(
|
|
"load session config: session file %q: previous_session_id is required when --previous-session-id %q is provided",
|
|
label,
|
|
expectedPreviousSessionID,
|
|
)
|
|
}
|
|
if actualPreviousSessionID != expectedPreviousSessionID {
|
|
return nil, fmt.Errorf(
|
|
"load session config: session file %q: previous_session_id mismatch: --previous-session-id %q does not match previous_session_id %q",
|
|
label,
|
|
expectedPreviousSessionID,
|
|
actualPreviousSessionID,
|
|
)
|
|
}
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
|
|
// LoadPublishLockStoreBytes loads a mutable session lock store with strict
|
|
// field checking and source validation.
|
|
func LoadPublishLockStoreBytes(
|
|
label string,
|
|
data []byte,
|
|
scriptorium *ScriptoriumConfig,
|
|
notarius *NotariusConfig,
|
|
) (*PublishLockStore, error) {
|
|
var store PublishLockStore
|
|
if err := decodeStrictYAMLFromReader("publish lock store", label, strings.NewReader(string(data)), &store); err != nil {
|
|
return nil, fmt.Errorf("load publish lock store: %w", err)
|
|
}
|
|
locks, err := ValidatePublishLockRules(store.Locks, scriptorium, notarius, "locks")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load publish lock store: %w", err)
|
|
}
|
|
store.Locks = locks
|
|
return &store, nil
|
|
}
|
|
|
|
// MarshalPublishLockStore serializes a mutable lock store as strict-compatible YAML.
|
|
func MarshalPublishLockStore(store *PublishLockStore) ([]byte, error) {
|
|
if store == nil {
|
|
store = &PublishLockStore{}
|
|
}
|
|
data, err := yaml.Marshal(store)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal publish lock store: %w", err)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// Load loads and resolves combined pipeline, campaign, and session configuration.
|
|
// Passing only a session path is supported for package-internal compatibility;
|
|
// in that form campaign.yml is expected next to the session file.
|
|
func Load(pipelinePath string, paths ...string) (*Config, error) {
|
|
campaignPath, sessionPath, err := campaignSessionPaths(paths...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
|
}
|
|
|
|
// LoadWithSessionOptions loads and resolves combined pipeline, campaign, and
|
|
// session configuration with expected session identity checks.
|
|
func LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath string, sessionOpts SessionLoadOptions) (*Config, error) {
|
|
pipelineCfg, err := LoadPipelineWithOptions(pipelinePath, PipelineLoadOptions{Profile: sessionOpts.Profile})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
campaignCfg, err := LoadCampaign(campaignPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
loaded, err := LoadPipelineCampaign(pipelinePath, pipelineCfg, campaignPath, campaignCfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return LoadSessionWithPipelineCampaignOptions(loaded, sessionPath, sessionOpts)
|
|
}
|
|
|
|
// LoadedPipelineCampaign retains one already loaded pipeline and campaign for
|
|
// subsequent local or remote session resolution. It prevents a command from
|
|
// reloading the root pipeline after campaign selection.
|
|
type LoadedPipelineCampaign struct {
|
|
PipelinePath string
|
|
Pipeline *PipelineConfig
|
|
CampaignPath string
|
|
Campaign *CampaignConfig
|
|
Party ResolvedParty
|
|
}
|
|
|
|
// LoadPipelineCampaign combines already loaded pipeline and campaign documents
|
|
// with their campaign-owned party source. Callers that later resolve a session
|
|
// retain this context rather than independently reimplementing party loading.
|
|
func LoadPipelineCampaign(pipelinePath string, pipeline *PipelineConfig, campaignPath string, campaign *CampaignConfig) (LoadedPipelineCampaign, error) {
|
|
if pipeline == nil {
|
|
return LoadedPipelineCampaign{}, fmt.Errorf("pipeline config is required")
|
|
}
|
|
if campaign == nil {
|
|
return LoadedPipelineCampaign{}, fmt.Errorf("campaign config is required")
|
|
}
|
|
party, err := resolveCampaignParty(campaignPath, campaign)
|
|
if err != nil {
|
|
return LoadedPipelineCampaign{}, err
|
|
}
|
|
if party.Mode == PartyModeCanonical {
|
|
if err := validateCanonicalPartySelection(campaign, nil); err != nil {
|
|
return LoadedPipelineCampaign{}, err
|
|
}
|
|
}
|
|
if err := expandPipelineArtifactFamilies(pipeline, party); err != nil {
|
|
return LoadedPipelineCampaign{}, err
|
|
}
|
|
return LoadedPipelineCampaign{
|
|
PipelinePath: pipelinePath,
|
|
Pipeline: pipeline,
|
|
CampaignPath: campaignPath,
|
|
Campaign: campaign,
|
|
Party: party,
|
|
}, nil
|
|
}
|
|
|
|
// LoadSessionWithPipelineCampaignOptions loads one local session and combines
|
|
// it with an already loaded pipeline and campaign.
|
|
func LoadSessionWithPipelineCampaignOptions(loaded LoadedPipelineCampaign, sessionPath string, sessionOpts SessionLoadOptions) (*Config, error) {
|
|
sessionCfg, err := LoadSessionWithOptions(sessionPath, sessionOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ResolveLoadedPipelineCampaign(loaded, sessionPath, sessionCfg, SessionSource{
|
|
Source: "session_config",
|
|
LocalPath: sessionPath,
|
|
})
|
|
}
|
|
|
|
// ResolveLoadedPipelineCampaign combines an already loaded pipeline and
|
|
// campaign with optional already loaded session data. A nil session preserves
|
|
// the resolved pipeline/campaign context for callers that need to locate or
|
|
// retrieve a session without rereading the root pipeline.
|
|
func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath string, sessionCfg *SessionConfig, sessionSource SessionSource) (*Config, error) {
|
|
if loaded.Pipeline == nil {
|
|
return nil, fmt.Errorf("pipeline config is required")
|
|
}
|
|
if loaded.Campaign == nil {
|
|
return nil, fmt.Errorf("campaign config is required")
|
|
}
|
|
if loaded.Party.Mode == "" {
|
|
party, err := resolveCampaignParty(loaded.CampaignPath, loaded.Campaign)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
loaded.Party = party
|
|
}
|
|
if loaded.Party.Mode == PartyModeCanonical {
|
|
if err := validateCanonicalPartySelection(loaded.Campaign, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
cfg := &Config{
|
|
Pipeline: loaded.Pipeline,
|
|
Campaign: loaded.Campaign,
|
|
Session: sessionCfg,
|
|
PipelinePath: loaded.PipelinePath,
|
|
CampaignPath: loaded.CampaignPath,
|
|
SessionPath: sessionPath,
|
|
SessionSource: sessionSource,
|
|
Party: loaded.Party,
|
|
}
|
|
if cfg.Party.Mode == PartyModeCanonical && sessionCfg != nil {
|
|
if err := validateCanonicalPartySelection(loaded.Campaign, sessionCfg); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if sessionCfg == nil {
|
|
return cfg, nil
|
|
}
|
|
|
|
stableInputs, err := mergeCampaignSession(loaded.Campaign, sessionCfg, loaded.CampaignPath, sessionPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(cfg.SessionSource.Source) == "" {
|
|
cfg.SessionSource.Source = "session_config"
|
|
}
|
|
if strings.TrimSpace(cfg.SessionSource.LocalPath) == "" {
|
|
cfg.SessionSource.LocalPath = sessionPath
|
|
}
|
|
if cfg.Party.Mode == PartyModeCanonical {
|
|
stableInputs.PlayersFile = virtualPlayersInput()
|
|
cfg.Session.Inputs.PlayersFile = ""
|
|
} else {
|
|
party, err := resolvePartyInput(stableInputs.PartyFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if party.Mode == PartyModeCanonical {
|
|
return nil, fmt.Errorf("session.inputs.party_file cannot select a canonical party; canonical parties are campaign-owned")
|
|
}
|
|
cfg.Party = party
|
|
if strings.TrimSpace(stableInputs.PlayersFile.Path) == "" {
|
|
return nil, fmt.Errorf("players_file is required with a legacy party")
|
|
}
|
|
}
|
|
cfg.StableInputs = stableInputs
|
|
return cfg, nil
|
|
}
|
|
|
|
// Resolve builds final stage-facing configuration from already loaded
|
|
// pipeline, campaign, and session documents.
|
|
func Resolve(pipelinePath string, pipelineCfg *PipelineConfig, campaignPath string, campaignCfg *CampaignConfig, sessionPath string, sessionCfg *SessionConfig, sessionSource SessionSource) (*Config, error) {
|
|
if sessionCfg == nil {
|
|
return nil, fmt.Errorf("session config is required")
|
|
}
|
|
loaded, err := LoadPipelineCampaign(pipelinePath, pipelineCfg, campaignPath, campaignCfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ResolveLoadedPipelineCampaign(loaded, sessionPath, sessionCfg, sessionSource)
|
|
}
|
|
|
|
func campaignSessionPaths(paths ...string) (campaignPath, sessionPath string, err error) {
|
|
switch len(paths) {
|
|
case 1:
|
|
sessionPath = paths[0]
|
|
campaignPath = filepath.Join(filepath.Dir(sessionPath), "campaign.yml")
|
|
case 2:
|
|
campaignPath = paths[0]
|
|
sessionPath = paths[1]
|
|
default:
|
|
return "", "", fmt.Errorf("load config: expected session path or campaign and session paths")
|
|
}
|
|
return campaignPath, sessionPath, nil
|
|
}
|
|
|
|
func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig, campaignPath, sessionPath string) (ResolvedStableInputs, error) {
|
|
if campaignCfg == nil {
|
|
return ResolvedStableInputs{}, fmt.Errorf("campaign config is required")
|
|
}
|
|
if sessionCfg == nil {
|
|
return ResolvedStableInputs{}, fmt.Errorf("session config is required")
|
|
}
|
|
if campaignCfg.Inputs.SpellCatalogFile != "" && strings.TrimSpace(campaignCfg.Inputs.SpellCatalogFile) == "" {
|
|
return ResolvedStableInputs{}, fmt.Errorf("campaign.inputs.spell_catalog_file must be non-empty when provided")
|
|
}
|
|
if sessionCfg.Inputs.SpellCatalogFile != "" && strings.TrimSpace(sessionCfg.Inputs.SpellCatalogFile) == "" {
|
|
return ResolvedStableInputs{}, fmt.Errorf("session.inputs.spell_catalog_file must be non-empty when provided")
|
|
}
|
|
|
|
campaignName := CampaignID(campaignCfg)
|
|
sessionCampaign := strings.TrimSpace(sessionCfg.Campaign)
|
|
if sessionCampaign != "" && campaignName != "" && sessionCampaign != campaignName {
|
|
return ResolvedStableInputs{}, fmt.Errorf(
|
|
"campaign/session config invalid: session campaign %q does not match campaign config %q",
|
|
sessionCampaign,
|
|
campaignName,
|
|
)
|
|
}
|
|
if sessionCampaign == "" {
|
|
sessionCfg.Campaign = campaignName
|
|
}
|
|
|
|
stable := ResolvedStableInputs{
|
|
SpeakersFile: selectStableInput(
|
|
campaignCfg.Inputs.SpeakersFile,
|
|
sessionCfg.Inputs.SpeakersFile,
|
|
campaignPath,
|
|
sessionPath,
|
|
),
|
|
AutocorrectFile: selectStableInput(
|
|
campaignCfg.Inputs.AutocorrectFile,
|
|
sessionCfg.Inputs.AutocorrectFile,
|
|
campaignPath,
|
|
sessionPath,
|
|
),
|
|
GlossaryFile: selectStableInput(
|
|
campaignCfg.Inputs.GlossaryFile,
|
|
sessionCfg.Inputs.GlossaryFile,
|
|
campaignPath,
|
|
sessionPath,
|
|
),
|
|
PlayersFile: selectStableInput(
|
|
campaignCfg.Inputs.PlayersFile,
|
|
sessionCfg.Inputs.PlayersFile,
|
|
campaignPath,
|
|
sessionPath,
|
|
),
|
|
PartyFile: selectStableInput(
|
|
campaignCfg.Inputs.PartyFile,
|
|
sessionCfg.Inputs.PartyFile,
|
|
campaignPath,
|
|
sessionPath,
|
|
),
|
|
SpellCatalogFile: selectStableInput(
|
|
campaignCfg.Inputs.SpellCatalogFile,
|
|
sessionCfg.Inputs.SpellCatalogFile,
|
|
campaignPath,
|
|
sessionPath,
|
|
),
|
|
}
|
|
|
|
sessionCfg.Inputs.SpeakersFile = stable.SpeakersFile.Path
|
|
sessionCfg.Inputs.AutocorrectFile = stable.AutocorrectFile.Path
|
|
sessionCfg.Inputs.GlossaryFile = stable.GlossaryFile.Path
|
|
sessionCfg.Inputs.PlayersFile = stable.PlayersFile.Path
|
|
sessionCfg.Inputs.PartyFile = stable.PartyFile.Path
|
|
sessionCfg.Inputs.SpellCatalogFile = stable.SpellCatalogFile.Path
|
|
return stable, nil
|
|
}
|
|
|
|
// CampaignID returns the canonical campaign identity from campaign config.
|
|
func CampaignID(cfg *CampaignConfig) string {
|
|
if cfg == nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(cfg.CampaignID)
|
|
}
|
|
|
|
func selectStableInput(campaignValue, sessionValue, campaignPath, sessionPath string) ResolvedInputFile {
|
|
if strings.TrimSpace(sessionValue) != "" {
|
|
return ResolvedInputFile{
|
|
Path: sessionValue,
|
|
ConfigPath: sessionPath,
|
|
Source: "session_config",
|
|
}
|
|
}
|
|
return ResolvedInputFile{
|
|
Path: campaignValue,
|
|
ConfigPath: campaignPath,
|
|
Source: "campaign_config",
|
|
}
|
|
}
|
|
|
|
func decodeStrictYAML(kind, path string, out any) error {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return fmt.Errorf("%s file %q: open: %w", kind, path, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
return decodeStrictYAMLFromReader(kind, path, f, out)
|
|
}
|
|
|
|
func decodeStrictYAMLFromReader(kind, path string, r io.Reader, out any) error {
|
|
dec := yaml.NewDecoder(r)
|
|
dec.KnownFields(true)
|
|
if err := dec.Decode(out); err != nil {
|
|
return fmt.Errorf("%s file %q: strict decode failed: %w", kind, path, err)
|
|
}
|
|
|
|
var extra yaml.Node
|
|
if err := dec.Decode(&extra); err == nil {
|
|
return fmt.Errorf("%s file %q: must contain exactly one YAML document", kind, path)
|
|
} else if err != io.EOF {
|
|
return fmt.Errorf("%s file %q: trailing content decode failed: %w", kind, path, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var sessionTemplatePlaceholderPattern = regexp.MustCompile(`\{\{[^}]*\}\}`)
|
|
var sessionTemplateVariablePattern = regexp.MustCompile(`\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}`)
|
|
|
|
func rejectSessionTemplatePlaceholders(label, content string) error {
|
|
placeholders := sessionTemplatePlaceholderPattern.FindAllString(content, -1)
|
|
if len(placeholders) == 0 {
|
|
return nil
|
|
}
|
|
seen := map[string]struct{}{}
|
|
vars := make([]string, 0, len(placeholders))
|
|
for _, placeholder := range placeholders {
|
|
name := strings.TrimSpace(placeholder)
|
|
if match := sessionTemplateVariablePattern.FindStringSubmatch(placeholder); len(match) > 1 {
|
|
name = match[1]
|
|
}
|
|
if _, ok := seen[name]; ok {
|
|
continue
|
|
}
|
|
seen[name] = struct{}{}
|
|
vars = append(vars, name)
|
|
}
|
|
sort.Strings(vars)
|
|
return fmt.Errorf(
|
|
"session file %q contains template placeholder(s): %s; session.yml must be concrete; run narratio session init to generate it",
|
|
label,
|
|
strings.Join(vars, ", "),
|
|
)
|
|
}
|
|
|
|
func shortName(path, fallback string) string {
|
|
base := filepath.Base(path)
|
|
if base == "." || base == string(filepath.Separator) {
|
|
return fallback
|
|
}
|
|
return base
|
|
}
|
|
|
|
func applyPipelineDefaults(cfg *PipelineConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
applyWorkspaceDefaults(&cfg.Workspace)
|
|
applyCampaignsDefaults(&cfg.Campaigns)
|
|
applyStorageDefaults(&cfg.Storage)
|
|
applySpoolDefaults(&cfg.Spool)
|
|
applyCacheDefaults(&cfg.Cache)
|
|
applyPublishDefaults(&cfg.Publish)
|
|
applyWhisperXDefaults(&cfg.WhisperX)
|
|
applySeriatimDefaults(&cfg.Seriatim)
|
|
applyAuditaDefaults(&cfg.Audita)
|
|
if cfg.Normalize == nil {
|
|
cfg.Normalize = &NormalizeConfig{}
|
|
}
|
|
applyNormalizeDefaults(cfg.Normalize)
|
|
if cfg.Trim == nil {
|
|
cfg.Trim = &TrimConfig{}
|
|
}
|
|
applyTrimDefaults(cfg.Trim)
|
|
if trimEnabled(cfg.Trim) && cfg.Scriptorium == nil {
|
|
cfg.Scriptorium = &ScriptoriumConfig{}
|
|
}
|
|
applyRenderDefaults(&cfg.Render)
|
|
applyScriptoriumDefaults(cfg.Scriptorium)
|
|
applyNotariusDefaults(cfg.Notarius)
|
|
applyNotificationDefaults(&cfg.Notification)
|
|
}
|
|
|
|
func applyNotificationDefaults(cfg *NotificationConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if strings.TrimSpace(cfg.Mode) == "" {
|
|
cfg.Mode = DefaultNotificationMode
|
|
}
|
|
}
|
|
|
|
func applyCampaignsDefaults(cfg *CampaignsConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.Root == "" {
|
|
cfg.Root = DefaultCampaignsRoot
|
|
}
|
|
}
|
|
|
|
func applyWorkspaceDefaults(cfg *WorkspaceConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.Root == "" {
|
|
cfg.Root = DefaultWorkspaceRoot
|
|
}
|
|
}
|
|
|
|
func applyStorageDefaults(cfg *StorageConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
backend := strings.ToLower(strings.TrimSpace(cfg.Backend))
|
|
if backend == "" {
|
|
backend = StorageBackendLocal
|
|
}
|
|
cfg.Backend = backend
|
|
if cfg.S3 == nil {
|
|
return
|
|
}
|
|
if cfg.S3.RootPrefix == "" {
|
|
cfg.S3.RootPrefix = DefaultStorageS3RootPrefix
|
|
}
|
|
if cfg.S3.AccessKeyIDEnv == "" {
|
|
cfg.S3.AccessKeyIDEnv = DefaultS3AccessKeyIDEnv
|
|
}
|
|
if cfg.S3.SecretKeyEnv == "" {
|
|
cfg.S3.SecretKeyEnv = DefaultS3SecretAccessKeyEnv
|
|
}
|
|
}
|
|
|
|
func applySpoolDefaults(cfg *SpoolConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.Root == "" {
|
|
cfg.Root = DefaultSpoolRoot
|
|
}
|
|
}
|
|
|
|
func applyCacheDefaults(cfg *CacheConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.Root == "" {
|
|
cfg.Root = DefaultCacheRoot
|
|
}
|
|
if cfg.S3Audio == nil {
|
|
cfg.S3Audio = boolPtr(DefaultCacheS3Audio)
|
|
}
|
|
}
|
|
|
|
func applyPublishDefaults(cfg **PublishConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if *cfg == nil {
|
|
*cfg = &PublishConfig{}
|
|
}
|
|
|
|
if (*cfg).Enabled == nil {
|
|
(*cfg).Enabled = boolPtr(DefaultArchiveEnabled)
|
|
}
|
|
if (*cfg).UploadRun == nil {
|
|
(*cfg).UploadRun = boolPtr(DefaultArchiveUploadRun)
|
|
}
|
|
if len((*cfg).Outputs) == 0 {
|
|
(*cfg).Outputs = append([]PublishOutputRule(nil), DefaultPublishOutputs...)
|
|
}
|
|
for i := range (*cfg).Outputs {
|
|
if (*cfg).Outputs[i].Required == nil {
|
|
(*cfg).Outputs[i].Required = boolPtr(true)
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyWhisperXDefaults(cfg *WhisperXConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.Language == "" {
|
|
cfg.Language = DefaultWhisperXLanguage
|
|
}
|
|
if cfg.Timeout == "" {
|
|
cfg.Timeout = DefaultWhisperXTimeout
|
|
}
|
|
if cfg.RetryDelay == "" {
|
|
cfg.RetryDelay = DefaultWhisperXRetryDelay
|
|
}
|
|
if cfg.Concurrency == nil {
|
|
cfg.Concurrency = intPtr(DefaultWhisperXConcurrency)
|
|
}
|
|
if cfg.Retries == nil {
|
|
cfg.Retries = intPtr(DefaultWhisperXRetries)
|
|
}
|
|
}
|
|
|
|
func intPtr(v int) *int {
|
|
p := v
|
|
return &p
|
|
}
|
|
|
|
func applySeriatimDefaults(cfg *SeriatimConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.Binary == "" {
|
|
cfg.Binary = DefaultSeriatimBinary
|
|
}
|
|
if cfg.Timeout == "" {
|
|
cfg.Timeout = DefaultSeriatimTimeout
|
|
}
|
|
if cfg.OutputSchema == "" {
|
|
cfg.OutputSchema = DefaultSeriatimOutputSchema
|
|
}
|
|
if cfg.CoalesceGap == nil {
|
|
cfg.CoalesceGap = float64Ptr(DefaultSeriatimCoalesceGap)
|
|
}
|
|
if cfg.Report == nil {
|
|
cfg.Report = boolPtr(DefaultSeriatimReport)
|
|
}
|
|
}
|
|
|
|
func applyAuditaDefaults(cfg *AuditaConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.Binary == "" {
|
|
cfg.Binary = DefaultAuditaBinary
|
|
}
|
|
if cfg.Timeout == "" {
|
|
cfg.Timeout = DefaultAuditaTimeout
|
|
}
|
|
if cfg.Report == nil {
|
|
cfg.Report = boolPtr(DefaultAuditaReport)
|
|
}
|
|
}
|
|
|
|
func applyScriptoriumDefaults(cfg *ScriptoriumConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.Binary == "" {
|
|
cfg.Binary = DefaultScriptoriumBinary
|
|
}
|
|
if cfg.Timeout == "" {
|
|
cfg.Timeout = DefaultScriptoriumTimeout
|
|
}
|
|
}
|
|
|
|
func applyNotariusDefaults(cfg *NotariusConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.Binary == "" {
|
|
cfg.Binary = DefaultNotariusBinary
|
|
}
|
|
if cfg.Timeout == "" {
|
|
cfg.Timeout = DefaultNotariusTimeout
|
|
}
|
|
}
|
|
|
|
func resolveNotariusPaths(cfg *PipelineConfig, pipelinePath string) error {
|
|
if cfg == nil || cfg.Notarius == nil || !cfg.Notarius.Enabled {
|
|
return nil
|
|
}
|
|
pipelineAbs, err := filepath.Abs(pipelinePath)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve pipeline config path %q: %w", pipelinePath, err)
|
|
}
|
|
baseDir := filepath.Dir(pipelineAbs)
|
|
resolve := func(value string) (string, error) {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
return "", nil
|
|
}
|
|
if !filepath.IsAbs(trimmed) {
|
|
trimmed = filepath.Join(baseDir, trimmed)
|
|
}
|
|
return filepath.Abs(trimmed)
|
|
}
|
|
|
|
resolvedConfigPath, err := resolve(cfg.Notarius.ConfigPath)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve pipeline.notarius.config_path: %w", err)
|
|
}
|
|
cfg.Notarius.ConfigPath = resolvedConfigPath
|
|
if strings.TrimSpace(cfg.Notarius.WorkingDirectory) == "" {
|
|
if resolvedConfigPath != "" {
|
|
cfg.Notarius.WorkingDirectory = filepath.Dir(resolvedConfigPath)
|
|
}
|
|
return nil
|
|
}
|
|
workingDirectory, err := resolve(cfg.Notarius.WorkingDirectory)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve pipeline.notarius.working_directory: %w", err)
|
|
}
|
|
cfg.Notarius.WorkingDirectory = workingDirectory
|
|
return nil
|
|
}
|
|
|
|
func applyTrimDefaults(cfg *TrimConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.Enabled == nil {
|
|
cfg.Enabled = boolPtr(DefaultTrimEnabled)
|
|
}
|
|
if strings.TrimSpace(cfg.OutputPath) == "" {
|
|
cfg.OutputPath = DefaultTrimOutputPath
|
|
}
|
|
if strings.TrimSpace(cfg.Bounds.PromptID) == "" {
|
|
cfg.Bounds.PromptID = DefaultTrimBoundsPromptID
|
|
}
|
|
if strings.TrimSpace(cfg.Bounds.TranscriptInputName) == "" {
|
|
cfg.Bounds.TranscriptInputName = DefaultTrimBoundsTranscriptInputName
|
|
}
|
|
if strings.TrimSpace(cfg.Bounds.OutputPath) == "" {
|
|
cfg.Bounds.OutputPath = DefaultTrimBoundsOutputPath
|
|
}
|
|
if cfg.Bounds.Timeout == "" {
|
|
cfg.Bounds.Timeout = DefaultTrimBoundsTimeout
|
|
}
|
|
if cfg.Seriatim.Report == nil {
|
|
cfg.Seriatim.Report = boolPtr(DefaultTrimSeriatimReport)
|
|
}
|
|
}
|
|
|
|
func trimEnabled(cfg *TrimConfig) bool {
|
|
return cfg != nil && cfg.Enabled != nil && *cfg.Enabled
|
|
}
|
|
|
|
func applyRenderDefaults(cfg **RenderConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if *cfg == nil {
|
|
*cfg = &RenderConfig{}
|
|
}
|
|
if (*cfg).Enabled == nil {
|
|
(*cfg).Enabled = boolPtr(DefaultRenderEnabled)
|
|
}
|
|
if strings.TrimSpace((*cfg).Format) == "" {
|
|
(*cfg).Format = DefaultRenderFormat
|
|
}
|
|
if strings.TrimSpace((*cfg).Title) == "" {
|
|
(*cfg).Title = DefaultRenderTitle
|
|
}
|
|
if (*cfg).IncludeTimestamps == nil {
|
|
(*cfg).IncludeTimestamps = boolPtr(DefaultRenderTimestamps)
|
|
}
|
|
if (*cfg).IncludeSegmentIDs == nil {
|
|
(*cfg).IncludeSegmentIDs = boolPtr(DefaultRenderSegmentIDs)
|
|
}
|
|
}
|
|
|
|
func applyNormalizeDefaults(cfg *NormalizeConfig) {
|
|
if cfg == nil {
|
|
return
|
|
}
|
|
if cfg.OutputSchema == "" {
|
|
cfg.OutputSchema = DefaultNormalizeOutputSchema
|
|
}
|
|
if cfg.OutputPath == "" && !cfg.outputPathWasSet() {
|
|
cfg.OutputPath = DefaultNormalizeOutputPath
|
|
}
|
|
if cfg.Report == nil {
|
|
cfg.Report = boolPtr(DefaultNormalizeReport)
|
|
}
|
|
}
|
|
|
|
func float64Ptr(v float64) *float64 {
|
|
p := v
|
|
return &p
|
|
}
|
|
|
|
func boolPtr(v bool) *bool {
|
|
p := v
|
|
return &p
|
|
}
|