feat(semantle,doantu): calibrate cosine score via normalized sigmoid

BGE embeddings occupy a narrow cone in vector space, so raw cosine of
two unrelated words already sits at ~0.40-0.55. Displaying `raw * 100`
made every random guess read as 40-70% warm, which defeated the warmth
UX.

format.js now applies a normalized sigmoid (FLOOR 0.40, CENTER 0.60,
SCALE 8) to remap raw cosine → displayed 0-100. Unrelated pairs drop
to ≤30, loose relation lands around 40-55, clear synonyms hit 85+, and
exact match stays at 100. Emoji buckets were rebased onto the calibrated
score; formatWarmth lost its sign column (calibrated output is always
non-negative).

render.js rounds once and feeds the integer to both formatWarmth and
warmthEmoji so the display value and bucket stay in sync.

Constants are empirical — retune if swapping to a non-BGE model.
This commit is contained in:
2026-04-23 00:33:54 +07:00
parent 4f7f6896c5
commit fd5a1d2903
9 changed files with 370 additions and 90 deletions
+8 -5
View File
@@ -95,9 +95,10 @@ describe("semantle/render", () => {
});
it("includes warmth emoji in each row", () => {
// calibrate(0.85) ≈ 90 → 🎯, calibrate(0.55) ≈ 29 → 😐
const guesses = [
{ word: "a", canonical: "a", similarity: 0.85 },
{ word: "b", canonical: "b", similarity: 0.3 },
{ word: "b", canonical: "b", similarity: 0.55 },
];
const result = renderBoard(guesses);
@@ -149,7 +150,8 @@ describe("semantle/render", () => {
const result = renderGuess(guess);
expect(result).toContain("apple");
expect(result).toContain("+75");
// calibrate(0.75) ≈ 76
expect(result).toContain("76");
expect(result).toContain("🔥");
});
@@ -173,9 +175,10 @@ describe("semantle/render", () => {
expect(renderGuess({ word: "b", canonical: "b", similarity: 0.15 })).toContain("🥶");
});
it("formats similarity with sign and padding", () => {
expect(renderGuess({ word: "a", canonical: "a", similarity: 0.05 })).toContain("+05");
expect(renderGuess({ word: "b", canonical: "b", similarity: -0.2 })).toContain("-20");
it("clips raw cosines below the calibration floor to 00", () => {
// raw 0.05 and raw -0.2 are both well below FLOOR (0.4) → display "00"
expect(renderGuess({ word: "a", canonical: "a", similarity: 0.05 })).toContain("00");
expect(renderGuess({ word: "b", canonical: "b", similarity: -0.2 })).toContain("00");
});
});
});