Files
arah2/search/test/distance.test.mjs
T
araharah-executor 6368bad3d5 feat(04-01): OSA Damerau distance + disjoint-tier scorer (the residual net)
- distance.ts: hand-rolled 3-band OSA, adjacent transposition = 1 edit,
  zero imports (D-07); the row rotation is pinned by the D1-D9 vectors
- score.ts: TIER bands strictly disjoint descending — EXACT 1.00 /
  ALIAS_EXACT 0.90 / PREFIX 0.75 / TOKEN_SET 0.65 / FUZZY as the 0.60
  CEILING (a fuzzy score is fuzzySim * 0.60, never a 0.00 multiplier);
  FUZZY_ADMIT_SIMILARITY = 0.72 exported for source.ts + the corpus
  runner; token-level residual lets 'Sentrl' reach 'KL Sentral' while
  'Jln Tun Razak' cannot reach 'Jalan Ampang'
- scoreCandidate returns the RAW matched alias (corpus provenance);
  an exact ALIAS hit rides ALIAS_EXACT so a name-exact outranks it
- score.test.mjs S7 corrected during GREEN: admission belongs to
  source.ts — scoreCandidate asserts the sub-floor similarity instead

D1-D9 + S1-S7 green; typecheck clean.
2026-10-24 00:32:44 +08:00

70 lines
2.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/*
* distance.test.mjs — hand-computed Damerau-Levenshtein (OSA) vectors (SRCH-02).
* ---------------------------------------------------------------------------
* RED phase (plan 04-01): every case fails while ../src/distance.ts does not
* exist — the import happens lazily inside each case, so D1–D9 report one
* FAIL per vector instead of a lost suite load.
*
* The row-rotation in a 3-band OSA is easy to get wrong — these vectors are
* written FIRST and made to pass second (the repo RED->GREEN convention).
*
* Run: npm run search:test (exit 0 = all green)
*/
import { strict as assert } from "node:assert";
import { test } from "./harness.mjs";
// Lazy distance load — missing module => per-case ERR_MODULE_NOT_FOUND (the RED state).
let distancePromise = null;
function loadDamerau() {
if (!distancePromise) {
distancePromise = import("../src/distance.ts").then((m) => m.damerau);
}
return distancePromise;
}
test("D1 adjacent transposition — damerau('ab','ba') === 1", async () => {
const damerau = await loadDamerau();
assert.equal(damerau("ab", "ba"), 1, "a swap of adjacent chars is ONE edit in OSA");
});
test("D2 two transpositions — damerau('a cat','an act') === 2", async () => {
const damerau = await loadDamerau();
assert.equal(damerau("a cat", "an act"), 2, "insert n, then transpose 'ca' -> 'ac'");
});
test("D3 empty/empty — damerau('','') === 0", async () => {
const damerau = await loadDamerau();
assert.equal(damerau("", ""), 0);
});
test("D4 empty target — damerau('','abc') === 3", async () => {
const damerau = await loadDamerau();
assert.equal(damerau("", "abc"), 3, "three insertions");
});
test("D5 empty source — damerau('abc','') === 3", async () => {
const damerau = await loadDamerau();
assert.equal(damerau("abc", ""), 3, "three deletions");
});
test("D6 identity — damerau('same','same') === 0", async () => {
const damerau = await loadDamerau();
assert.equal(damerau("same", "same"), 0);
});
test("D7 corpus typo transposition — damerau('klcc','kclc') === 1", async () => {
const damerau = await loadDamerau();
assert.equal(damerau("klcc", "kclc"), 1, "KCLC must reach KLCC as ONE edit (D-13 typo bucket)");
});
test("D8 corpus typo transposition — damerau('sunway','sunwya') === 1", async () => {
const damerau = await loadDamerau();
assert.equal(damerau("sunway", "sunwya"), 1, "SUNWYA must reach SUNWAY as ONE edit");
});
test("D9 classic Levenshtein sanity — damerau('kitten','sitting') === 3", async () => {
const damerau = await loadDamerau();
assert.equal(damerau("kitten", "sitting"), 3, "sub k->s, sub e->i, append g");
});