// Package missionclass is the pure deterministic mission and labor classifier. It tags federal record // text with structured mission areas (the missionclass.Codes vocabulary) using a keyword gazetteer, the // WS2 alias layer, and a curated PSC and NAICS crosswalk, each inference carrying the mechanism and the // evidence phrase so every tag is traceable. The package is pure: no clock, no randomness, no network, // and no DB, so its output is byte stable for fixed input. Mission tags are NEVER a scoring input and // never reference the eeo schema. package missionclass import ( "regexp" "sort" "strings" ) // Mechanism records HOW a mission was inferred, so every tag is traceable (the WS7 acceptance criterion). const ( MechKeyword = "keyword" MechAlias = "alias" MechPSC = "psc" MechNAICS = "naics" ) // Hit is one mission inference with its provenance. type Hit struct { Code string Mechanism string Evidence string // the matched phrase or code } type form struct { code string re *regexp.Regexp text string // canonical evidence phrase for this form } // wbForm compiles a case insensitive, word boundary anchored matcher for a phrase. Any single space in a // multi word phrase becomes \s+, so a line break or a double space between tokens still matches. The // phrase is regex quoted, so a literal containing a metacharacter is matched safely. func wbForm(code, phrase string) form { return form{code: code, text: phrase, re: regexp.MustCompile(`(?i)\b` + strings.ReplaceAll(regexp.QuoteMeta(phrase), " ", `\s+`) + `\b`)} } // missionForms is iterated in Codes order. Keep in sync with ai/mission.go missionVocabulary. var missionForms = []form{ wbForm("SIGINT", "sigint"), wbForm("SIGINT", "signals intelligence"), wbForm("GEOINT", "geoint"), wbForm("GEOINT", "geospatial intelligence"), wbForm("HUMINT", "humint"), wbForm("HUMINT", "human intelligence"), wbForm("MASINT", "masint"), wbForm("MASINT", "measurement and signature intelligence"), wbForm("OSINT", "osint"), wbForm("OSINT", "open source intelligence"), wbForm("SAT_C2", "satellite c2"), wbForm("SAT_C2", "satellite command and control"), wbForm("CDS", "cross domain solution"), wbForm("CDS", "cross domain solutions"), wbForm("COMSEC", "comsec"), wbForm("COMSEC", "communications security"), wbForm("TEMPEST", "tempest"), wbForm("EW", "electronic warfare"), } // ClassifyKeyword returns one Hit per mission code whose keyword form appears in text, in Codes order. func ClassifyKeyword(text string) []Hit { if strings.TrimSpace(text) == "" { return nil } found := map[string]Hit{} for _, f := range missionForms { if _, ok := found[f.code]; ok { continue } if f.re.MatchString(text) { found[f.code] = Hit{Code: f.code, Mechanism: MechKeyword, Evidence: f.text} } } return ordered(found) } // AliasIndex maps a lowercased mission bearing phrase to a mission code. It is built from the curated WS2 // alias rows and the category 'mission_area' skills, so the alias layer (not a cosine threshold) carries // acronym and synonym knowledge. Phrases are matched on a word boundary, whitespace tolerant. type AliasIndex struct { forms []form } // NewAliasIndex builds an AliasIndex from a phrase to code map. The forms are appended in sorted key // order so the build is deterministic and the resulting index iterates the same way every run. func NewAliasIndex(phraseToCode map[string]string) AliasIndex { keys := make([]string, 0, len(phraseToCode)) for k := range phraseToCode { keys = append(keys, k) } sort.Strings(keys) // deterministic build order ai := AliasIndex{} for _, k := range keys { ai.forms = append(ai.forms, wbForm(phraseToCode[k], k)) } return ai } // ClassifyAlias returns one Hit per mission code whose alias phrase appears in text, in Codes order. func ClassifyAlias(text string, idx AliasIndex) []Hit { if strings.TrimSpace(text) == "" { return nil } found := map[string]Hit{} for _, f := range idx.forms { if _, ok := found[f.code]; ok { continue } if f.re.MatchString(text) { found[f.code] = Hit{Code: f.code, Mechanism: MechAlias, Evidence: f.text} } } return ordered(found) } // ordered returns the hits sorted by the canonical Codes order, for byte stable output. func ordered(m map[string]Hit) []Hit { rank := map[string]int{} for i, c := range Codes { rank[c] = i } out := make([]Hit, 0, len(m)) for _, h := range m { out = append(out, h) } sort.Slice(out, func(i, j int) bool { return rank[out[i].Code] < rank[out[j].Code] }) return out }