package classifier import "regexp" // GatingResult indicates the outcome of keyword-based gating for Tier 2 stories. type GatingResult int const ( GatingPost GatingResult = iota // Stage 1: definite post GatingDiscard // Stage 2: definite discard GatingAmbiguous // Stage 3: escalate to LLM ) // Stage 1: Definite post patterns — stories with clear public significance. // Patterns use multi-word context to reduce false positives (e.g. "Star Wars" won't match). var postPatterns = []*regexp.Regexp{ // Named heads of state / major international bodies regexp.MustCompile(`(?i)\b(president|prime minister|chancellor|pope)\s+\w+`), regexp.MustCompile(`(?i)\b(UN|United Nations|WHO|NATO|EU|European Union|IMF|World Bank)\b`), // Casualty language with numbers regexp.MustCompile(`(?i)\b\d+\s*(dead|killed|injured|casualties|wounded)\b`), regexp.MustCompile(`(?i)\b(dead|killed|injured|casualties|wounded)\s*\d+\b`), // Disaster language — require a location-like context word nearby regexp.MustCompile(`(?i)\b(earthquake|hurricane|typhoon|tsunami|wildfire|tornado)\b.*\b(hit|struck|destroyed|devastated|kills|deaths)\b`), // Conflict language — require multi-word phrases to avoid "Star Wars", "culture war" regexp.MustCompile(`(?i)\b(military invasion|armed coup|airstrike\s+\w+|assassination\s+\w+|car bombing|suicide bombing)\b`), regexp.MustCompile(`(?i)\bwar\s+(in|on|with|against|between)\b`), // Crisis language — require context word nearby regexp.MustCompile(`(?i)\b(state of emergency|economic crisis|economic collapse|disease outbreak|pandemic declared|pandemic phase|famine)\b`), // Major financial scale regexp.MustCompile(`(?i)\$\d+\s*(bn|tn|billion|trillion)\b`), regexp.MustCompile(`(?i)\b(trillion|billion)\s*(dollar|euro|pound)\b`), } // Stage 2: Definite discard patterns — stories unlikely to warrant posting. var discardPatterns = []*regexp.Regexp{ // Local/municipal scope regexp.MustCompile(`(?i)\b(city council|municipal|town hall|zoning|local police)\b`), // Sports scores and routine results regexp.MustCompile(`(?i)\b(final score|match result|league standings|defeated|innings)\b`), regexp.MustCompile(`(?i)\b\d+-\d+\s*(win|loss|defeat|victory)\b`), // Entertainment/celebrity lifestyle regexp.MustCompile(`(?i)\b(red carpet|award show|celebrity gossip|dating rumor|relationship drama|album release)\b`), // Routine corporate earnings regexp.MustCompile(`(?i)\b(quarterly earnings|Q[1-4] results|earnings call|fiscal quarter)\b`), } // RunGating evaluates Tier 2 gating stages on headline + lede. // Returns the gating result and a reason string for logging. func RunGating(headline, lede string) (GatingResult, string) { combined := headline + " " + lede // Stage 1: Definite post for _, re := range postPatterns { if re.MatchString(combined) { return GatingPost, "stage1: " + re.String() } } // Stage 2: Definite discard for _, re := range discardPatterns { if re.MatchString(combined) { return GatingDiscard, "stage2: " + re.String() } } // Stage 3: Ambiguous — escalate to LLM return GatingAmbiguous, "stage3: no keyword match" }