package matching import ( "github.com/missionworkforce/missionmatch/internal/modules/matching/engine" "github.com/missionworkforce/missionmatch/internal/modules/matching/intelligence" "github.com/missionworkforce/missionmatch/internal/platform/extraction" ) // missioncheck_convert.go is the extraction to engine boundary, the "zod" half of the Mission Check // "model plus zod" step: the deterministic extractor proposes raw fields and these functions clamp them // onto the pure engine input. They are the authority. Every default here is explicit and is surfaced in // the result as an assumption, never presented as a verified fact (clearance especially is self reported). // parseLevel decodes a clearance level code, falling back to the fail safe Unknown (which scores 0) on // an empty or unrecognized code rather than erroring. parsePoly is the same for the polygraph. func parseLevel(code string) engine.ClearanceLevel { if code == "" { return engine.ClearanceUnknown } if l, err := engine.ParseClearanceLevel(code); err == nil { return l } return engine.ClearanceUnknown } func parsePoly(code string) engine.Polygraph { if code == "" { return engine.PolyNone } if p, err := engine.ParsePolygraph(code); err == nil { return p } return engine.PolyNone } // extractionStatus is the self reported clearance status for a parsed resume. It is Active ONLY when // the text asserts a current clearance AND the level is at least Confidential; otherwise Unknown, which // scores 0 on the clearance component. This is the conservative, self reported default: the tool never // credits a clearance the resume did not assert, and the real check is the customer FSO through DISS. func extractionStatus(rx extraction.RawExtraction, level engine.ClearanceLevel) engine.ClearanceStatus { if rx.ClearanceActive && level >= engine.ClearanceConfidential { return engine.StatusActive } return engine.StatusUnknown } // engineCandidateFromExtraction builds the in memory engine.Candidate from a parsed resume. ID is a non // PII placeholder echoed only into the MatchResult; no name, email, or resume text reaches the engine. // Skills carry an empty SkillID so the engine matches them by name (then the injected semantic matcher), // exactly what an unsaved resume needs. Available is assumed true so availability does not spuriously // halve the location component for a candidate the tool was never told is engaged. func engineCandidateFromExtraction(rx extraction.RawExtraction) engine.Candidate { level := parseLevel(rx.ClearanceLevel) skills := make([]engine.CandidateSkill, 0, len(rx.Skills)) for _, s := range rx.Skills { skills = append(skills, engine.CandidateSkill{Name: s}) } return engine.Candidate{ ID: "mission_check", Clearance: engine.Clearance{Level: level, Poly: parsePoly(rx.ClearancePoly), Status: extractionStatus(rx, level)}, Skills: skills, Certifications: rx.Certifications, CustomerExperience: rx.Agencies, YearsExperience: rx.YearsExperience, Location: rx.Location, Available: true, } } // engineRequirementFromExtraction builds the in memory engine.Requirement from a parsed job description // (Mode B). Every parsed skill becomes a mandatory required skill with an empty SkillID (matched by // name then semantics). The first detected agency is the required customer; an empty one scores 100. func engineRequirementFromExtraction(rx extraction.RawExtraction) engine.Requirement { reqSkills := make([]engine.RequiredSkill, 0, len(rx.Skills)) for _, s := range rx.Skills { reqSkills = append(reqSkills, engine.RequiredSkill{Name: s, Mandatory: true}) } agency := "" if len(rx.Agencies) > 0 { agency = rx.Agencies[0] } return engine.Requirement{ ID: "pasted_jd", RequiredLevel: parseLevel(rx.ClearanceLevel), RequiredPoly: parsePoly(rx.ClearancePoly), RequiredSkills: reqSkills, RequiredCerts: rx.Certifications, CustomerAgency: agency, RequiredYears: rx.YearsExperience, WorkLocation: rx.Location, } } // parsedCandidateFromExtraction projects the resume extraction into the reviewable, savable field set // the result surfaces. Clearance is emitted as DB codes (Unknown to "unknown", None to "none"), the // same self reported values that, if saved, are stored verbatim and never marked verified. func parsedCandidateFromExtraction(rx extraction.RawExtraction) ParsedCandidate { level := parseLevel(rx.ClearanceLevel) return ParsedCandidate{ FullName: rx.FullName, ClearanceLevel: level.Code(), ClearancePoly: parsePoly(rx.ClearancePoly).Code(), ClearanceStatus: extractionStatus(rx, level).Code(), YearsExperience: rx.YearsExperience, Location: rx.Location, Skills: rx.Skills, Certifications: rx.Certifications, CustomerExperience: rx.Agencies, } } // parsedRequirementFromExtraction projects the JD extraction into the requirement summary the Mode B // result panel renders, so it reflects the job description that was actually scored. Required level is // the DB code (Unknown to "unknown"), the same code the engine scored against. func parsedRequirementFromExtraction(rx extraction.RawExtraction) ParsedRequirement { agency := "" if len(rx.Agencies) > 0 { agency = rx.Agencies[0] } return ParsedRequirement{ Title: "Pasted job description", RequiredLevel: parseLevel(rx.ClearanceLevel).Code(), CustomerAgency: agency, KeyPersonnel: false, } } // factsFromExtraction builds the clearance facts for assembleScreenView from a parsed resume. The // resume parse carries no dates this pass, so granted, expires, and reinvestigation are nil; the // clearance intelligence panel renders without them rather than inventing them. func factsFromExtraction(rx extraction.RawExtraction) intelligence.ClearanceFacts { level := parseLevel(rx.ClearanceLevel) return intelligence.ClearanceFacts{ Level: level, Poly: parsePoly(rx.ClearancePoly), Status: extractionStatus(rx, level), CustomerAgencies: rx.Agencies, Provenance: "self_reported", // a parsed resume is always self reported, never verified } }