2026-07-03 16:56:00 +00:00
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 {
2026-07-05 18:03:23 +00:00
if err := validateScriptorium ( c . Scriptorium ) ; err != nil {
2026-07-03 16:56:00 +00:00
return err
}
2026-07-08 02:18:59 +00:00
if err := validateWorkspace ( c . Workspace ) ; err != nil {
return err
}
2026-07-03 16:56:00 +00:00
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" )
}
2026-07-05 18:03:23 +00:00
return validatePipelineProfiles ( c . Pipelines )
2026-07-03 16:56:00 +00:00
}
2026-07-05 18:03:23 +00:00
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" )
2026-07-03 16:56:00 +00:00
}
return nil
}
2026-07-08 02:18:59 +00:00
func validateWorkspace ( cfg WorkspaceConfig ) error {
if cfg . Diagnostics . retentionSet {
switch cfg . Diagnostics . Retention {
case "" , diagnostics . RetentionAuto , diagnostics . RetentionAlways , diagnostics . RetentionNever :
default :
return fmt . Errorf ( "workspace diagnostics retention %q is not supported" , cfg . Diagnostics . Retention )
}
}
return nil
}
2026-07-03 16:56:00 +00:00
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 )
}
}
2026-07-05 18:03:23 +00:00
func validatePipelineProfiles ( profiles map [ string ] pipeline . PipelineProfile ) error {
2026-07-03 16:56:00 +00:00
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 )
}
2026-07-05 18:03:23 +00:00
if err := validateBinding ( id , "" , "input" , profile . Input , false ) ; err != nil {
2026-07-03 16:56:00 +00:00
return err
}
2026-07-05 18:03:23 +00:00
if err := validateBinding ( id , "" , "chunk" , profile . Chunk , true ) ; err != nil {
2026-07-03 16:56:00 +00:00
return err
}
2026-07-05 18:03:23 +00:00
if err := validateBinding ( id , "" , "output" , profile . Output , false ) ; err != nil {
2026-07-03 16:56:00 +00:00
return err
}
2026-07-05 14:21:36 +00:00
if err := validateReferenceMap ( id , "" , profile . References ) ; err != nil {
return err
}
2026-07-03 16:56:00 +00:00
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 )
}
2026-07-05 14:21:36 +00:00
if err := validateReferenceMap ( id , laneID , lane . References ) ; err != nil {
return err
}
2026-07-05 18:03:23 +00:00
if err := validateBinding ( id , laneID , "extract" , lane . Extract , true ) ; err != nil {
2026-07-03 16:56:00 +00:00
return err
}
2026-07-07 19:10:55 +00:00
if err := validateBinding ( id , laneID , "merge" , lane . Merge , true ) ; err != nil {
2026-07-03 16:56:00 +00:00
return err
}
2026-07-05 18:03:23 +00:00
if err := validateBinding ( id , laneID , "normalize" , lane . Normalize , true ) ; err != nil {
2026-07-03 16:56:00 +00:00
return err
}
2026-07-07 15:32:35 -05:00
if len ( lane . Validators ) > 0 {
2026-07-07 21:21:12 +00:00
return fmt . Errorf ( "pipeline %q lane %q validators are not supported at artifact lane level; use extract.validators, merge.validators, or normalize.validators" , id , laneID )
2026-07-03 16:56:00 +00:00
}
}
}
return nil
}
2026-07-05 16:18:00 +00:00
func validateBinding (
pipelineID string ,
laneID string ,
slot string ,
binding pipeline . ModuleBinding ,
referencesAllowed bool ,
) error {
2026-07-05 18:03:23 +00:00
if err := validateBindingLLMProfile ( pipelineID , laneID , slot , binding ) ; err != nil {
2026-07-05 16:18:00 +00:00
return err
}
2026-07-07 19:10:55 +00:00
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 )
}
2026-07-07 21:21:12 +00:00
if err := validateValidatorOverride ( pipelineID , laneID , slot , binding . Validators ) ; err != nil {
return err
}
2026-07-05 16:18:00 +00:00
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 )
}
2026-07-07 21:21:12 +00:00
func validateValidatorOverride ( pipelineID string , laneID string , slot string , override pipeline . ValidatorOverride ) error {
if ! override . Set {
return nil
}
switch slot {
case "chunk" , "extract" , "merge" , "normalize" :
default :
return fmt . Errorf ( "%s validators are not supported" , referenceContext ( pipelineID , laneID , slot ) )
}
for i , validator := range override . Validators {
context := fmt . Sprintf ( "%s validators[%d]" , referenceContext ( pipelineID , laneID , slot ) , i )
if strings . TrimSpace ( validator . Module ) == "" {
return fmt . Errorf ( "%s module must not be empty" , context )
}
if len ( validator . References ) > 0 {
return fmt . Errorf ( "%s references are not supported" , context )
}
if validator . Validators . Set {
return fmt . Errorf ( "%s nested validators are not supported" , context )
}
if validator . Retries != 0 {
return fmt . Errorf ( "%s retries are not supported" , context )
}
if validator . LLMProfile != "" && strings . TrimSpace ( validator . LLMProfile ) == "" {
return fmt . Errorf ( "%s llm_profile must not be empty when set" , context )
}
}
return nil
}
2026-07-05 14:21:36 +00:00
func validateReferenceMap ( pipelineID string , laneID string , references map [ string ] string ) error {
2026-07-05 16:18:00 +00:00
return validateReferenceMapForContext ( pipelineID , laneID , "" , references )
}
func validateReferenceMapForContext ( pipelineID string , laneID string , slot string , references map [ string ] string ) error {
2026-07-05 14:21:36 +00:00
seen := make ( map [ string ] struct { } , len ( references ) )
for rawSlotName , rawSource := range references {
slotName := strings . TrimSpace ( rawSlotName )
if slotName == "" {
2026-07-05 16:18:00 +00:00
return fmt . Errorf ( "%s reference slot name must not be empty" , referenceContext ( pipelineID , laneID , slot ) )
2026-07-05 14:21:36 +00:00
}
if _ , ok := seen [ slotName ] ; ok {
2026-07-05 16:18:00 +00:00
return fmt . Errorf ( "%s reference slot %q is duplicated after trimming" , referenceContext ( pipelineID , laneID , slot ) , slotName )
2026-07-05 14:21:36 +00:00
}
seen [ slotName ] = struct { } { }
if strings . TrimSpace ( rawSource ) == "" {
2026-07-05 16:18:00 +00:00
return fmt . Errorf ( "%s reference slot %q source must not be empty" , referenceContext ( pipelineID , laneID , slot ) , slotName )
2026-07-05 14:21:36 +00:00
}
}
return nil
}
2026-07-05 16:18:00 +00:00
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 )
}
2026-07-03 16:56:00 +00:00
func validateBindingLLMProfile (
pipelineID string ,
laneID string ,
slot string ,
binding pipeline . ModuleBinding ,
) error {
2026-07-05 18:03:23 +00:00
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 )
2026-07-03 16:56:00 +00:00
}
2026-07-05 18:03:23 +00:00
return fmt . Errorf ( "pipeline %q %s llm_profile must not be empty when set" , pipelineID , slot )
2026-07-03 16:56:00 +00:00
}
2026-07-05 18:03:23 +00:00
return nil
2026-07-03 16:56:00 +00:00
}