Skip to content

Commit 05c6c68

Browse files
AryanBVclaude
andcommitted
Phase 2: Chapter predictor with scoped semantic search - fixes brake pads issue
Root cause fix for "brake pads for cars" returning wrong heading (8701 Tractors instead of 8708 Brakes). Problem: Functional override correctly forced Ch.87, but hierarchical navigation picked the FIRST heading instead of the semantically BEST heading. Solution: Use scoped semantic search within the forced chapter to find the correct heading based on query meaning. New files: - chapter-predictor.service.ts: Predicts chapters, handles functional overrides and ambiguous terms - test-phase2.ts, test-root-fix.ts: Test scripts for validation Key changes: - multi-candidate-search.service.ts: - Added getScopedSemanticCandidates() for chapter-scoped search - Added getBestHeadingInChapter() using MAX score (not average) - Fixed aggregation to prioritize highest individual score - llm-navigator.service.ts: - Updated functional override handling to use semantic search - Now finds 8708 (brakes) instead of 8701 (tractors) - chapter-triggers.json: - Added more vehicle part keywords (shock absorber, steering, etc.) Test results (6/7 passed): - ✅ "brake pads for cars" → 8708.30.00 - ✅ "ceramic brake pads" → 8708 - ✅ "automotive brake disc" → 8708 - ✅ "car shock absorber" → 8708 - ✅ "wooden furniture" → 9403 - ✅ "stainless steel vacuum flask" → 9617 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent cb8e232 commit 05c6c68

7 files changed

Lines changed: 1323 additions & 31 deletions

