package pipeline import ( "fmt" "sort" "strings" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" ) // ModuleDependencies contains run-scoped collaborators shared by constructed // modules. Implementations retain only the dependencies they use. type ModuleDependencies struct { LLM contracts.StructuredLLMClient } // BuildRequest contains the stable dependencies and configured options used to // construct one module or validator for a run. type BuildRequest struct { Dependencies ModuleDependencies Options map[string]any References contracts.ReferenceSet } // OptionValidator validates one module binding without constructing it. type OptionValidator func(map[string]any) error func rejectUnconfiguredOptions(options map[string]any) error { return RejectUnknownOptions(options) } func validateRegisteredOptions(validator OptionValidator, options map[string]any) error { if validator == nil { return fmt.Errorf("option validator must not be nil") } return validator(cloneOptions(options)) } func cloneBuildRequest(request BuildRequest) BuildRequest { return BuildRequest{ Dependencies: request.Dependencies, Options: cloneOptions(request.Options), References: CloneReferenceSet(request.References), } } // RejectUnknownOptions provides the common strict-map check used by module- // owned option decoders. Values remain the implementation's responsibility. func RejectUnknownOptions(options map[string]any, allowed ...string) error { known := make(map[string]struct{}, len(allowed)) for _, key := range allowed { key = strings.TrimSpace(key) if key != "" { known[key] = struct{}{} } } unknown := make([]string, 0) for key := range options { if _, ok := known[key]; !ok { unknown = append(unknown, key) } } if len(unknown) == 0 { return nil } sort.Strings(unknown) if len(unknown) == 1 { return fmt.Errorf("unknown option %q", unknown[0]) } return fmt.Errorf("unknown options %q", unknown) }