package replay import ( "fmt" "regexp" "strings" ) // fillerPatterns are the hedge phrases the spec rejects. Compiled once // per package — the gate runs on every replay call. var fillerPatterns = []*regexp.Regexp{ regexp.MustCompile(`(?i)as an ai`), regexp.MustCompile(`(?i)i cannot`), regexp.MustCompile(`(?i)i'?m sorry, but`), regexp.MustCompile(`(?i)i don'?t have access`), regexp.MustCompile(`(?i)i am unable to`), } // ValidateResponse runs the deterministic gate on a model response. // Empty / too-short / hedge-bearing / context-disconnected responses // fail. Matches replay.ts:validateResponse one-to-one. func ValidateResponse(response string, bundle *ContextBundle) ValidationResult { trimmed := strings.TrimSpace(response) var reasons []string if len(trimmed) == 0 { return ValidationResult{Passed: false, Reasons: []string{"empty response"}} } if len(trimmed) < 80 { reasons = append(reasons, fmt.Sprintf("response too short (%d chars; min 80)", len(trimmed))) } for _, re := range fillerPatterns { if re.MatchString(trimmed) { reasons = append(reasons, fmt.Sprintf("filler/hedge phrase detected: %s", re.String())) } } // Soft anchor: if a validation checklist was supplied, the response // should share at least one token with it (≥3 chars per tokenize()). if bundle != nil && len(bundle.ValidationSteps) > 0 { checklistTokens := map[string]struct{}{} for _, s := range bundle.ValidationSteps { for t := range tokenize(s) { checklistTokens[t] = struct{}{} } } respTokens := tokenize(trimmed) overlap := 0 for t := range checklistTokens { if _, ok := respTokens[t]; ok { overlap++ } } if len(checklistTokens) > 0 && overlap == 0 { reasons = append(reasons, "response shares no tokens with validation checklist (may not have followed prior patterns)") } } return ValidationResult{Passed: len(reasons) == 0, Reasons: reasonsOrEmpty(reasons)} } func reasonsOrEmpty(r []string) []string { if r == nil { return []string{} } return r }