test(04-01): RED — OSA distance vectors (D1-D9) + disjoint-tier scorer suite (S1-S7)
- 9 hand-computed Damerau-Levenshtein vectors (transpositions, empties, kitten/sitting sanity, the corpus's klcc/kclc + sunway/sunwya typos) - 7 scorer laws: tier literals + disjointness, min-length guard before exact, exact-outranks-fuzzy, mid-word PREFIX vs token-boundary TOKEN_SET, fuzzy residual 0.75*0.60 arithmetic, the 0.72 admission boundary (Penang/Padang 0.667 refuses), matchedAlias raw provenance All 16 new cases fail while src/distance.ts + src/score.ts are absent.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/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}`,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user