package config import ( "fmt" "io" configkit "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) // LoadedConfig keeps the finalized runtime configuration together with the // private ConfigKit inspection snapshot that produced it. type LoadedConfig struct { Config Config result *configkit.Result[Document] } // RuntimeConfig returns an independently owned runtime configuration. func (loaded *LoadedConfig) RuntimeConfig() Config { if loaded == nil { return Config{} } return cloneConfig(loaded.Config) } // WriteEffective renders ConfigKit's frozen, redacted effective document. func (loaded *LoadedConfig) WriteEffective(output io.Writer) error { if loaded == nil || loaded.result == nil { return fmt.Errorf("loaded configuration is unavailable") } return loaded.result.WriteEffective(output) } // WriteExplanation renders ConfigKit's safe explanation for a JSON-pointer // path without exposing ConfigKit types at this package boundary. func (loaded *LoadedConfig) WriteExplanation(output io.Writer, pointer string) error { if loaded == nil || loaded.result == nil { return fmt.Errorf("loaded configuration is unavailable") } path, err := configkit.ParsePath(pointer) if err != nil { return fmt.Errorf("parse configuration explanation path: %w", err) } return loaded.result.WriteExplanation(output, path) } func loadConfig(path string, options LoadOptions) (*LoadedConfig, error) { result, err := loadDocument(path, options) if err != nil { return nil, err } return newLoadedConfig(result) } // Load reads a version 5 configuration file into runtime configuration and a // private inspection snapshot. func Load(path string, options LoadOptions) (*LoadedConfig, error) { return loadConfig(path, options) } func loadConfigSource(source configkit.Source, options LoadOptions) (*LoadedConfig, error) { result, err := loadDocumentSource(source, options) if err != nil { return nil, err } return newLoadedConfig(result) } func newLoadedConfig(result *configkit.Result[Document]) (*LoadedConfig, error) { if result == nil { return nil, fmt.Errorf("configuration result is unavailable") } runtime, err := documentToConfig(result.Config) if err != nil { return nil, err } return &LoadedConfig{Config: runtime, result: result}, nil } func documentToConfig(document Document) (Config, error) { config := Default() config.Pipelines = documentPipelinesToRuntime(document.Pipelines) if document.Concurrency != nil { config.Concurrency.StageWorkers = documentIntMap(document.Concurrency.StageWorkers) if document.Concurrency.TotalLLM != nil { config.Concurrency.TotalLLM = *document.Concurrency.TotalLLM } } if document.PromptKit != nil { if document.PromptKit.ProfileDir != nil { config.PromptKit.ProfileDir = *document.PromptKit.ProfileDir } if document.PromptKit.ProfileFile != nil { config.PromptKit.ProfileFile = *document.PromptKit.ProfileFile } if document.PromptKit.LocalBackend != nil { backend := PromptKitLocalBackendConfig{} if document.PromptKit.LocalBackend.Endpoint != nil { backend.Endpoint = *document.PromptKit.LocalBackend.Endpoint } if document.PromptKit.LocalBackend.ConcurrencyLimit != nil { backend.ConcurrencyLimit = *document.PromptKit.LocalBackend.ConcurrencyLimit } config.PromptKit.LocalBackend = &backend } } if document.Output != nil && document.Output.Directory != nil { config.Output.Directory = *document.Output.Directory } if document.Cache != nil { if document.Cache.ChunkPlans != nil { if document.Cache.ChunkPlans.Directory != nil { config.Cache.ChunkPlans.Directory = *document.Cache.ChunkPlans.Directory } if document.Cache.ChunkPlans.Mode != nil { mode, err := pipeline.ParseChunkCacheMode(*document.Cache.ChunkPlans.Mode) if err != nil { return Config{}, fmt.Errorf("convert chunk-plan cache mode: %w", err) } config.Cache.ChunkPlans.Mode = mode } } if document.Cache.Checkpoints != nil { if document.Cache.Checkpoints.Enabled != nil { config.Cache.Checkpoints.Enabled = *document.Cache.Checkpoints.Enabled } if document.Cache.Checkpoints.Directory != nil { config.Cache.Checkpoints.Directory = *document.Cache.Checkpoints.Directory } } } if document.Debug != nil && document.Debug.Directory != nil { config.Debug.Directory = *document.Debug.Directory } return config, nil } func documentPipelinesToRuntime(documents map[string]PipelineDocument) map[string]pipeline.PipelineProfile { if documents == nil { return nil } profiles := make(map[string]pipeline.PipelineProfile, len(documents)) for id, document := range documents { profiles[id] = documentPipelineToRuntime(id, document) } return profiles } func documentPipelineToRuntime(id string, document PipelineDocument) pipeline.PipelineProfile { profile := pipeline.PipelineProfile{ ID: id, StructuredOutputRepairAttempts: cloneDocumentInt(document.StructuredOutputRepairAttempts), ValidationPolicy: documentPolicyToRuntime(document.ValidationPolicy), Input: documentBindingToRuntime(document.Input), Artifacts: documentLanesToRuntime(document.Artifacts), References: documentReferencesToRuntime(document.References), } if document.LLMProfile != nil { profile.LLMProfile = *document.LLMProfile } if document.Chunk != nil { profile.Chunk = documentBindingToRuntime(*document.Chunk) } if document.Output != nil { profile.Output = documentBindingToRuntime(*document.Output) } if document.Steps != nil { profile.Steps = make([]pipeline.PipelineStepProfile, len(document.Steps)) for index, step := range document.Steps { profile.Steps[index] = pipeline.PipelineStepProfile{ ID: step.ID, Artifacts: documentLanesToRuntime(step.Artifacts), References: documentReferencesToRuntime(step.References), } } } return profile } func documentLanesToRuntime(documents map[string]ArtifactLaneDocument) map[string]pipeline.ArtifactLaneProfile { if documents == nil { return nil } lanes := make(map[string]pipeline.ArtifactLaneProfile, len(documents)) for id, document := range documents { lane := pipeline.ArtifactLaneProfile{ Extract: documentBindingToRuntime(document.Extract), References: documentReferencesToRuntime(document.References), } if document.Merge != nil { lane.Merge = documentBindingToRuntime(*document.Merge) } if document.Normalize != nil { lane.Normalize = documentBindingToRuntime(*document.Normalize) } if document.Validators != nil { lane.Validators = make([]pipeline.ModuleBinding, len(document.Validators)) for index, validator := range document.Validators { lane.Validators[index] = documentValidatorToRuntime(validator) } } lanes[id] = lane } return lanes } func documentBindingToRuntime(document BindingDocument) pipeline.ModuleBinding { binding := pipeline.ModuleBinding{ Module: document.Module, StructuredOutputRepairAttempts: cloneDocumentInt(document.StructuredOutputRepairAttempts), ValidationPolicy: documentPolicyToRuntime(document.ValidationPolicy), Retries: document.Retries, Options: documentOptions(document.Options), References: documentReferencesToRuntime(document.References), } if document.LLMProfile != nil { binding.LLMProfile = *document.LLMProfile } if document.Validators != nil { binding.Validators.Set = true binding.Validators.Validators = make([]pipeline.ModuleBinding, len(document.Validators)) for index, validator := range document.Validators { binding.Validators.Validators[index] = documentValidatorToRuntime(validator) } } return binding } func documentValidatorToRuntime(document ValidatorDocument) pipeline.ModuleBinding { binding := pipeline.ModuleBinding{ Module: document.Module, StructuredOutputRepairAttempts: cloneDocumentInt(document.StructuredOutputRepairAttempts), Retries: document.Retries, Options: documentOptions(document.Options), } if document.LLMProfile != nil { binding.LLMProfile = *document.LLMProfile } return binding } func documentReferencesToRuntime(documents map[string]ReferenceDocument) map[string]pipeline.ReferenceSource { if documents == nil { return nil } references := make(map[string]pipeline.ReferenceSource, len(documents)) for name, document := range documents { if document.Artifact != nil { references[name] = pipeline.GeneratedReference(document.Artifact.Step, document.Artifact.Lane) continue } if document.Path != nil { references[name] = pipeline.ExternalReference(*document.Path) } } return references } func documentPolicyToRuntime(document *ValidationPolicyDocument) *pipeline.ValidationPolicyOverride { if document == nil { return nil } policy := pipeline.ValidationPolicyOverride{} if document.ProducerStructuralFailure != nil { value := *document.ProducerStructuralFailure policy.ProducerStructuralFailure = &value } if document.SemanticRejection != nil { value := *document.SemanticRejection policy.SemanticRejection = &value } if document.ValidatorFailure != nil { value := *document.ValidatorFailure policy.ValidatorFailure = &value } return &policy } func documentIntMap(values map[string]int) map[string]int { if values == nil { return nil } result := make(map[string]int, len(values)) for key, value := range values { result[key] = value } return result } func cloneDocumentInt(value *int) *int { if value == nil { return nil } result := *value return &result } func documentOptions(values map[string]any) map[string]any { if values == nil { return nil } result := make(map[string]any, len(values)) for key, value := range values { result[key] = cloneDocumentOption(value) } return result } func cloneDocumentOption(value any) any { switch typed := value.(type) { case map[string]any: return documentOptions(typed) case []any: result := make([]any, len(typed)) for index, item := range typed { result[index] = cloneDocumentOption(item) } return result default: return typed } }