@@ -1519,6 +1519,99 @@ describe("memory index", () => {
15191519 }
15201520 } ) ;
15211521
1522+ it ( "keeps overlapping keyword-backed hybrid hits when all scores are below minScore" , async ( ) => {
1523+ const cfg = createCfg ( {
1524+ storePath : path . join ( workspaceDir , "index-hybrid-overlap-relaxed-fallback.sqlite" ) ,
1525+ minScore : 0.35 ,
1526+ hybrid : { enabled : true , vectorWeight : 0.7 , textWeight : 0.3 } ,
1527+ } ) ;
1528+ const manager = await getFreshManager ( cfg ) ;
1529+ try {
1530+ const overlappingResult = {
1531+ path : "memory/overlap.md" ,
1532+ startLine : 3 ,
1533+ endLine : 4 ,
1534+ source : "memory" as const ,
1535+ snippet : "overlap keyword vector" ,
1536+ score : 0.2 ,
1537+ vectorScore : 0.1 ,
1538+ textScore : 0.5 ,
1539+ } ;
1540+ const selectHybridSearchResults = Reflect . get ( manager , "selectHybridSearchResults" ) as (
1541+ merged : Array < typeof overlappingResult > ,
1542+ keywordResults : Array < typeof overlappingResult & { id : string } > ,
1543+ maxResults : number ,
1544+ minScore : number ,
1545+ ) => Array < typeof overlappingResult > ;
1546+
1547+ const results = selectHybridSearchResults . call (
1548+ manager ,
1549+ [ overlappingResult ] ,
1550+ [ { ...overlappingResult , id : "overlap" } ] ,
1551+ 10 ,
1552+ 0.35 ,
1553+ ) ;
1554+
1555+ expect ( results . map ( ( result ) => result . path ) ) . toStrictEqual ( [ "memory/overlap.md" ] ) ;
1556+ expect ( results [ 0 ] ?. vectorScore ) . toBeGreaterThan ( 0 ) ;
1557+ expect ( results [ 0 ] ?. textScore ) . toBeGreaterThan ( 0 ) ;
1558+ } finally {
1559+ await manager . close ?.( ) ;
1560+ }
1561+ } ) ;
1562+
1563+ it ( "preserves hybrid merger order when retaining keyword-only hits" , async ( ) => {
1564+ const cfg = createCfg ( {
1565+ storePath : path . join ( workspaceDir , "index-hybrid-keyword-preserves-merge-order.sqlite" ) ,
1566+ minScore : 0.35 ,
1567+ hybrid : { enabled : true , vectorWeight : 0.7 , textWeight : 0.3 } ,
1568+ } ) ;
1569+ const manager = await getFreshManager ( cfg ) ;
1570+ try {
1571+ const keywordOnlyResult = {
1572+ path : "memory/keyword-first.md" ,
1573+ startLine : 2 ,
1574+ endLine : 2 ,
1575+ source : "memory" as const ,
1576+ snippet : "keyword first" ,
1577+ score : 0.3 ,
1578+ vectorScore : 0 ,
1579+ textScore : 1 ,
1580+ } ;
1581+ const strictVectorResult = {
1582+ path : "memory/vector-second.md" ,
1583+ startLine : 1 ,
1584+ endLine : 1 ,
1585+ source : "memory" as const ,
1586+ snippet : "strict vector second" ,
1587+ score : 0.63 ,
1588+ vectorScore : 0.9 ,
1589+ textScore : 0 ,
1590+ } ;
1591+ const selectHybridSearchResults = Reflect . get ( manager , "selectHybridSearchResults" ) as (
1592+ merged : Array < typeof keywordOnlyResult > ,
1593+ keywordResults : Array < typeof keywordOnlyResult & { id : string } > ,
1594+ maxResults : number ,
1595+ minScore : number ,
1596+ ) => Array < typeof keywordOnlyResult > ;
1597+
1598+ const results = selectHybridSearchResults . call (
1599+ manager ,
1600+ [ keywordOnlyResult , strictVectorResult ] ,
1601+ [ { ...keywordOnlyResult , id : "keyword-only" } ] ,
1602+ 10 ,
1603+ 0.35 ,
1604+ ) ;
1605+
1606+ expect ( results . map ( ( result ) => result . path ) ) . toStrictEqual ( [
1607+ "memory/keyword-first.md" ,
1608+ "memory/vector-second.md" ,
1609+ ] ) ;
1610+ } finally {
1611+ await manager . close ?.( ) ;
1612+ }
1613+ } ) ;
1614+
15221615 it ( "reports vector availability after probe" , async ( ) => {
15231616 const cfg = createCfg ( { storePath : indexVectorPath , vectorEnabled : true } ) ;
15241617 const manager = await getPersistentManager ( cfg ) ;
0 commit comments