From 40dcf5dc7489a6cefa2140d913acd6e4050c2e15 Mon Sep 17 00:00:00 2001 From: Test User Date: Sat, 27 Jun 2026 14:56:58 +0800 Subject: [PATCH] fix(search): infer support query domain from SUPPORT_TAGS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit inferQueryDomain only scored TECHNICAL_TAGS and OPS_TAGS, so any query made of support tokens (faq/onboarding/guide/user/…) was classified as 'neutral'. Under DOMAIN_WEIGHT.neutral, support entries get a 0.3 weight while neutral entries get 0.85 — so a genuine support question ranked a matching support doc *below* an unrelated neutral doc, the opposite of the intended ranking. This is also asymmetric with inferDomain (which scores all three tag sets) and contradicts inferQueryDomain's own doc comment ("uses the same tag sets … so the two sides of the matching are symmetric"). Score SUPPORT_TAGS too, with the same technical > ops > support tie-breaking as inferDomain. Added a regression test that builds a real index and asserts a support-domain query ranks the support entry above an equal-raw-score neutral entry (fails before the fix: 2.4 vs 6.8). Co-Authored-By: Claude --- src/__tests__/search-domain-weighting.test.ts | 35 +++++++++++++++++++ src/utils/search-index.ts | 12 ++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/__tests__/search-domain-weighting.test.ts b/src/__tests__/search-domain-weighting.test.ts index 3feb64d..fe6853f 100644 --- a/src/__tests__/search-domain-weighting.test.ts +++ b/src/__tests__/search-domain-weighting.test.ts @@ -149,6 +149,41 @@ describe('domain-weighted search scoring', () => { expect(overrideResult!.score).toBeGreaterThan(normalResult!.score); }); + it('support-domain query boosts support entries above neutral (query domain inferred from support tokens)', async () => { + // inferQueryDomain must recognise SUPPORT_TAGS (faq/onboarding/guide/…). + // A query made of support tokens should be classified as a 'support' query, + // so a support entry (DOMAIN_WEIGHT.support.support = 1.0) outranks a neutral + // entry (DOMAIN_WEIGHT.support.neutral = 0.85) even with an equal raw score. + // Both entries below are identical except for their frontmatter domain. + const learningsDir = path.join(tmpDir, 'learnings'); + await fse.ensureDir(learningsDir); + + await fse.writeFile( + path.join(learningsDir, 'onboarding-faq-support.md'), + '---\ntitle: "onboarding faq"\ndomain: support\n---\nDetails.\n', + ); + await fse.writeFile( + path.join(learningsDir, 'onboarding-faq-neutral.md'), + '---\ntitle: "onboarding faq"\ndomain: neutral\n---\nDetails.\n', + ); + + await buildIndex({ learningsDir, indexPath }); + const index = await loadIndex(indexPath); + expect(index).not.toBeNull(); + + // "onboarding" and "faq" are both SUPPORT_TAGS → query domain = support + const results = search('onboarding faq', index!); + expect(results.length).toBe(2); + + const supportEntry = results.find((r) => r.entry.domain === 'support'); + const neutralEntry = results.find((r) => r.entry.domain === 'neutral'); + expect(supportEntry).toBeDefined(); + expect(neutralEntry).toBeDefined(); + + expect(supportEntry!.score).toBeGreaterThan(neutralEntry!.score); + expect(results[0].entry.domain).toBe('support'); + }); + it('built index carries domain field on every entry (version 4)', async () => { const learningsDir = path.join(tmpDir, 'learnings'); await fse.ensureDir(learningsDir); diff --git a/src/utils/search-index.ts b/src/utils/search-index.ts index 17a0f15..35427d7 100644 --- a/src/utils/search-index.ts +++ b/src/utils/search-index.ts @@ -105,14 +105,18 @@ const DOMAIN_WEIGHT: Record> = function inferQueryDomain(queryTokens: string[]): KnowledgeDomain { let techScore = 0; let opsScore = 0; + let supportScore = 0; for (const t of queryTokens) { if (TECHNICAL_TAGS.has(t)) techScore++; if (OPS_TAGS.has(t)) opsScore++; + if (SUPPORT_TAGS.has(t)) supportScore++; } - if (opsScore > techScore) return 'ops'; - if (techScore > opsScore) return 'technical'; - if (techScore > 0) return 'technical'; // tie \u2192 technical - return 'neutral'; + const maxScore = Math.max(techScore, opsScore, supportScore); + if (maxScore === 0) return 'neutral'; + // Tie-breaking mirrors inferDomain: technical > ops > support. + if (techScore === maxScore) return 'technical'; + if (opsScore === maxScore) return 'ops'; + return 'support'; } // Type bonuses: skills/rules already represent curated, high-confidence knowledge.