backend/scripts/test-phase2.ts

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/**
2+
* PHASE 2: Chapter Predictor Test Script
3+
*
4+
* Tests the chapter predictor functionality including:
5+
* - Functional overrides (brake pads, toys, furniture, vacuum flasks)
6+
* - Ambiguous terms (coffee, tea)
7+
* - Chapter predictions
8+
*
9+
* Run with: npx ts-node scripts/test-phase2.ts
10+
*/
11+
12+
import dotenv from 'dotenv';
13+
dotenv.config();
14+
15+
// Import the chapter predictor functions
16+
import {
17+
predictChapters,
18+
checkFunctionalOverrides,
19+
checkAmbiguousTerms,
20+
explainChapterPredictions,
21+
getPredictedChaptersArray
22+
} from '../src/services/chapter-predictor.service';
23+
24+
// Import the multi-candidate search to test full flow
25+
import { semanticSearchMulti } from '../src/services/multi-candidate-search.service';
26+
27+
const colors = {
28+
reset: '\x1b[0m',
29+
bright: '\x1b[1m',
30+
green: '\x1b[32m',
31+
yellow: '\x1b[33m',
32+
blue: '\x1b[34m',
33+
cyan: '\x1b[36m',
34+
red: '\x1b[31m',
35+
magenta: '\x1b[35m',
36+
};
37+
38+
function log(msg: string, color = colors.reset) {
39+
console.log(`${color}${msg}${colors.reset}`);
40+
}
41+
42+
interface TestCase {
43+
query: string;
44+
expectedChapter: string;
45+
testType: 'functional_override' | 'ambiguous' | 'prediction' | 'full_search';
46+
description: string;
47+
}
48+
49+
const testCases: TestCase[] = [
50+
// CRITICAL: Functional override tests - these must pass
51+
{
52+
query: 'brake pads for cars',
53+
expectedChapter: '87',
54+
testType: 'functional_override',
55+
description: 'Brake pads should go to Ch.87 (vehicle parts), NOT cosmetic pads'
56+
},
57+
{
58+
query: 'ceramic brake pads',
59+
expectedChapter: '87',
60+
testType: 'functional_override',
61+
description: 'Ceramic brake pads should go to Ch.87 (vehicle parts), NOT ceramics Ch.69'
62+
},
63+
{
64+
query: 'automotive brake pads',
65+
expectedChapter: '87',
66+
testType: 'functional_override',
67+
description: 'Automotive brake pads should go to Ch.87'
68+
},
69+
{
70+
query: 'plastic toys',
71+
expectedChapter: '95',
72+
testType: 'functional_override',
73+
description: 'Plastic toys should go to Ch.95 (toys), NOT plastics Ch.39'
74+
},
75+
{
76+
query: 'wooden furniture',
77+
expectedChapter: '94',
78+
testType: 'functional_override',
79+
description: 'Wooden furniture should go to Ch.94 (furniture), NOT wood Ch.44'
80+
},
81+
{
82+
query: 'vacuum flask',
83+
expectedChapter: '96',
84+
testType: 'functional_override',
85+
description: 'Vacuum flask should go to Ch.96 (miscellaneous)'
86+
},
87+
88+
// Ambiguous term tests
89+
{
90+
query: 'coffee',
91+
expectedChapter: '', // Should ask for disambiguation
92+
testType: 'ambiguous',
93+
description: 'Generic "coffee" should trigger disambiguation question'
94+
},
95+
96+
// Clear prediction tests (not ambiguous)
97+
{
98+
query: 'instant coffee',
99+
expectedChapter: '21',
100+
testType: 'prediction',
101+
description: 'Instant coffee should predict Ch.21'
102+
},
103+
{
104+
query: 'roasted coffee beans',
105+
expectedChapter: '09',
106+
testType: 'prediction',
107+
description: 'Roasted coffee beans should predict Ch.09'
108+
},
109+
110+
// Full search tests
111+
{
112+
query: 'brake pads for cars',
113+
expectedChapter: '87',
114+
testType: 'full_search',
115+
description: 'Full search for brake pads should return 87xx codes'
116+
},
117+
{
118+
query: 'ceramic brake pads',
119+
expectedChapter: '87',
120+
testType: 'full_search',
121+
description: 'Full search for ceramic brake pads should return 87xx codes'
122+
},
123+
];
124+
125+
async function runTest(testCase: TestCase): Promise<boolean> {
126+
log(`\n${'─'.repeat(80)}`, colors.cyan);
127+
log(`Testing: "${testCase.query}"`, colors.bright);
128+
log(`Expected: Ch.${testCase.expectedChapter || 'DISAMBIGUATION'}`, colors.blue);
129+
log(`Type: ${testCase.testType}`, colors.yellow);
130+
log(`${testCase.description}`, colors.reset);
131+
log('', colors.reset);
132+
133+
let passed = false;
134+
135+
try {
136+
switch (testCase.testType) {
137+
case 'functional_override': {
138+
const override = checkFunctionalOverrides(testCase.query);
139+
if (override) {
140+
log(` Functional Override: Ch.${override.forceChapter}`, colors.green);
141+
log(` Reason: ${override.reason}`, colors.reset);
142+
passed = override.forceChapter === testCase.expectedChapter;
143+
} else {
144+
log(` No functional override found!`, colors.red);
145+
passed = false;
146+
}
147+
break;
148+
}
149+
150+
case 'ambiguous': {
151+
const ambiguity = checkAmbiguousTerms(testCase.query);
152+
if (ambiguity) {
153+
log(` Ambiguous term: "${ambiguity.term}"`, colors.yellow);
154+
log(` Question: ${ambiguity.info.disambiguationQuestion}`, colors.reset);
155+
log(` Options:`, colors.reset);
156+
ambiguity.info.options.forEach(opt => {
157+
log(` - ${opt.label} → Ch.${opt.chapter}`, colors.reset);
158+
});
159+
passed = true; // We expected ambiguity
160+
} else {
161+
log(` Not detected as ambiguous`, colors.red);
162+
passed = false;
163+
}
164+
break;
165+
}
166+
167+
case 'prediction': {
168+
const result = predictChapters(testCase.query);
169+
log(` Predictions:`, colors.reset);
170+
result.predictions.slice(0, 3).forEach((p, i) => {
171+
const isExpected = p.chapter === testCase.expectedChapter;
172+
const marker = isExpected ? '✓' : ' ';
173+
const color = isExpected ? colors.green : colors.reset;
174+
log(` ${marker} ${i + 1}. Ch.${p.chapter} (${p.name}): ${(p.confidence * 100).toFixed(1)}%`, color);
175+
});
176+
177+
if (result.functionalOverride) {
178+
log(` Functional Override: Ch.${result.functionalOverride.chapter}`, colors.magenta);
179+
passed = result.functionalOverride.chapter === testCase.expectedChapter;
180+
} else {
181+
const topChapter = result.predictions[0]?.chapter;
182+
passed = topChapter === testCase.expectedChapter;
183+
}
184+
break;
185+
}
186+
187+
case 'full_search': {
188+
log(` Running full semantic search...`, colors.yellow);
189+
const results = await semanticSearchMulti(testCase.query, 20);
190+
191+
// Check if majority of top 10 results are from expected chapter
192+
const top10 = results.slice(0, 10);
193+
const fromExpectedChapter = top10.filter(r => r.code.startsWith(testCase.expectedChapter));
194+
const percentage = (fromExpectedChapter.length / top10.length) * 100;
195+
196+
log(` Top 10 results:`, colors.reset);
197+
top10.forEach((r, i) => {
198+
const isExpected = r.code.startsWith(testCase.expectedChapter);
199+
const color = isExpected ? colors.green : colors.red;
200+
log(` ${i + 1}. ${r.code}: ${r.description?.substring(0, 50)}... (score: ${r.score.toFixed(1)})`, color);
201+
});
202+
203+
log(` From Ch.${testCase.expectedChapter}: ${fromExpectedChapter.length}/10 (${percentage.toFixed(0)}%)`, colors.cyan);
204+
205+
// Pass if at least 70% are from expected chapter
206+
passed = percentage >= 70;
207+
break;
208+
}
209+
}
210+
} catch (error) {
211+
log(` ERROR: ${error}`, colors.red);
212+
passed = false;
213+
}
214+
215+
if (passed) {
216+
log(` ✅ PASSED`, colors.green);
217+
} else {
218+
log(` ❌ FAILED`, colors.red);
219+
}
220+
221+
return passed;
222+
}
223+
224+
async function main() {
225+
console.log('\n');
226+
log('╔═══════════════════════════════════════════════════════════════════════════╗', colors.magenta);
227+
log('║ PHASE 2: CHAPTER PREDICTOR TEST SUITE ║', colors.magenta);
228+
log('╚═══════════════════════════════════════════════════════════════════════════╝', colors.magenta);
229+
230+
const results: { test: string; passed: boolean }[] = [];
231+
232+
for (const testCase of testCases) {
233+
const passed = await runTest(testCase);
234+
results.push({ test: testCase.query, passed });
235+
}
236+
237+
// Summary
238+
console.log('\n');
239+
log('═'.repeat(80), colors.cyan);
240+
log(' TEST SUMMARY', colors.bright);
241+
log('═'.repeat(80), colors.cyan);
242+
243+
const passedCount = results.filter(r => r.passed).length;
244+
const failedCount = results.filter(r => !r.passed).length;
245+
246+
results.forEach(r => {
247+
const status = r.passed ? '✅ PASS' : '❌ FAIL';
248+
const color = r.passed ? colors.green : colors.red;
249+
log(` ${status} ${r.test}`, color);
250+
});
251+
252+
console.log('\n');
253+
log(` Total: ${results.length} | Passed: ${passedCount} | Failed: ${failedCount}`, colors.bright);
254+
255+
if (failedCount === 0) {
256+
log('\n 🎉 ALL TESTS PASSED!', colors.green);
257+
} else {
258+
log(`\n ⚠️ ${failedCount} TEST(S) FAILED`, colors.red);
259+
}
260+
261+
process.exit(failedCount > 0 ? 1 : 0);
262+
}
263+
264+
main().catch(console.error);

