package contracts import ( "context" "fmt" "gitea.maximumdirect.net/eric/audita/internal/core/chunking" "gitea.maximumdirect.net/eric/audita/internal/core/config" "gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/framework/outcomes" "gitea.maximumdirect.net/eric/audita/internal/framework/proposals" "gitea.maximumdirect.net/eric/audita/internal/framework/structuredoutput" "gitea.maximumdirect.net/eric/audita/internal/framework/validators" ) type StructuredLLMClient = structuredoutput.Client // TranscriptModule is the minimal contract for framework-integrated modules. type TranscriptModule interface { Key() string ReplacementPolicy() proposals.ReplacementPolicy Validators() []Validator Propose(ctx context.Context, req ProposalRequest) (ProposalResult, error) } // Validator evaluates candidate proposals and returns one decision per proposal index. type Validator interface { Name() string Validate(ctx context.Context, req ValidationRequest) (validators.Result, error) } type StructuredCompletionRequest = structuredoutput.Request type StructuredCompletionResponse = structuredoutput.Response // ModuleRunSpec identifies one resolved module instance in a pipeline. type ModuleRunSpec struct { ModuleKey string `json:"module_key"` InstanceName string `json:"instance_name"` ReplacementPolicy proposals.ReplacementPolicy `json:"replacement_policy"` } // SectionMetadata captures chunk-level metadata without embedding full segment payloads. type SectionMetadata struct { Index int `json:"section_index"` StartSegmentID int `json:"start_segment_id"` EndSegmentID int `json:"end_segment_id"` EstimatedTokens int `json:"estimated_tokens"` SegmentCount int `json:"segment_count"` } // SectionMetadataFromSection converts a chunking section into framework metadata. func SectionMetadataFromSection(section chunking.Section) SectionMetadata { return SectionMetadata{ Index: section.Index, StartSegmentID: section.StartSegmentID, EndSegmentID: section.EndSegmentID, EstimatedTokens: section.EstimatedTokens, SegmentCount: len(section.Segments), } } // ExecutionContext carries shared execution state for proposal generation and validation. type ExecutionContext struct { // ReportDiagnosticError may be called concurrently; it must not affect processing. ReportDiagnosticError func(error) `json:"-"` Config *config.Config `json:"-"` WorkingTranscript *schema.Transcript `json:"-"` Glossary *schema.Glossary `json:"-"` Section *SectionMetadata `json:"section,omitempty"` DiagnosticsDir string `json:"diagnostics_dir,omitempty"` } // ProposalRequest is the input to module proposal generation. type ProposalRequest struct { ExecutionContext RunSpec ModuleRunSpec `json:"run_spec"` LLMClient StructuredLLMClient `json:"-"` } type ProposalResult struct { Proposals []proposals.CorrectionProposal `json:"proposals,omitempty"` Operations []outcomes.Operation `json:"operations,omitempty"` } // ValidationRequest is the input to validator execution. type ValidationRequest = validators.Request // ResolveModuleRunSpecs deterministically resolves instance names from logical keys. // Repeated keys are suffixed with _ (1-based), while singleton keys keep their raw key. func ResolveModuleRunSpecs(moduleKeys []string) ([]ModuleRunSpec, error) { totals := make(map[string]int, len(moduleKeys)) for i, key := range moduleKeys { if key == "" { return nil, fmt.Errorf("module key at index %d must not be empty", i) } totals[key]++ } seen := make(map[string]int, len(totals)) specs := make([]ModuleRunSpec, len(moduleKeys)) for i, key := range moduleKeys { seen[key]++ name := key if totals[key] > 1 { name = fmt.Sprintf("%s_%d", key, seen[key]) } specs[i] = ModuleRunSpec{ ModuleKey: key, InstanceName: name, } } return specs, nil }