package config import ( "path/filepath" "strings" "gitea.maximumdirect.net/eric/notarius/internal/core/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) const SupportedFileConfigVersion = 2 type Config struct { Scriptorium ScriptoriumConfig `json:"scriptorium,omitempty"` Pipelines map[string]pipeline.PipelineProfile `json:"pipelines"` Concurrency ConcurrencyConfig `json:"concurrency"` Diagnostics DiagnosticsConfig `json:"diagnostics"` Workspace WorkspaceConfig `json:"workspace"` } type ScriptoriumConfig struct { ProfileDir string `json:"profile_dir,omitempty"` ProfileFile string `json:"profile_file,omitempty"` } type ConcurrencyConfig struct { TotalLLM int `json:"total_llm"` StageWorkers map[string]int `json:"stage_workers"` extractWorkersConfigured bool defaultedExtractWorkers int } type DiagnosticsConfig struct { WorkDir string `json:"work_dir"` Retention diagnostics.RetentionMode `json:"retention"` } type WorkspaceConfig struct { Directory string `json:"directory,omitempty"` Diagnostics WorkspaceDiagnosticsConfig `json:"diagnostics"` Resume WorkspaceResumeConfig `json:"resume"` Debug WorkspaceDebugConfig `json:"debug"` } type WorkspaceDiagnosticsConfig struct { Enabled bool `json:"enabled"` Retention diagnostics.RetentionMode `json:"retention,omitempty"` enabledSet bool retentionSet bool } type WorkspaceResumeConfig struct { Enabled bool `json:"enabled"` } type WorkspaceDebugConfig struct { Enabled bool `json:"enabled"` } func Default() Config { return Config{ Pipelines: map[string]pipeline.PipelineProfile{}, Concurrency: ConcurrencyConfig{ TotalLLM: 1, StageWorkers: map[string]int{"extract": 1}, defaultedExtractWorkers: 1, }, Diagnostics: DiagnosticsConfig{ WorkDir: "/tmp/notarius", Retention: diagnostics.RetentionAuto, }, Workspace: WorkspaceConfig{ Diagnostics: WorkspaceDiagnosticsConfig{ Enabled: true, }, }, } } func (c *Config) RecomputeEffectiveDiagnostics() { if c == nil { return } if dir := c.workspaceDirectory(); dir != "" { c.Diagnostics.WorkDir = filepath.Join(dir, "diagnostics") } if c.Workspace.Diagnostics.retentionSet { c.Diagnostics.Retention = c.Workspace.Diagnostics.Retention } } func (c Config) DiagnosticsEnabled() bool { if !c.Workspace.Diagnostics.enabledSet { return true } return c.Workspace.Diagnostics.Enabled } func (c Config) workspaceDirectory() string { dir := strings.TrimSpace(c.Workspace.Directory) if dir == "" { return "" } return filepath.Clean(dir) } func cloneConfig(in Config) Config { out := in out.Concurrency.StageWorkers = cloneIntMap(in.Concurrency.StageWorkers) out.Pipelines = make(map[string]pipeline.PipelineProfile, len(in.Pipelines)) for key, profile := range in.Pipelines { out.Pipelines[key] = clonePipelineProfile(profile) } return out } func cloneIntMap(in map[string]int) map[string]int { if len(in) == 0 { return nil } out := make(map[string]int, len(in)) for key, value := range in { out[key] = value } return out } func (c *ConcurrencyConfig) recomputeStageWorkerDefaults() { if c == nil { return } if c.StageWorkers == nil { c.StageWorkers = make(map[string]int) } if !c.extractWorkersConfigured { if value, ok := c.StageWorkers["extract"]; ok && (c.defaultedExtractWorkers == 0 || value != c.defaultedExtractWorkers) { c.extractWorkersConfigured = true return } c.StageWorkers["extract"] = c.TotalLLM c.defaultedExtractWorkers = c.TotalLLM } } func clonePipelineProfile(in pipeline.PipelineProfile) pipeline.PipelineProfile { out := in out.Input = cloneModuleBinding(in.Input) out.Chunk = cloneModuleBinding(in.Chunk) out.Output = cloneModuleBinding(in.Output) out.References = cloneStringMap(in.References) if len(in.Artifacts) > 0 { out.Artifacts = make(map[string]pipeline.ArtifactLaneProfile, len(in.Artifacts)) for key, lane := range in.Artifacts { out.Artifacts[key] = cloneArtifactLaneProfile(lane) } } return out } func cloneArtifactLaneProfile(in pipeline.ArtifactLaneProfile) pipeline.ArtifactLaneProfile { out := in out.Extract = cloneModuleBinding(in.Extract) out.Merge = cloneModuleBinding(in.Merge) out.Normalize = cloneModuleBinding(in.Normalize) out.References = cloneStringMap(in.References) if len(in.Validators) > 0 { out.Validators = make([]pipeline.ModuleBinding, len(in.Validators)) for i, binding := range in.Validators { out.Validators[i] = cloneModuleBinding(binding) } } return out } func cloneStringMap(in map[string]string) map[string]string { if len(in) == 0 { return nil } out := make(map[string]string, len(in)) for key, value := range in { out[key] = value } return out } func cloneModuleBinding(in pipeline.ModuleBinding) pipeline.ModuleBinding { out := in if len(in.Options) > 0 { out.Options = cloneOptions(in.Options) } out.References = cloneStringMap(in.References) out.Validators = cloneValidatorOverride(in.Validators) return out } func cloneValidatorOverride(in pipeline.ValidatorOverride) pipeline.ValidatorOverride { out := pipeline.ValidatorOverride{Set: in.Set} if len(in.Validators) > 0 { out.Validators = make([]pipeline.ModuleBinding, len(in.Validators)) for i, binding := range in.Validators { out.Validators[i] = cloneModuleBinding(binding) } } return out } func cloneOptions(in map[string]any) map[string]any { if len(in) == 0 { return nil } out := make(map[string]any, len(in)) for key, value := range in { out[key] = cloneOptionValue(value) } return out } func cloneOptionValue(value any) any { switch typed := value.(type) { case map[string]any: return cloneOptions(typed) case []any: out := make([]any, len(typed)) for i, item := range typed { out[i] = cloneOptionValue(item) } return out default: return typed } }