backend/scripts/test-root-fix.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* ROOT CAUSE FIX: Test Script
3+
*
4+
* Verifies that scoped semantic search correctly finds the best heading
5+
* when functional override is active.
6+
*
7+
* Run with: npx ts-node scripts/test-root-fix.ts
8+
*/
9+
10+
import { getBestHeadingInChapter, getScopedSemanticCandidates } from '../src/services/multi-candidate-search.service';
11+
import { checkFunctionalOverrides } from '../src/services/chapter-predictor.service';
12+
13+
async function testRootCauseFix() {
14+
console.log('='.repeat(80));
15+
console.log('ROOT CAUSE FIX: Testing Scoped Semantic Search');
16+
console.log('='.repeat(80));
17+
console.log();
18+
19+
const testCases = [
20+
{
21+
query: 'brake pads for cars',
22+
expectedChapter: '87',
23+
expectedHeading: '8708', // Parts and accessories of vehicles
24+
description: 'Should find 8708 (vehicle parts), NOT 8701 (tractors)'
25+
},
26+
{
27+
query: 'ceramic brake pads',
28+
expectedChapter: '87',
29+
expectedHeading: '8708',
30+
description: 'Should find 8708 (vehicle parts), NOT 69xx (ceramics)'
31+
},
32+
{
33+
query: 'automotive brake disc',
34+
expectedChapter: '87',
35+
expectedHeading: '8708',
36+
description: 'Should find 8708 (vehicle parts)'
37+
},
38+
{
39+
query: 'car shock absorber',
40+
expectedChapter: '87',
41+
expectedHeading: '8708',
42+
description: 'Should find 8708 (suspension parts)'
43+
},
44+
{
45+
query: 'plastic toys for children',
46+
expectedChapter: '95',
47+
expectedHeading: '9503',
48+
description: 'Should find 9503 (toys), NOT 39xx (plastics)'
49+
},
50+
{
51+
query: 'wooden furniture',
52+
expectedChapter: '94',
53+
expectedHeading: '9403',
54+
description: 'Should find 9403 (furniture), NOT 44xx (wood)'
55+
},
56+
{
57+
query: 'stainless steel vacuum flask',
58+
expectedChapter: '96',
59+
expectedHeading: '9617',
60+
description: 'Should find 9617 (vacuum flasks)'
61+
}
62+
];
63+
64+
let passed = 0;
65+
let failed = 0;
66+
67+
for (const test of testCases) {
68+
console.log('-'.repeat(60));
69+
console.log(`Test: "${test.query}"`);
70+
console.log(`Expected: Ch.${test.expectedChapter}, Heading ${test.expectedHeading}`);
71+
console.log(`Description: ${test.description}`);
72+
console.log();
73+
74+
// Step 1: Check functional override
75+
const override = checkFunctionalOverrides(test.query);
76+
if (!override) {
77+
console.log(' ❌ FAILED: No functional override detected');
78+
failed++;
79+
continue;
80+
}
81+
82+
if (override.forceChapter !== test.expectedChapter) {
83+
console.log(` ❌ FAILED: Wrong chapter. Got ${override.forceChapter}, expected ${test.expectedChapter}`);
84+
failed++;
85+
continue;
86+
}
87+
console.log(` ✓ Functional override: Ch.${override.forceChapter} (${override.reason})`);
88+
89+
// Step 2: Get best heading using scoped semantic search
90+
const bestHeading = await getBestHeadingInChapter(test.query, test.expectedChapter);
91+
92+
if (!bestHeading) {
93+
console.log(' ❌ FAILED: Could not find best heading');
94+
failed++;
95+
continue;
96+
}
97+
98+
const gotHeading = bestHeading.code.substring(0, 4);
99+
if (gotHeading === test.expectedHeading) {
100+
console.log(` ✓ Best heading: ${bestHeading.code} (score: ${bestHeading.score.toFixed(1)})`);
101+
console.log(` ${bestHeading.description.substring(0, 60)}...`);
102+
console.log(' ✓ PASSED');
103+
passed++;
104+
} else {
105+
console.log(` ❌ FAILED: Wrong heading. Got ${gotHeading}, expected ${test.expectedHeading}`);
106+
console.log(` ${bestHeading.code}: ${bestHeading.description.substring(0, 60)}...`);
107+
failed++;
108+
}
109+
}
110+
111+
console.log();
112+
console.log('='.repeat(80));
113+
console.log(`RESULTS: ${passed}/${testCases.length} passed, ${failed} failed`);
114+
console.log('='.repeat(80));
115+
116+
// Additional test: Show top 5 candidates for "brake pads for cars"
117+
console.log();
118+
console.log('DETAILED TEST: Top candidates for "brake pads for cars" in Ch.87');
119+
console.log('-'.repeat(60));
120+
121+
const candidates = await getScopedSemanticCandidates('brake pads for cars', '87', 10);
122+
candidates.forEach((c, i) => {
123+
console.log(`${i + 1}. ${c.code}: ${c.description?.substring(0, 50)}... (score: ${c.score.toFixed(1)})`);
124+
});
125+
126+
// Exit
127+
process.exit(failed > 0 ? 1 : 0);
128+
}
129+
130+
testRootCauseFix().catch(error => {
131+
console.error('Test error:', error);
132+
process.exit(1);
133+
});

0 commit comments

Comments
 (0)