Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/matching/engine/engine.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 4b8e7ce594c068621867f9c9deed0ec64ae6c553 |
| Branch | ws3-clearance |
| Path or archive member | internal/modules/matching/engine/engine.go |
| Original SHA-256 | ba54d623e6af7b33738d0b123a8464a7fef752effc23fa581aa741d555637677 |
| Git blob SHA | 0ee3b7dfc19f709571894c53c9839fba916fc7af |
| Reading-copy SHA-256 | ba54d623e6af7b33738d0b123a8464a7fef752effc23fa581aa741d555637677 |
Recovered source transcript
Download complete source text ↓394 lines · 14.0 KB
package engine
import (
"context"
"fmt"
"sort"
"strings"
"github.com/shopspring/decimal"
)
// ModelVersion is the single source of the scoring model version string. It is stamped onto every
// frozen slate_members decision and registered in aedt_model_versions, so an AEDT decision proves
// which weights, band cuts, and semantic threshold produced it. Bump this whenever the weights, the
// band cuts, OR the semantic threshold change, and register the new version (CLAUDE.md responsible AI
// rule, NIST AI RMF model card). v1.1.0 recalibrated DefaultSemanticThreshold from 0.80 to 0.62; the
// weights and band cuts are unchanged from v1.0.0.
const ModelVersion = "v1.3.0"
// Fixed component weights (CLAUDE.md, do NOT change without explicit instruction).
// They MUST sum to exactly 1.00; TestWeightsSumToOne enforces it.
var (
WeightClearance = decimal.RequireFromString("0.25")
WeightSkills = decimal.RequireFromString("0.25")
WeightCerts = decimal.RequireFromString("0.15")
WeightCustomer = decimal.RequireFromString("0.15")
WeightExperience = decimal.RequireFromString("0.10")
WeightLocation = decimal.RequireFromString("0.10")
)
// DefaultSemanticThreshold is the cosine-similarity cutoff at/above which the injected
// SkillMatcher may count a semantic (non-exact) skill match. Below it, a skill is unmet.
//
// Calibrated on the live bge-small-en-v1.5 vectors (the production embedder), not guessed. On a set
// of known synonym and adjacent skill pairs versus known unrelated pairs, the genuine matches scored
// 0.68 to 0.94 (cosine) while every unrelated pair scored at or below 0.57; the two groups separate
// in the gap [0.57, 0.68]. 0.62 sits in that gap with margin on each side: above every unrelated
// pair (no invented match) and below every genuine synonym (no discarded match). The earlier 0.80
// was tuned for a different embedder and silently discarded real bge matches (a clear synonym scored
// 0.74, under the old cutoff). Acronym to expansion cases (for example SIGINT to Signals
// Intelligence) sit below the unrelated ceiling and are not reachable by ANY safe single threshold;
// those belong to the skills ontology alias layer, not to this cosine cutoff. See the calibration
// harness TestSemanticThresholdCalibration. Changing this value is a model change: bump ModelVersion
// and register it (the frozen test enforces both).
var DefaultSemanticThreshold = decimal.RequireFromString("0.62")
// Band cut points (CLAUDE.md): Green >= 90, Yellow 75–89.99, Red < 75. Inclusive lower bounds.
var (
BandGreenMin = decimal.NewFromInt(90)
BandYellowMin = decimal.NewFromInt(75)
)
// shared decimal constants
var (
zero = decimal.Zero
fifty = decimal.NewFromInt(50)
hundred = decimal.NewFromInt(100)
two = decimal.NewFromInt(2)
)
// ScoreCandidate scores one candidate against one requirement and returns a full, explainable,
// reproducible MatchResult. It is PURE: no DB, no I/O, no clock, no randomness. Semantic skill
// matching is delegated to the injected sm (nil is allowed, then only exact skill matches count).
//
// It NEVER reads a demographic/EEO attribute: the Candidate type carries none, and this function
// references no such field. Every sub-score is decimal (never float64) so the same inputs always
// yield byte-identical scores and explanations.
func ScoreCandidate(ctx context.Context, c Candidate, r Requirement, sm SkillMatcher) MatchResult {
res := MatchResult{CandidateID: c.ID, RequirementID: r.ID}
clRaw, clRat, clGaps := scoreClearance(c.Clearance, r)
skRaw, skRat, skGaps, matched := scoreSkills(ctx, c.Skills, r.RequiredSkills, sm)
ceRaw, ceRat, ceGaps := scoreCerts(c.Certifications, r.RequiredCerts)
cuRaw, cuRat := scoreCustomer(c.CustomerExperience, r.CustomerAgency)
exRaw, exRat := scoreExperience(c.YearsExperience, r.RequiredYears)
loRaw, loRat := scoreLocation(c, r)
// Round every sub score to the numeric(7,4) column scale BEFORE persisting and before building the
// trace, so the stored component columns, the jsonb trace, and the total are one consistent set and
// re summing the components reproduces the total. Without this the column (DB rounded to 4) and the
// trace (full precision) disagree for any non terminating ratio.
res.Clearance = clRaw.Round(4)
res.Skills = skRaw.Round(4)
res.Certifications = ceRaw.Round(4)
res.CustomerExperience = cuRaw.Round(4)
res.Experience = exRaw.Round(4)
res.Location = loRaw.Round(4)
res.MatchedSkills = matched
res.Trace = []ComponentScore{
newComponent("Clearance", WeightClearance, res.Clearance, clRat),
newComponent("Skills", WeightSkills, res.Skills, skRat),
newComponent("Certifications", WeightCerts, res.Certifications, ceRat),
newComponent("CustomerExperience", WeightCustomer, res.CustomerExperience, cuRat),
newComponent("Experience", WeightExperience, res.Experience, exRat),
newComponent("LocationAvailability", WeightLocation, res.Location, loRat),
}
total := zero
for _, comp := range res.Trace {
total = total.Add(comp.Weighted)
}
res.Total = total.Round(2)
res.Band = bandFor(res.Total)
gaps := make([]string, 0, len(clGaps)+len(skGaps)+len(ceGaps))
gaps = append(gaps, clGaps...)
gaps = append(gaps, skGaps...)
gaps = append(gaps, ceGaps...)
res.GapSummary = gaps
res.Explanation = buildExplanation(res)
return res
}
func newComponent(name string, weight, raw decimal.Decimal, rationale string) ComponentScore {
return ComponentScore{
Component: name,
Weight: weight,
Raw: raw,
Weighted: raw.Mul(weight).Round(4),
Rationale: rationale,
}
}
// ---------- Clearance (0 or 100; exact-or-higher, active, poly-satisfied) ----------
func scoreClearance(held Clearance, r Requirement) (decimal.Decimal, string, []string) {
// A requirement below Confidential means no clearance is required, everyone passes.
if r.RequiredLevel < ClearanceConfidential {
return hundred, "No clearance required for this role.", nil
}
reqDesc := r.RequiredLevel.String()
if r.RequiredPoly != PolyNone {
reqDesc += " + " + r.RequiredPoly.String()
}
switch {
case held.Status != StatusActive:
g := fmt.Sprintf("Requires active %s; candidate clearance is %s.", reqDesc, statusLabel(held.Status))
return zero, g + " Scores 0.", []string{g}
case held.Level < r.RequiredLevel:
g := fmt.Sprintf("Requires %s; candidate holds %s.", reqDesc, held.Level.String())
return zero, g + " Scores 0.", []string{g}
case held.Poly < r.RequiredPoly:
g := fmt.Sprintf("Requires %s; candidate has %s.", r.RequiredPoly.String(), held.Poly.String())
return zero, g + " Scores 0.", []string{g}
case !hasAll(held.Compartments, r.RequiredCompartments):
missing := missingCompartments(held.Compartments, r.RequiredCompartments)
g := fmt.Sprintf("Requires compartments %v; candidate lacks %v.", r.RequiredCompartments, missing)
return zero, g + " Scores 0.", []string{g}
default:
return hundred, fmt.Sprintf("Active %s meets required %s.", held.Level.String(), reqDesc), nil
}
}
func statusLabel(s ClearanceStatus) string {
switch s {
case StatusActive:
return "active"
case StatusExpired:
return "expired"
case StatusInactive:
return "inactive"
default:
return "unknown/none"
}
}
// ---------- Skills (fraction of required met: exact first, then injected semantic) ----------
func scoreSkills(ctx context.Context, candSkills []CandidateSkill, required []RequiredSkill, sm SkillMatcher) (decimal.Decimal, string, []string, []SkillMatch) {
if len(required) == 0 {
return hundred, "No specific skills required.", nil, nil
}
// Normalize the iteration order so MatchedSkills output is deterministic regardless of caller order.
req := make([]RequiredSkill, len(required))
copy(req, required)
sort.SliceStable(req, func(i, j int) bool {
if req[i].SkillID != req[j].SkillID {
return req[i].SkillID < req[j].SkillID
}
return req[i].Name < req[j].Name
})
matched := make([]SkillMatch, 0, len(req))
var gaps []string
met := 0
for _, rs := range req {
m, ok := matchOne(ctx, rs, candSkills, sm)
matched = append(matched, m)
switch {
case ok:
met++
case rs.Mandatory:
gaps = append(gaps, fmt.Sprintf("Missing mandatory skill: %s.", rs.Name))
}
}
raw := ratio(met, len(req))
rat := fmt.Sprintf("Matched %d of %d required skills (%s%%).", met, len(req), raw.StringFixed(0))
return raw, rat, gaps, matched
}
// matchOne resolves one required skill: exact by SkillID, then exact by normalized name, then the
// injected semantic matcher. It always returns a SkillMatch row (Matched=false when nothing met).
func matchOne(ctx context.Context, rs RequiredSkill, candSkills []CandidateSkill, sm SkillMatcher) (SkillMatch, bool) {
for _, cs := range candSkills {
if (rs.SkillID != "" && cs.SkillID == rs.SkillID) || equalFold(cs.Name, rs.Name) {
return SkillMatch{
RequiredSkillID: rs.SkillID,
RequiredSkillName: rs.Name,
CandidateSkillID: cs.SkillID,
CandidateName: cs.Name,
Similarity: decimal.NewFromInt(1),
Exact: true,
Matched: true,
}, true
}
}
if sm != nil {
if m, ok := sm.Match(ctx, rs, candSkills, DefaultSemanticThreshold); ok {
// The engine owns the authoritative required-side identity and the match flags;
// the matcher only supplies the candidate side and the similarity.
m.RequiredSkillID = rs.SkillID
m.RequiredSkillName = rs.Name
m.Exact = false
m.Matched = true
return m, true
}
}
return SkillMatch{RequiredSkillID: rs.SkillID, RequiredSkillName: rs.Name, Matched: false}, false
}
// ---------- Certifications (fraction held, exact normalized name) ----------
func scoreCerts(held, required []string) (decimal.Decimal, string, []string) {
if len(required) == 0 {
return hundred, "No certifications required.", nil
}
req := append([]string(nil), required...)
sort.Strings(req)
heldSet := normalizeSet(held)
met := 0
var gaps []string
for _, rc := range req {
if heldSet[normalize(rc)] {
met++
} else {
gaps = append(gaps, fmt.Sprintf("Missing required certification: %s.", rc))
}
}
raw := ratio(met, len(req))
return raw, fmt.Sprintf("Holds %d of %d required certifications.", met, len(req)), gaps
}
// ---------- Customer experience (binary: prior experience with the required agency) ----------
func scoreCustomer(experience []string, agency string) (decimal.Decimal, string) {
if strings.TrimSpace(agency) == "" {
return hundred, "No specific customer experience required."
}
if normalizeSet(experience)[normalize(agency)] {
return hundred, fmt.Sprintf("Has prior experience with %s.", agency)
}
return zero, fmt.Sprintf("No prior experience with %s.", agency)
}
// ---------- Experience years (full at/above requirement, else linear fraction) ----------
func scoreExperience(years, required int) (decimal.Decimal, string) {
if required <= 0 {
return hundred, "No minimum years of experience required."
}
if years >= required {
return hundred, fmt.Sprintf("%d years meets the %d year requirement.", years, required)
}
if years < 0 {
years = 0
}
raw := ratio(years, required)
return raw, fmt.Sprintf("%d of %d required years (%s%%).", years, required, raw.StringFixed(0))
}
// ---------- Location & availability (average of location fit and availability) ----------
func scoreLocation(c Candidate, r Requirement) (decimal.Decimal, string) {
var loc decimal.Decimal
var locReason string
switch {
case strings.TrimSpace(r.WorkLocation) == "":
loc, locReason = hundred, "no fixed work location"
case r.RemoteAllowed && c.Remote:
loc, locReason = hundred, "remote work satisfies location"
case normalize(c.Location) == normalize(r.WorkLocation):
loc, locReason = hundred, fmt.Sprintf("located in %s", r.WorkLocation)
case c.Relocatable:
loc, locReason = fifty, fmt.Sprintf("willing to relocate to %s", r.WorkLocation)
default:
loc, locReason = zero, fmt.Sprintf("not located in %s and not relocatable", r.WorkLocation)
}
avail, availReason := zero, "not currently available"
if c.Available {
avail, availReason = hundred, "available now"
}
raw := loc.Add(avail).Div(two)
return raw, fmt.Sprintf("Location/availability: %s; %s.", locReason, availReason)
}
// ---------- helpers ----------
func bandFor(total decimal.Decimal) Band {
switch {
case total.GreaterThanOrEqual(BandGreenMin):
return BandGreen
case total.GreaterThanOrEqual(BandYellowMin):
return BandYellow
default:
return BandRed
}
}
func buildExplanation(res MatchResult) string {
var b strings.Builder
fmt.Fprintf(&b, "%s (%s/100). ", res.Band.Label(), res.Total.StringFixed(2))
if len(res.GapSummary) == 0 {
b.WriteString("No mandatory gaps.")
} else {
fmt.Fprintf(&b, "%d gap(s): %s", len(res.GapSummary), strings.Join(res.GapSummary, " "))
}
return strings.TrimSpace(b.String())
}
// ratio returns n/d as a 0–100 decimal (deterministic via shopspring's fixed DivisionPrecision).
func ratio(n, d int) decimal.Decimal {
if d == 0 {
return hundred
}
return decimal.NewFromInt(int64(n)).Div(decimal.NewFromInt(int64(d))).Mul(hundred)
}
func normalize(s string) string { return strings.ToLower(strings.TrimSpace(s)) }
func equalFold(a, b string) bool {
return strings.EqualFold(strings.TrimSpace(a), strings.TrimSpace(b))
}
func normalizeSet(items []string) map[string]bool {
set := make(map[string]bool, len(items))
for _, it := range items {
set[normalize(it)] = true
}
return set
}
// hasAll reports whether held carries every required compartment. An empty requirement is always met,
// so existing inputs (no required compartments) are unaffected and the frozen scores do not change.
func hasAll(held, required []string) bool {
if len(required) == 0 {
return true
}
set := map[string]bool{}
for _, h := range held {
set[strings.ToUpper(strings.TrimSpace(h))] = true
}
for _, r := range required {
if !set[strings.ToUpper(strings.TrimSpace(r))] {
return false
}
}
return true
}
func missingCompartments(held, required []string) []string {
set := map[string]bool{}
for _, h := range held {
set[strings.ToUpper(strings.TrimSpace(h))] = true
}
var out []string
for _, r := range required {
if !set[strings.ToUpper(strings.TrimSpace(r))] {
out = append(out, r)
}
}
return out
}