@@ -37,6 +37,12 @@ import {
3737 mergeClaimGraphLogs ,
3838 applyClaimGraphLog ,
3939 expandQueryWithGraph ,
40+ cosineSimilarity ,
41+ normalizeVector ,
42+ createPackFingerprint ,
43+ serializeSidecar ,
44+ parseSidecar ,
45+ validateSidecarForPack ,
4046} from '../dist/index.js' ;
4147import { mountPack as mountPackNode } from '../dist/node.js' ;
4248
@@ -613,6 +619,132 @@ async function testSemanticRerankErrorAndDefaults() {
613619 ) ;
614620}
615621
622+ async function testSemanticSidecarRerankAndValidation ( ) {
623+ const docs = [
624+ { id : 'a' , text : 'alpha beta alpha beta alpha beta river stone' } ,
625+ { id : 'b' , text : 'alpha beta solar wind' } ,
626+ ] ;
627+ const pack = await mountPack ( { src : await buildPack ( docs ) } ) ;
628+ const sidecar = {
629+ version : 1 ,
630+ packFingerprint : createPackFingerprint ( pack ) ,
631+ modelId : 'qwen3-embedding:4b' ,
632+ dimension : 2 ,
633+ metric : 'cosine' ,
634+ createdAt : new Date ( ) . toISOString ( ) ,
635+ blocks : [
636+ { blockId : 0 , vector : [ 1 , 0 ] } ,
637+ { blockId : 1 , vector : [ 0 , 1 ] } ,
638+ ] ,
639+ } ;
640+
641+ const lexical = query ( pack , 'alpha beta' , { topK : 2 , queryExpansion : { enabled : false } } ) ;
642+ const reranked = query ( pack , 'alpha beta' , {
643+ topK : 2 ,
644+ queryExpansion : { enabled : false } ,
645+ semantic : {
646+ enabled : true ,
647+ sidecarPath : serializeSidecar ( sidecar ) ,
648+ provider : { type : 'ollama' , modelId : 'qwen3-embedding:4b' } ,
649+ queryEmbedding : new Float32Array ( [ 0 , 1 ] ) ,
650+ force : true ,
651+ blend : { enabled : false } ,
652+ } ,
653+ } ) ;
654+
655+ assert . notEqual ( reranked [ 0 ] ?. source , lexical [ 0 ] ?. source , 'expected sidecar rerank to update ordering' ) ;
656+ assert . equal ( reranked [ 0 ] ?. evidence ?. retrieval , 'hybrid' ) ;
657+
658+ assert . throws (
659+ ( ) => validateSidecarForPack ( { sidecar : { ...sidecar , modelId : 'other' } , pack, modelId : 'qwen3-embedding:4b' } ) ,
660+ / S e m a n t i c m o d e l m i s m a t c h /
661+ ) ;
662+ assert . throws (
663+ ( ) => validateSidecarForPack ( { sidecar : { ...sidecar , packFingerprint : 'fnv1a-deadbeef' } , pack, modelId : 'qwen3-embedding:4b' } ) ,
664+ / p a c k f i n g e r p r i n t m i s m a t c h /
665+ ) ;
666+
667+ const loaded = parseSidecar ( serializeSidecar ( sidecar ) ) ;
668+ assert . deepEqual ( loaded , sidecar , 'expected semantic sidecar round trip to remain stable' ) ;
669+ }
670+
671+ async function testSemanticEvidenceScoresRemainCorrectAfterRerank ( ) {
672+ const docs = [
673+ { id : 'lex-a' , text : 'alpha beta alpha beta alpha beta river stone' } ,
674+ { id : 'lex-b' , text : 'alpha beta solar wind' } ,
675+ ] ;
676+ const pack = await mountPack ( {
677+ src : await buildPack ( docs , {
678+ semantic : {
679+ enabled : true ,
680+ modelId : 'test-model' ,
681+ embeddings : [ new Float32Array ( [ 1 , 0 ] ) , new Float32Array ( [ 0 , 1 ] ) ] ,
682+ quantization : { type : 'int8_l2norm' , perVectorScale : true } ,
683+ } ,
684+ } ) ,
685+ } ) ;
686+
687+ const lexical = query ( pack , 'alpha beta' , {
688+ topK : 2 ,
689+ queryExpansion : { enabled : false } ,
690+ } ) ;
691+ const lexicalScores = new Map ( lexical . map ( ( h ) => [ h . blockId , h . evidence ?. lexicalScore ?? h . score ] ) ) ;
692+ const reranked = query ( pack , 'alpha beta' , {
693+ topK : 2 ,
694+ queryExpansion : { enabled : false } ,
695+ semantic : {
696+ enabled : true ,
697+ queryEmbedding : new Float32Array ( [ 0 , 1 ] ) ,
698+ force : true ,
699+ blend : { enabled : true , wLex : 0.5 , wSem : 0.5 } ,
700+ } ,
701+ } ) ;
702+
703+ assert . notEqual (
704+ reranked [ 0 ] ?. source ,
705+ lexical [ 0 ] ?. source ,
706+ 'expected semantic rerank to change ordering'
707+ ) ;
708+ for ( const hit of reranked ) {
709+ const before = lexicalScores . get ( hit . blockId ) ;
710+ assert . equal (
711+ hit . evidence ?. lexicalScore ,
712+ before ,
713+ 'expected evidence.lexicalScore to preserve pre-rerank lexical score'
714+ ) ;
715+ assert . equal ( hit . evidence ?. retrieval , 'hybrid' ) ;
716+ assert . equal ( typeof hit . evidence ?. semanticScore , 'number' ) ;
717+ assert . equal ( typeof hit . evidence ?. blendedScore , 'number' ) ;
718+ }
719+ }
720+
721+ async function testLexicalOnlyEvidenceRemainsUnchanged ( ) {
722+ const docs = [
723+ { id : 'a' , text : 'alpha beta gamma' } ,
724+ { id : 'b' , text : 'alpha beta delta' } ,
725+ ] ;
726+ const pack = await mountPack ( { src : await buildPack ( docs ) } ) ;
727+ const hits = query ( pack , 'alpha beta' , {
728+ topK : 2 ,
729+ queryExpansion : { enabled : false } ,
730+ } ) ;
731+ assert . ok ( hits . length > 0 , 'expected lexical query to return hits' ) ;
732+ for ( const hit of hits ) {
733+ assert . equal ( hit . evidence ?. retrieval , 'lexical' ) ;
734+ assert . equal ( typeof hit . evidence ?. lexicalScore , 'number' ) ;
735+ assert . equal ( hit . evidence ?. semanticScore , undefined ) ;
736+ assert . equal ( hit . evidence ?. blendedScore , undefined ) ;
737+ }
738+ }
739+
740+ async function testCosineHelpers ( ) {
741+ const a = normalizeVector ( new Float32Array ( [ 3 , 4 ] ) ) ;
742+ const b = normalizeVector ( new Float32Array ( [ 3 , 4 ] ) ) ;
743+ const c = normalizeVector ( new Float32Array ( [ 4 , - 3 ] ) ) ;
744+ assert . ok ( Math . abs ( cosineSimilarity ( a , b ) - 1 ) < 1e-6 , 'expected same vector cosine to be 1' ) ;
745+ assert . ok ( Math . abs ( cosineSimilarity ( a , c ) ) < 1e-6 , 'expected orthogonal vector cosine to be ~0' ) ;
746+ }
747+
616748async function testSemanticFixtureAndHelpers ( ) {
617749 const pack = await buildSemanticFixturePack ( ) ;
618750 assert . ok (
@@ -1638,6 +1770,10 @@ await testLexConfidenceDeterministic();
16381770await testSemanticRerankLowConfidence ( ) ;
16391771await testSemanticRerankRespectsConfidenceAndForce ( ) ;
16401772await testSemanticRerankErrorAndDefaults ( ) ;
1773+ await testSemanticSidecarRerankAndValidation ( ) ;
1774+ await testSemanticEvidenceScoresRemainCorrectAfterRerank ( ) ;
1775+ await testLexicalOnlyEvidenceRemainsUnchanged ( ) ;
1776+ await testCosineHelpers ( ) ;
16411777await testSmartQuotePhrase ( ) ;
16421778await testFirstBlockRetrieval ( ) ;
16431779await testNearDuplicateDedupe ( ) ;
0 commit comments