feat(canonical-lexicon): phrase-override katmanı — adjective+head yanlış-kovalar
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

Skordan önce yüksek-güven phrase kuralları: "<sistem> yağı$" → Yağlar (end-
anchor, "motor yağı filtresi/pompası" hariç), "(cam|silecek|kalorifer) motoru"
→ Elektrik, "zamanlama (dişli|zincir|kayış)" → Triger. ~1400 motor/şanzıman
yanlış-kovası + ~230 silecek-motoru + ~790 zamanlama düzelir. 29/29 test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 06:48:40 +03:00
parent 75dbadcb47
commit 6e556e198a
2 changed files with 32 additions and 0 deletions

View File

@@ -225,6 +225,20 @@ describe("mapToCanonical", () => {
expect(mapToCanonical("burkma cubugu").slug).toBe("suspansiyon");
});
it("phrase overrides fix adjective+head mis-buckets", () => {
// "<system> yağı" = fluid, not the system
expect(mapToCanonical("motor yagi").slug).toBe("yaglar-sivilar");
expect(mapToCanonical("sanziman yagi").slug).toBe("yaglar-sivilar");
// but the PARTS keep their bucket (end-anchor excludes them)
expect(mapToCanonical("motor yagi filtresi").slug).toBe("filtreler");
expect(mapToCanonical("motor yagi pompasi").slug).toBe("motor");
// body electric motors → elektrik, not engine
expect(mapToCanonical("cam silecek motoru").slug).toBe("elektrik");
expect(mapToCanonical("cam motoru").slug).toBe("elektrik");
// timing gear → triger, not şanzıman
expect(mapToCanonical("zamanlama dislileri").slug).toBe("triger-zincir-rulman");
});
it("returns null when no keyword matches", () => {
expect(mapToCanonical("romork duzenegi cikarilabilir").slug).toBeNull();
// genuinely generic Mercedes names stay unmapped (better than wrong bucket)

View File

@@ -317,7 +317,25 @@ export interface CanonicalMatch {
* score; a runner-up within 1 point (no strong keyword decisively separated the
* two) is flagged ambiguous. Confidence scales with the winning margin.
*/
// High-confidence PHRASE overrides, checked before the bag-of-words score.
// They fix "adjective + head-noun" cases where a strong keyword wins on the
// modifier instead of the head: "motor yağı" scores motor(3) but the head noun
// is the oil. Order doesn't matter (first match wins); keep each tight.
const OVERRIDES: [RegExp, string][] = [
// "<system> yağı/yağları" (head = oil) → fluid. END-ANCHORED so the PARTS
// "motor yağı filtresi / pompası / soğutucusu" are NOT captured (they keep
// scoring to filtre / motor / soğutma).
[/(motor|sanziman|diferansiyel|direksiyon|hidrolik|fren|torpido) yag(i|lari)$/, "yaglar-sivilar"],
// body/electric motors (window/wiper/blower), not the engine.
[/(cam|silecek|kalorifer|kaput) motor/, "elektrik"],
// timing gear/chain/belt → triger, not şanzıman via the weak "dişli".
[/zamanlama (disli|zincir|kayis|seti|gergi|kasnak)/, "triger-zincir-rulman"],
];
export function mapToCanonical(folded: string): CanonicalMatch {
for (const [re, slug] of OVERRIDES) {
if (re.test(folded)) return { slug, ambiguous: false, confidence: 0.95 };
}
const toks = tokens(folded);
let bestSlug: string | null = null;
let best = 0;