172 lines
5.5 KiB
Go
172 lines
5.5 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/core/diagnostics"
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
|
)
|
|
|
|
func (c Config) Validate() error {
|
|
if err := validateScriptorium(c.Scriptorium); err != nil {
|
|
return err
|
|
}
|
|
if err := validateDiagnostics(c.Diagnostics); err != nil {
|
|
return err
|
|
}
|
|
if c.Concurrency.TotalLLM <= 0 {
|
|
return fmt.Errorf("total LLM concurrency must be greater than zero")
|
|
}
|
|
return validatePipelineProfiles(c.Pipelines)
|
|
}
|
|
|
|
func validateScriptorium(cfg ScriptoriumConfig) error {
|
|
if strings.TrimSpace(cfg.ProfileDir) != "" && strings.TrimSpace(cfg.ProfileFile) != "" {
|
|
return fmt.Errorf("scriptorium profile_dir and profile_file are mutually exclusive")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateDiagnostics(cfg DiagnosticsConfig) error {
|
|
if strings.TrimSpace(cfg.WorkDir) == "" {
|
|
return fmt.Errorf("diagnostics work dir must not be empty")
|
|
}
|
|
switch cfg.Retention {
|
|
case "", diagnostics.RetentionAuto, diagnostics.RetentionAlways, diagnostics.RetentionNever:
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("diagnostics retention %q is not supported", cfg.Retention)
|
|
}
|
|
}
|
|
|
|
func validatePipelineProfiles(profiles map[string]pipeline.PipelineProfile) error {
|
|
seen := make(map[string]struct{}, len(profiles))
|
|
for rawID, profile := range profiles {
|
|
id := strings.TrimSpace(rawID)
|
|
if id == "" {
|
|
return fmt.Errorf("pipeline id must not be empty")
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
return fmt.Errorf("pipeline id %q is duplicated after trimming", id)
|
|
}
|
|
seen[id] = struct{}{}
|
|
|
|
if profile.ID != "" && strings.TrimSpace(profile.ID) != id {
|
|
return fmt.Errorf("pipeline %q profile id %q does not match map key", id, profile.ID)
|
|
}
|
|
if err := validateBinding(id, "", "input", profile.Input, false); err != nil {
|
|
return err
|
|
}
|
|
if err := validateBinding(id, "", "chunk", profile.Chunk, true); err != nil {
|
|
return err
|
|
}
|
|
if err := validateBinding(id, "", "output", profile.Output, false); err != nil {
|
|
return err
|
|
}
|
|
if err := validateReferenceMap(id, "", profile.References); err != nil {
|
|
return err
|
|
}
|
|
for rawLaneID, lane := range profile.Artifacts {
|
|
laneID := strings.TrimSpace(rawLaneID)
|
|
if laneID == "" {
|
|
return fmt.Errorf("pipeline %q artifact lane id must not be empty", id)
|
|
}
|
|
if err := validateReferenceMap(id, laneID, lane.References); err != nil {
|
|
return err
|
|
}
|
|
if err := validateBinding(id, laneID, "extract", lane.Extract, true); err != nil {
|
|
return err
|
|
}
|
|
if err := validateBinding(id, laneID, "merge", lane.Merge, true); err != nil {
|
|
return err
|
|
}
|
|
if err := validateBinding(id, laneID, "normalize", lane.Normalize, true); err != nil {
|
|
return err
|
|
}
|
|
if len(lane.Validators) > 0 {
|
|
return fmt.Errorf("pipeline %q lane %q validators are not supported by the current raw validation runner", id, laneID)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateBinding(
|
|
pipelineID string,
|
|
laneID string,
|
|
slot string,
|
|
binding pipeline.ModuleBinding,
|
|
referencesAllowed bool,
|
|
) error {
|
|
if err := validateBindingLLMProfile(pipelineID, laneID, slot, binding); err != nil {
|
|
return err
|
|
}
|
|
if binding.Retries < 0 {
|
|
if laneID != "" {
|
|
return fmt.Errorf("pipeline %q lane %q %s retries must be greater than or equal to zero", pipelineID, laneID, slot)
|
|
}
|
|
return fmt.Errorf("pipeline %q %s retries must be greater than or equal to zero", pipelineID, slot)
|
|
}
|
|
if len(binding.References) == 0 {
|
|
return nil
|
|
}
|
|
if !referencesAllowed {
|
|
if laneID != "" {
|
|
return fmt.Errorf("pipeline %q lane %q %s references are not supported", pipelineID, laneID, slot)
|
|
}
|
|
return fmt.Errorf("pipeline %q %s references are not supported", pipelineID, slot)
|
|
}
|
|
return validateReferenceMapForContext(pipelineID, laneID, slot, binding.References)
|
|
}
|
|
|
|
func validateReferenceMap(pipelineID string, laneID string, references map[string]string) error {
|
|
return validateReferenceMapForContext(pipelineID, laneID, "", references)
|
|
}
|
|
|
|
func validateReferenceMapForContext(pipelineID string, laneID string, slot string, references map[string]string) error {
|
|
seen := make(map[string]struct{}, len(references))
|
|
for rawSlotName, rawSource := range references {
|
|
slotName := strings.TrimSpace(rawSlotName)
|
|
if slotName == "" {
|
|
return fmt.Errorf("%s reference slot name must not be empty", referenceContext(pipelineID, laneID, slot))
|
|
}
|
|
if _, ok := seen[slotName]; ok {
|
|
return fmt.Errorf("%s reference slot %q is duplicated after trimming", referenceContext(pipelineID, laneID, slot), slotName)
|
|
}
|
|
seen[slotName] = struct{}{}
|
|
if strings.TrimSpace(rawSource) == "" {
|
|
return fmt.Errorf("%s reference slot %q source must not be empty", referenceContext(pipelineID, laneID, slot), slotName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func referenceContext(pipelineID string, laneID string, slot string) string {
|
|
if laneID != "" && slot != "" {
|
|
return fmt.Sprintf("pipeline %q lane %q %s", pipelineID, laneID, slot)
|
|
}
|
|
if laneID != "" {
|
|
return fmt.Sprintf("pipeline %q lane %q", pipelineID, laneID)
|
|
}
|
|
if slot != "" {
|
|
return fmt.Sprintf("pipeline %q %s", pipelineID, slot)
|
|
}
|
|
return fmt.Sprintf("pipeline %q", pipelineID)
|
|
}
|
|
|
|
func validateBindingLLMProfile(
|
|
pipelineID string,
|
|
laneID string,
|
|
slot string,
|
|
binding pipeline.ModuleBinding,
|
|
) error {
|
|
if binding.LLMProfile != "" && strings.TrimSpace(binding.LLMProfile) == "" {
|
|
if laneID != "" {
|
|
return fmt.Errorf("pipeline %q lane %q %s llm_profile must not be empty when set", pipelineID, laneID, slot)
|
|
}
|
|
return fmt.Errorf("pipeline %q %s llm_profile must not be empty when set", pipelineID, slot)
|
|
}
|
|
return nil
|
|
}
|