#!/usr/bin/env node /* * score.test.mjs — the disjoint-tier scorer suite (D-09, SRCH-02). * ---------------------------------------------------------------- * RED phase (plan 04-01 task 2): every case fails while * ../src/score.ts does not exist (lazy import => per-case FAIL). * * The ordering law (RESEARCH Pitfall 4): an exact match can NEVER be * outranked by a fuzzy one — the ordering is a property of the TIER, * not of the arithmetic. The tier bands are strictly disjoint and * descending: * * EXACT 1.00 > ALIAS_EXACT 0.90 > PREFIX 0.75 > TOKEN_SET 0.65 > FUZZY < 0.60 * * FUZZY is the band's CEILING (0.60), not a zero multiplier: a fuzzy * score is similarity * TIER.FUZZY, always inside (0, 0.60). * * S1 tier disjointness + exact literals * S2 min-length guard fires BEFORE the exact tier * S3 exact outranks fuzzy, always * S4 prefix tier (mid-word prefix) * S5 token-set tier (token-boundary superset; non-subset is refused) * S6 fuzzy residual arithmetic + the 0.72 admission boundary * S7 matchedAlias provenance (the RAW alias string comes back) * * Run: npm run search:test (exit 0 = all green) */ import { strict as assert } from "node:assert"; import { test } from "./harness.mjs"; // Lazy score load — missing module => per-case ERR_MODULE_NOT_FOUND (the RED state). let scorePromise = null; function loadScore() { if (!scorePromise) { scorePromise = import("../src/score.ts"); } return scorePromise; } // Lazy distance load (S6 pins the hand-computed Penang/Padang OSA distance). let distancePromise = null; function loadDamerau() { if (!distancePromise) { distancePromise = import("../src/distance.ts").then((m) => m.damerau); } return distancePromise; } test("S1 tier disjointness — exact literals, strictly descending bands, FUZZY is the 0.60 ceiling", async () => { const { TIER } = await loadScore(); assert.equal(TIER.EXACT, 1.0); assert.equal(TIER.ALIAS_EXACT, 0.9); assert.equal(TIER.PREFIX, 0.75); assert.equal(TIER.TOKEN_SET, 0.65); assert.equal(TIER.FUZZY, 0.6); assert.ok(TIER.EXACT > TIER.ALIAS_EXACT); assert.ok(TIER.ALIAS_EXACT > TIER.PREFIX); assert.ok(TIER.PREFIX > TIER.TOKEN_SET); assert.ok(TIER.TOKEN_SET > TIER.FUZZY); const { scoreOne } = await loadScore(); const fuzzy = scoreOne("kclc", "klcc"); assert.ok(fuzzy > 0, "a near-miss fuzzy score is strictly positive"); assert.ok(fuzzy < TIER.FUZZY, "a fuzzy score stays under the 0.60 ceiling"); }); test("S2 min-length guard — either side under 3 chars scores 0, even on an exact match", async () => { const { scoreOne } = await loadScore(); assert.equal(scoreOne("ab", "abcd"), 0); assert.equal(scoreOne("kl", "kl"), 0, "the guard fires BEFORE the exact tier"); }); test("S3 exact outranks fuzzy — always", async () => { const { scoreOne, TIER } = await loadScore(); assert.ok(scoreOne("klcc", "klcc") > scoreOne("klcc", "klca")); assert.ok(scoreOne("klcc", "klcc") > TIER.FUZZY); }); test("S4 prefix tier — mid-word prefix hits PREFIX", async () => { const { scoreOne, TIER } = await loadScore(); assert.equal(scoreOne("jalan amp", "jalan ampang"), TIER.PREFIX); }); test("S5 token-set tier — token-boundary superset is TOKEN_SET; a non-subset never is", async () => { const { scoreOne, TIER } = await loadScore(); assert.equal(scoreOne("taman bangsar", "taman bangsar kib"), TIER.TOKEN_SET); const nonSubset = scoreOne("taman bangsar", "taman bukit"); assert.notEqual(nonSubset, TIER.TOKEN_SET); assert.ok(nonSubset >= 0 && nonSubset < TIER.FUZZY, "a non-subset lands in the residual band, never TOKEN_SET"); }); test("S6 fuzzy residual + admission boundary — 0.75 admits, the Penang/Padang shape (0.667) refuses", async () => { const { scoreOne, TIER, FUZZY_ADMIT_SIMILARITY } = await loadScore(); const damerau = await loadDamerau(); // "kclc" vs "klcc": one transposition over 4 chars -> sim exactly 0.75. assert.equal(scoreOne("kclc", "klcc"), 0.75 * TIER.FUZZY); const klccSim = 1 - 1 / 4; assert.equal(klccSim, 0.75); assert.ok(klccSim >= FUZZY_ADMIT_SIMILARITY, "sim 0.75 reaches the 0.72 admission floor"); // "penang" vs "padang": two substitutions over 6 chars -> sim 0.667, refused. const penangPadangDistance = damerau("penang", "padang"); const penangSim = 1 - penangPadangDistance / 6; assert.equal(penangPadangDistance, 2); assert.ok(penangSim < FUZZY_ADMIT_SIMILARITY, "sim 0.667 stays below the 0.72 admission floor"); assert.ok(FUZZY_ADMIT_SIMILARITY === 0.72); }); test("S7 matchedAlias provenance — the RAW alias string comes back", async () => { const { scoreCandidate, TIER } = await loadScore(); const hit = scoreCandidate("klcc", ["Suria KLCC", "KLCC"]); assert.ok(hit !== null); assert.equal(hit.matchedAlias, "KLCC", "the matched alias is the RAW string, not the normalized form"); assert.ok( hit.score === TIER.EXACT || hit.score === TIER.ALIAS_EXACT, `an exact alias match lands in the exact/alias-exact band, got ${hit.score}`, ); });