//go:build integration package integration import ( "fmt" "sort" "testing" "github.com/missionworkforce/missionmatch/internal/modules/matching/engine" "github.com/missionworkforce/missionmatch/internal/platform/inference" ) // skillPair is one labeled comparison: two skill names that either SHOULD match semantically // (synonyms or adjacent skills, the engine must count them as a met skill) or SHOULD NOT // (unrelated, the engine must leave the skill unmet). It is the calibration unit. type skillPair struct { a, b string } // synonymPairs are the should match group: real cleared workforce skills written under two // different but synonymous or directly adjacent names. On a correctly calibrated threshold every // one of these resolves as a semantic match, so a candidate who lists "AWS Cloud Platform" still // satisfies a requirement that asks for "Amazon Web Services". var synonymPairs = []skillPair{ {"Amazon Web Services", "AWS Cloud Platform"}, {"Kubernetes", "Container Orchestration"}, {"Penetration Testing", "Ethical Hacking"}, {"Incident Response", "Security Incident Handling"}, {"Python Programming", "Python Software Development"}, {"Threat Hunting", "Threat Detection"}, {"Linux System Administration", "Unix System Administration"}, {"Data Analysis", "Data Analytics"}, {"Signals Intelligence", "SIGINT"}, {"Vulnerability Assessment", "Vulnerability Scanning"}, {"Network Engineering", "Network Infrastructure Engineering"}, {"Cloud Security", "Cloud Security Engineering"}, } // unrelatedPairs are the should not match group: a real cleared workforce skill paired with an // unrelated skill from another domain entirely. On a correctly calibrated threshold none of these // resolves as a match, so the engine never invents a skill a candidate does not hold. var unrelatedPairs = []skillPair{ {"Amazon Web Services", "Underwater Basket Weaving"}, {"Kubernetes", "Pastry Baking"}, {"Penetration Testing", "Floral Arrangement"}, {"Incident Response", "Landscape Gardening"}, {"Python Programming", "Ballroom Dancing"}, {"Threat Hunting", "Oil Painting"}, {"Linux System Administration", "Marine Biology"}, {"Data Analysis", "Automotive Repair"}, {"Signals Intelligence", "Creative Writing"}, {"Vulnerability Assessment", "Wedding Photography"}, {"Network Engineering", "Pottery"}, {"Cloud Security", "Beekeeping"}, } // cosine embeds both names through the live bge plane, normalizes exactly as the backfill does (so // the result equals the stored vectors' cosine), and returns the dot product, which for L2 // normalized vectors is the cosine similarity. func cosine(t *testing.T, client *inference.Client, p skillPair) float64 { t.Helper() va, vb := embedNorm(t, client, p.a), embedNorm(t, client, p.b) var s float64 for i := range va { s += float64(va[i]) * float64(vb[i]) } return s } // TestSemanticThresholdCalibration is the calibration harness. It scores every known should match // (synonym) and should not (unrelated) skill pair on the live bge-small vectors, reports the full // cosine distribution of each group, and asserts the engine's DefaultSemanticThreshold separates // the two groups with margin: it sits strictly below the lowest true synonym AND strictly above // the highest unrelated pair, with a guard band on each side. This is the evidence behind the // chosen threshold; if a future model or skill set narrows the gap, this test fails loudly rather // than letting a miscalibrated threshold silently discard real matches or invent false ones. func TestSemanticThresholdCalibration(t *testing.T) { client := testPlane(t) synSims := make([]float64, 0, len(synonymPairs)) for _, p := range synonymPairs { s := cosine(t, client, p) synSims = append(synSims, s) t.Logf("SYNONYM %-32s ~ %-34s cosine=%.4f", p.a, p.b, s) } unrSims := make([]float64, 0, len(unrelatedPairs)) for _, p := range unrelatedPairs { s := cosine(t, client, p) unrSims = append(unrSims, s) t.Logf("UNRELATED %-32s ~ %-34s cosine=%.4f", p.a, p.b, s) } synMin, synMax, synMean := stats(synSims) unrMin, unrMax, unrMean := stats(unrSims) t.Logf("SYNONYM group: min=%.4f mean=%.4f max=%.4f (n=%d)", synMin, synMean, synMax, len(synSims)) t.Logf("UNRELATED group: min=%.4f mean=%.4f max=%.4f (n=%d)", unrMin, unrMean, unrMax, len(unrSims)) thr, _ := engine.DefaultSemanticThreshold.Float64() t.Logf("engine DefaultSemanticThreshold = %.4f (ModelVersion %s)", thr, engine.ModelVersion) // The decision the calibration must protect is the confusion at the live threshold, not a clean // full separation: on a 384 dim model, an acronym (SIGINT) sits below its expansion AND below the // unrelated ceiling, so NO single cosine cutoff can capture it; that case belongs to the ontology // alias layer. The threshold's job is therefore to admit every GENUINE synonym while admitting NO // unrelated pair. var falsePos, trueNeg int for i, s := range unrSims { if s >= thr { falsePos++ t.Logf("FALSE POSITIVE at threshold %.4f: %q ~ %q cosine=%.4f", thr, unrelatedPairs[i].a, unrelatedPairs[i].b, s) } else { trueNeg++ } } var truePos int var synMisses []string for i, s := range synSims { if s >= thr { truePos++ } else { synMisses = append(synMisses, fmt.Sprintf("%q~%q=%.4f", synonymPairs[i].a, synonymPairs[i].b, s)) } } t.Logf("CONFUSION at threshold %.4f: synonyms matched %d/%d, unrelated rejected %d/%d", thr, truePos, len(synSims), trueNeg, len(unrSims)) if len(synMisses) > 0 { t.Logf("synonym misses (acronym or initialism class, served by the ontology alias layer, not the cosine cutoff): %v", synMisses) } // HARD: no unrelated pair may leak through. A false positive means the engine would invent a skill // the candidate does not hold, the unacceptable failure for an AEDT tool. if falsePos != 0 { t.Fatalf("CALIBRATION: threshold %.4f admits %d unrelated pair(s) as matches; raise DefaultSemanticThreshold above the unrelated ceiling %.4f", thr, falsePos, unrMax) } // HARD: the threshold must clear the unrelated ceiling with margin, so embedding noise cannot push // an unrelated pair over the line. const minMargin = 0.03 if thr < unrMax+minMargin { t.Fatalf("CALIBRATION: threshold %.4f has too little margin over the unrelated ceiling %.4f (need >= %.4f); raise DefaultSemanticThreshold", thr, unrMax, unrMax+minMargin) } // HARD: at most the acronym class may fall below the threshold; every other synonym must match, or // the cutoff is too high and is discarding real matches (the exact defect this recalibration fixed). if len(synMisses) > 1 { t.Fatalf("CALIBRATION: threshold %.4f discards %d synonym pairs (only the acronym class is allowed to miss): %v; lower DefaultSemanticThreshold", thr, len(synMisses), synMisses) } t.Logf("OK: threshold %.4f admits every genuine synonym and no unrelated pair (margin %.4f over the unrelated ceiling)", thr, thr-unrMax) } // stats returns min, max, mean of a non empty slice. func stats(xs []float64) (mn, mx, mean float64) { sorted := append([]float64(nil), xs...) sort.Float64s(sorted) mn, mx = sorted[0], sorted[len(sorted)-1] var sum float64 for _, x := range xs { sum += x } return mn, mx, sum / float64(len(xs)) }