package contracts import ( "context" "encoding/json" "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/proposals" ) // StructuredLLMClient provides provider-agnostic structured completion. type StructuredLLMClient interface { CompleteStructured(ctx context.Context, req StructuredCompletionRequest) (StructuredCompletionResponse, error) } // 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) ([]proposals.CorrectionProposal, error) } // Validator evaluates candidate proposals and returns one decision per proposal index. type Validator interface { Name() string Validate(ctx context.Context, req ValidationRequest) ([]ValidationDecision, error) } // StructuredCompletionRequest is a transport-neutral structured completion request. type StructuredCompletionRequest struct { StageName string `json:"stage_name"` Messages []LLMMessage `json:"messages"` ResponseSchema json.RawMessage `json:"response_schema,omitempty"` } // StructuredCompletionResponse is a transport-neutral structured completion response payload. type StructuredCompletionResponse struct { Content json.RawMessage `json:"content"` } // LLMMessage is a minimal chat message shape for LLM prompts. type LLMMessage struct { Role string `json:"role"` Content string `json:"content"` } // 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"` } // 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, } } // ExecutionContext carries shared execution state for proposal generation and validation. type ExecutionContext struct { 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:"-"` } // ValidationRequest is the input to validator execution. type ValidationRequest struct { ExecutionContext RunSpec ModuleRunSpec `json:"run_spec"` CandidateProposals []proposals.EnrichedCorrectionProposal `json:"candidate_proposals"` } // ValidationDecision is one validator decision for one proposal index. type ValidationDecision struct { ProposalIndex int `json:"proposal_index"` Approved bool `json:"approved"` Confidence *float64 `json:"confidence,omitempty"` Reason string `json:"reason,omitempty"` } // 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 }