package ai import ( "regexp" "strings" ) // Mission scoping (WS6 Option A). A RAG question can name a mission area (SIGINT, GEOINT, ...). When it // does, the retrieval is constrained to candidates whose full_name, headline, or location text matches // that mission keyword, using the existing full text index (search_tsv). This is the ONLY mission filter // available without structured mission tags on candidates and requirements, which are WS7 work: it filters // on that candidate TEXT, not on a semantic mission alignment, and not on the resume body. The limit is // documented in docs/AI_RESEARCH.md. // // The vocabulary mirrors the curated mission_spine seed (migration 0021_ontology.sql), which is the // source of truth. The ten terms change rarely and are controlled IC vocabulary, so mirroring them in // code (rather than a database round trip on every question) keeps retrieval deterministic and avoids a // dependency from the AI path onto a reference query. If mission_spine grows, extend this list to match. // missionPattern pairs the canonical full text keyword fed to websearch_to_tsquery with the natural // language forms a user might type. Acronyms and short tokens are matched on a word boundary so a bare // acronym never matches inside another word. type missionPattern struct { canonical string // the keyword passed to the mission scoped query ($mission_query) forms []*regexp.Regexp // question phrasings that select this mission } // wb compiles a case insensitive, word boundary anchored matcher for a mission form. Any single space in a // multi word form becomes \s+, so a line break or a double space between phrase tokens still matches (for // example "satellite command and control"). The forms contain no regex metacharacters, so the literal is // safe to embed directly. func wb(s string) *regexp.Regexp { return regexp.MustCompile(`(?i)\b` + strings.ReplaceAll(s, " ", `\s+`) + `\b`) } // missionVocabulary is iterated in mission_spine order, so when a question names more than one mission the // result is deterministic (the earliest spine term wins). Order MUST match migration 0021. var missionVocabulary = []missionPattern{ {"SIGINT", []*regexp.Regexp{wb("sigint"), wb("signals intelligence")}}, {"GEOINT", []*regexp.Regexp{wb("geoint"), wb("geospatial intelligence")}}, {"HUMINT", []*regexp.Regexp{wb("humint"), wb("human intelligence")}}, {"MASINT", []*regexp.Regexp{wb("masint"), wb("measurement and signature intelligence")}}, {"OSINT", []*regexp.Regexp{wb("osint"), wb("open source intelligence")}}, {"Satellite C2", []*regexp.Regexp{wb("satellite c2"), wb("satellite command and control")}}, {"Cross Domain Solution", []*regexp.Regexp{wb("cross domain solution"), wb("cross domain solutions")}}, {"COMSEC", []*regexp.Regexp{wb("comsec"), wb("communications security")}}, {"TEMPEST", []*regexp.Regexp{wb("tempest")}}, {"Electronic Warfare", []*regexp.Regexp{wb("electronic warfare")}}, } // MissionFromQuestion returns the canonical mission keyword named in the question, or "" if none is named. // The returned value is drawn only from the fixed mission vocabulary above, never from free user text, so // it can never carry a demographic reference into the retrieval path. func MissionFromQuestion(question string) string { if question == "" { return "" } for _, m := range missionVocabulary { for _, form := range m.forms { if form.MatchString(question) { return m.canonical } } } return "" }