// Package extraction turns plain resume or job description TEXT into a flat set of proposed structured // fields (the RawExtraction). It is the server side half of the W1 Mission Check "model plus zod" // step: the browser does the keyless file to text conversion, and this package does the structured // extraction behind auth so no provider key ever reaches the browser bundle. // // It is deliberately domain light: it emits stable DB code strings (for example "ts_sci", "secret") // and raw skill, certification, and agency mentions, but it does NOT import the scoring engine. The // matching module is the authority that maps a RawExtraction onto the engine input (the normalizer), // so the extractor stays a pure, testable text parser with no scoring or database dependency. // // This pass ships only the deterministic extractor, which is production safe for the closed vocabulary // fields (clearance, certifications, agencies) and a curated skills gazetteer. A future provider backed // extractor (self hosted vLLM for CUI in production) can implement the same interface behind the port // without touching the scoring path. package extraction import ( "context" "fmt" "github.com/missionworkforce/missionmatch/internal/platform/config" ) // RawExtraction is a flat set of PROPOSED field values parsed from a resume or a job description. // Clearance fields carry stable DB codes ("ts_sci", "secret", "full_scope", "ci"), empty when not // found. Every other string is a raw mention. The matching module clamps and maps these onto the // engine input; the extractor never asserts a value is verified. type RawExtraction struct { FullName string // resume only, used only to prefill the optional Save Candidate form ClearanceLevel string // DB code: "confidential" | "secret" | "top_secret" | "ts_sci" | "" (none found) ClearancePoly string // DB code: "ci" | "full_scope" | "" (none found) ClearanceActive bool // the text asserts a current clearance and does not say expired or inactive Skills []string // raw skill mentions resolved against the gazetteer Certifications []string // raw certification mentions resolved against the gazetteer Agencies []string // raw customer or agency mentions resolved against the gazetteer YearsExperience int // largest plausible years figure found Location string // a detected work location, empty when none found } // HasSignal reports whether enough was parsed to score against. An empty or garbage upload that yields // no skills, no clearance, no years, and no certifications is rejected by the caller rather than scored. func (rx RawExtraction) HasSignal() bool { return len(rx.Skills) > 0 || rx.ClearanceLevel != "" || rx.YearsExperience > 0 || len(rx.Certifications) > 0 } // Extractor parses resume or job description text into a RawExtraction. Implementations MUST be // deterministic for a given input so Mission Check is reproducible (the AEDT mandate). type Extractor interface { // ExtractResume parses resume text into proposed candidate fields. ExtractResume(ctx context.Context, text string) (RawExtraction, error) // ExtractRequirement parses a pasted job description into proposed requirement fields. ExtractRequirement(ctx context.Context, text string) (RawExtraction, error) } // New selects the extractor from config. This pass supports only the deterministic extractor; an empty // provider also selects it so a default deployment works with no extra configuration. A provider that // is named but not yet implemented is a hard error so a misconfiguration never falls back silently. func New(cfg config.Extraction) (Extractor, error) { switch cfg.Provider { case config.ExtractorDeterministic, "": return NewDeterministic(), nil default: return nil, fmt.Errorf("extraction: provider %q is not implemented in this build", cfg.Provider) } }